Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-09 21:09:15 +00:00
parent 72c331ebf5
commit d8d0344cc3
52 changed files with 641 additions and 281 deletions

View file

@ -1 +1 @@
58bf16b78b3c99757a2f283a5befe57a2cb7f009
3822772ed121b764fdcc5011d199c37ef76e06a9

View file

@ -76,7 +76,7 @@ export default {
const inviteTo = this.isProject ? 'toProject' : 'toGroup';
return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
name: this.name.toUpperCase(),
name: this.name,
});
},
toastOptions() {
@ -215,10 +215,14 @@ export default {
searchField: s__('InviteMembersModal|GitLab member or Email address'),
placeHolder: s__('InviteMembersModal|Search for members to invite'),
toGroup: {
introText: s__("InviteMembersModal|You're inviting members to the %{name} group"),
introText: s__(
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
),
},
toProject: {
introText: s__("InviteMembersModal|You're inviting members to the %{name} project"),
introText: s__(
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
),
},
},
group: {
@ -226,10 +230,14 @@ export default {
searchField: s__('InviteMembersModal|Select a group to invite'),
placeHolder: s__('InviteMembersModal|Search for a group to invite'),
toGroup: {
introText: s__("InviteMembersModal|You're inviting a group to the %{name} group"),
introText: s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
),
},
toProject: {
introText: s__("InviteMembersModal|You're inviting a group to the %{name} project"),
introText: s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
),
},
},
accessLevel: s__('InviteMembersModal|Choose a role permission'),
@ -253,7 +261,13 @@ export default {
:header-close-label="$options.labels.headerCloseLabel"
>
<div>
<p ref="introText">{{ introText }}</p>
<p ref="introText">
<gl-sprintf :message="introText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{
$options.labels[inviteeType].searchField

View file

@ -1,13 +1,10 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlLink,
GlIcon,
},
components: { GlButton },
props: {
displayText: {
type: String,
@ -24,6 +21,11 @@ export default {
required: false,
default: '',
},
variant: {
type: String,
required: false,
default: undefined,
},
},
methods: {
openModal() {
@ -34,10 +36,13 @@ export default {
</script>
<template>
<gl-link :class="classes" data-qa-selector="invite_members_button" @click="openModal">
<div v-if="icon" class="nav-icon-container">
<gl-icon :size="16" :name="icon" />
</div>
<span class="nav-item-name"> {{ displayText }} </span>
</gl-link>
<gl-button
:class="classes"
:icon="icon"
:variant="variant"
data-qa-selector="invite_members_button"
@click="openModal"
>
{{ displayText }}
</gl-button>
</template>

View file

@ -1,11 +1,10 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
DETAILS_PAGE_TITLE,
UPDATED_AT,
CLEANUP_UNSCHEDULED_TEXT,
CLEANUP_SCHEDULED_TEXT,
@ -20,11 +19,16 @@ import {
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
} from '../../constants/index';
export default {
name: 'DetailsHeader',
components: { GlSprintf, GlButton, TitleArea, MetadataItem },
components: { GlButton, GlIcon, TitleArea, MetadataItem },
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
image: {
@ -73,9 +77,12 @@ export default {
deleteButtonDisabled() {
return this.disabled || !this.image.canDelete;
},
},
i18n: {
DETAILS_PAGE_TITLE,
rootImageTooltip() {
return !this.image.name ? ROOT_IMAGE_TOOLTIP : '';
},
imageName() {
return this.image.name || ROOT_IMAGE_TEXT;
},
},
};
</script>
@ -84,12 +91,15 @@ export default {
<title-area :metadata-loading="metadataLoading">
<template #title>
<span data-testid="title">
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ image.name }}
</template>
</gl-sprintf>
{{ imageName }}
</span>
<gl-icon
v-if="rootImageTooltip"
v-gl-tooltip="rootImageTooltip"
class="gl-text-blue-600"
name="information-o"
:aria-label="rootImageTooltip"
/>
</template>
<template #metadata-tags-count>
<metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />

View file

@ -13,6 +13,7 @@ import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
ROOT_IMAGE_TEXT,
} from '../../constants/index';
import DeleteButton from '../delete_button.vue';
@ -74,6 +75,9 @@ export default {
}
return null;
},
imageName() {
return this.item.name ? this.item.path : `${this.item.path}/ ${ROOT_IMAGE_TEXT}`;
},
},
};
</script>
@ -95,7 +99,7 @@ export default {
data-qa-selector="registry_image_content"
:to="{ name: 'details', params: { id } }"
>
{{ item.path }}
{{ imageName }}
</router-link>
<clipboard-button
v-if="item.location"

View file

@ -0,0 +1,3 @@
import { s__ } from '~/locale';
export const ROOT_IMAGE_TEXT = s__('ContainerRegistry|Root image');

View file

@ -2,7 +2,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
// Translations strings
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
export const DELETE_TAG_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while marking the tag for deletion.',
);
@ -53,7 +52,8 @@ export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
'ContainerRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
);
export const MISSING_OR_DELETE_IMAGE_BREADCRUMB = s__(
export const MISSING_OR_DELETED_IMAGE_BREADCRUMB = s__(
'ContainerRegistry|Image repository not found',
);
@ -112,6 +112,10 @@ export const FAILED_DELETION_STATUS_MESSAGE = s__(
'ContainerRegistry|This image repository has failed to be deleted',
);
export const ROOT_IMAGE_TOOLTIP = s__(
'ContainerRegistry|Image repository with no name located at the project URL.',
);
// Parameters
export const DEFAULT_PAGE = 1;

View file

@ -1,3 +1,4 @@
export * from './common';
export * from './expiration_policies';
export * from './quick_start';
export * from './list';

View file

@ -24,7 +24,8 @@ import {
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETE_IMAGE_BREADCRUMB,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
} from '../constants/index';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
@ -116,7 +117,9 @@ export default {
},
methods: {
updateBreadcrumb() {
const name = this.image?.name || MISSING_OR_DELETE_IMAGE_BREADCRUMB;
const name = this.image?.id
? this.image?.name || ROOT_IMAGE_TEXT
: MISSING_OR_DELETED_IMAGE_BREADCRUMB;
this.breadCrumbState.updateName(name);
},
deleteTags(toBeDeleted) {

View file

@ -184,7 +184,7 @@ export default {
<slot name="sub-heading"></slot>
</div>
<slot name="action-buttons"></slot>
<slot name="action-buttons" :is-collapsible="isCollapsible"></slot>
<button
v-if="isCollapsible"

View file

@ -3,7 +3,7 @@
module Emails
module Pipelines
def pipeline_success_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'Succesful')
pipeline_mail(pipeline, recipients, 'Successful')
end
def pipeline_failed_email(pipeline, recipients)

View file

@ -14,22 +14,30 @@ class Experiment < ApplicationRecord
find_or_create_by!(name: name).record_group_and_variant!(group, variant)
end
def self.record_conversion_event(name, user)
find_or_create_by!(name: name).record_conversion_event_for_user(user)
def self.record_conversion_event(name, user, context = {})
find_or_create_by!(name: name).record_conversion_event_for_user(user, context)
end
# Create or update the recorded experiment_user row for the user in this experiment.
def record_user_and_group(user, group_type, context = {})
experiment_user = experiment_users.find_or_initialize_by(user: user)
merged_context = experiment_user.context.deep_merge(context.deep_stringify_keys)
experiment_user.update!(group_type: group_type, context: merged_context)
experiment_user.update!(group_type: group_type, context: merged_context(experiment_user, context))
end
def record_conversion_event_for_user(user)
experiment_users.find_by(user: user, converted_at: nil)&.touch(:converted_at)
def record_conversion_event_for_user(user, context = {})
experiment_user = experiment_users.find_by(user: user)
return unless experiment_user
experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context))
end
def record_group_and_variant!(group, variant)
experiment_subjects.find_or_initialize_by(group: group).update!(variant: variant)
end
private
def merged_context(experiment_user, new_context)
experiment_user.context.deep_merge(new_context.deep_stringify_keys)
end
end

View file

@ -50,6 +50,7 @@ module MergeRequests
track_title_and_desc_edits(changed_fields)
track_discussion_lock_toggle(merge_request, changed_fields)
track_time_estimate_and_spend_edits(merge_request, old_timelogs, changed_fields)
track_labels_change(merge_request, old_labels)
notify_if_labels_added(merge_request, old_labels)
notify_if_mentions_added(merge_request, old_mentioned_users)
@ -113,6 +114,12 @@ module MergeRequests
merge_request_activity_counter.track_time_spent_changed_action(user: current_user) if old_timelogs != merge_request.timelogs
end
def track_labels_change(merge_request, old_labels)
return if Set.new(merge_request.labels) == Set.new(old_labels)
merge_request_activity_counter.track_labels_changed_action(user: current_user)
end
def notify_if_labels_added(merge_request, old_labels)
added_labels = merge_request.labels - old_labels
@ -191,6 +198,8 @@ module MergeRequests
return unless merge_request.previous_changes.include?('milestone_id')
merge_request_activity_counter.track_milestone_changed_action(user: current_user)
if merge_request.milestone.nil?
notification_service.async.removed_milestone_merge_request(merge_request, current_user)
else

View file

@ -18,7 +18,7 @@
.gl-w-half.gl-xs-w-full
.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- if can_manage_members && !can_invite_members_for_group?(@group)
%hr.gl-mt-4

View file

@ -2,7 +2,7 @@
%section.settings.no-animate#cleanup{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Repository cleanup')
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Repository cleanup')
%button.btn.gl-button.btn-default.js-settings-toggle
= expanded ? _('Collapse') : _('Expand')
%p

View file

@ -20,7 +20,7 @@
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'projects/invite_members_modal', project: @project
- else

View file

@ -0,0 +1,5 @@
---
title: Project Settings Repository Cleanup header expands/collapses on click / tap
merge_request: 55232
author: Daniel Schömer
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add tracking to merge request labels/milestone changes
merge_request: 55484
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Use Root Image for images with missing name
merge_request: 54693
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix typo in pipeline status email
merge_request: 55412
author: Gabriel Berke-Williams
type: fixed

View file

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_labels_changed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55484
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true

View file

@ -0,0 +1,8 @@
---
name: usage_data_i_code_review_user_milestone_changed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55484
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true

View file

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_milestone_changed_monthly
description: Count of unique users per month who changed milestone of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55484
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View file

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_labels_changed_monthly
description: Count of unique users per month who changed labels of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55484
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View file

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_milestone_changed_weekly
description: Count of unique users per week who changed milestone of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55484
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View file

@ -0,0 +1,20 @@
---
key_path: redis_hll_counters.code_review.i_code_review_user_labels_changed_weekly
description: Count of unique users per week who changed labels of a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55484
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View file

@ -354,3 +354,13 @@ These are aliases for `scope` and `expires_in` respectively, and have been inclu
prevent breaking changes introduced in [doorkeeper 5.0.2](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions#from-4x-to-5x).
Don't rely on these fields as they will be removed in a later release.
## OAuth2 tokens and GitLab registries
Standard OAuth2 tokens support different degrees of access to GitLab registries, as they:
- Do not allow users to authenticate to:
- The GitLab [Container registry](../user/packages/container_registry/index.md#authenticate-with-the-container-registry).
- Packages listed in the GitLab [Package registry](../user/packages/package_registry/index.md).
- Allow users to get, list, and delete registries through
the [Container registry API](container_registry.md).

View file

@ -9,7 +9,9 @@ type: reference, api
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6373) in GitLab 8.15.
Snippets API operates on [snippets](../user/snippets.md).
Snippets API operates on [snippets](../user/snippets.md). Related APIs exist for
[project snippets](project_snippets.md) and
[moving snippets between storages](snippet_repository_storage_moves.md).
## Snippet visibility level
@ -289,14 +291,12 @@ Parameters:
| `content` | string | no | Deprecated: Use `files` instead. Content of a snippet |
| `description` | string | no | Description of a snippet |
| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level) |
| `files` | array of hashes | no | An array of snippet files |
| `files` | array of hashes | sometimes | An array of snippet files. Required when updating snippets with multiple files. |
| `files:action` | string | yes | Type of action to perform on the file, one of: 'create', 'update', 'delete', 'move' |
| `files:file_path` | string | no | File path of the snippet file |
| `files:previous_path` | string | no | Previous path of the snippet file |
| `files:content` | string | no | Content of the snippet file |
Updates to snippets with multiple files *must* use the `files` attribute.
Example request:
```shell

View file

@ -8468,6 +8468,30 @@ Status: `data_available`
Tiers:
### `redis_hll_counters.code_review.i_code_review_user_labels_changed_monthly`
Count of unique users per month who changed labels of a MR
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210302110607_i_code_review_user_labels_changed_monthly.yml)
Group: `group::code review`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_labels_changed_weekly`
Count of unique users per week who changed labels of a MR
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210302110548_i_code_review_user_labels_changed_weekly.yml)
Group: `group::code review`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_marked_as_draft_monthly`
Missing description
@ -8516,6 +8540,30 @@ Status: `data_available`
Tiers:
### `redis_hll_counters.code_review.i_code_review_user_milestone_changed_monthly`
Count of unique users per month who changed milestone of a MR
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210302110520_i_code_review_user_milestone_changed_monthly.yml)
Group: `group::code review`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_milestone_changed_weekly`
Count of unique users per week who changed milestone of a MR
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210302110403_i_code_review_user_milestone_changed_weekly.yml)
Group: `group::code review`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly`
Count of unique users per month who locked a MR

View file

@ -246,7 +246,7 @@ To move an issue to another epic:
If you have the necessary [permissions](../../permissions.md) to close an issue and create an
epic in the immediate parent group, you can promote an issue to an epic with the `/promote`
[quick action](../../project/quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
[quick action](../../project/quick_actions.md#issues-merge-requests-and-epics).
Only issues from projects that are in groups can be promoted. When you attempt to promote a confidential
issue, a warning is displayed. Promoting a confidential issue to an epic makes all information
related to the issue public as epics are public to group members.

View file

@ -17,7 +17,7 @@ team members can join swiftly without requesting a link.
## Adding a Zoom meeting to an issue
To associate a Zoom meeting with an issue, you can use GitLab
[quick actions](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
[quick actions](../quick_actions.md#issues-merge-requests-and-epics).
In an issue, leave a comment using the `/zoom` quick action followed by a valid Zoom link:

View file

@ -298,7 +298,7 @@ To promote an issue to an epic:
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**) button.
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
Read more about promoting an issue to an epic on the [Manage epics page](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic).
@ -313,5 +313,5 @@ To add an issue to an [iteration](../../group/iterations/index.md):
1. Click an iteration you'd like to associate this issue with.
You can also use the `/iteration`
[quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics)
[quick action](../quick_actions.md#issues-merge-requests-and-epics)
in a comment or description field.

View file

@ -27,7 +27,7 @@ There are several ways to flag a merge request as a draft:
the beginning of the merge request's title, or click **Start the title with Draft:**
below the **Title** field.
- **Commenting in an existing merge request**: Add the `/draft`
[quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics)
[quick action](../quick_actions.md#issues-merge-requests-and-epics)
in a comment. This quick action is a toggle, and can be repeated to change the status
again. This quick action discards any other text in the comment.
- **Creating a commit**: Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the
@ -53,7 +53,7 @@ When a merge request is ready to be merged, you can remove the `Draft` flag in s
from the beginning of the title, or click **Remove the Draft: prefix from the title**
below the **Title** field.
- **Commenting in an existing merge request**: Add the `/draft`
[quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics)
[quick action](../quick_actions.md#issues-merge-requests-and-epics)
in a comment in the merge request. This quick action is a toggle, and can be repeated
to change the status back. This quick action discards any other text in the comment.

View file

@ -110,7 +110,7 @@ dropdown menu.
It is also possible to manage multiple assignees:
- When creating a merge request.
- Using [quick actions](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
- Using [quick actions](../quick_actions.md#issues-merge-requests-and-epics).
### Reviewer

View file

@ -5,7 +5,7 @@ group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# GitLab Quick Actions
# GitLab quick actions **(FREE)**
> - Introduced in [GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/26672):
> once an action is executed, an alert appears when a quick action is successfully applied.
@ -15,115 +15,113 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> `/` into a description or comment field, all available quick actions are displayed in a scrollable list.
> - The rebase quick action was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49800) in GitLab 13.8.
Quick actions are textual shortcuts for common actions on issues, epics, merge requests,
and commits that are usually done by clicking buttons or dropdowns in the GitLab UI.
You can enter these commands in the description or in comments of issues, epics, merge requests, and commits.
Each command should be on a separate line in order to be properly detected and executed.
Quick actions are text-based shortcuts for common actions that are usually done
by selecting buttons or dropdowns in the GitLab user interface. You can enter
these commands in the descriptions or comments of issues, epics, merge requests,
and commits.
## Quick Actions for issues, merge requests and epics
Be sure to enter each quick action on a separate line to allow GitLab to
properly detect and execute the commands.
The following quick actions are applicable to descriptions, discussions and threads in:
## Parameters
- Issues
- Merge requests
- Epics **(PREMIUM)**
Many quick actions require a parameter. For example, the `/assign` quick action
requires a username. GitLab uses [autocomplete characters](autocomplete_characters.md)
with quick actions to help users enter parameters, by providing a list of
available values.
| Command | Issue | Merge request | Epic | Action |
| :------------------------------------ | :---- | :------------ | :--- | :------------------------------------------------------------------------------------------------------------------------------ |
| `/approve` | | ✓ | | Approve the merge request. **(STARTER)** |
| `/assign @user` | ✓ | ✓ | | Assign one user. |
| `/assign @user1 @user2` | ✓ | ✓ | | Assign multiple users. **(STARTER)** |
| `/assign me` | ✓ | ✓ | | Assign yourself. |
| `/assign_reviewer @user` or `/reviewer @user` or `/request_review @user` | | ✓ | | Assign one user as a reviewer. |
| `/assign_reviewer @user1 @user2` or `/reviewer @user1 @user2` or `/request_review @user1 @user2` | | ✓ | | Assign multiple users as reviewers. **(STARTER)** |
| `/assign_reviewer me` or `/reviewer me` or `/request_review me` | | ✓ | | Assign yourself as a reviewer. |
| `/award :emoji:` | ✓ | ✓ | ✓ | Toggle emoji award. |
| `/child_epic <epic>` | | | ✓ | Add child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). **(ULTIMATE)** |
| `/clear_weight` | ✓ | | | Clear weight. **(STARTER)** |
| `/clone <path/to/project> [--with_notes]`| ✓ | | | Clone the issue to given project, or the current one if no arguments are given ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9421) in GitLab 13.7). Copies as much data as possible as long as the target project contains equivalent labels, milestones, and so on. Does not copy comments or system notes unless `--with_notes` is provided as an argument. |
| `/close` | ✓ | ✓ | ✓ | Close. |
| `/confidential` | ✓ | | | Make confidential. |
| `/copy_metadata <!merge_request>` | ✓ | ✓ | | Copy labels and milestone from another merge request in the project. |
| `/copy_metadata <#issue>` | ✓ | ✓ | | Copy labels and milestone from another issue in the project. |
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue. |
| `/done` | ✓ | ✓ | ✓ | Mark to do as done. |
| `/draft` | | ✓ | | Toggle the draft status. |
| `/due <date>` | ✓ | | | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
| `/duplicate <#issue>` | ✓ | | | Close this issue and mark as a duplicate of another issue. **(FREE)** Also, mark both as related. **(STARTER)** |
| `/epic <epic>` | ✓ | | | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. **(PREMIUM)** |
| `/estimate <<W>w <DD>d <hh>h <mm>m>` | ✓ | ✓ | | Set time estimate. For example, `/estimate 1w 3d 2h 14m`. |
| `/invite_email email1 email2` | ✓ | | | Add up to 6 e-mail participants. This action is behind feature flag `issue_email_participants` |
| `/iteration *iteration:"iteration name"` | ✓ | | | Set iteration. For example, to set the `Late in July` iteration: `/iteration *iteration:"Late in July"` ([introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)). **(STARTER)** |
| `/label ~label1 ~label2` | ✓ | ✓ | ✓ | Add one or more labels. Label names can also start without a tilde (`~`), but mixed syntax is not supported. |
| `/lock` | ✓ | ✓ | | Lock the discussions. |
| `/merge` | | ✓ | | Merge changes. Depending on the project setting, this may be [when the pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md), adding to a [Merge Train](../../ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md), etc. |
| `/milestone %milestone` | ✓ | ✓ | | Set milestone. |
| `/move <path/to/project>` | ✓ | | | Move this issue to another project. |
| `/parent_epic <epic>` | | | ✓ | Set parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/-/issues/10556)). **(ULTIMATE)** |
| `/promote` | ✓ | | | Promote issue to epic. **(PREMIUM)** |
| `/publish` | ✓ | | | Publish issue to an associated [Status Page](../../operations/incident_management/status_page.md) ([Introduced in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906)) **(ULTIMATE)** |
| `/reassign @user1 @user2` | ✓ | ✓ | | Replace current assignees with those specified. **(STARTER)** |
| `/rebase` | | ✓ | | Rebase source branch. This schedules a background task that attempts to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` is ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. If there are merge conflicts, GitLab displays a message that a rebase cannot be scheduled. Rebase failures are displayed with the merge request status. |
| `/reassign_reviewer @user1 @user2` | | ✓ | | Replace current reviewers with those specified. **(STARTER)** |
| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace current labels with those specified. |
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related. **(STARTER)** |
| `/remove_child_epic <epic>` | | | ✓ | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). **(ULTIMATE)** |
| `/remove_due_date` | ✓ | | | Remove due date. |
| `/remove_epic` | ✓ | | | Remove from epic. **(PREMIUM)** |
| `/remove_estimate` | ✓ | ✓ | | Remove time estimate. |
| `/remove_iteration` | ✓ | | | Remove iteration ([introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)) **(STARTER)** |
| `/remove_milestone` | ✓ | ✓ | | Remove milestone. |
| `/remove_parent_epic` | | | ✓ | Remove parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/-/issues/10556)). **(ULTIMATE)** |
| `/remove_time_spent` | ✓ | ✓ | | Remove time spent. |
| `/remove_zoom` | ✓ | | | Remove Zoom meeting from this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). |
| `/reopen` | ✓ | ✓ | ✓ | Reopen. |
| `/shrug <comment>` | ✓ | ✓ | ✓ | Append the comment with `¯\_(ツ)_/¯`. |
| `/spend <time(-<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | ✓ | ✓ | | Subtract spent time. Optionally, specify the date that time was spent on. For example, `/spend time(-1h 30m)` or `/spend time(-1h 30m) date(2018-08-26)`. |
| `/spend <time(<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | ✓ | ✓ | | Add spent time. Optionally, specify the date that time was spent on. For example, `/spend time(1h 30m)` or `/spend time(1h 30m) date(2018-08-26)`. |
| `/submit_review` | | ✓ | | Submit a pending review ([introduced in GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/issues/8041)). **(PREMIUM)** |
| `/subscribe` | ✓ | ✓ | ✓ | Subscribe to notifications. |
| `/tableflip <comment>` | ✓ | ✓ | ✓ | Append the comment with `(╯°□°)╯︵ ┻━┻`. |
| `/target_branch <local branch name>` | | ✓ | | Set target branch. |
| `/title <new title>` | ✓ | ✓ | ✓ | Change title. |
| `/todo` | ✓ | ✓ | ✓ | Add a to-do item. |
| `/unassign @user1 @user2` | ✓ | ✓ | | Remove specific assignees. **(STARTER)** |
| `/unassign` | | ✓ | | Remove all assignees. |
| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | | ✓ | | Remove specific reviewers. **(STARTER)** |
| `/unassign_reviewer` or `/remove_reviewer` | | ✓ | | Remove all reviewers. |
| `/unlabel ~label1 ~label2` or `/remove_label ~label1 ~label2` | ✓ | ✓ | ✓ | Remove specified labels. |
| `/unlabel` or `/remove_label` | ✓ | ✓ | ✓ | Remove all labels. |
| `/unlock` | ✓ | ✓ | | Unlock the discussions. |
| `/unsubscribe` | ✓ | ✓ | ✓ | Unsubscribe from notifications. |
| `/weight <value>` | ✓ | | | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on. **(STARTER)** |
| `/wip` | | ✓ | | Toggle the draft status. |
| `/zoom <Zoom URL>` | ✓ | | | Add Zoom meeting to this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). |
## Autocomplete characters
Many quick actions require a parameter, for example: username, milestone, and
label. [Autocomplete characters](autocomplete_characters.md) can make it easier
to enter a parameter, compared to selecting items from a list.
## Quick actions parameters
The easiest way to set parameters for quick actions is to use autocomplete. If
you manually enter a parameter, it must be enclosed in double quotation marks
If you manually enter a parameter, it must be enclosed in double quotation marks
(`"`), unless it contains only these characters:
1. ASCII letters.
1. Numerals (0-9).
1. Underscore (`_`), hyphen (`-`), question mark (`?`), dot (`.`), or ampersand (`&`).
- ASCII letters
- Numbers (0-9)
- Underscore (`_`), hyphen (`-`), question mark (`?`), dot (`.`), or ampersand (`&`)
Parameters are also case-sensitive. Autocomplete handles this, and the insertion
Parameters are case-sensitive. Autocomplete handles this, and the insertion
of quotation marks, automatically.
## Quick actions for commit messages
## Issues, merge requests, and epics
The following quick actions are applicable to descriptions, discussions, and
threads. Some quick actions might not be available to all subscription tiers.
| Command | Issue | Merge request | Epic | Action |
|:--------------------------------------|:-----------------------|:-----------------------|:-----------------------|:-------|
| `/approve` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Approve the merge request. |
| `/assign @user` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Assign one user. |
| `/assign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Assign multiple users. |
| `/assign me` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Assign yourself. |
| `/assign_reviewer @user` or `/reviewer @user` or `/request_review @user` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Assign one user as a reviewer. |
| `/assign_reviewer @user1 @user2` or `/reviewer @user1 @user2` or `/request_review @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Assign multiple users as reviewers. |
| `/assign_reviewer me` or `/reviewer me` or `/request_review me` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Assign yourself as a reviewer. |
| `/award :emoji:` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Toggle emoji award. |
| `/child_epic <epic>` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Add child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). |
| `/clear_weight` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Clear weight. |
| `/clone <path/to/project> [--with_notes]`| **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Clone the issue to given project, or the current one if no arguments are given ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9421) in GitLab 13.7). Copies as much data as possible as long as the target project contains equivalent labels, milestones, and so on. Does not copy comments or system notes unless `--with_notes` is provided as an argument. |
| `/close` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Close. |
| `/confidential` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Make confidential. |
| `/copy_metadata <!merge_request>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Copy labels and milestone from another merge request in the project. |
| `/copy_metadata <#issue>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Copy labels and milestone from another issue in the project. |
| `/create_merge_request <branch name>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Create a new merge request starting from the current issue. |
| `/done` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Mark to do as done. |
| `/draft` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Toggle the draft status. |
| `/due <date>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
| `/duplicate <#issue>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Close this issue and mark as a duplicate of another issue. **(FREE)** Also, mark both as related. |
| `/epic <epic>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. |
| `/estimate <<W>w <DD>d <hh>h <mm>m>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Set time estimate. For example, `/estimate 1w 3d 2h 14m`. |
| `/invite_email email1 email2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add up to six email participants. This action is behind feature flag `issue_email_participants`. |
| `/iteration *iteration:"iteration name"` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set iteration. For example, to set the `Late in July` iteration: `/iteration *iteration:"Late in July"` ([introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)). |
| `/label ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Add one or more labels. Label names can also start without a tilde (`~`), but mixed syntax is not supported. |
| `/lock` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Lock the discussions. |
| `/merge` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Merge changes. Depending on the project setting, this may be [when the pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md), or adding to a [Merge Train](../../ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md). |
| `/milestone %milestone` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Set milestone. |
| `/move <path/to/project>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Move this issue to another project. |
| `/parent_epic <epic>` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Set parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/-/issues/10556)). |
| `/promote` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Promote issue to epic. |
| `/publish` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Publish issue to an associated [Status Page](../../operations/incident_management/status_page.md) ([Introduced in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906)) |
| `/reassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Replace current assignees with those specified. |
| `/rebase` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Rebase source branch. This schedules a background task that attempts to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` is ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. If there are merge conflicts, GitLab displays a message that a rebase cannot be scheduled. Rebase failures are displayed with the merge request status. |
| `/reassign_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Replace current reviewers with those specified. |
| `/relabel ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Replace current labels with those specified. |
| `/relate #issue1 #issue2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Mark issues as related. |
| `/remove_child_epic <epic>` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab/-/issues/7330)). |
| `/remove_due_date` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove due date. |
| `/remove_epic` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove from epic. |
| `/remove_estimate` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove time estimate. |
| `/remove_iteration` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove iteration ([introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)). |
| `/remove_milestone` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove milestone. |
| `/remove_parent_epic` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Remove parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/-/issues/10556)). |
| `/remove_time_spent` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove time spent. |
| `/remove_zoom` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove Zoom meeting from this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). |
| `/reopen` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Reopen. |
| `/shrug <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `¯\_(ツ)_/¯`. |
| `/spend <time(-<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Subtract spent time. Optionally, specify the date that time was spent on. For example, `/spend time(-1h 30m)` or `/spend time(-1h 30m) date(2018-08-26)`. |
| `/spend <time(<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Add spent time. Optionally, specify the date that time was spent on. For example, `/spend time(1h 30m)` or `/spend time(1h 30m) date(2018-08-26)`. |
| `/submit_review` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Submit a pending review ([introduced in GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/issues/8041)). |
| `/subscribe` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Subscribe to notifications. |
| `/tableflip <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `(╯°□°)╯︵ ┻━┻`. |
| `/target_branch <local branch name>` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Set target branch. |
| `/title <new title>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Change title. |
| `/todo` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Add a to-do item. |
| `/unassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific assignees. |
| `/unassign` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all assignees. |
| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific reviewers. |
| `/unassign_reviewer` or `/remove_reviewer` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all reviewers. |
| `/unlabel ~label1 ~label2` or `/remove_label ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove specified labels. |
| `/unlabel` or `/remove_label` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove all labels. |
| `/unlock` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Unlock the discussions. |
| `/unsubscribe` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Unsubscribe from notifications. |
| `/weight <value>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on. |
| `/wip` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Toggle the draft status. |
| `/zoom <Zoom URL>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add Zoom meeting to this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). |
## Commit messages
The following quick actions are applicable for commit messages:
| Command | Action |
| :---------------------- | :---------------------------------------- |
| `/tag v1.2.3 <message>` | Tags this commit with an optional message |
|:----------------------- |:------------------------------------------|
| `/tag v1.2.3 <message>` | Tags the commit with an optional message. |
<!-- ## Troubleshooting

