Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-15 15:07:42 +00:00
parent f26f31d2fd
commit fa7c331b76
100 changed files with 1167 additions and 255 deletions

View File

@ -104,15 +104,20 @@ workflow:
# If `$GITLAB_INTERNAL` isn't set, don't create a pipeline.
- if: '$GITLAB_INTERNAL == null'
when: never
# For last 3 stable branches, create a pipeline with failure notifications.
- if: '$CI_COMMIT_BRANCH =~ /^15-[6|7|8]-stable(-ee)?$/'
variables:
<<: *ruby2-variables
NOTIFY_PIPELINE_FAILURE_CHANNEL: "releases"
CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
BROKEN_BRANCH_INCIDENTS_PROJECT: "gitlab-org/release/tasks"
BROKEN_BRANCH_INCIDENTS_PROJECT_TOKEN: "${BROKEN_STABLE_INCIDENTS_PROJECT_TOKEN}"
PIPELINE_NAME: 'Ruby 2 $CI_COMMIT_BRANCH branch pipeline'
# For stable, auto-deploy, and security branches, create a pipeline.
- if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable(-ee)?$/'
variables:
<<: *ruby2-variables
NOTIFY_PIPELINE_FAILURE_CHANNEL: "releases"
PIPELINE_NAME: 'Ruby 2 $CI_COMMIT_BRANCH branch pipeline'
CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
BROKEN_BRANCH_INCIDENTS_PROJECT: "gitlab-org/release/tasks"
BROKEN_BRANCH_INCIDENTS_PROJECT_TOKEN: "${BROKEN_STABLE_INCIDENTS_PROJECT_TOKEN}"
- if: '$CI_COMMIT_BRANCH =~ /^\d+-\d+-auto-deploy-\d+$/'
variables:
<<: *ruby2-variables

View File

@ -1 +1 @@
994a537cefce6044125fa420cd7536a6efda0618
a41c0909051714c0d6b58f8000589f82a66804a5

View File

