diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-10 00:09:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-10 00:09:15 +0300 |
commit | d8d0344cc3602e2625b7bbd79360b3e5b20b8870 (patch) | |
tree | 600a1eae6972365ac8a746054abf223318841068 | |
parent | 72c331ebf56ba3d49a79ec799de84e790748adef (diff) |
Add latest changes from gitlab-org/gitlab@master
52 files changed, 645 insertions, 285 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 2b795e2c604..0d61c12bd48 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -58bf16b78b3c99757a2f283a5befe57a2cb7f009 +3822772ed121b764fdcc5011d199c37ef76e06a9 diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index 2ab49b6c760..47f1405c980 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -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 diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index f8cc74511d9..666693e934f 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -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> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue index a4b4c08bc34..f46068acd68 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue @@ -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" /> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue index a09844b59bc..0373a84b271 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue @@ -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" diff --git a/app/assets/javascripts/registry/explorer/constants/common.js b/app/assets/javascripts/registry/explorer/constants/common.js new file mode 100644 index 00000000000..dc71ef8450b --- /dev/null +++ b/app/assets/javascripts/registry/explorer/constants/common.js @@ -0,0 +1,3 @@ +import { s__ } from '~/locale'; + +export const ROOT_IMAGE_TEXT = s__('ContainerRegistry|Root image'); diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index 3f04538a18b..7220f9646db 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -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; diff --git a/app/assets/javascripts/registry/explorer/constants/index.js b/app/assets/javascripts/registry/explorer/constants/index.js index 10816e12ead..6886356d8e2 100644 --- a/app/assets/javascripts/registry/explorer/constants/index.js +++ b/app/assets/javascripts/registry/explorer/constants/index.js @@ -1,3 +1,4 @@ +export * from './common'; export * from './expiration_policies'; export * from './quick_start'; export * from './list'; diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index 0403467468a..2f515356fa7 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -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) { diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index 9d0631fbc01..4e59d0d81c1 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -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" diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 0b830f4ee5e..fb1f70723fd 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -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) diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 354b1e0b6b9..ac8b6516d02 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -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 diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index ae0d8bba9d4..f5e14797f7e 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -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 diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index de9859230d5..df39e6297f6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -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 diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml index f9af116fbff..b0112be0e3d 100644 --- a/app/views/projects/cleanup/_show.html.haml +++ b/app/views/projects/cleanup/_show.html.haml @@ -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 diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 509092b64c9..425f76bd8d6 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -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 diff --git a/changelogs/unreleased/205484-08-project-settings-headers-repository-cleanup.yml b/changelogs/unreleased/205484-08-project-settings-headers-repository-cleanup.yml new file mode 100644 index 00000000000..a0f79c935da --- /dev/null +++ b/changelogs/unreleased/205484-08-project-settings-headers-repository-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: Project Settings Repository Cleanup header expands/collapses on click / tap +merge_request: 55232 +author: Daniel Schömer +type: changed diff --git a/changelogs/unreleased/292824-track-label-milestone-change.yml b/changelogs/unreleased/292824-track-label-milestone-change.yml new file mode 100644 index 00000000000..4430cbda898 --- /dev/null +++ b/changelogs/unreleased/292824-track-label-milestone-change.yml @@ -0,0 +1,5 @@ +--- +title: Add tracking to merge request labels/milestone changes +merge_request: 55484 +author: +type: other diff --git a/changelogs/unreleased/320901-container-page-says-image-repository-not-found.yml b/changelogs/unreleased/320901-container-page-says-image-repository-not-found.yml new file mode 100644 index 00000000000..a9494b43b2e --- /dev/null +++ b/changelogs/unreleased/320901-container-page-says-image-repository-not-found.yml @@ -0,0 +1,5 @@ +--- +title: Use Root Image for images with missing name +merge_request: 54693 +author: +type: changed diff --git a/changelogs/unreleased/55412-typo-in-pipeline-status-email.yml b/changelogs/unreleased/55412-typo-in-pipeline-status-email.yml new file mode 100644 index 00000000000..7fc2fb9416e --- /dev/null +++ b/changelogs/unreleased/55412-typo-in-pipeline-status-email.yml @@ -0,0 +1,5 @@ +--- +title: Fix typo in pipeline status email +merge_request: 55412 +author: Gabriel Berke-Williams +type: fixed diff --git a/config/feature_flags/development/usage_data_i_code_review_user_labels_changed.yml b/config/feature_flags/development/usage_data_i_code_review_user_labels_changed.yml new file mode 100644 index 00000000000..d70c6c963e8 --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_labels_changed.yml @@ -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 diff --git a/config/feature_flags/development/usage_data_i_code_review_user_milestone_changed.yml b/config/feature_flags/development/usage_data_i_code_review_user_milestone_changed.yml new file mode 100644 index 00000000000..0f64b7639c5 --- /dev/null +++ b/config/feature_flags/development/usage_data_i_code_review_user_milestone_changed.yml @@ -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 diff --git a/config/metrics/counts_28d/20210302110520_i_code_review_user_milestone_changed_monthly.yml b/config/metrics/counts_28d/20210302110520_i_code_review_user_milestone_changed_monthly.yml new file mode 100644 index 00000000000..4edc596dca1 --- /dev/null +++ b/config/metrics/counts_28d/20210302110520_i_code_review_user_milestone_changed_monthly.yml @@ -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 diff --git a/config/metrics/counts_28d/20210302110607_i_code_review_user_labels_changed_monthly.yml b/config/metrics/counts_28d/20210302110607_i_code_review_user_labels_changed_monthly.yml new file mode 100644 index 00000000000..713473cc832 --- /dev/null +++ b/config/metrics/counts_28d/20210302110607_i_code_review_user_labels_changed_monthly.yml @@ -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 diff --git a/config/metrics/counts_7d/20210302110403_i_code_review_user_milestone_changed_weekly.yml b/config/metrics/counts_7d/20210302110403_i_code_review_user_milestone_changed_weekly.yml new file mode 100644 index 00000000000..2f158edf0af --- /dev/null +++ b/config/metrics/counts_7d/20210302110403_i_code_review_user_milestone_changed_weekly.yml @@ -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 diff --git a/config/metrics/counts_7d/20210302110548_i_code_review_user_labels_changed_weekly.yml b/config/metrics/counts_7d/20210302110548_i_code_review_user_labels_changed_weekly.yml new file mode 100644 index 00000000000..14710eb040a --- /dev/null +++ b/config/metrics/counts_7d/20210302110548_i_code_review_user_labels_changed_weekly.yml @@ -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 diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 92d109cac5f..ab106b4fb44 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -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). diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 3b7184c9933..cf1cfa567db 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -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 diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 6f021ae5ae7..ad95e041c2f 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -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 diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index 98aed0b52a1..3c5e140965a 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -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. diff --git a/doc/user/project/issues/associate_zoom_meeting.md b/doc/user/project/issues/associate_zoom_meeting.md index d81fe19c5b9..f98e94c66ae 100644 --- a/doc/user/project/issues/associate_zoom_meeting.md +++ b/doc/user/project/issues/associate_zoom_meeting.md @@ -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: diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index f1739726cf8..cfb22881431 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -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. diff --git a/doc/user/project/merge_requests/drafts.md b/doc/user/project/merge_requests/drafts.md index 522fef0be71..45348c3ae66 100644 --- a/doc/user/project/merge_requests/drafts.md +++ b/doc/user/project/merge_requests/drafts.md @@ -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. diff --git a/doc/user/project/merge_requests/getting_started.md b/doc/user/project/merge_requests/getting_started.md index 3cc69ef82f5..cf1a359c56f 100644 --- a/doc/user/project/merge_requests/getting_started.md +++ b/doc/user/project/merge_requests/getting_started.md @@ -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 diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index d7a6d412511..284deabb33c 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -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 for issues, merge requests and epics - -The following quick actions are applicable to descriptions, discussions and threads in: - -- Issues -- Merge requests -- Epics **(PREMIUM)** - -| 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 +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. + +Be sure to enter each quick action on a separate line to allow GitLab to +properly detect and execute the commands. + +## Parameters + +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. + +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 diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb index 2b38b12c914..32f9f1b3157 100644 --- a/lib/gitlab/experimentation/controller_concern.rb +++ b/lib/gitlab/experimentation/controller_concern.rb @@ -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) diff --git a/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml b/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml index 7857c1b3b1d..4c2355d526a 100644 --- a/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml +++ b/lib/gitlab/usage_data_counters/aggregated_metrics/code_review.yml @@ -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 diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml index 5a7084f346e..18c5dc73de2 100644 --- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml @@ -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 diff --git a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb index a5122aa6ee0..eb28a387a97 100644 --- a/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb @@ -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) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 51c127effed..0e524204aaa 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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" diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb index cacabdda22d..65374263f45 100644 --- a/spec/features/groups/container_registry_spec.rb +++ b/spec/features/groups/container_registry_spec.rb @@ -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 diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb index d0ad6668c07..40d0260eafd 100644 --- a/spec/features/projects/container_registry_spec.rb +++ b/spec/features/projects/container_registry_spec.rb @@ -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 diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 028133c54ea..5ca5d855038 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -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."); }); }); }); diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js index 8beadf12a36..f362aace1df 100644 --- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js index 3fa3a2ae1de..b50ed87a563 100644 --- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js @@ -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', () => { diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js index d6ee871341b..6c897b983f7 100644 --- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js @@ -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(); diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index 65c58bf9874..76baf4f72c9 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -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', () => { diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index 1cebe37bea5..bb9d273995a 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index d0c3456aa26..6486a5a22ba 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -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 diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb index 3ac68721357..a29835f3439 100644 --- a/spec/mailers/emails/pipelines_spec.rb +++ b/spec/mailers/emails/pipelines_spec.rb @@ -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 diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb index 22bbf2df8fd..09dd1766acc 100644 --- a/spec/models/experiment_spec.rb +++ b/spec/models/experiment_spec.rb @@ -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 diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 86007c9cc72..7a7f684c6d0 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -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 |