View file

@ -72,12 +72,12 @@ module Gitlab
::Experiment.add_user(experiment_key, tracking_group(experiment_key, nil, subject: subject), current_user, context)
end
def record_experiment_conversion_event(experiment_key)
def record_experiment_conversion_event(experiment_key, context = {})
return if dnt_enabled?
return unless current_user
return unless Experimentation.active?(experiment_key)
::Experiment.record_conversion_event(experiment_key, current_user)
::Experiment.record_conversion_event(experiment_key, current_user, context)
end
def experiment_tracking_category_and_group(experiment_key, subject: nil)

View file

@ -49,7 +49,9 @@
'i_code_review_user_time_estimate_changed',
'i_code_review_user_time_spent_changed',
'i_code_review_user_assignees_changed',
'i_code_review_user_reviewers_changed'
'i_code_review_user_reviewers_changed',
'i_code_review_user_milestone_changed',
'i_code_review_user_labels_changed'
]
- name: code_review_category_monthly_active_users
operator: OR
@ -92,7 +94,9 @@
'i_code_review_user_time_estimate_changed',
'i_code_review_user_time_spent_changed',
'i_code_review_user_assignees_changed',
'i_code_review_user_reviewers_changed'
'i_code_review_user_reviewers_changed',
'i_code_review_user_milestone_changed',
'i_code_review_user_labels_changed'
]
- name: code_review_extension_category_monthly_active_users
operator: OR