@ -6,6 +6,7 @@ export const STATUS_REOPENED = 'reopened';
export const TITLE_LENGTH_MAX = 255;
export const TYPE_EPIC = 'epic';
export const TYPE_ISSUE = 'issue';
export const IssuableStatusText = {

View File

@ -4,8 +4,8 @@ import Visibility from 'visibilityjs';
import { createAlert } from '~/flash';
import {
IssuableStatusText,
IssuableType,
STATUS_CLOSED,
TYPE_EPIC,
TYPE_ISSUE,
WorkspaceType,
} from '~/issues/constants';
@ -277,7 +277,7 @@ export default {
return IssuableStatusText[this.issuableStatus];
},
shouldShowStickyHeader() {
return [TYPE_ISSUE, IssuableType.Epic].includes(this.issuableType);
return [TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType);
},
},
created() {

View File

@ -1,6 +1,6 @@
<script>
import { mapActions } from 'vuex';
import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { fetchPolicies } from '~/lib/graphql';
import { confidentialityQueries } from '~/sidebar/constants';
import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
@ -28,7 +28,7 @@ export default {
},
},
created() {
if (this.issuableType !== TYPE_ISSUE && this.issuableType !== IssuableType.Epic) {
if (this.issuableType !== TYPE_ISSUE && this.issuableType !== TYPE_EPIC) {
return;
}

View File

@ -171,11 +171,12 @@ export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipe
export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFromLayersListBare);
// TODO: handle REST / MR values
// See https://gitlab.com/gitlab-org/gitlab/-/issues/367547
export const keepLatestDownstreamPipelines = (downstreamPipelines = []) => {
// handles GraphQL
return downstreamPipelines.filter((pipeline) => {
if (pipeline.source_job) {
return !pipeline?.source_job?.retried || false;
}
return !pipeline?.sourceJob?.retried || false;
});
};

View File

@ -2,6 +2,7 @@
import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
@ -115,6 +116,10 @@ export default {
eventHub.$off('openConfirmationModal', this.setModalData);
},
methods: {
getDownstreamPipelines(pipeline) {
const downstream = pipeline.triggered;
return keepLatestDownstreamPipelines(downstream);
},
setModalData(data) {
this.pipelineId = data.pipeline.id;
this.pipeline = data.pipeline;
@ -171,7 +176,7 @@ export default {
<template #cell(stages)="{ item }">
<pipeline-mini-graph
:downstream-pipelines="item.triggered"
:downstream-pipelines="getDownstreamPipelines(item)"
:pipeline-path="item.path"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"

View File

@ -1,7 +1,7 @@
<script>
import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { IssuableType, WorkspaceType } from '~/issues/constants';
import { TYPE_EPIC, WorkspaceType } from '~/issues/constants';
import { confidentialityInfoText } from '~/vue_shared/constants';
export default {
@ -25,7 +25,7 @@ export default {
computed: {
confidentialBodyText() {
return confidentialityInfoText(
this.issuableType === IssuableType.Epic ? WorkspaceType.group : WorkspaceType.project,
this.issuableType === TYPE_EPIC ? WorkspaceType.group : WorkspaceType.project,
this.issuableType,
);
},

View File

@ -4,7 +4,7 @@ import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labe
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import { issuableLabelsQueries } from '../../../constants';
@ -269,7 +269,7 @@ export default {
...updateVariables,
operationMode: MutationOperationMode.Replace,
};
case IssuableType.Epic:
case TYPE_EPIC:
return {
iid: currentIid,
groupPath: this.fullPath,
@ -330,7 +330,7 @@ export default {
labelIds: [labelId],
operationMode: MutationOperationMode.Remove,
};
case IssuableType.Epic:
case TYPE_EPIC:
return {
iid: this.iid,
removeLabelIds: [getIdFromGraphQLId(labelId)],

View File

@ -8,7 +8,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
import { IssuableType, TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
import { IssuableType, TYPE_EPIC, TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
import { __ } from '~/locale';
import {
defaultEpicSort,
@ -155,7 +155,7 @@ export default {
},
isEpic() {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
return this.issuableAttribute === IssuableType.Epic;
return this.issuableAttribute === TYPE_EPIC;
},
issuableAttributeQuery() {
return this.issuableAttributesQueries[this.issuableAttribute];

View File

@ -3,7 +3,7 @@ import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab
import { kebabCase, snakeCase } from 'lodash';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -153,7 +153,7 @@ export default {
},
isEpic() {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
return this.issuableAttribute === IssuableType.Epic;
return this.issuableAttribute === TYPE_EPIC;
},
formatIssuableAttribute() {
return {

View File

@ -1,7 +1,7 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { IssuableType, TYPE_EPIC } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -105,7 +105,7 @@ export default {
return ICON_ON;
},
parentIsGroup() {
return this.issuableType === IssuableType.Epic;
return this.issuableType === TYPE_EPIC;
},
subscribeDisabledDescription() {
return sprintf(__('Disabled by %{parent} owner'), {

View File

@ -3,7 +3,7 @@ import { s__, __, sprintf } from '~/locale';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { IssuableType, TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
import { IssuableType, TYPE_EPIC, TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
import updateTestCaseLabelsMutation from './components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
import epicLabelsQuery from './components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
@ -86,7 +86,7 @@ export const participantsQueries = {
[IssuableType.MergeRequest]: {
query: getMergeRequestParticipants,
},
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicParticipantsQuery,
},
[IssuableType.Alert]: {
@ -109,7 +109,7 @@ export const confidentialityQueries = {
query: issueConfidentialQuery,
mutation: updateIssueConfidentialMutation,
},
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicConfidentialQuery,
mutation: updateEpicConfidentialMutation,
},
@ -122,7 +122,7 @@ export const referenceQueries = {
[IssuableType.MergeRequest]: {
query: mergeRequestReferenceQuery,
},
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicReferenceQuery,
},
};
@ -147,7 +147,7 @@ export const issuableLabelsQueries = {
mutation: updateMergeRequestLabelsMutation,
mutationName: 'mergeRequestSetLabels',
},
[IssuableType.Epic]: {
[TYPE_EPIC]: {
issuableQuery: epicLabelsQuery,
mutation: updateEpicLabelsMutation,
mutationName: 'updateEpic',
@ -182,7 +182,7 @@ export const subscribedQueries = {
query: issueSubscribedQuery,
mutation: updateIssueSubscriptionMutation,
},
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicSubscribedQuery,
mutation: updateEpicSubscriptionMutation,
},
@ -211,14 +211,14 @@ export const dueDateQueries = {
query: issueDueDateQuery,
mutation: updateIssueDueDateMutation,
},
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicDueDateQuery,
mutation: updateEpicDueDateMutation,
},
};
export const startDateQueries = {
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicStartDateQuery,
mutation: updateEpicStartDateMutation,
},
@ -283,7 +283,7 @@ export const issuableAttributesQueries = {
};
export const todoQueries = {
[IssuableType.Epic]: {
[TYPE_EPIC]: {
query: epicTodoQuery,
},
[TYPE_ISSUE]: {

View File

@ -1,5 +1,6 @@
<script>
import { GlBadge, GlButton, GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import GitlabVersionCheckBadge from '~/gitlab_version_check/components/gitlab_version_check_badge.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import { __ } from '~/locale';
@ -11,6 +12,7 @@ export default {
GlButton,
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GitlabVersionCheckBadge,
},
i18n: {
help: __('Help'),
@ -21,6 +23,7 @@ export default {
contribute: __('Contribute to GitLab'),
feedback: __('Provide feedback'),
shortcuts: __('Keyboard shortcuts'),
version: __('Your GitLab version'),
whatsnew: __("What's new"),
},
props: {
@ -35,9 +38,18 @@ export default {
};
},
computed: {
items() {
return [
{
itemGroups() {
return {
versionCheck: {
items: [
{
text: this.$options.i18n.version,
href: helpPagePath('update/index'),
version: `${this.sidebarData.gitlab_version.major}.${this.sidebarData.gitlab_version.minor}`,
},
],
},
helpLinks: {
items: [
{ text: this.$options.i18n.help, href: helpPagePath() },
{ text: this.$options.i18n.support, href: this.sidebarData.support_path },
@ -51,7 +63,7 @@ export default {
{ text: this.$options.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
],
},
{
helpActions: {
items: [
{
text: this.$options.i18n.shortcuts,
@ -67,7 +79,10 @@ export default {
},
].filter(Boolean),
},
];
};
},
updateSeverity() {
return this.sidebarData.gitlab_version_check?.severity;
},
},
methods: {
@ -120,8 +135,34 @@ export default {
</gl-button>
</template>
<gl-disclosure-dropdown-group :group="items[0]" />
<gl-disclosure-dropdown-group :group="items[1]" bordered @action="handleAction">
<gl-disclosure-dropdown-group
v-if="sidebarData.show_version_check"
:group="itemGroups.versionCheck"
>
<template #list-item="{ item }">
<a
:href="item.href"
tabindex="-1"
class="gl-display-flex gl-flex-direction-column gl-line-height-24 gl-text-gray-900 gl-hover-text-gray-900 gl-hover-text-decoration-none"
>
<span class="gl-font-sm gl-font-weight-bold">
{{ item.text }}
<gl-emoji data-name="rocket" />
</span>
<span>
<span class="gl-mr-2">{{ item.version }}</span>
<gitlab-version-check-badge v-if="updateSeverity" :status="updateSeverity" size="sm" />
</span>
</a>
</template>
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
:group="itemGroups.helpLinks"
:bordered="sidebarData.show_version_check"
/>
<gl-disclosure-dropdown-group :group="itemGroups.helpActions" bordered @action="handleAction">
<template #list-item="{ item }">
<button
tabindex="-1"

View File

@ -11,6 +11,7 @@ import {
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@ -86,6 +87,10 @@ export default {
},
},
computed: {
downstreamPipelines() {
const downstream = this.pipeline.triggered;
return keepLatestDownstreamPipelines(downstream);
},
hasPipeline() {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
@ -274,7 +279,7 @@ export default {
<span class="gl-align-items-center gl-display-inline-flex">
<pipeline-mini-graph
v-if="pipeline.details.stages"
:downstream-pipelines="pipeline.triggered"
:downstream-pipelines="downstreamPipelines"
:is-merge-train="isMergeTrain"
:pipeline-path="pipeline.path"
:stages="pipeline.details.stages"

View File

@ -90,7 +90,7 @@ module IssuableActions
end
def destroy
Issuable::DestroyService.new(project: issuable.project, current_user: current_user).execute(issuable)
Issuable::DestroyService.new(container: issuable.project, current_user: current_user).execute(issuable)
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."

View File

@ -191,7 +191,7 @@ class Projects::IssuesController < Projects::ApplicationController
new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project)
@issue = ::Issues::MoveService.new(project: project, current_user: current_user).execute(issue, new_project)
@issue = ::Issues::MoveService.new(container: project, current_user: current_user).execute(issue, new_project)
end
respond_to do |format|

View File

@ -18,7 +18,7 @@ module Mutations
target_project = resolve_project(full_path: target_project_path).sync
begin
moved_issue = ::Issues::MoveService.new(project: source_project, current_user: current_user).execute(issue, target_project)
moved_issue = ::Issues::MoveService.new(container: source_project, current_user: current_user).execute(issue, target_project)
rescue ::Issues::MoveService::MoveError => e
errors = e.message
end

View File

@ -20,7 +20,7 @@ module Mutations
work_item = authorized_find!(id: id)
result = ::WorkItems::DeleteService.new(
project: work_item.project,
container: work_item.project,
current_user: current_user
).execute(work_item)

View File

@ -65,6 +65,13 @@ module Types
field :merge_requests_count, GraphQL::Types::Int, null: false,
description: 'Number of merge requests that close the issue on merge.',
resolver: Resolvers::MergeRequestsCountResolver
field :related_merge_requests, Types::MergeRequestType.connection_type,
null: true,
description: 'Merge requests related to the issue. This field can only be resolved for one issue in any single request.' do
extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
end
field :relative_position, GraphQL::Types::Int, null: true,
description: 'Relative position of the issue (used for positioning in epic tree and issue boards).'
field :upvotes, GraphQL::Types::Int,
@ -180,6 +187,17 @@ module Types
Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.duplicated_to_id).find
end
def related_merge_requests
# rubocop: disable CodeReuse/ActiveRecord
MergeRequest.where(
id: ::Issues::ReferencedMergeRequestsService.new(project: object.project, current_user: current_user)
.execute(object)
.first
.map(&:id)
)
# rubocop: enable CodeReuse/ActiveRecord
end
def discussion_locked
!!object.discussion_locked
end

View File

@ -48,7 +48,10 @@ module SidebarsHelper
support_path: support_url,
display_whats_new: display_whats_new?,
whats_new_most_recent_release_items_count: whats_new_most_recent_release_items_count,
whats_new_version_digest: whats_new_version_digest
whats_new_version_digest: whats_new_version_digest,
show_version_check: show_version_check?,
gitlab_version: Gitlab.version_info,
gitlab_version_check: gitlab_version_check
}
end

View File

@ -2,8 +2,6 @@
module WebHooks
module WebHooksHelper
EXPIRY_TTL = 1.hour
def show_project_hook_failed_callout?(project:)
return false if project_hook_page?
return false unless current_user
@ -12,17 +10,11 @@ module WebHooks
# Assumes include of Users::CalloutsHelper
return false if web_hook_disabled_dismissed?(project)
any_project_hook_failed?(project) # Most expensive query last
project.fetch_web_hook_failure
end
private
def any_project_hook_failed?(project)
Rails.cache.fetch("any_web_hook_failed:#{project.id}", expires_in: EXPIRY_TTL) do
ProjectHook.for_projects(project).disabled.exists?
end
end
def project_hook_page?
current_controller?('projects/hooks') || current_controller?('projects/hook_logs')
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module WebHooks
module HasWebHooks
extend ActiveSupport::Concern
WEB_HOOK_CACHE_EXPIRY = 1.hour
def any_hook_failed?
hooks.disabled.exists?
end
def web_hook_failure_redis_key
"any_web_hook_failed:#{id}"
end
def last_failure_redis_key
"web_hooks:last_failure:project-#{id}"
end
def get_web_hook_failure
Gitlab::Redis::SharedState.with do |redis|
current = redis.get(web_hook_failure_redis_key)
Gitlab::Utils.to_boolean(current) if current
end
end
def fetch_web_hook_failure
Gitlab::Redis::SharedState.with do |_redis|
current = get_web_hook_failure
next current unless current.nil?
cache_web_hook_failure
end
end
def cache_web_hook_failure(state = any_hook_failed?)
Gitlab::Redis::SharedState.with do |redis|
redis.set(web_hook_failure_redis_key, state.to_s, ex: WEB_HOOK_CACHE_EXPIRY)
state
end
end
end
end

View File

@ -46,14 +46,18 @@ class ProjectHook < WebHook
override :update_last_failure
def update_last_failure
return if executable?
if executable?
project.cache_web_hook_failure if project.get_web_hook_failure # may need update
else
project.cache_web_hook_failure(true) # definitely failing, no need to check
key = "web_hooks:last_failure:project-#{project_id}"
time = Time.current.utc.iso8601
Gitlab::Redis::SharedState.with do |redis|
last_failure_key = project.last_failure_redis_key
time = Time.current.utc.iso8601
prev = redis.get(last_failure_key)
Gitlab::Redis::SharedState.with do |redis|
prev = redis.get(key)
redis.set(key, time) if !prev || prev < time
redis.set(last_failure_key, time) if !prev || prev < time
end
end
end
end

View File

@ -41,6 +41,7 @@ class Project < ApplicationRecord
include BlocksUnsafeSerialization
include Subquery
include IssueParent
include WebHooks::HasWebHooks
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override

View File

@ -872,10 +872,24 @@ class Repository
end
def merge(user, source_sha, merge_request, message)
merge_to_branch(user,
source_sha: source_sha,
target_branch: merge_request.target_branch,
message: message) do |commit_id|
merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
nil # Return value does not matter.
end
end
def merge_to_branch(user, source_sha:, target_branch:, message:, target_sha: nil)
with_cache_hooks do
raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id)
nil # Return value does not matter.
raw_repository.merge(user,
source_sha: source_sha,
target_branch: target_branch,
message: message,
target_sha: target_sha
) do |commit_id|
yield commit_id if block_given?
end
end
end
@ -884,13 +898,19 @@ class Repository
raw.delete_refs(...)
end
def ff_merge(user, source, target_branch, merge_request: nil)
def ff_merge(user, source, target_branch, target_sha: nil, merge_request: nil)
their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil?
merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id)
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
with_cache_hooks do
raw.ff_merge(user,
source_sha: their_commit_id,
target_branch: target_branch,
target_sha: target_sha
)
end
end
def revert(

View File

@ -14,7 +14,7 @@ class UserSyncedAttributesMetadata < ApplicationRecord
def read_only_attributes
return [] unless sync_profile_from_provider?
SYNCABLE_ATTRIBUTES.select { |key| synced?(key) }
self.class.syncable_attributes.select { |key| synced?(key) }
end
def synced?(attribute)
@ -25,6 +25,20 @@ class UserSyncedAttributesMetadata < ApplicationRecord
write_attribute("#{attribute}_synced", value)
end
class << self
def syncable_attributes
return SYNCABLE_ATTRIBUTES if sync_name?
SYNCABLE_ATTRIBUTES - %i[name]
end
private
def sync_name?
Gitlab.config.ldap.sync_name
end
end
private
def sync_profile_from_provider?

View File

@ -15,6 +15,9 @@ class TriggeredPipelineEntity < Grape::Entity
expose :name do |pipeline|
pipeline.source_job&.name
end
expose :retried do |pipeline|
pipeline.source_job&.retried
end
end
expose :path do |pipeline|

View File

@ -128,9 +128,9 @@ module DesignManagement
target_repository.raw.merge(
git_user,
source_sha,
merge_branch,
'CopyDesignCollectionService finalize merge'
source_sha: source_sha,
target_branch: merge_branch,
message: 'CopyDesignCollectionService finalize merge'
) { nil }
target_design_collection.end_copy!

View File

@ -7,6 +7,11 @@ module Issuable
alias_method :old_project, :project
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(original_entity, target_parent)
@original_entity = original_entity
@target_parent = target_parent

View File

@ -2,6 +2,11 @@
module Issuable
class DestroyService < IssuableBaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issuable)
after_destroy(issuable) if issuable.destroy
end

View File

@ -109,7 +109,7 @@ module Issues
target_project != issue.project
update(issue)
Issues::MoveService.new(project: project, current_user: current_user).execute(issue, target_project)
Issues::MoveService.new(container: project, current_user: current_user).execute(issue, target_project)
end
private
@ -139,7 +139,7 @@ module Issues
# we've pre-empted this from running in #execute, so let's go ahead and update the Issue now.
update(issue)
Issues::CloneService.new(project: project, current_user: current_user).execute(issue, target_project, with_notes: with_notes)
Issues::CloneService.new(container: project, current_user: current_user).execute(issue, target_project, with_notes: with_notes)
end
def create_merge_request_from_quick_action

View File

@ -4,14 +4,14 @@ module TasksToBeDone
class BaseService < ::IssuableBaseService
LABEL_PREFIX = 'tasks to be done'
def initialize(project:, current_user:, assignee_ids: [])
def initialize(container:, current_user:, assignee_ids: [])
params = {
assignee_ids: assignee_ids,
title: title,
description: description,
add_labels: label_name
}
super(project: project, current_user: current_user, params: params)
super(project: container, current_user: current_user, params: params)
end
def execute

View File

@ -25,7 +25,7 @@ module WorkItems
break ::ServiceResponse.error(message: replacement_result.errors, http_status: 422) if replacement_result.error?
delete_result = ::WorkItems::DeleteService.new(
project: @task.project,
container: @task.project,
current_user: @current_user
).execute(@task)

View File

@ -0,0 +1,6 @@
.form-check.gl-mb-3
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
= label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label'
.form-text.text-muted
= _('If enabled, only protected branches will be mirrored.')
= link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'

View File

@ -27,12 +27,7 @@
= render 'projects/mirrors/mirror_repos_form', f: f
.form-check.gl-mb-3
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
= label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label'
.form-text.text-muted
= _('If enabled, only protected branches will be mirrored.')
= link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
= render 'projects/mirrors/branch_filter'
.panel-footer
= f.submit _('Mirror repository'), class: 'js-mirror-submit', name: :update_remote_mirror, pajamas_button: true, data: { qa_selector: 'mirror_repository_button' }

View File

@ -26,7 +26,9 @@
- @project.remote_mirrors.each_with_index do |mirror, index|
- next if mirror.new_record?
%tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row_container' } }
%td{ data: { qa_selector: 'mirror_repository_url_content' } }= mirror.safe_url || _('Invalid URL')
%td{ data: { qa_selector: 'mirror_repository_url_content' } }
= mirror.safe_url || _('Invalid URL')
= render_if_exists 'projects/mirrors/mirror_branches_setting_badge', record: mirror
%td= _('Push')
%td
= mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')

View File

@ -4,6 +4,7 @@
= rm_f.hidden_field :enabled, value: '1'
= rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+"
= rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden'
= render_if_exists partial: 'projects/mirrors/branch_name_regex', locals: { f: rm_f }
= rm_f.hidden_field :keep_divergent_refs, class: 'js-mirror-keep-divergent-refs-hidden'
= render partial: 'projects/mirrors/ssh_host_keys', locals: { f: rm_f }
= render partial: 'projects/mirrors/authentication_method', locals: { f: rm_f }

View File

@ -17,7 +17,7 @@ module TasksToBeDone
member_task.tasks_to_be_done.each do |task|
service_class(task)
.new(project: project, current_user: current_user, assignee_ids: assignee_ids)
.new(container: project, current_user: current_user, assignee_ids: assignee_ids)
.execute
end
end

View File

@ -27,6 +27,7 @@ end
# backwards compatibility, we only have one host
if Settings.ldap['enabled'] || Rails.env.test?
Settings.ldap['sync_name'] = true if Settings.ldap['sync_name'].nil?
if Settings.ldap['host'].present?
# We detected old LDAP configuration syntax. Update the config to make it
# look like it was entered with the new syntax.

View File

@ -8,57 +8,11 @@ end
# This overrides the display name for Ukraine to 'Ukraine (except the Crimea, Donetsk, and Luhansk regions)'
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/374946
# To be removed after https://gitlab.com/gitlab-org/gitlab/issues/14784 is implemented
# Data fetched is based on https://github.com/countries/countries/blob/master/lib/countries/data/countries/UA.yaml
ISO3166::Data.register(
continent: "Europe",
address_format: "|-
{{recipient}}
{{street}}
{{city}} {{region_short}}
{{postalcode}}
{{country}}",
alpha2: "UA",
alpha3: "UKR",
country_code: '380',
international_prefix: '810',
ioc: "UKR",
gec: "UP",
name: "Ukraine (except the Crimea, Donetsk, and Luhansk regions)",
national_destination_code_lengths: [2],
national_number_lengths: [8, 9],
national_prefix: '8',
number: '804',
region: "Europe",
subregion: "Eastern Europe",
world_region: "EMEA",
un_locode: "UA",
nationality: "Ukrainian",
vat_rates: {
standard: 20
},
reduced: [7],
super_reduced: {
parking: { postal_code: true }
},
unofficial_names: %w(Ukraine Ucrania ウクライナ Oekraïne Украина Україна Украіна),
languages_official: ["uk"],
languages_spoken: ["uk"],
geo: {
latitude: 48.379433,
latitude_dec: '48.92656326293945',
longitude: 31.16558,
longitude_dec: '31.47578239440918',
max_latitude: 52.37958099999999,
max_longitude: 40.2285809,
min_latitude: 44.2924,
min_longitude: 22.137159,
bounds: {
northeast: { lat: 52.37958099999999, lng: 40.2285809 },
southwest: { lat: 44.2924, lng: 22.137159 }
}
},
currency_code: "UAH",
start_of_week: "monday"
ISO3166::Data.new('UA')
.call
.deep_symbolize_keys
.merge({ name: 'Ukraine (except the Crimea, Donetsk, and Luhansk regions)' })
)
# Updating the display name of Taiwan, from `Taiwan, Province of China` to `Taiwan`

View File

@ -0,0 +1,13 @@
- title: "External field in GraphQL ReleaseAssetLink type"
announcement_milestone: "15.9"
announcement_date: "2023-02-22"
removal_milestone: "16.0"
removal_date: "2023-05-22"
breaking_change: true
reporter: ahmed.hemdan
body: |
In the [GraphQL API](https://docs.gitlab.com/ee/api/graphql), the `external` field of [`ReleaseAssetLink` type](https://docs.gitlab.com/ee/api/graphql/reference/index.html#releaseassetlink) was used to indicate whether a [release link](https://docs.gitlab.com/ee/user/project/releases/release_fields.html#links) is internal or external to your GitLab instance.
As of GitLab 15.9, we treat all release links as external, and therefore, this field is deprecated in GitLab 15.9, and will be removed in GitLab 16.0.
To avoid any disruptions to your workflow, please stop using the `external` field because it will be removed and will not be replaced.
stage: Release
issue_url:

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class RecountEpicCacheCountsV3 < Gitlab::Database::Migration[2.1]
MIGRATION = 'ThirdRecountEpicCacheCounts'
DELAY_INTERVAL = 2.minutes.to_i
BATCH_SIZE = 200
MAX_BATCH_SIZE = 1000
SUB_BATCH_SIZE = 20
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:epics,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
max_batch_size: MAX_BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE,
gitlab_schema: :gitlab_main
)
end
def down
delete_batched_background_migration(MIGRATION, :epics, :id, [])
end
end

View File

@ -0,0 +1 @@
48c3039b24ab063a550419d3883b3c6308709e0ef9eacc0f4f1cdc3c99fb4148

View File

@ -13263,6 +13263,7 @@ Relationship between an epic and an issue.
| <a id="epicissuenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
| <a id="epicissueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
| <a id="epicissueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
| <a id="epicissuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) |
| <a id="epicissuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) |
| <a id="epicissuerelationpath"></a>`relationPath` | [`String`](#string) | URI path of the epic-issue relation. |
| <a id="epicissuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). |
@ -15009,6 +15010,7 @@ Describes an issuable resource link for incident issues.
| <a id="issuenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
| <a id="issueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
| <a id="issueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
| <a id="issuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) |
| <a id="issuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) |
| <a id="issuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). |
| <a id="issueseverity"></a>`severity` | [`IssuableSeverity`](#issuableseverity) | Severity level of the incident. |

View File

@ -161,6 +161,22 @@ During the transition to the GitLab Observability UI, we will migrate the [GitLa
<div class="deprecation removal-160 breaking-change">
### External field in GraphQL ReleaseAssetLink type
Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
In the [GraphQL API](https://docs.gitlab.com/ee/api/graphql), the `external` field of [`ReleaseAssetLink` type](https://docs.gitlab.com/ee/api/graphql/reference/index.html#releaseassetlink) was used to indicate whether a [release link](https://docs.gitlab.com/ee/user/project/releases/release_fields.html#links) is internal or external to your GitLab instance.
As of GitLab 15.9, we treat all release links as external, and therefore, this field is deprecated in GitLab 15.9, and will be removed in GitLab 16.0.
To avoid any disruptions to your workflow, please stop using the `external` field because it will be removed and will not be replaced.
</div>
<div class="deprecation removal-160 breaking-change">
### External field in Releases and Release Links APIs
Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>

View File

@ -86,9 +86,7 @@ To disable these notifications:
### Custom additional text in deactivation emails **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355964) in GitLab 15.9
[with a flag](../../../administration/feature_flags.md) named `deactivation_email_additional_text`.
Disabled by default.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355964) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `deactivation_email_additional_text`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an

View File

@ -172,7 +172,7 @@ To do it:
issues.each do |issue|
if issue.state != "closed" && issue.moved_to.nil?
Issues::MoveService.new(project: project, current_user: admin_user).execute(issue, target_project)
Issues::MoveService.new(container: project, current_user: admin_user).execute(issue, target_project)
else
puts "issue with id: #{issue.id} and title: #{issue.title} was not moved"
end

View File

@ -378,5 +378,5 @@ with a backup of the instance ready to be restored, just in case.
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
Issuable::DestroyService.new(project: m.project, current_user: u).execute(m)
Issuable::DestroyService.new(container: m.project, current_user: u).execute(m)
```

View File

@ -376,7 +376,7 @@ module API
not_found!('Project') unless new_project
begin
issue = ::Issues::MoveService.new(project: user_project, current_user: current_user).execute(issue, new_project)
issue = ::Issues::MoveService.new(container: user_project, current_user: current_user).execute(issue, new_project)
present issue, with: Entities::Issue, current_user: current_user, project: user_project
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
@ -403,7 +403,7 @@ module API
not_found!('Project') unless target_project
begin
issue = ::Issues::CloneService.new(project: user_project, current_user: current_user)
issue = ::Issues::CloneService.new(container: user_project, current_user: current_user)
.execute(issue, target_project, with_notes: params[:with_notes])
present issue, with: Entities::Issue, current_user: current_user, project: target_project
rescue ::Issues::CloneService::CloneError => error
@ -424,7 +424,7 @@ module API
authorize!(:destroy_issue, issue)
destroy_conditionally!(issue) do |issue|
Issuable::DestroyService.new(project: user_project, current_user: current_user).execute(issue)
Issuable::DestroyService.new(container: user_project, current_user: current_user).execute(issue)
end
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -312,7 +312,7 @@ module API
authorize!(:destroy_merge_request, merge_request)
destroy_conditionally!(merge_request) do |merge_request|
Issuable::DestroyService.new(project: user_project, current_user: current_user).execute(merge_request)
Issuable::DestroyService.new(container: user_project, current_user: current_user).execute(merge_request)
end
end

View File

@ -258,7 +258,7 @@ module Gitlab
metadata = gl_user.build_user_synced_attributes_metadata
if sync_profile_from_provider?
UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
UserSyncedAttributesMetadata.syncable_attributes.each do |key|
if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
gl_user.public_send("#{key}=".to_sym, auth_hash.public_send(key)) # rubocop:disable GitlabSecurity/PublicSend
metadata.set_attribute_synced(key, true)

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop: disable Style/Documentation
class ThirdRecountEpicCacheCounts < Gitlab::BackgroundMigration::BatchedMigrationJob
feature_category :database
def perform; end
end
# rubocop: enable Style/Documentation
end
end
# rubocop: disable Layout/LineLength
# we just want to re-enqueue the previous BackfillEpicCacheCounts migration,
# because it's a EE-only migation and it's a module, we just prepend new
# RecountEpicCacheCounts with existing batched migration module (which is same in both cases)
Gitlab::BackgroundMigration::ThirdRecountEpicCacheCounts.prepend_mod_with('Gitlab::BackgroundMigration::BackfillEpicCacheCounts')
# rubocop: enable Layout/LineLength

View File

@ -652,15 +652,23 @@ module Gitlab
end
end
def merge(user, source_sha, target_branch, message, &block)
def merge(user, source_sha:, target_branch:, message:, target_sha: nil, &block)
wrapped_gitaly_errors do
gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block)
gitaly_operation_client.user_merge_branch(user,
source_sha: source_sha,
target_branch: target_branch,
message: message,
target_sha: target_sha,
&block)
end
end
def ff_merge(user, source_sha, target_branch)
def ff_merge(user, source_sha:, target_branch:, target_sha: nil)
wrapped_gitaly_errors do
gitaly_operation_client.user_ff_branch(user, source_sha, target_branch)
gitaly_operation_client.user_ff_branch(user,
source_sha: source_sha,
target_branch: target_branch,
target_sha: target_sha)
end
end

View File

@ -152,7 +152,7 @@ module Gitlab
response.commit_id
end
def user_merge_branch(user, source_sha, target_branch, message)
def user_merge_branch(user, source_sha:, target_branch:, message:, target_sha: nil)
request_enum = QueueEnumerator.new
response_enum = gitaly_client_call(
@repository.storage,
@ -168,6 +168,7 @@ module Gitlab
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha,
branch: encode_binary(target_branch),
expected_old_oid: target_sha,
message: encode_binary(message),
timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i)
)
@ -184,7 +185,6 @@ module Gitlab
raise Gitlab::Git::CommitError, 'failed to apply merge to branch' unless branch_update.commit_id.present?
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
@ -207,12 +207,13 @@ module Gitlab
request_enum.close
end
def user_ff_branch(user, source_sha, target_branch)
def user_ff_branch(user, source_sha:, target_branch:, target_sha: nil)
request = Gitaly::UserFFBranchRequest.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit_id: source_sha,
branch: encode_binary(target_branch)
branch: encode_binary(target_branch),
expected_old_oid: target_sha
)
response = gitaly_client_call(

View File

@ -2,6 +2,16 @@
module Gitlab
module Redis
# Match signature in
# https://github.com/rails/rails/blob/v6.1.7.2/activesupport/lib/active_support/cache/redis_cache_store.rb#L59
ERROR_HANDLER = ->(method:, returning:, exception:) do
Gitlab::ErrorTracking.log_exception(
exception,
method: method,
returning: returning.inspect
)
end
class Cache < ::Gitlab::Redis::Wrapper
CACHE_NAMESPACE = 'cache:gitlab'
@ -12,7 +22,8 @@ module Gitlab
redis: pool,
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
namespace: CACHE_NAMESPACE,
expires_in: default_ttl_seconds
expires_in: default_ttl_seconds,
error_handler: ::Gitlab::Redis::ERROR_HANDLER
}
end

View File

@ -10,7 +10,11 @@ module Gitlab
end
def cache_store
@cache_store ||= ActiveSupport::Cache::RedisCacheStore.new(redis: pool, namespace: Cache::CACHE_NAMESPACE)
@cache_store ||= ActiveSupport::Cache::RedisCacheStore.new(
redis: pool,
namespace: Cache::CACHE_NAMESPACE,
error_handler: ::Gitlab::Redis::ERROR_HANDLER
)
end
private

View File

@ -14,7 +14,8 @@ module Gitlab
redis: pool,
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
namespace: Cache::CACHE_NAMESPACE,
expires_in: Cache.default_ttl_seconds
expires_in: Cache.default_ttl_seconds,
error_handler: ::Gitlab::Redis::ERROR_HANDLER
)
end
end

View File

@ -29,7 +29,7 @@ module Gitlab
return Gitlab::SlashCommands::Presenters::Access.new.not_found
end
new_issue = ::Issues::MoveService.new(project: project, current_user: current_user)
new_issue = ::Issues::MoveService.new(container: project, current_user: current_user)
.execute(old_issue, target_project)
presenter(new_issue).present(old_issue)

View File

@ -4107,6 +4107,9 @@ msgstr ""
msgid "All Members"
msgstr ""
msgid "All branch names must match %{link_start}this regular expression%{link_end}. If empty, any branch name is allowed."
msgstr ""
msgid "All branches"
msgstr ""
@ -8740,6 +8743,9 @@ msgstr ""
msgid "Choose which Git strategy to use when fetching the project."
msgstr ""
msgid "Choose which branches should be mirrored"
msgstr ""
msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr ""
@ -16773,6 +16779,9 @@ msgstr ""
msgid "Exactly one of %{attributes} is required"
msgstr ""
msgid "Example"
msgstr ""
msgid "Example: (feature|hotfix)\\/*"
msgstr ""
@ -21088,6 +21097,9 @@ msgstr ""
msgid "IdentityVerification|For added security, you'll need to verify your identity in a few quick steps."
msgstr ""
msgid "IdentityVerification|For added security, you'll need to verify your identity."
msgstr ""
msgid "IdentityVerification|For added security, you'll need to verify your identity. We've sent a verification code to %{email}"
msgstr ""
@ -21187,6 +21199,9 @@ msgstr ""
msgid "IdentityVerification|We sent a new code to +%{phoneNumber}"
msgstr ""
msgid "IdentityVerification|We've sent a verification code to %{email}"
msgstr ""
msgid "IdentityVerification|We've sent a verification code to +%{phoneNumber}"
msgstr ""
@ -21235,6 +21250,9 @@ msgstr ""
msgid "If disabled, only administrators can configure repository mirroring."
msgstr ""
msgid "If enabled, all branches will be mirrored."
msgstr ""
msgid "If enabled, only protected branches will be mirrored."
msgstr ""
@ -27329,6 +27347,12 @@ msgstr ""
msgid "Minutes"
msgstr ""
msgid "Mirror all branches"
msgstr ""
msgid "Mirror branches"
msgstr ""
msgid "Mirror direction"
msgstr ""
@ -27341,6 +27365,9 @@ msgstr ""
msgid "Mirror settings are only available to GitLab administrators."
msgstr ""
msgid "Mirror specific branches"
msgstr ""
msgid "Mirror user"
msgstr ""
@ -29481,9 +29508,6 @@ msgstr ""
msgid "OnDemandScans|Unable to fetch runner tags. Try reloading the page."
msgstr ""
msgid "OnDemandScans|Verify"
msgstr ""
msgid "OnDemandScans|Verify configuration"
msgstr ""
@ -38614,6 +38638,9 @@ msgstr ""
msgid "SecurityReports|Activity"
msgstr ""
msgid "SecurityReports|Add comment and dismiss"
msgstr ""
msgid "SecurityReports|Add or remove projects to monitor in the security area. Projects included in this list will have their results displayed in the security dashboard and vulnerability report."
msgstr ""
@ -40781,6 +40808,9 @@ msgstr ""
msgid "Spam log successfully submitted as ham."
msgstr ""
msgid "Specific branches"
msgstr ""
msgid "Specified URL cannot be used: \"%{reason}\""
msgstr ""
@ -49362,6 +49392,9 @@ msgstr ""
msgid "Your GitLab instance allows anyone to register for an account, which is a security risk on public-facing GitLab instances. You should deactivate new sign ups if public users aren't expected to register for an account."
msgstr ""
msgid "Your GitLab version"
msgstr ""
msgid "Your Groups"
msgstr ""

View File

@ -9,6 +9,7 @@ RSpec.describe 'Projects > Settings > Repository settings', feature_category: :p
before do
stub_feature_flags(branch_rules: false)
stub_feature_flags(mirror_only_branches_match_regex: false)
project.add_role(user, role)
sign_in(user)
end

View File

@ -1,4 +1,4 @@
const downstream = {
export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/612',
@ -17,7 +17,7 @@ const downstream = {
},
sourceJob: {
id: 'gid://gitlab/Ci::Bridge/532',
retried: false,
retried: includeSourceJobRetried ? false : null,
},
__typename: 'Pipeline',
},
@ -38,7 +38,7 @@ const downstream = {
},
sourceJob: {
id: 'gid://gitlab/Ci::Bridge/531',
retried: true,
retried: includeSourceJobRetried ? true : null,
},
__typename: 'Pipeline',
},
@ -59,13 +59,13 @@ const downstream = {
},
sourceJob: {
id: 'gid://gitlab/Ci::Bridge/530',
retried: true,
retried: includeSourceJobRetried ? true : null,
},
__typename: 'Pipeline',
},
],
__typename: 'PipelineConnection',
};
});
const upstream = {
id: 'gid://gitlab/Ci::Pipeline/610',
@ -161,7 +161,7 @@ export const mockDownstreamQueryResponse = {
pipeline: {
path: '/root/ci-project/-/pipelines/790',
id: 'pipeline-1',
downstream,
downstream: mockDownstreamPipelinesGraphql(),
upstream: null,
},
__typename: 'Project',
@ -176,7 +176,7 @@ export const mockUpstreamDownstreamQueryResponse = {
pipeline: {
id: 'pipeline-1',
path: '/root/ci-project/-/pipelines/790',
downstream,
downstream: mockDownstreamPipelinesGraphql(),
upstream,
},
__typename: 'Project',

View File

@ -23,8 +23,19 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:build_test) { create(:ci_build, pipeline: pipeline, stage: 'test') }
let!(:build_deploy_failed) { create(:ci_build, status: :failed, pipeline: pipeline, stage: 'deploy') }
let(:bridge) { create(:ci_bridge, pipeline: pipeline) }
let(:retried_bridge) { create(:ci_bridge, :retried, pipeline: pipeline) }
let(:downstream_pipeline) { create(:ci_pipeline, :with_job) }
let(:retried_downstream_pipeline) { create(:ci_pipeline, :with_job) }
let!(:ci_sources_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
let!(:retried_ci_sources_pipeline) do
create(:ci_sources_pipeline, pipeline: retried_downstream_pipeline, source_job: retried_bridge)
end
before do
sign_in(user)
project.add_developer(user)
end
it 'pipelines/pipelines.json' do

View File

@ -8,10 +8,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import {
IssuableStatusText,
IssuableType,
STATUS_CLOSED,
STATUS_OPEN,
STATUS_REOPENED,
TYPE_EPIC,
TYPE_ISSUE,
} from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
@ -485,11 +485,11 @@ describe('Issuable output', () => {
});
it.each`
issuableType | issuableStatus | statusIcon
${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
${IssuableType.Epic} | ${STATUS_OPEN} | ${'epic'}
${IssuableType.Epic} | ${STATUS_CLOSED} | ${'epic-closed'}
issuableType | issuableStatus | statusIcon
${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
async ({ issuableType, issuableStatus, statusIcon }) => {

View File

@ -1,7 +1,7 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import LockedWarning, { i18n } from '~/issues/show/components/locked_warning.vue';
describe('LockedWarning component', () => {
@ -21,7 +21,7 @@ describe('LockedWarning component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
describe.each([TYPE_ISSUE, IssuableType.Epic])('with issuableType set to %s', (issuableType) => {
describe.each([TYPE_ISSUE, TYPE_EPIC])('with issuableType set to %s', (issuableType) => {
let alert;
let link;
beforeEach(() => {

View File

@ -121,6 +121,14 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength);
});
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
expect(pipeline.triggered).toHaveLength(2);
expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();

View File

@ -3,6 +3,7 @@ import {
makeLinksFromNodes,
filterByAncestors,
generateColumnsFromLayersListBare,
keepLatestDownstreamPipelines,
listByLayers,
parseData,
removeOrphanNodes,
@ -10,6 +11,8 @@ import {
} from '~/pipelines/components/parsing_utils';
import { createNodeDict } from '~/pipelines/utils';
import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data';
import { mockDownstreamPipelinesGraphql } from '../commit/mock_data';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
@ -159,3 +162,37 @@ describe('DAG visualization parsing utilities', () => {
});
});
});
describe('linked pipeline utilities', () => {
describe('keepLatestDownstreamPipelines', () => {
it('filters data from GraphQL', () => {
const downstream = mockDownstreamPipelinesGraphql().nodes;
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(downstream).toHaveLength(3);
expect(latestDownstream).toHaveLength(1);
});
it('filters data from REST', () => {
const downstream = mockDownstreamPipelinesRest();
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(downstream).toHaveLength(2);
expect(latestDownstream).toHaveLength(1);
});
it('returns downstream pipelines if sourceJob.retried is null', () => {
const downstream = mockDownstreamPipelinesGraphql({ includeSourceJobRetried: false }).nodes;
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(latestDownstream).toHaveLength(downstream.length);
});
it('returns downstream pipelines if source_job.retried is null', () => {
const downstream = mockDownstreamPipelinesRest({ includeSourceJobRetried: false });
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(latestDownstream).toHaveLength(downstream.length);
});
});
});

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
@ -37,7 +37,7 @@ const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a proble
const updateLabelsMutation = {
[TYPE_ISSUE]: updateIssueLabelsMutation,
[IssuableType.MergeRequest]: updateMergeRequestLabelsMutation,
[IssuableType.Epic]: updateEpicLabelsMutation,
[TYPE_EPIC]: updateEpicLabelsMutation,
[IssuableType.TestCase]: updateTestCaseLabelsMutation,
};
@ -215,7 +215,7 @@ describe('LabelsSelectRoot', () => {
issuableType
${TYPE_ISSUE}
${IssuableType.MergeRequest}
${IssuableType.Epic}
${TYPE_EPIC}
${IssuableType.TestCase}
`('when updating labels for $issuableType', ({ issuableType }) => {
const label = { id: 'gid://gitlab/ProjectLabel/2' };

View File

@ -14,6 +14,8 @@ jest.mock('~/whats_new');
describe('HelpCenter component', () => {
let wrapper;
const GlEmoji = { template: '<img/>' };
const findDropdownGroup = (i = 0) => {
return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i);
};
@ -24,6 +26,7 @@ describe('HelpCenter component', () => {
const createWrapper = (sidebarData) => {
wrapper = mountExtended(HelpCenter, {
propsData: { sidebarData },
stubs: { GlEmoji },
});
};
@ -52,6 +55,18 @@ describe('HelpCenter component', () => {
]);
});
describe('with Gitlab version check feature enabled', () => {
beforeEach(() => {
createWrapper({ ...sidebarData, show_version_check: true });
});
it('shows version information as first item', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
{ text: HelpCenter.i18n.version, href: helpPagePath('update/index'), version: '16.0' },
]);
});
});
describe('showKeyboardShortcuts', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$refs.dropdown, 'close');

View File

@ -71,4 +71,7 @@ export const sidebarData = {
display_whats_new: true,
whats_new_most_recent_release_items_count: 5,
whats_new_version_digest: 1,
show_version_check: false,
gitlab_version: { major: 16, minor: 0 },
gitlab_version_check: { severity: 'success' },
};

View File

@ -111,6 +111,14 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
expect(mockData.pipeline.triggered).toHaveLength(2);
expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('should render pipeline coverage information', () => {
it('should render coverage percentage', () => {
expect(findPipelineCoverage().text()).toMatch(

View File

@ -19,7 +19,13 @@ describe('app/assets/javascripts/vue_merge_request_widget/components/report_widg
expect(wrapper.isVisible()).toBe(false);
});
it('shows the container when children have no content', async () => {
it('hides the container when children has only empty spaces', async () => {
createComponent({ slot: `<span><b>&nbsp;<br/>\t\r\n</b></span>&nbsp;` });
await nextTick();
expect(wrapper.isVisible()).toBe(false);
});
it('shows the container when a child has content', async () => {
createComponent({ slot: `<span><b>test</b></span>` });
await nextTick();
expect(wrapper.isVisible()).toBe(true);

View File

@ -1,5 +1,94 @@
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
export const mockDownstreamPipelinesRest = ({ includeSourceJobRetried = true } = {}) => [
{
id: 632,
user: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'https://gdk.test:3000/root',
show_status: false,
path: '/root',
},
active: false,
coverage: null,
source: 'parent_pipeline',
source_job: {
name: 'bridge_job',
retried: includeSourceJobRetried ? false : null,
},
path: '/kitchen-sink/bakery/-/pipelines/632',
details: {
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/kitchen-sink/bakery/-/pipelines/632',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
},
project: {
id: 21,
name: 'bakery',
full_path: '/kitchen-sink/bakery',
full_name: 'kitchen-sink / bakery',
refs_url: '/kitchen-sink/bakery/refs',
},
},
{
id: 633,
user: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'https://gdk.test:3000/root',
show_status: false,
path: '/root',
},
active: false,
coverage: null,
source: 'parent_pipeline',
source_job: {
name: 'bridge_job',
retried: includeSourceJobRetried ? true : null,
},
path: '/kitchen-sink/bakery/-/pipelines/633',
details: {
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/kitchen-sink/bakery/-/pipelines/633',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
},
project: {
id: 21,
name: 'bakery',
full_path: '/kitchen-sink/bakery',
full_name: 'kitchen-sink / bakery',
refs_url: '/kitchen-sink/bakery/refs',
},
},
];
export const artifacts = [
{
text: 'result.txt',
@ -207,6 +296,7 @@ export default {
retry_path: '/root/acets-app/pipelines/172/retry',
created_at: '2017-04-07T12:27:19.520Z',
updated_at: '2017-04-07T15:28:44.800Z',
triggered: mockDownstreamPipelinesRest(),
},
pipelineCoverageDelta: '15.25',
buildsWithCoverage: [

View File

@ -1,7 +1,7 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { WorkspaceType, IssuableType, TYPE_ISSUE } from '~/issues/constants';
import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
@ -28,9 +28,9 @@ describe('ConfidentialityBadge', () => {
});
it.each`
workspaceType | issuableType | expectedTooltip
${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
workspaceType | issuableType | expectedTooltip
${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {

View File

@ -72,7 +72,10 @@ RSpec.describe SidebarsHelper do
support_path: helper.support_url,
display_whats_new: helper.display_whats_new?,
whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count,
whats_new_version_digest: helper.whats_new_version_digest
whats_new_version_digest: helper.whats_new_version_digest,
show_version_check: helper.show_version_check?,
gitlab_version: Gitlab.version_info,
gitlab_version_check: helper.gitlab_version_check
})
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe WebHooks::WebHooksHelper do
RSpec.describe WebHooks::WebHooksHelper, :clean_gitlab_redis_shared_state, feature_category: :integrations do
let_it_be_with_reload(:project) { create(:project) }
let(:current_user) { nil }
@ -43,20 +43,12 @@ RSpec.describe WebHooks::WebHooksHelper do
expect(helper).to be_show_project_hook_failed_callout(project: project)
end
it 'caches the DB calls until the TTL', :use_clean_rails_memory_store_caching, :request_store do
it 'stores a value' do
Gitlab::Redis::SharedState.with do |redis|
expect(redis).to receive(:set).with(anything, 'true', ex: 1.hour)
end
helper.show_project_hook_failed_callout?(project: project)
travel_to((described_class::EXPIRY_TTL - 1.second).from_now) do
expect do
helper.show_project_hook_failed_callout?(project: project)
end.not_to exceed_query_limit(0)
end
travel_to((described_class::EXPIRY_TTL + 1.second).from_now) do
expect do
helper.show_project_hook_failed_callout?(project: project)
end.to exceed_query_limit(0)
end
end
end

View File

@ -418,25 +418,54 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :authentication_and_
end
context "and LDAP user has an account already" do
let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
context 'when sync_name is disabled' do
before do
allow(Gitlab.config.ldap).to receive(:sync_name).and_return(false)
end
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
oauth_user.save # rubocop:disable Rails/SaveBang
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.name).to eql 'John Doe'
expect(gl_user.email).to eql 'john@example.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
[
{ provider: 'ldapmain', extern_uid: dn },
{ provider: 'twitter', extern_uid: uid }
]
)
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.name).to eql 'John Doe'
expect(gl_user.email).to eql 'john@example.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
[
{ provider: 'ldapmain', extern_uid: dn },
{ provider: 'twitter', extern_uid: uid }
]
)
end
end
context 'when sync_name is enabled' do
let!(:existing_user) { create(:omniauth_user, name: 'John Swift', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
expect(gl_user.name).to eql 'John Swift'
expect(gl_user.email).to eql 'john@example.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array(
[
{ provider: 'ldapmain', extern_uid: dn },
{ provider: 'twitter', extern_uid: uid }
]
)
end
end
end

View File

@ -2106,16 +2106,22 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
let(:repository) { mutable_repository }
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
let(:target_branch) { 'test-merge-target-branch' }
let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
before do
repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f')
repository.create_branch(target_branch, target_sha)
end
it 'can perform a merge' do
merge_commit_id = nil
result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id|
merge_commit_id = commit_id
end
result =
repository.merge(user,
source_sha: source_sha,
target_branch: target_branch,
target_sha: target_sha,
message: 'Test merge') do |commit_id|
merge_commit_id = commit_id
end
expect(result.newrev).to eq(merge_commit_id)
expect(result.repo_created).to eq(false)
@ -2124,10 +2130,15 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
it 'returns nil if there was a concurrent branch update' do
concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
result = repository.merge(user, source_sha, target_branch, 'Test merge') do
# This ref update should make the merge fail
repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id)
end
result =
repository.merge(user,
source_sha: source_sha,
target_branch: target_branch,
target_sha: target_sha,
message: 'Test merge') do |_commit_id|
# This ref update should make the merge fail
repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id)
end
# This 'nil' signals that the merge was not applied
expect(result).to be_nil
@ -2147,7 +2158,13 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
repository.create_branch(target_branch, branch_head)
end
subject { repository.ff_merge(user, source_sha, target_branch) }
subject do
repository.ff_merge(user,
source_sha: source_sha,
target_branch: target_branch,
target_sha: branch_head
)
end
shared_examples '#ff_merge' do
it 'performs a ff_merge' do
@ -2159,7 +2176,7 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
context 'with a non-existing target branch' do
subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
subject { repository.ff_merge(user, source_sha: source_sha, target_branch: 'this-isnt-real') }
it 'throws an ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
@ -2187,8 +2204,9 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
it "calls Gitaly's OperationService" do
expect_any_instance_of(Gitlab::GitalyClient::OperationService)
.to receive(:user_ff_branch).with(user, source_sha, target_branch)
.and_return(nil)
.to receive(:user_ff_branch).with(
user, source_sha: source_sha, target_branch: target_branch, target_sha: branch_head
).and_return(nil)
subject
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::OperationService do
RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@ -279,12 +279,39 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
describe '#user_merge_branch' do
let(:target_branch) { 'master' }
let(:target_sha) { repository.commit(target_branch).sha }
let(:source_sha) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
let(:message) { 'Merge a branch' }
subject { client.user_merge_branch(user, source_sha, target_branch, message) {} }
subject do
client.user_merge_branch(user,
source_sha: source_sha,
target_branch: target_branch,
target_sha: target_sha,
message: message
) {}
end
it 'sends a user_merge_branch message', :freeze_time do
first_request =
Gitaly::UserMergeBranchRequest.new(
repository: repository.gitaly_repository,
user: gitaly_user,
commit_id: source_sha,
branch: target_branch,
expected_old_oid: target_sha,
message: message,
timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i)
)
second_request = Gitaly::UserMergeBranchRequest.new(apply: true)
expect_next_instance_of(Gitlab::GitalyClient::QueueEnumerator) do |instance|
expect(instance).to receive(:push).with(first_request).and_call_original
expect(instance).to receive(:push).with(second_request).and_call_original
expect(instance).to receive(:close)
end
it 'sends a user_merge_branch message' do
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
expect(subject.newrev).to be_present
expect(subject.repo_created).to be(false)
@ -431,12 +458,14 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
describe '#user_ff_branch' do
let(:target_branch) { 'my-branch' }
let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:request) do
Gitaly::UserFFBranchRequest.new(
repository: repository.gitaly_repository,
branch: target_branch,
commit_id: source_sha,
expected_old_oid: target_sha,
user: gitaly_user
)
end
@ -457,7 +486,13 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
.and_return(response)
end
subject { client.user_ff_branch(user, source_sha, target_branch) }
subject do
client.user_ff_branch(user,
source_sha: source_sha,
target_branch: target_branch,
target_sha: target_sha
)
end
it 'sends a user_ff_branch message and returns a BranchUpdate object' do
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)

View File

@ -26,5 +26,22 @@ RSpec.describe Gitlab::Redis::Cache do
expect(described_class.active_support_config[:expires_in]).to eq(1.day)
end
context 'when encountering an error' do
let(:cache) { ActiveSupport::Cache::RedisCacheStore.new(**described_class.active_support_config) }
subject { cache.read('x') }
before do
described_class.with do |redis|
allow(redis).to receive(:get).and_raise(::Redis::CommandError)
end
end
it 'logs error' do
expect(::Gitlab::ErrorTracking).to receive(:log_exception)
subject
end
end
end
end

View File

@ -4,4 +4,21 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::RateLimiting do
include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache
describe '.cache_store' do
context 'when encountering an error' do
subject { described_class.cache_store.read('x') }
before do
described_class.with do |redis|
allow(redis).to receive(:get).and_raise(::Redis::CommandError)
end
end
it 'logs error' do
expect(::Gitlab::ErrorTracking).to receive(:log_exception)
subject
end
end
end
end

View File

@ -17,5 +17,20 @@ RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do
it 'has a default ttl of 8 hours' do
expect(described_class.cache_store.options[:expires_in]).to eq(8.hours)
end
context 'when encountering an error' do
subject { described_class.cache_store.read('x') }
before do
described_class.with do |redis|
allow(redis).to receive(:get).and_raise(::Redis::CommandError)
end
end
it 'logs error' do
expect(::Gitlab::ErrorTracking).to receive(:log_exception)
subject
end
end
end
end

View File

@ -8,7 +8,7 @@ RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove do
let_it_be(:other_project) { create(:project) }
let_it_be(:old_issue, reload: true) { create(:issue, project: project) }
let(:new_issue) { Issues::MoveService.new(project: project, current_user: user).execute(old_issue, other_project) }
let(:new_issue) { Issues::MoveService.new(container: project, current_user: user).execute(old_issue, other_project) }
let(:attachment) { subject[:attachments].first }
subject { described_class.new(new_issue).present(old_issue) }

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RecountEpicCacheCountsV3, :migration, feature_category: :portfolio_management do
let(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules a batched background migration' do
migrate!
expect(migration).to have_scheduled_batched_migration(
table_name: :epics,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
max_batch_size: described_class::MAX_BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end

View File

@ -79,17 +79,91 @@ RSpec.describe ProjectHook, feature_category: :integrations do
end
describe '#update_last_failure', :clean_gitlab_redis_shared_state do
let(:hook) { build(:project_hook) }
let_it_be(:hook) { create(:project_hook) }
def last_failure
Gitlab::Redis::SharedState.with do |redis|
redis.get(hook.project.last_failure_redis_key)
end
end
def any_failed?
Gitlab::Redis::SharedState.with do |redis|
Gitlab::Utils.to_boolean(redis.get(hook.project.web_hook_failure_redis_key))
end
end
it 'is a method of this class' do
expect { hook.update_last_failure }.not_to raise_error
end
context 'when the hook is executable' do
it 'does not update the state' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
let(:redis_key) { hook.project.web_hook_failure_redis_key }
hook.update_last_failure
def redis_value
any_failed?
end
context 'when the state was previously failing' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_key, true)
end
end
it 'does update the state' do
expect { hook.update_last_failure }.to change { redis_value }.to(false)
end
context 'when there is another failing sibling hook' do
before do
create(:project_hook, :permanently_disabled, project: hook.project)
end
it 'does not update the state' do
expect { hook.update_last_failure }.not_to change { redis_value }.from(true)
end
it 'caches the current value' do
Gitlab::Redis::SharedState.with do |redis|
expect(redis).to receive(:set).with(redis_key, 'true', ex: 1.hour).and_call_original
end
hook.update_last_failure
end
end
end
context 'when the state was previously unknown' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.del(redis_key)
end
end
it 'does not update the state' do
expect { hook.update_last_failure }.not_to change { redis_value }.from(nil)
end
end
context 'when the state was previously not failing' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_key, false)
end
end
it 'does not update the state' do
expect { hook.update_last_failure }.not_to change { redis_value }.from(false)
end
it 'does not cache the current value' do
Gitlab::Redis::SharedState.with do |redis|
expect(redis).not_to receive(:set)
end
hook.update_last_failure
end
end
end
@ -98,28 +172,34 @@ RSpec.describe ProjectHook, feature_category: :integrations do
allow(hook).to receive(:executable?).and_return(false)
end
def last_failure
Gitlab::Redis::SharedState.with do |redis|
redis.get("web_hooks:last_failure:project-#{hook.project.id}")
end
end
context 'there is no prior value', :freeze_time do
it 'updates the state' do
it 'updates last_failure' do
expect { hook.update_last_failure }.to change { last_failure }.to(Time.current)
end
it 'updates any_failed?' do
expect { hook.update_last_failure }.to change { any_failed? }.to(true)
end
end
context 'there is a prior value, from before now' do
context 'when there is a prior last_failure, from before now' do
it 'updates the state' do
the_future = 1.minute.from_now
hook.update_last_failure
travel_to(the_future) do
expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601)
end
end
it 'does not change the failing state' do
the_future = 1.minute.from_now
hook.update_last_failure
travel_to(the_future) do
expect { hook.update_last_failure }.not_to change { any_failed? }.from(true)
end
end
end
context 'there is a prior value, from after now' do

View File

@ -443,7 +443,7 @@ RSpec.describe WebHook, feature_category: :integrations do
describe '#update_last_failure' do
it 'is a method of this class' do
expect { described_class.new.update_last_failure }.not_to raise_error
expect { described_class.new(project: project).update_last_failure }.not_to raise_error
end
end

View File

@ -8826,6 +8826,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
it_behaves_like 'something that has web-hooks' do
let_it_be_with_reload(:object) { create(:project) }
def create_hook
create(:project_hook, project: object)
end
end
private
def finish_job(export_job)

View File

@ -1848,6 +1848,8 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#expire_root_ref_cache' do
let(:project) { create(:project) }
it 'expires the root reference cache' do
repository.root_ref
@ -1949,6 +1951,40 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
describe '#merge_to_branch' do
let(:merge_request) do
create(:merge_request, source_branch: 'feature', target_branch: project.default_branch, source_project: project)
end
it 'merges two branches and returns the merge commit id' do
message = 'New merge commit'
merge_commit_id =
repository.merge_to_branch(user,
source_sha: merge_request.diff_head_sha,
target_branch: merge_request.target_branch,
target_sha: repository.commit(merge_request.target_branch).sha,
message: message)
expect(repository.commit(merge_commit_id).message).to eq(message)
expect(repository.commit(merge_request.target_branch).sha).to eq(merge_commit_id)
end
it 'does not merge if target branch has been changed' do
target_sha = project.commit.sha
repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: project.default_branch)
merge_commit_id =
repository.merge_to_branch(user,
source_sha: merge_request.diff_head_sha,
target_branch: merge_request.target_branch,
target_sha: target_sha,
message: 'New merge commit')
expect(merge_commit_id).to be_nil
end
end
describe '#merge_to_ref' do
let(:merge_request) do
create(:merge_request, source_branch: 'feature',
@ -1975,15 +2011,20 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#ff_merge' do
let(:target_branch) { 'ff-target' }
let(:merge_request) do
create(:merge_request, source_branch: 'feature', target_branch: target_branch, source_project: project)
end
before do
repository.add_branch(user, 'ff-target', 'feature~5')
repository.add_branch(user, target_branch, 'feature~5')
end
it 'merges the code and return the commit id' do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
merge_commit_id = repository.ff_merge(user,
merge_request.diff_head_sha,
merge_request.target_branch,
target_sha: repository.commit(merge_request.target_branch).sha,
merge_request: merge_request)
merge_commit = repository.commit(merge_commit_id)
@ -1992,14 +2033,24 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
merge_commit_id = repository.ff_merge(user,
merge_request.diff_head_sha,
merge_request.target_branch,
target_sha: repository.commit(merge_request.target_branch).sha,
merge_request: merge_request)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
it 'does not merge if target branch has been changed' do
target_sha = project.commit(target_branch).sha
repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: target_branch)
merge_commit_id = repository.ff_merge(user, merge_request.diff_head_sha, target_branch, target_sha: target_sha)
expect(merge_commit_id).to be_nil
end
end
describe '#rebase' do

View File

@ -154,6 +154,47 @@ RSpec.describe 'Query.issue(id)', feature_category: :team_planning do
end
end
context 'when selecting `related_merge_requests`' do
let(:issue_fields) { ['relatedMergeRequests { nodes { id } }'] }
let_it_be(:user) { create(:user) }
let_it_be(:mr_project) { project }
let!(:merge_request) do
attributes = {
author: user,
source_project: mr_project,
target_project: mr_project,
source_branch: 'master',
target_branch: 'test',
description: "See #{issue.to_reference}"
}
create(:merge_request, attributes).tap do |merge_request|
create(:note, :system, project: issue.project, noteable: issue,
author: user, note: merge_request.to_reference(full: true))
end
end
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'returns the related merge request' do
expect(issue_data['relatedMergeRequests']['nodes']).to include a_hash_including({
'id' => merge_request.to_global_id.to_s
})
end
context 'no permission to related merge request' do
let_it_be(:mr_project) { create(:project, :private) }
it 'does not return the related merge request' do
expect(issue_data['relatedMergeRequests']['nodes']).to be_empty
end
end
end
context 'when there is a confidential issue' do
let!(:confidential_issue) do
create(:issue, :confidential, project: project)

View File

@ -16,7 +16,7 @@ RSpec.describe 'getting Alert Management Alerts', feature_category: :incident_ma
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('AlertManagementAlert', excluded: ['assignees'])}
#{all_graphql_fields_for('AlertManagementAlert', excluded: %w[assignees relatedMergeRequests])}
}
QUERY
end

View File

@ -65,7 +65,7 @@ RSpec.describe IssueEntity do
before do
project.add_developer(member)
public_project.add_developer(member)
Issues::MoveService.new(project: public_project, current_user: member).execute(issue, project)
Issues::MoveService.new(container: public_project, current_user: member).execute(issue, project)
end
context 'when user cannot read target project' do

View File

@ -182,6 +182,7 @@ RSpec.describe PipelineDetailsEntity do
expect(source_jobs[cross_project_pipeline.id][:name]).to eq('cross-project')
expect(source_jobs[child_pipeline.id][:name]).to eq('child')
expect(source_jobs[child_pipeline.id][:retried]).to eq false
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe Issuable::DestroyService do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
subject(:service) { described_class.new(project: project, current_user: user) }
subject(:service) { described_class.new(container: project, current_user: user) }
describe '#execute' do
context 'when issuable is an issue' do

View File

@ -22,7 +22,7 @@ RSpec.describe Issues::CloneService do
let(:with_notes) { false }
subject(:clone_service) do
described_class.new(project: old_project, current_user: user)
described_class.new(container: old_project, current_user: user)
end
shared_context 'user can clone issue' do

View File

@ -20,7 +20,7 @@ RSpec.describe Issues::MoveService do
end
subject(:move_service) do
described_class.new(project: old_project, current_user: user)
described_class.new(container: old_project, current_user: user)
end
shared_context 'user can move issue' do

View File

@ -18,7 +18,7 @@ RSpec.describe TasksToBeDone::BaseService do
subject(:service) do
TasksToBeDone::CreateCiTaskService.new(
project: project,
container: project,
current_user: current_user,
assignee_ids: assignee_ids
)

View File

@ -16,7 +16,7 @@ RSpec.describe WorkItems::DeleteService do
end
describe '#execute' do
subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) }
subject(:result) { described_class.new(container: project, current_user: user).execute(work_item) }
context 'when user can delete the work item' do
it { is_expected.to be_success }

View File

@ -0,0 +1,107 @@
# frozen_string_literal: true
RSpec.shared_examples 'something that has web-hooks' do
describe '#any_hook_failed?', :clean_gitlab_redis_shared_state do
subject { object.any_hook_failed? }
context 'when there are no hooks' do
it { is_expected.to eq(false) }
end
context 'when there are hooks' do
before do
create_hook
create_hook
end
it { is_expected.to eq(false) }
context 'when there is a failed hook' do
before do
hook = create_hook
hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
end
it { is_expected.to eq(true) }
end
end
end
describe '#cache_web_hook_failure', :clean_gitlab_redis_shared_state do
context 'when no value is passed' do
it 'stores the return value of #any_hook_failed? and passes it back' do
allow(object).to receive(:any_hook_failed?).and_return(true)
Gitlab::Redis::SharedState.with do |r|
expect(r).to receive(:set)
.with(object.web_hook_failure_redis_key, 'true', ex: 1.hour)
.and_call_original
end
expect(object.cache_web_hook_failure).to eq(true)
end
end
context 'when a value is passed' do
it 'stores the value and passes it back' do
expect(object).not_to receive(:any_hook_failed?)
Gitlab::Redis::SharedState.with do |r|
expect(r).to receive(:set)
.with(object.web_hook_failure_redis_key, 'foo', ex: 1.hour)
.and_call_original
end
expect(object.cache_web_hook_failure(:foo)).to eq(:foo)
end
end
end
describe '#get_web_hook_failure', :clean_gitlab_redis_shared_state do
subject { object.get_web_hook_failure }
context 'when no value is stored' do
it { is_expected.to be_nil }
end
context 'when stored as true' do
before do
object.cache_web_hook_failure(true)
end
it { is_expected.to eq(true) }
end
context 'when stored as false' do
before do
object.cache_web_hook_failure(false)
end
it { is_expected.to eq(false) }
end
end
describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do
context 'when a value has not been stored' do
it 'does not call #any_hook_failed?' do
expect(object.get_web_hook_failure).to be_nil
expect(object).to receive(:any_hook_failed?).and_return(true)
expect(object.fetch_web_hook_failure).to eq(true)
expect(object.get_web_hook_failure).to eq(true)
end
end
context 'when a value has been stored' do
before do
object.cache_web_hook_failure(true)
end
it 'does not call #any_hook_failed?' do
expect(object).not_to receive(:any_hook_failed?)
expect(object.fetch_web_hook_failure).to eq(true)
end
end
end
end

View File

@ -5,7 +5,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('issues'.classify)}
#{all_graphql_fields_for('issues'.classify, excluded: ['relatedMergeRequests'])}
}
QUERY
end
@ -683,6 +683,28 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
end
context 'when selecting `related_merge_requests`' do
let(:fields) do
<<~QUERY
nodes {
relatedMergeRequests {
nodes {
id
}
}
}
QUERY
end
it 'limits the field to 1 execution' do
post_query
expect_graphql_errors_to_include(
'"relatedMergeRequests" field can be requested only for 1 Issue(s) at a time.'
)
end
end
it 'includes a web_url' do
post_query

View File

@ -20,7 +20,7 @@ RSpec.describe TasksToBeDone::CreateWorker do
expect(service_class)
.to receive(:new)
.with(project: member_task.project, current_user: current_user, assignee_ids: assignee_ids)
.with(container: member_task.project, current_user: current_user, assignee_ids: assignee_ids)
.and_call_original
end