Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-07 09:10:26 +00:00
parent 362b615a84
commit f4c6fbb86f
59 changed files with 628 additions and 405 deletions

View File

@ -149,16 +149,13 @@ setup-test-env:
- .rails-job-base
- .setup-test-env-cache
- .rails:rules:code-backstage-qa
- .use-pg12
stage: prepare
variables:
GITLAB_TEST_EAGER_LOAD: "0"
SETUP_DB: "false"
script:
- run_timed_command "scripts/setup-test-env"
- echo -e "\e[0Ksection_start:`date +%s`:gitaly-test-build[collapsed=true]\r\e[0KCompiling Gitaly binaries"
- run_timed_command "bundle exec ruby -I. -e 'require \"config/environment\"; TestEnv.init'"
- run_timed_command "scripts/gitaly-test-build" # Do not use 'bundle exec' here
- echo -e "\e[0Ksection_end:`date +%s`:gitaly-test-build\r\e[0K"
artifacts:
expire_in: 7d
paths:

View File

@ -1 +1 @@
fcea557863c9433fe1ed9f0fd8d446c4a592891e
8fd337f0f718f257ae72a66c464143a395af4c05

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
@ -454,7 +454,9 @@ const Api = {
})
.then(({ data }) => (callback ? callback(data) : data))
.catch(() => {
flash(__('Something went wrong while fetching projects'));
createFlash({
message: __('Something went wrong while fetching projects'),
});
if (callback) {
callback();
}
@ -642,7 +644,11 @@ const Api = {
params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
.catch(() =>
createFlash({
message: __('Something went wrong while fetching projects'),
}),
);
},
branches(id, query = '', options = {}) {

View File

@ -0,0 +1,11 @@
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
const MARKDOWN_PATH = '/api/:version/markdown';
export function getMarkdown(options) {
const url = buildApiUrl(MARKDOWN_PATH);
return axios.post(url, {
...options,
});
}

View File

@ -54,6 +54,7 @@ export default {
'isEmptyStage',
'selectedStage',
'selectedStageEvents',
'selectedStageError',
'stages',
'summary',
'startDate',
@ -72,6 +73,14 @@ export default {
selectedStageReady() {
return !this.isLoadingStage && this.selectedStage;
},
emptyStageTitle() {
return this.selectedStageError
? this.selectedStageError
: __("We don't have enough data to show this stage.");
},
emptyStageText() {
return !this.selectedStageError ? this.selectedStage.emptyStageText : '';
},
},
methods: {
...mapActions([
@ -206,9 +215,9 @@ export default {
<gl-empty-state
v-if="displayNotEnoughData"
class="js-empty-state"
:description="selectedStage.emptyStageText"
:description="emptyStageText"
:svg-path="noDataSvgPath"
:title="__('We don\'t have enough data to show this stage.')"
:title="emptyStageTitle"
/>
<component
:is="selectedStage.component"

View File

@ -33,7 +33,14 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.get(`${requestPath}/events/${selectedStage.name}.json`, {
params: { 'cycle_analytics[start_date]': startDate },
})
.then(({ data }) => commit(types.RECEIVE_STAGE_DATA_SUCCESS, data))
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) {
commit(types.RECEIVE_STAGE_DATA_ERROR, data.error);
} else {
commit(types.RECEIVE_STAGE_DATA_SUCCESS, data);
}
})
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
};

View File

@ -44,10 +44,11 @@ export default {
state.selectedStageEvents = decorateEvents(events, selectedStage);
state.hasError = false;
},
[types.RECEIVE_STAGE_DATA_ERROR](state) {
[types.RECEIVE_STAGE_DATA_ERROR](state, error) {
state.isLoadingStage = false;
state.isEmptyStage = true;
state.selectedStageEvents = [];
state.hasError = true;
state.selectedStageError = error;
},
};

View File

@ -9,6 +9,7 @@ export default () => ({
stats: [],
selectedStage: {},
selectedStageEvents: [],
selectedStageError: '',
medians: {},
hasError: false,
isLoading: false,

View File

@ -883,21 +883,6 @@ export const approximateDuration = (seconds = 0) => {
return n__('1 day', '%d days', seconds < ONE_DAY_LIMIT ? 1 : days);
};
/**
* A utility function which helps creating a date object
* for a specific date. Accepts the year, month and day
* returning a date object for the given params.
*
* @param {Int} year the full year as a number i.e. 2020
* @param {Int} month the month index i.e. January => 0
* @param {Int} day the day as a number i.e. 23
*
* @return {Date} the date object from the params
*/
export const dateFromParams = (year, month, day) => {
return new Date(year, month, day);
};
/**
* A utility function which computes the difference in seconds
* between 2 dates.

View File

@ -1,6 +1,8 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
i18n: {
@ -9,7 +11,9 @@ export default {
},
components: {
GlButton,
WebIdeLink,
},
mixins: [glFeatureFlagsMixin()],
props: {
editPath: {
type: String,
@ -24,7 +28,14 @@ export default {
</script>
<template>
<div>
<web-ide-link
v-if="glFeatures.consolidatedEditButton"
class="gl-mr-3"
:edit-url="editPath"
:web-ide-url="webIdePath"
:is-blob="true"
/>
<div v-else>
<gl-button class="gl-mr-2" category="primary" variant="confirm" :href="editPath">
{{ $options.i18n.edit }}
</gl-button>

View File

@ -1,6 +1,7 @@
export * from './api/groups_api';
export * from './api/projects_api';
export * from './api/user_api';
export * from './api/markdown_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests. See https://stackoverflow.com/a/53307822/1063392.

View File

@ -0,0 +1,121 @@
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
const i18n = {
I18N_EDIT: __('Edit'),
I18N_PAUSE: __('Pause'),
I18N_RESUME: __('Resume'),
};
export default {
components: {
GlButton,
GlButtonGroup,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runner: {
type: Object,
required: true,
},
},
data() {
return {
updating: false,
};
},
computed: {
runnerNumericalId() {
return getIdFromGraphQLId(this.runner.id);
},
runnerUrl() {
// TODO implement using webUrl from the API
return `${gon.gitlab_url || ''}/admin/runners/${this.runnerNumericalId}`;
},
isActive() {
return this.runner.active;
},
toggleActiveIcon() {
return this.isActive ? 'pause' : 'play';
},
toggleActiveTitle() {
if (this.updating) {
// Prevent a "sticky" tooltip: If this button is disabled,
// mouseout listeners will not run and the tooltip will
// stay stuck on the button.
return '';
}
return this.isActive ? i18n.I18N_PAUSE : i18n.I18N_RESUME;
},
},
methods: {
async onToggleActive() {
this.updating = true;
// TODO In HAML iteration we had a confirmation modal via:
// data-confirm="_('Are you sure?')"
// this may not have to ported, this is an easily reversible operation
try {
const toggledActive = !this.runner.active;
const {
data: {
runnerUpdate: { errors },
},
} = await this.$apollo.mutate({
mutation: updateRunnerMutation,
variables: {
input: {
id: this.runner.id,
active: toggledActive,
},
},
});
if (errors && errors.length) {
this.onError(new Error(errors[0]));
}
} catch (e) {
this.onError(e);
} finally {
this.updating = false;
}
},
onError(error) {
// TODO Render errors when "delete" action is done
// `active` toggle would not fail due to user input.
throw error;
},
},
i18n,
};
</script>
<template>
<gl-button-group>
<gl-button
v-gl-tooltip.hover.viewport
:title="$options.i18n.I18N_EDIT"
:aria-label="$options.i18n.I18N_EDIT"
icon="pencil"
:href="runnerUrl"
data-testid="edit-runner"
/>
<gl-button
v-gl-tooltip.hover.viewport
:title="toggleActiveTitle"
:aria-label="toggleActiveTitle"
:icon="toggleActiveIcon"
:loading="updating"
data-testid="toggle-active-runner"
@click="onToggleActive"
/>
<!-- TODO add delete action to update runners -->
</gl-button-group>
</template>

View File

@ -3,6 +3,7 @@ import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatNumber, sprintf, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerNameCell from './cells/runner_name_cell.vue';
import RunnerTypeCell from './cells/runner_type_cell.vue';
import RunnerTags from './runner_tags.vue';
@ -32,6 +33,7 @@ export default {
GlTable,
GlSkeletonLoader,
TimeAgo,
RunnerActionsCell,
RunnerNameCell,
RunnerTags,
RunnerTypeCell,
@ -132,8 +134,8 @@ export default {
<template v-else>{{ __('Never') }}</template>
</template>
<template #cell(actions)>
<!-- TODO add actions to update runners -->
<template #cell(actions)="{ item }">
<runner-actions-cell :runner="item" />
</template>
</gl-table>
</div>

View File

@ -1,3 +1,4 @@
#import "~/runner/graphql/runner_node.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getRunners(
@ -19,17 +20,7 @@ query getRunners(
sort: $sort
) {
nodes {
id
description
runnerType
shortSha
version
revision
ipAddress
active
locked
tagList
contactedAt
...RunnerNode
}
pageInfo {
...PageInfo

View File

@ -0,0 +1,13 @@
fragment RunnerNode on CiRunner {
id
description
runnerType
shortSha
version
revision
ipAddress
active
locked
tagList
contactedAt
}

View File

@ -0,0 +1,10 @@
#import "~/runner/graphql/runner_node.fragment.graphql"
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
runner {
...RunnerNode
}
errors
}
}

View File

@ -1,5 +1,6 @@
<script>
import * as Sentry from '@sentry/browser';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@ -43,6 +44,10 @@ export default {
apollo: {
runners: {
query: getRunnersQuery,
// Runners can be updated by users directly in this list.
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() {
return this.variables;
},

View File

@ -29,6 +29,7 @@ export const fetchProjects = ({ commit, state }, search) => {
};
if (groupId) {
// TODO (https://gitlab.com/gitlab-org/gitlab/-/issues/323331): For errors `createFlash` is called twice; in `callback` and in `Api.groupProjects`
Api.groupProjects(groupId, search, {}, callback);
} else {
// The .catch() is due to the API method not handling a rejection properly

View File

@ -22,7 +22,13 @@ import { __ } from '~/locale';
import SmartInterval from '~/smart_interval';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MergeRequest from '../../../merge_request';
import { AUTO_MERGE_STRATEGIES, DANGER, CONFIRM, WARNING } from '../../constants';
import {
AUTO_MERGE_STRATEGIES,
DANGER,
CONFIRM,
WARNING,
MT_MERGE_STRATEGY,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MergeRequestStore from '../../stores/mr_widget_store';
@ -223,7 +229,7 @@ export default {
return PIPELINE_SUCCESS_STATE;
},
mergeButtonVariant() {
if (this.status === PIPELINE_FAILED_STATE) {
if (this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed) {
return DANGER;
}
@ -286,6 +292,9 @@ export default {
shaMismatchLink() {
return this.mr.mergeRequestDiffsPath;
},
showDangerMessageForMergeTrain() {
return this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY && this.isPipelineFailed;
},
},
mounted() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
@ -499,7 +508,7 @@ export default {
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
variant="info"
:variant="mergeButtonVariant"
data-qa-selector="merge_moment_dropdown"
toggle-class="btn-icon js-merge-moment"
>
@ -579,6 +588,14 @@ export default {
</gl-sprintf>
</span>
</div>
<div
v-if="showDangerMessageForMergeTrain"
class="gl-mt-5 gl-text-gray-500"
data-testid="failed-pipeline-merge-train-text"
>
{{ __('The latest pipeline for this merge request did not complete successfully.') }}
</div>
</div>
</div>
<merge-train-helper-text

View File

@ -39,6 +39,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml)
end
def new

View File

@ -30,8 +30,6 @@ class ProtectedBranch < ApplicationRecord
end
def self.allow_force_push?(project, ref_name)
return false unless ::Feature.enabled?(:allow_force_push_to_protected_branches, project, default_enabled: :yaml)
project.protected_branches.allowing_force_push.matching(ref_name).any?
end

View File

@ -22,12 +22,10 @@
= s_("ProtectedBranch|Allowed to merge")
%th
= s_("ProtectedBranch|Allowed to push")
- if ::Feature.enabled?(:allow_force_push_to_protected_branches, @project, default_enabled: :yaml)
%th
= s_("ProtectedBranch|Allow force push")
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow force push for all users with push access.'), 'aria-hidden': 'true' }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-500')
%th
= s_("ProtectedBranch|Allow force push")
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow force push for all users with push access.'), 'aria-hidden': 'true' }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-500')
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_table_head'

View File

@ -21,13 +21,12 @@
= f.label :push_access_levels_attributes, s_("ProtectedBranch|Allowed to push:"), class: 'col-md-2 text-left text-md-right'
.col-md-10
= yield :push_access_levels
- if ::Feature.enabled?(:allow_force_push_to_protected_branches, @project, default_enabled: :yaml)
.form-group.row
= f.label :allow_force_push, s_("ProtectedBranch|Allow force push:"), class: 'col-md-2 gl-text-left text-md-right'
.col-md-10
= render "shared/buttons/project_feature_toggle", class_list: "js-force-push-toggle project-feature-toggle"
.form-text.gl-text-gray-600.gl-mt-0
= s_("ProtectedBranch|Allow force push for all users with push access.")
.form-group.row
= f.label :allow_force_push, s_("ProtectedBranch|Allow force push:"), class: 'col-md-2 gl-text-left text-md-right'
.col-md-10
= render "shared/buttons/project_feature_toggle", class_list: "js-force-push-toggle project-feature-toggle"
.form-text.gl-text-gray-600.gl-mt-0
= s_("ProtectedBranch|Allow force push for all users with push access.")
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f
.card-footer
= f.submit s_('ProtectedBranch|Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_button' }

View File

@ -33,6 +33,5 @@
%p.small
= _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence }
- if ::Feature.enabled?(:allow_force_push_to_protected_branches, @project, default_enabled: :yaml)
%td
= render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allow force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name }
%td
= render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allow force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name }

View File

@ -1,8 +0,0 @@
---
name: allow_force_push_to_protected_branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55261
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323431
milestone: '13.10'
type: development
group: group::source code
default_enabled: true

View File

@ -1,7 +1,7 @@
---
name: combined_menu
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56249
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321904
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324086
milestone: '13.10'
type: development
group: group::editor

View File

@ -28,6 +28,7 @@ GET /projects/:id/repository/commits
| `with_stats` | boolean | no | Stats about each commit are added to the response |
| `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit |
| `order` | string | no | List commits in order. Possible values: `default`, [`topo`](https://git-scm.com/docs/git-log#Documentation/git-log.txt---topo-order). Defaults to `default`, the commits are shown in reverse chronological order. |
| `trailers` | boolean | no | Parse and include [Git trailers](https://git-scm.com/docs/git-interpret-trailers) for every commit |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits"

View File

@ -84,4 +84,4 @@ processes.
For time periods in which no merge requests were deployed, the charts render a
red, dashed line.
These charts are only available for projects.
These charts are available for both groups and projects.

View File

@ -179,9 +179,7 @@ command line or a Git client application.
## Allow force push on protected branches
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15611) in GitLab 13.10 behind a disabled feature flag.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-allow-force-push-on-protected-branches).
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/323431) in GitLab 14.0.
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
@ -249,25 +247,6 @@ run CI/CD pipelines and execute actions on jobs that are related to those branch
See [Security on protected branches](../../ci/pipelines/index.md#pipeline-security-on-protected-branches)
for details about the pipelines security model.
## Enable or disable allow force push on protected branches **(FREE SELF)**
Allow force push on protected branches is ready for
production use. It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:allow_force_push_to_protected_branches)
```
To disable it:
```ruby
Feature.disable(:allow_force_push_to_protected_branches)
```
## Changelog
- **13.5**: [Allow Deploy keys to push to protected branches once more](https://gitlab.com/gitlab-org/gitlab/-/issues/30769).

View File

@ -1,5 +1,4 @@
# frozen_string_literal: true
require 'mime/types'
module API
@ -41,6 +40,7 @@ module API
optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges'
optional :order, type: String, desc: 'List commits in order', default: 'default', values: %w[default topo]
optional :trailers, type: Boolean, desc: 'Parse and include Git trailers for every commit', default: false
use :pagination
end
get ':id/repository/commits' do
@ -62,7 +62,8 @@ module API
after: after,
all: all,
first_parent: first_parent,
order: order)
order: order,
trailers: params[:trailers])
serializer = with_stats ? Entities::CommitWithStats : Entities::Commit

View File

@ -9,6 +9,7 @@ module API
expose :safe_message, as: :message
expose :author_name, :author_email, :authored_date
expose :committer_name, :committer_email, :committed_date
expose :trailers
expose :web_url do |commit, _options|
Gitlab::UrlBuilder.build(commit)

View File

@ -24,26 +24,10 @@ module Gitlab::Ci
@key_width = badge.customization.dig(:key_width)
end
def key_text
if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
@key_text
else
@entity.to_s
end
end
def value_text
@status ? ("%.2f%%" % @status) : 'unknown'
end
def key_width
if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
@key_width
else
62
end
end
def value_width
@status ? 54 : 58
end

View File

@ -28,26 +28,10 @@ module Gitlab::Ci
@key_width = badge.customization.dig(:key_width)
end
def key_text
if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
@key_text
else
@entity.to_s
end
end
def value_text
STATUS_RENAME[@status.to_s] || @status.to_s
end
def key_width
if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
@key_width
else
62
end
end
def value_width
54
end

View File

@ -8,6 +8,7 @@ module Gitlab::Ci
class Template
MAX_KEY_TEXT_SIZE = 64
MAX_KEY_WIDTH = 512
DEFAULT_KEY_WIDTH = 62
def initialize(badge)
@entity = badge.entity
@ -15,7 +16,11 @@ module Gitlab::Ci
end
def key_text
raise NotImplementedError
if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
@key_text
else
@entity.to_s
end
end
def value_text
@ -23,7 +28,11 @@ module Gitlab::Ci
end
def key_width
raise NotImplementedError
if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
@key_width
else
DEFAULT_KEY_WIDTH
end
end
def value_width

View File

@ -32,10 +32,13 @@ module Gitlab
return true unless location
job_data_consistency = worker_class.get_data_consistency
job[:data_consistency] = job_data_consistency.to_s
if replica_caught_up?(location)
job[:database_chosen] = 'replica'
false
elsif worker_class.get_data_consistency == :delayed && not_yet_retried?(job)
elsif job_data_consistency == :delayed && not_yet_retried?(job)
job[:database_chosen] = 'retry'
raise JobReplicaNotUpToDate, "Sidekiq job #{worker_class} JID-#{job['jid']} couldn't use the replica."\
" Replica was not up to date."

View File

@ -120,7 +120,7 @@ module Gitlab
raise "storage #{storage.inspect} is missing a gitaly_address"
end
unless %w(tcp unix tls).include?(URI(address).scheme)
unless URI(address).scheme.in?(%w(tcp unix tls))
raise "Unsupported Gitaly address: #{address.inspect} does not use URL scheme 'tcp' or 'unix' or 'tls'"
end

View File

@ -52,7 +52,7 @@ module Gitlab
@legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
storage['path'] = Deprecated
@hash = ActiveSupport::HashWithIndifferentAccess.new(storage)
@hash = storage.with_indifferent_access
end
def gitaly_address

View File

@ -3,7 +3,7 @@
namespace :gitlab do
namespace :gitaly do
desc 'GitLab | Gitaly | Install or upgrade gitaly'
task :install, [:dir, :storage_path, :repo] => :with_gitlab_helpers do |t, args|
task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
unless args.dir.present? && args.storage_path.present?

View File

@ -6,7 +6,3 @@ StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
task gitlab_environment: :environment do
extend SystemCheck::Helpers
end
task :with_gitlab_helpers do
extend SystemCheck::Helpers
end

View File

@ -5720,6 +5720,9 @@ msgstr ""
msgid "CICDAnalytics|Deployment frequency"
msgstr ""
msgid "CICDAnalytics|Lead time"
msgstr ""
msgid "CICDAnalytics|Projects with releases"
msgstr ""
@ -32625,6 +32628,9 @@ msgstr ""
msgid "The latest artifacts created by jobs in the most recent successful pipeline will be stored."
msgstr ""
msgid "The latest pipeline for this merge request did not complete successfully."
msgstr ""
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
msgstr ""

View File

@ -207,7 +207,7 @@
"commander": "^2.18.0",
"custom-jquery-matchers": "^2.1.0",
"docdash": "^1.0.2",
"eslint": "7.27.0",
"eslint": "7.28.0",
"eslint-import-resolver-jest": "3.0.0",
"eslint-import-resolver-webpack": "0.13.1",
"eslint-plugin-jasmine": "4.1.2",

View File

@ -1,66 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'request_store'
require 'rake'
require 'active_support/dependencies'
require 'active_support/dependencies/autoload'
require 'active_support/core_ext/numeric'
require 'active_support/string_inquirer'
module Rails
extend self
def root
Pathname.new(File.expand_path('..', __dir__))
end
def env
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
end
end
ActiveSupport::Dependencies.autoload_paths << 'lib'
load File.expand_path('../lib/tasks/gitlab/helpers.rake', __dir__)
load File.expand_path('../lib/tasks/gitlab/gitaly.rake', __dir__)
# Required for config/0_inject_enterprise_edition_module.rb, lib/gitlab/access.rb
require_dependency File.expand_path('../lib/gitlab', __dir__)
require_dependency File.expand_path('../config/initializers/0_inject_enterprise_edition_module', __dir__)
# Require for lib/gitlab/gitaly_client/storage_settings.rb and config/initializers/1_settings.rb
require 'active_support/hash_with_indifferent_access'
# Required for lib/gitlab/visibility_level.rb and lib/gitlab/safe_request_store.rb
require 'active_support/concern'
require 'active_support/core_ext/module/delegation'
# Required for lib/system_check/helpers.rb
require_dependency File.expand_path('../lib/gitlab/task_helpers', __dir__)
# Required for lib/tasks/gitlab/helpers.rake
require_dependency File.expand_path('../lib/system_check/helpers', __dir__)
# Required for config/initializers/1_settings.rb
require 'omniauth'
require 'omniauth-github'
require 'etc'
require_dependency File.expand_path('../lib/gitlab/access', __dir__)
require_dependency File.expand_path('../config/initializers/1_settings', __dir__)
Gitlab.ee do
load File.expand_path('../ee/lib/tasks/gitlab/indexer.rake', __dir__)
require_dependency File.expand_path('../ee/lib/gitlab/elastic/indexer', __dir__)
require_dependency File.expand_path('../lib/gitlab/utils/override', __dir__)
end
require_dependency File.expand_path('../spec/support/helpers/test_env', __dir__)
TestEnv.init

View File

@ -5,7 +5,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import ProjectSelect from '~/boards/components/project_select_deprecated.vue';
import { ListType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { deprecatedCreateFlash as flash } from '~/flash';
import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
@ -237,8 +237,10 @@ describe('ProjectSelect component', () => {
await searchForProject('foobar');
expect(flash).toHaveBeenCalledTimes(1);
expect(flash).toHaveBeenCalledWith('Something went wrong while fetching projects');
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while fetching projects',
});
});
describe('with non-empty search result', () => {

View File

@ -2,6 +2,8 @@
exports[`Value stream analytics component isEmptyStage = true renders the empty stage with \`Not enough data\` message 1`] = `"<gl-empty-state-stub title=\\"We don't have enough data to show this stage.\\" svgpath=\\"path/to/no/data\\" description=\\"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
exports[`Value stream analytics component isEmptyStage = true with a selectedStageError renders the empty stage with \`There is too much data to calculate\` message 1`] = `"<gl-empty-state-stub title=\\"There is too much data to calculate\\" svgpath=\\"path/to/no/data\\" description=\\"\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
exports[`Value stream analytics component isLoading = true renders the path navigation component with prop \`loading\` set to true 1`] = `"<path-navigation-stub loading=\\"true\\" stages=\\"\\" selectedstage=\\"[object Object]\\" class=\\"js-path-navigation gl-w-full gl-pb-2\\"></path-navigation-stub>"`;
exports[`Value stream analytics component without enough permissions renders the empty stage with \`You need permission\` message 1`] = `"<gl-empty-state-stub title=\\"You need permission.\\" svgpath=\\"path/to/no/access\\" description=\\"Want to see the data? Please ask an administrator for access.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;

View File

@ -55,6 +55,7 @@ describe('Value stream analytics component', () => {
isEmptyStage: false,
selectedStageEvents,
selectedStage,
selectedStageError: '',
},
});
});
@ -133,6 +134,22 @@ describe('Value stream analytics component', () => {
it('renders the empty stage with `Not enough data` message', () => {
expect(findEmptyStage().html()).toMatchSnapshot();
});
describe('with a selectedStageError', () => {
beforeEach(() => {
wrapper = createComponent({
initialState: {
selectedStage,
isEmptyStage: true,
selectedStageError: 'There is too much data to calculate',
},
});
});
it('renders the empty stage with `There is too much data to calculate` message', () => {
expect(findEmptyStage().html()).toMatchSnapshot();
});
});
});
describe('without enough permissions', () => {

View File

@ -106,6 +106,32 @@ describe('Project Value Stream Analytics actions', () => {
expectedActions: [],
}));
describe('with a successful request, but an error in the payload', () => {
const tooMuchDataError = 'Too much data';
beforeEach(() => {
state = {
requestPath: mockRequestPath,
startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
mock.onGet(mockStagePath).reply(httpStatusCodes.OK, { error: tooMuchDataError });
});
it(`commits the 'RECEIVE_STAGE_DATA_ERROR' mutation`, () =>
testAction({
action: actions.fetchStageData,
state,
payload: { error: tooMuchDataError },
expectedMutations: [
{ type: 'REQUEST_STAGE_DATA' },
{ type: 'RECEIVE_STAGE_DATA_ERROR', payload: tooMuchDataError },
],
expectedActions: [],
}));
});
describe('with a failing request', () => {
beforeEach(() => {
state = {

View File

@ -889,17 +889,6 @@ describe('localTimeAgo', () => {
});
});
describe('dateFromParams', () => {
it('returns the expected date object', () => {
const expectedDate = new Date('2019-07-17T00:00:00.000Z');
const date = datetimeUtility.dateFromParams(2019, 6, 17);
expect(date.getYear()).toBe(expectedDate.getYear());
expect(date.getMonth()).toBe(expectedDate.getMonth());
expect(date.getDate()).toBe(expectedDate.getDate());
});
});
describe('differenceInSeconds', () => {
const startDateTime = new Date('2019-07-17T00:00:00.000Z');

View File

@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BlobHeaderEdit from '~/repository/components/blob_header_edit.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
const DEFAULT_PROPS = {
editPath: 'some_file.js/edit',
@ -10,12 +11,17 @@ const DEFAULT_PROPS = {
describe('BlobHeaderEdit component', () => {
let wrapper;
const createComponent = (props = {}) => {
const createComponent = (consolidatedEditButton = false, props = {}) => {
wrapper = shallowMount(BlobHeaderEdit, {
propsData: {
...DEFAULT_PROPS,
...props,
},
provide: {
glFeatures: {
consolidatedEditButton,
},
},
});
};
@ -27,6 +33,7 @@ describe('BlobHeaderEdit component', () => {
const findButtons = () => wrapper.findAll(GlButton);
const findEditButton = () => findButtons().at(0);
const findWebIdeButton = () => findButtons().at(1);
const findWebIdeLink = () => wrapper.find(WebIdeLink);
it('renders component', () => {
createComponent();
@ -60,4 +67,16 @@ describe('BlobHeaderEdit component', () => {
expect(findWebIdeButton().text()).toBe('Web IDE');
expect(findWebIdeButton()).not.toBeDisabled();
});
it('renders WebIdeLink component', () => {
createComponent(true);
const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS;
expect(findWebIdeLink().props()).toMatchObject({
editUrl,
webIdeUrl,
isBlob: true,
});
});
});

View File

@ -0,0 +1,109 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
import updateRunnerMutation from '~/runner/graphql/update_runner.mutation.graphql';
const mockId = '1';
describe('RunnerTypeCell', () => {
let wrapper;
let mutate;
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
const createComponent = ({ active = true } = {}, options) => {
wrapper = extendedWrapper(
shallowMount(RunnerActionCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
active,
},
},
mocks: {
$apollo: {
mutate,
},
},
...options,
}),
);
};
beforeEach(() => {
mutate = jest.fn();
});
afterEach(() => {
mutate.mockReset();
wrapper.destroy();
});
it('Displays the runner edit link with the correct href', () => {
createComponent();
expect(findEditBtn().attributes('href')).toBe('/admin/runners/1');
});
describe.each`
state | label | icon | isActive | newActiveValue
${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false}
${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
`('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
beforeEach(() => {
mutate.mockResolvedValue({
data: {
runnerUpdate: {
runner: {
id: `gid://gitlab/Ci::Runner/1`,
__typename: 'CiRunner',
},
},
},
});
createComponent({ active: isActive });
});
it(`Displays a ${icon} button`, () => {
expect(findToggleActiveBtn().props('loading')).toBe(false);
expect(findToggleActiveBtn().props('icon')).toBe(icon);
expect(findToggleActiveBtn().attributes('title')).toBe(label);
expect(findToggleActiveBtn().attributes('aria-label')).toBe(label);
});
it(`After clicking the ${icon} button, the button has a loading state`, async () => {
await findToggleActiveBtn().vm.$emit('click');
expect(findToggleActiveBtn().props('loading')).toBe(true);
expect(findToggleActiveBtn().attributes('title')).toBe('');
expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
});
describe(`When clicking on the ${icon} button`, () => {
beforeEach(async () => {
await findToggleActiveBtn().vm.$emit('click');
await waitForPromises();
});
it(`The apollo mutation to set active to ${newActiveValue} is called`, () => {
expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith({
mutation: updateRunnerMutation,
variables: {
input: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
active: newActiveValue,
},
},
});
});
it('The button does not have a loading state', () => {
expect(findToggleActiveBtn().props('loading')).toBe(false);
});
});
});
});

View File

@ -17,7 +17,7 @@ describe('RunnerList', () => {
const findHeaders = () => wrapper.findAll('th');
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
const findCell = ({ row = 0, fieldKey }) =>
findRows().at(row).find(`[data-testid="td-${fieldKey}"]`);
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
@ -93,7 +93,12 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
expect(findCell({ fieldKey: 'actions' }).text()).toBe('');
// Actions
const actions = findCell({ fieldKey: 'actions' });
expect(actions.findByTestId('edit-runner').exists()).toBe(true);
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
it('Links to the runner page', () => {

View File

@ -20,9 +20,8 @@ describe('Global Search Store Actions', () => {
let mock;
let state;
const noCallback = () => {};
const flashCallback = () => {
expect(createFlash).toHaveBeenCalledTimes(1);
const flashCallback = (callCount) => {
expect(createFlash).toHaveBeenCalledTimes(callCount);
createFlash.mockClear();
};
@ -37,19 +36,21 @@ describe('Global Search Store Actions', () => {
});
describe.each`
action | axiosMock | type | expectedMutations | callback
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${noCallback}
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${noCallback}
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, callback }) => {
action | axiosMock | type | expectedMutations | flashCallCount
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${2}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => callback());
return testAction({ action, state, expectedMutations }).then(() =>
flashCallback(flashCallCount),
);
});
});
});

View File

@ -6,31 +6,7 @@ RSpec.describe Gitlab::Ci::Badge::Coverage::Template do
let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) }
let(:template) { described_class.new(badge) }
describe '#key_text' do
it 'says coverage by default' do
expect(template.key_text).to eq 'coverage'
end
context 'when custom key_text is defined' do
before do
allow(badge).to receive(:customization).and_return({ key_text: "custom text" })
end
it 'returns custom value' do
expect(template.key_text).to eq "custom text"
end
context 'when its size is larger than the max allowed value' do
before do
allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 })
end
it 'returns default value' do
expect(template.key_text).to eq 'coverage'
end
end
end
end
it_behaves_like 'a badge template', 'coverage'
describe '#value_text' do
context 'when coverage is known' do
@ -60,32 +36,6 @@ RSpec.describe Gitlab::Ci::Badge::Coverage::Template do
end
end
describe '#key_width' do
it 'is fixed by default' do
expect(template.key_width).to eq 62
end
context 'when custom key_width is defined' do
before do
allow(badge).to receive(:customization).and_return({ key_width: 101 })
end
it 'returns custom value' do
expect(template.key_width).to eq 101
end
context 'when it is larger than the max allowed value' do
before do
allow(badge).to receive(:customization).and_return({ key_width: 513 })
end
it 'returns default value' do
expect(template.key_width).to eq 62
end
end
end
end
describe '#value_width' do
context 'when coverage is known' do
it 'is narrower when coverage is known' do

View File

@ -6,31 +6,7 @@ RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do
let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) }
let(:template) { described_class.new(badge) }
describe '#key_text' do
it 'says pipeline by default' do
expect(template.key_text).to eq 'pipeline'
end
context 'when custom key_text is defined' do
before do
allow(badge).to receive(:customization).and_return({ key_text: 'custom text' })
end
it 'returns custom value' do
expect(template.key_text).to eq 'custom text'
end
context 'when its size is larger than the max allowed value' do
before do
allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 })
end
it 'returns default value' do
expect(template.key_text).to eq 'pipeline'
end
end
end
end
it_behaves_like 'a badge template', 'pipeline'
describe '#value_text' do
it 'is status value' do
@ -38,32 +14,6 @@ RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do
end
end
describe '#key_width' do
it 'is fixed by default' do
expect(template.key_width).to eq 62
end
context 'when custom key_width is defined' do
before do
allow(badge).to receive(:customization).and_return({ key_width: 101 })
end
it 'returns custom value' do
expect(template.key_width).to eq 101
end
context 'when it is larger than the max allowed value' do
before do
allow(badge).to receive(:customization).and_return({ key_width: 513 })
end
it 'returns default value' do
expect(template.key_width).to eq 62
end
end
end
end
describe 'widths and text anchors' do
it 'has fixed width and text anchors' do
expect(template.width).to eq 116

View File

@ -31,14 +31,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
end
end
shared_examples_for 'job marked with chosen database' do
it 'yields and sets database chosen', :aggregate_failures do
expect { |b| middleware.call(worker, job, double(:queue), &b) }.to yield_control
expect(job[:database_chosen]).to eq('primary')
end
end
shared_examples_for 'stick to the primary' do
it 'sticks to the primary' do
middleware.call(worker, job, double(:queue)) do
@ -47,8 +39,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
end
end
shared_examples_for 'replica is up to date' do |location|
it 'do not stick to the primary', :aggregate_failures do
shared_examples_for 'replica is up to date' do |location, data_consistency|
it 'does not stick to the primary', :aggregate_failures do
expect(middleware).to receive(:replica_caught_up?).with(location).and_return(true)
middleware.call(worker, job, double(:queue)) do
@ -57,6 +49,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
expect(job[:database_chosen]).to eq('replica')
end
it "updates job hash with data_consistency :#{data_consistency}" do
middleware.call(worker, job, double(:queue)) do
expect(job).to include(data_consistency: data_consistency.to_s)
end
end
end
shared_examples_for 'sticks based on data consistency' do |data_consistency|
@ -77,7 +75,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
allow(middleware).to receive(:replica_caught_up?).and_return(true)
end
it_behaves_like 'replica is up to date', '0/D525E3A8'
it_behaves_like 'replica is up to date', '0/D525E3A8', data_consistency
end
context 'when database primary location is set' do
@ -87,7 +85,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
allow(middleware).to receive(:replica_caught_up?).and_return(true)
end
it_behaves_like 'replica is up to date', '0/D525E3A8'
it_behaves_like 'replica is up to date', '0/D525E3A8', data_consistency
end
context 'when database location is not set' do
@ -171,7 +169,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
end
include_examples 'stick to the primary'
include_examples 'job marked with chosen database'
it 'updates job hash with primary database chosen', :aggregate_failures do
expect { |b| middleware.call(worker, job, double(:queue), &b) }.to yield_control
expect(job[:database_chosen]).to eq('primary')
end
end
end
end

View File

@ -284,6 +284,18 @@ RSpec.describe API::Commits do
end
end
end
context 'with the optional trailers parameter' do
it 'includes the Git trailers' do
get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user)
commit = json_response[0]
expect(commit['trailers']).to eq(
'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
)
end
end
end
end

View File

@ -230,10 +230,6 @@ RSpec.configure do |config|
Gitlab::Database.set_open_transactions_baseline
end
config.append_before do
Thread.current[:current_example_group] = ::RSpec.current_example.metadata[:example_group]
end
config.append_after do
Gitlab::Database.reset_open_transactions_baseline
end

View File

@ -1,8 +1,7 @@
# frozen_string_literal: true
require 'parallel'
module TestEnv
extend ActiveSupport::Concern
extend self
ComponentFailedToInstallError = Class.new(StandardError)
@ -95,40 +94,50 @@ module TestEnv
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
REPOS_STORAGE = 'default'
SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage')
SETUP_METHODS = %i[setup_gitaly setup_gitlab_shell setup_workhorse setup_factory_repo setup_forked_repo].freeze
# Can be overriden
def setup_methods
SETUP_METHODS
end
# Test environment
#
# See gitlab.yml.example test section for paths
#
def init
def init(opts = {})
unless Rails.env.test?
puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
exit 1
end
start = Time.now
# Disable mailer for spinach tests
disable_mailer if opts[:mailer] == false
clean_test_path
# Install components in parallel as most of the setup is I/O.
Parallel.each(setup_methods) do |method|
public_send(method)
end
setup_gitlab_shell
post_init
setup_gitaly
puts "\nTest environment set up in #{Time.now - start} seconds"
# Feature specs are run through Workhorse
setup_workhorse
# Create repository for FactoryBot.create(:project)
setup_factory_repo
# Create repository for FactoryBot.create(:forked_project_with_submodules)
setup_forked_repo
end
# Can be overriden
def post_init
start_gitaly(gitaly_dir)
included do |config|
config.append_before do
set_current_example_group
end
end
def disable_mailer
allow_any_instance_of(NotificationService).to receive(:mailer)
.and_return(double.as_null_object)
end
def enable_mailer
allow_any_instance_of(NotificationService).to receive(:mailer)
.and_call_original
end
# Clean /tmp/tests
@ -155,11 +164,12 @@ module TestEnv
end
def setup_gitaly
install_gitaly_args = [gitaly_dir, repos_path, gitaly_url].compact.join(',')
component_timed_setup('Gitaly',
install_dir: gitaly_dir,
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install",
task_args: [gitaly_dir, repos_path, gitaly_url].compact) do
task: "gitlab:gitaly:install[#{install_gitaly_args}]") do
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path },
@ -180,6 +190,8 @@ module TestEnv
)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
start_gitaly(gitaly_dir)
end
def gitaly_socket_path
@ -261,18 +273,19 @@ module TestEnv
raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds"
end
# Feature specs are run through Workhorse
def setup_workhorse
start = Time.now
return if skip_compile_workhorse?
puts "\n==> Setting up GitLab Workhorse..."
FileUtils.rm_rf(workhorse_dir)
Gitlab::SetupHelper::Workhorse.compile_into(workhorse_dir)
Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil)
File.write(workhorse_tree_file, workhorse_tree) if workhorse_source_clean?
puts "==> GitLab Workhorse set up in #{Time.now - start} seconds...\n"
puts " GitLab Workhorse set up in #{Time.now - start} seconds...\n"
end
def skip_compile_workhorse?
@ -336,12 +349,10 @@ module TestEnv
ENV.fetch('GITLAB_WORKHORSE_URL', nil)
end
# Create repository for FactoryBot.create(:project)
def setup_factory_repo
setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA)
end
# Create repository for FactoryBot.create(:forked_project_with_submodules)
# This repo has a submodule commit that is not present in the main test
# repository.
def setup_forked_repo
@ -352,18 +363,20 @@ module TestEnv
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
puts "\n==> Setting up #{repo_name} repository in #{repo_path}..."
start = Time.now
system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n"
puts " #{repo_path} set up in #{Time.now - start} seconds...\n"
end
set_repo_refs(repo_path, refs)
unless File.directory?(repo_path_bare)
puts "\n==> Setting up #{repo_name} bare repository in #{repo_path_bare}..."
start = Time.now
# We must copy bare repositories because we will push to them.
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --quiet --bare -- #{repo_path} #{repo_path_bare}))
puts "==> #{repo_path_bare} set up in #{Time.now - start} seconds...\n"
puts " #{repo_path_bare} set up in #{Time.now - start} seconds...\n"
end
end
@ -455,6 +468,10 @@ module TestEnv
private
def set_current_example_group
Thread.current[:current_example_group] = ::RSpec.current_example.metadata[:example_group]
end
# These are directories that should be preserved at cleanup time
def test_dirs
@test_dirs ||= %w[
@ -509,7 +526,7 @@ module TestEnv
end
end
def component_timed_setup(component, install_dir:, version:, task:, task_args: [])
def component_timed_setup(component, install_dir:, version:, task:)
start = Time.now
ensure_component_dir_name_is_correct!(component, install_dir)
@ -518,16 +535,17 @@ module TestEnv
return if File.exist?(install_dir) && ci?
if component_needs_update?(install_dir, version)
puts "\n==> Setting up #{component}..."
# Cleanup the component entirely to ensure we start fresh
FileUtils.rm_rf(install_dir)
unless Rake::Task[task].invoke(*task_args)
unless system('rake', task)
raise ComponentFailedToInstallError
end
yield if block_given?
puts "==> #{component} set up in #{Time.now - start} seconds...\n"
puts " #{component} set up in #{Time.now - start} seconds...\n"
end
rescue ComponentFailedToInstallError
puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
RSpec.shared_examples 'a badge template' do |badge_type|
describe '#key_text' do
it "says #{badge_type} by default" do
expect(template.key_text).to eq(badge_type)
end
context 'when custom key_text is defined' do
before do
allow(badge).to receive(:customization).and_return({ key_text: "custom text" })
end
it 'returns custom value' do
expect(template.key_text).to eq("custom text")
end
context 'when its size is larger than the max allowed value' do
before do
allow(badge).to receive(:customization).and_return({ key_text: 't' * (::Gitlab::Ci::Badge::Template::MAX_KEY_TEXT_SIZE + 1) } )
end
it 'returns default value' do
expect(template.key_text).to eq(badge_type)
end
end
end
end
describe '#key_width' do
let_it_be(:default_key_width) { ::Gitlab::Ci::Badge::Template::DEFAULT_KEY_WIDTH }
it 'is fixed by default' do
expect(template.key_width).to eq(default_key_width)
end
context 'when custom key_width is defined' do
before do
allow(badge).to receive(:customization).and_return({ key_width: 101 })
end
it 'returns custom value' do
expect(template.key_width).to eq(101)
end
context 'when it is larger than the max allowed value' do
before do
allow(badge).to receive(:customization).and_return({ key_width: ::Gitlab::Ci::Badge::Template::MAX_KEY_WIDTH + 1 })
end
it 'returns default value' do
expect(template.key_width).to eq(default_key_width)
end
end
end
end
end

View File

@ -847,15 +847,15 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@eslint/eslintrc@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.1.tgz#442763b88cecbe3ee0ec7ca6d6dd6168550cbf14"
integrity sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==
"@eslint/eslintrc@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.2.tgz#f63d0ef06f5c0c57d76c4ab5f63d3835c51b0179"
integrity sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
espree "^7.3.0"
globals "^12.1.0"
globals "^13.9.0"
ignore "^4.0.6"
import-fresh "^3.2.1"
js-yaml "^3.13.1"
@ -5024,13 +5024,13 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
eslint@7.27.0:
version "7.27.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.27.0.tgz#665a1506d8f95655c9274d84bd78f7166b07e9c7"
integrity sha512-JZuR6La2ZF0UD384lcbnd0Cgg6QJjiCwhMD6eU4h/VGPcVGwawNNzKU41tgokGXnfjOOyI6QIffthhJTPzzuRA==
eslint@7.28.0:
version "7.28.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.28.0.tgz#435aa17a0b82c13bb2be9d51408b617e49c1e820"
integrity sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.1"
"@eslint/eslintrc" "^0.4.2"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@ -5047,7 +5047,7 @@ eslint@7.27.0:
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
glob-parent "^5.1.2"
globals "^13.6.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
@ -5732,7 +5732,7 @@ gettext-extractor@^3.5.3:
pofile "1.0.x"
typescript "2 - 4"
glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0:
glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@ -5812,17 +5812,10 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^12.1.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13"
integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==
dependencies:
type-fest "^0.8.1"
globals@^13.6.0:
version "13.8.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3"
integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==
globals@^13.6.0, globals@^13.9.0:
version "13.9.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb"
integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==
dependencies:
type-fest "^0.20.2"