View file

@ -194,3 +194,13 @@
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_reviewers_changed
- name: i_code_review_user_milestone_changed
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_milestone_changed
- name: i_code_review_user_labels_changed
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_labels_changed

View file

@ -42,6 +42,8 @@ module Gitlab
MR_ASSIGNEES_CHANGED_ACTION = 'i_code_review_user_assignees_changed'
MR_REVIEWERS_CHANGED_ACTION = 'i_code_review_user_reviewers_changed'
MR_INCLUDING_CI_CONFIG_ACTION = 'o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile'
MR_MILESTONE_CHANGED_ACTION = 'i_code_review_user_milestone_changed'
MR_LABELS_CHANGED_ACTION = 'i_code_review_user_labels_changed'
class << self
def track_mr_diffs_action(merge_request:)
@ -191,6 +193,14 @@ module Gitlab
track_unique_action_by_user(MR_INCLUDING_CI_CONFIG_ACTION, user)
end
def track_milestone_changed_action(user:)
track_unique_action_by_user(MR_MILESTONE_CHANGED_ACTION, user)
end
def track_labels_changed_action(user:)
track_unique_action_by_user(MR_LABELS_CHANGED_ACTION, user)
end
private
def track_unique_action_by_merge_request(action, merge_request)

View file

@ -7955,9 +7955,6 @@ msgid_plural "ContainerRegistry|%{count} Tags"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
msgid "ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted."
msgstr ""
@ -8060,6 +8057,9 @@ msgstr ""
msgid "ContainerRegistry|Image repository will be deleted"
msgstr ""
msgid "ContainerRegistry|Image repository with no name located at the project URL."
msgstr ""
msgid "ContainerRegistry|Image tags"
msgstr ""
@ -8125,6 +8125,9 @@ msgstr ""
msgid "ContainerRegistry|Remove these tags"
msgstr ""
msgid "ContainerRegistry|Root image"
msgstr ""
msgid "ContainerRegistry|Run cleanup:"
msgstr ""
@ -16681,16 +16684,16 @@ msgstr ""
msgid "InviteMembersModal|Some of the members could not be added"
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{name} group"
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{name} project"
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project."
msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{name} group"
msgid "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group."
msgstr ""
msgid "InviteMembersModal|You're inviting members to the %{name} project"
msgid "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project."
msgstr ""
msgid "InviteMembers|Invite a group"

View file

@ -67,7 +67,13 @@ RSpec.describe 'Container Registry', :js do
end
it 'shows the image title' do
expect(page).to have_content 'my/image tags'
expect(page).to have_content 'my/image'
end
it 'shows the image tags' do
expect(page).to have_content 'Image tags'
first_tag = first('[data-testid="name"]')
expect(first_tag).to have_content 'latest'
end
it 'user removes a specific tag from container repository' do

View file

@ -82,7 +82,13 @@ RSpec.describe 'Container Registry', :js do
end
it 'shows the image title' do
expect(page).to have_content 'my/image tags'
expect(page).to have_content 'my/image'
end
it 'shows the image tags' do
expect(page).to have_content 'Image tags'
first_tag = first('[data-testid="name"]')
expect(first_tag).to have_content '1'
end
it 'user removes a specific tag from container repository' do

View file

@ -129,7 +129,7 @@ describe('InviteMembersModal', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteMembersToProjectWrapper();
expect(findIntroText()).toBe("You're inviting members to the TEST NAME project");
expect(findIntroText()).toBe("You're inviting members to the test name project.");
});
});
@ -137,7 +137,7 @@ describe('InviteMembersModal', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteGroupToProjectWrapper();
expect(findIntroText()).toBe("You're inviting a group to the TEST NAME project");
expect(findIntroText()).toBe("You're inviting a group to the test name project.");
});
});
});
@ -147,7 +147,7 @@ describe('InviteMembersModal', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteMembersToGroupWrapper();
expect(wrapper.html()).toContain("You're inviting members to the TEST NAME group");
expect(findIntroText()).toBe("You're inviting members to the test name group.");
});
});
@ -155,7 +155,7 @@ describe('InviteMembersModal', () => {
it('includes the correct invitee, type, and formatted name', () => {
wrapper = createInviteGroupToGroupWrapper();
expect(wrapper.html()).toContain("You're inviting a group to the TEST NAME group");
expect(findIntroText()).toBe("You're inviting a group to the test name group.");
});
});
});

View file

@ -1,9 +1,8 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
const displayText = 'Invite team members';
const icon = 'plus';
const createComponent = (props = {}) => {
return shallowMount(InviteMembersTrigger, {
@ -23,36 +22,14 @@ describe('InviteMembersTrigger', () => {
});
describe('displayText', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
wrapper = createComponent();
});
it('includes the correct displayText for the link', () => {
expect(findLink().text()).toBe(displayText);
});
});
describe('icon', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
it('includes the correct icon when an icon is sent', () => {
wrapper = createComponent({ icon });
expect(findIcon().attributes('name')).toBe(icon);
});
it('does not include an icon when icon is not sent', () => {
wrapper = createComponent();
expect(findIcon().exists()).toBe(false);
});
it('does not include an icon when empty string is sent', () => {
wrapper = createComponent({ icon: '' });
expect(findIcon().exists()).toBe(false);
it('includes the correct displayText for the button', () => {
expect(findButton().text()).toBe(displayText);
});
});
});

View file

@ -1,9 +1,9 @@
import { GlSprintf, GlButton } from '@gitlab/ui';
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/registry/explorer/components/details_page/details_header.vue';
import {
DETAILS_PAGE_TITLE,
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
@ -13,6 +13,8 @@ import {
CLEANUP_SCHEDULED_TOOLTIP,
CLEANUP_ONGOING_TOOLTIP,
CLEANUP_UNFINISHED_TOOLTIP,
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
} from '~/registry/explorer/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@ -41,6 +43,7 @@ describe('Details Header', () => {
const findTagsCount = () => findByTestId('tags-count');
const findCleanup = () => findByTestId('cleanup');
const findDeleteButton = () => wrapper.find(GlButton);
const findInfoIcon = () => wrapper.find(GlIcon);
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@ -51,8 +54,10 @@ describe('Details Header', () => {
const mountComponent = (propsData = { image: defaultImage }) => {
wrapper = shallowMount(component, {
propsData,
directives: {
GlTooltip: createMockDirective(),
},
stubs: {
GlSprintf,
TitleArea,
},
});
@ -62,15 +67,41 @@ describe('Details Header', () => {
wrapper.destroy();
wrapper = null;
});
describe('image name', () => {
describe('missing image name', () => {
it('root image ', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
it('has the correct title ', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
expect(findTitle().text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
});
expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
});
it('shows imageName in the title', () => {
mountComponent();
expect(findTitle().text()).toContain('foo');
it('has an icon', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
expect(findInfoIcon().exists()).toBe(true);
expect(findInfoIcon().props('name')).toBe('information-o');
});
it('has a tooltip', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip');
expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP);
});
});
describe('with image name present', () => {
it('shows image.name ', () => {
mountComponent();
expect(findTitle().text()).toContain('foo');
});
it('has no icon', () => {
mountComponent();
expect(findInfoIcon().exists()).toBe(false);
});
});
});
describe('delete button', () => {

View file

@ -12,6 +12,7 @@ import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
ROOT_IMAGE_TEXT,
} from '~/registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
@ -73,8 +74,8 @@ describe('Image List Row', () => {
mountComponent();
const link = findDetailsLink();
expect(link.html()).toContain(item.path);
expect(link.props('to')).toMatchObject({
expect(link.text()).toBe(item.path);
expect(findDetailsLink().props('to')).toMatchObject({
name: 'details',
params: {
id: getIdFromGraphQLId(item.id),
@ -82,6 +83,12 @@ describe('Image List Row', () => {
});
});
it(`when the image has no name appends ${ROOT_IMAGE_TEXT} to the path`, () => {
mountComponent({ item: { ...item, name: '' } });
expect(findDetailsLink().text()).toBe(`${item.path}/ ${ROOT_IMAGE_TEXT}`);
});
it('contains a clipboard button', () => {
mountComponent();
const button = findClipboardButton();

View file

@ -17,6 +17,8 @@ import {
UNFINISHED_STATUS,
DELETE_SCHEDULED,
ALERT_DANGER_IMAGE,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
} from '~/registry/explorer/constants';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
@ -515,6 +517,26 @@ describe('Details Page', () => {
expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name);
});
it(`when the image is missing set the breadcrumb to ${MISSING_OR_DELETED_IMAGE_BREADCRUMB}`, async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
await waitForApolloRequestRender();
expect(breadCrumbState.updateName).toHaveBeenCalledWith(MISSING_OR_DELETED_IMAGE_BREADCRUMB);
});
it(`when the image has no name set the breadcrumb to ${ROOT_IMAGE_TEXT}`, async () => {
mountComponent({
resolver: jest
.fn()
.mockResolvedValue(graphQLImageDetailsMock({ ...containerRepositoryMock, name: null })),
});
await waitForApolloRequestRender();
expect(breadCrumbState.updateName).toHaveBeenCalledWith(ROOT_IMAGE_TEXT);
});
});
describe('when the image has a status different from null', () => {

View file

@ -534,7 +534,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
it 'records the conversion event for the experiment & user' do
expect(::Experiment).to receive(:record_conversion_event).with(:test_experiment, user)
expect(::Experiment).to receive(:record_conversion_event).with(:test_experiment, user, {})
record_conversion_event
end

View file

@ -370,4 +370,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
it_behaves_like 'not tracked merge request unique event'
end
end
describe '.track_milestone_changed_action' do
subject { described_class.track_milestone_changed_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_MILESTONE_CHANGED_ACTION }
end
end
describe '.track_labels_changed_action' do
subject { described_class.track_labels_changed_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_LABELS_CHANGED_ACTION }
end
end
end

View file

@ -89,7 +89,7 @@ RSpec.describe Emails::Pipelines do
let(:sha) { project.commit(ref).sha }
it_behaves_like 'correct pipeline information' do
let(:status) { 'Succesful' }
let(:status) { 'Successful' }
let(:status_text) { "Pipeline ##{pipeline.id} has passed!" }
end
end

View file

@ -98,10 +98,11 @@ RSpec.describe Experiment do
describe '.record_conversion_event' do
let_it_be(:user) { build(:user) }
let_it_be(:context) { { a: 42 } }
let(:experiment_key) { :test_experiment }
subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user) }
subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user, context) }
context 'when no matching experiment exists' do
it 'creates the experiment and uses it' do
@ -127,22 +128,79 @@ RSpec.describe Experiment do
it 'sends record_conversion_event_for_user to the experiment instance' do
expect_next_found_instance_of(described_class) do |experiment|
expect(experiment).to receive(:record_conversion_event_for_user).with(user)
expect(experiment).to receive(:record_conversion_event_for_user).with(user, context)
end
record_conversion_event
end
end
end
shared_examples 'experiment user with context' do
let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } }
let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } }
before do
subject
experiment.record_user_and_group(user, :experimental, {})
end
it 'has an initial context with stringified keys' do
expect(ExperimentUser.last.context).to eq(initial_expected_context)
end
context 'when updated' do
before do
subject
experiment.record_user_and_group(user, :experimental, new_context)
end
context 'with an empty context' do
let_it_be(:new_context) { {} }
it 'keeps the initial context' do
expect(ExperimentUser.last.context).to eq(initial_expected_context)
end
end
context 'with string keys' do
let_it_be(:new_context) { { f: :some_symbol } }
it 'adds new symbols stringified' do
expected_context = initial_expected_context.merge('f' => 'some_symbol')
expect(ExperimentUser.last.context).to eq(expected_context)
end
end
context 'with atomic values or array values' do
let_it_be(:new_context) { { b: 97, d: [99] } }
it 'overrides the values' do
expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] }
expect(ExperimentUser.last.context).to eq(expected_context)
end
end
context 'with nested hashes' do
let_it_be(:new_context) { { c: { g: 107 } } }
it 'inserts nested additional values in the same keys' do
expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 })
expect(ExperimentUser.last.context).to eq(expected_context)
end
end
end
end
describe '#record_conversion_event_for_user' do
let_it_be(:user) { create(:user) }
let_it_be(:experiment) { create(:experiment) }
let_it_be(:context) { { a: 42 } }
subject(:record_conversion_event_for_user) { experiment.record_conversion_event_for_user(user) }
subject { experiment.record_conversion_event_for_user(user, context) }
context 'when no existing experiment_user record exists for the given user' do
it 'does not update or create an experiment_user record' do
expect { record_conversion_event_for_user }.not_to change { ExperimentUser.all.to_a }
expect { subject }.not_to change { ExperimentUser.all.to_a }
end
end
@ -151,7 +209,13 @@ RSpec.describe Experiment do
let!(:experiment_user) { create(:experiment_user, experiment: experiment, user: user, converted_at: 2.days.ago) }
it 'does not update the converted_at value' do
expect { record_conversion_event_for_user }.not_to change { experiment_user.converted_at }
expect { subject }.not_to change { experiment_user.converted_at }
end
it_behaves_like 'experiment user with context' do
before do
experiment.record_user_and_group(user, :experimental, context)
end
end
end
@ -159,7 +223,13 @@ RSpec.describe Experiment do
let(:experiment_user) { create(:experiment_user, experiment: experiment, user: user) }
it 'updates the converted_at value' do
expect { record_conversion_event_for_user }.to change { experiment_user.reload.converted_at }
expect { subject }.to change { experiment_user.reload.converted_at }
end
it_behaves_like 'experiment user with context' do
before do
experiment.record_user_and_group(user, :experimental, context)
end
end
end
end
@ -196,24 +266,25 @@ RSpec.describe Experiment do
describe '#record_user_and_group' do
let_it_be(:experiment) { create(:experiment) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { :control }
let_it_be(:context) { { a: 42 } }
let(:group) { :control }
let(:context) { { a: 42 } }
subject(:record_user_and_group) { experiment.record_user_and_group(user, group, context) }
subject { experiment.record_user_and_group(user, group, context) }
context 'when an experiment_user does not yet exist for the given user' do
it 'creates a new experiment_user record' do
expect { record_user_and_group }.to change(ExperimentUser, :count).by(1)
expect { subject }.to change(ExperimentUser, :count).by(1)
end
it 'assigns the correct group_type to the experiment_user' do
record_user_and_group
subject
expect(ExperimentUser.last.group_type).to eq('control')
end
it 'adds the correct context to the experiment_user' do
record_user_and_group
subject
expect(ExperimentUser.last.context).to eq({ 'a' => 42 })
end
end
@ -225,72 +296,18 @@ RSpec.describe Experiment do
end
it 'does not create a new experiment_user record' do
expect { record_user_and_group }.not_to change(ExperimentUser, :count)
expect { subject }.not_to change(ExperimentUser, :count)
end
context 'but the group_type and context has changed' do
let(:group) { :experimental }
it 'updates the existing experiment_user record with group_type' do
expect { record_user_and_group }.to change { ExperimentUser.last.group_type }
expect { subject }.to change { ExperimentUser.last.group_type }
end
end
end
context 'when a context already exists' do
let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } }
let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } }
before do
record_user_and_group
experiment.record_user_and_group(user, :control, {})
end
it 'has an initial context with stringified keys' do
expect(ExperimentUser.last.context).to eq(initial_expected_context)
end
context 'when updated' do
before do
record_user_and_group
experiment.record_user_and_group(user, :control, new_context)
end
context 'with an empty context' do
let_it_be(:new_context) { {} }
it 'keeps the initial context' do
expect(ExperimentUser.last.context).to eq(initial_expected_context)
end
end
context 'with string keys' do
let_it_be(:new_context) { { f: :some_symbol } }
it 'adds new symbols stringified' do
expected_context = initial_expected_context.merge('f' => 'some_symbol')
expect(ExperimentUser.last.context).to eq(expected_context)
end
end
context 'with atomic values or array values' do
let_it_be(:new_context) { { b: 97, d: [99] } }
it 'overrides the values' do
expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] }
expect(ExperimentUser.last.context).to eq(expected_context)
end
end
context 'with nested hashes' do
let_it_be(:new_context) { { c: { g: 107 } } }
it 'inserts nested additional values in the same keys' do
expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 })
expect(ExperimentUser.last.context).to eq(expected_context)
end
end
end
it_behaves_like 'experiment user with context'
end
end
end

View file

@ -187,6 +187,24 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
it 'tracks milestone change' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_milestone_changed_action).once.with(user: user)
opts[:milestone] = milestone
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
it 'track labels change' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_labels_changed_action).once.with(user: user)
opts[:label_ids] = [label2.id]
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
context 'assignees' do
context 'when assignees changed' do
it 'tracks assignees changed event' do