Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-09 09:07:42 +00:00
parent 7b29a4f84e
commit 44d4b37b52
67 changed files with 731 additions and 649 deletions

View File

@ -1,8 +1,17 @@
<!-- Title suggestion: [Feature flag] Enable description of feature -->
<!--
Set the main issue link: The main issue is the one that describes the problem to solve,
the one this feature flag is being added for. For example:
[main-issue]: https://gitlab.com/gitlab-org/gitlab/-/issues/123456
-->
[main-issue]: MAIN-ISSUE-LINK
## Summary
This issue is to rollout [the feature](ISSUE LINK) on production,
This issue is to rollout [the feature][main-issue] on production,
that is currently behind the `<feature-flag-name>` feature flag.
<!-- Short description of what the feature is about and link to relevant other issues. -->
@ -89,7 +98,7 @@ _Consider adding links to check for Sentry errors, Production logs for 5xx, 302s
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production.
If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
- [ ] Ensure that documentation has been updated ([More info](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#features-that-became-enabled-by-default)).
- [ ] Announce on [the feature issue](ISSUE LINK) an estimated time this will be enabled on GitLab.com.
- [ ] Leave a comment on [the feature issue][main-issue] announcing estimated time when this feature flag will be enabled on GitLab.com.
- [ ] Ensure that any breaking changes have been announced following the [release post process](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes) to ensure GitLab customers are aware.
- [ ] Notify `#support_gitlab-com` and your team channel ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)).
@ -104,7 +113,7 @@ For visibility, all `/chatops` commands that target production should be execute
- [ ] `/chatops run feature set <feature-flag-name> <rollout-percentage> --random`
- Enable the feature globally on production environment.
- [ ] `/chatops run feature set <feature-flag-name> true`
- [ ] Announce on [the feature issue](ISSUE LINK) that the feature has been globally enabled.
- [ ] Leave a comment on [the feature issue][main-issue] announcing that the feature has been globally enabled.
- [ ] Wait for [at least one day for the verification term](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release).
### (Optional) Release the feature with the feature flag
@ -122,7 +131,7 @@ To do so, follow these steps:
- [ ] `/chatops run release check <merge-request-url> <milestone>`
- [ ] Consider cleaning up the feature flag from all environments by running these chatops command in `#production` channel. Otherwise these settings may override the default enabled.
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature will be released in the current milestone.
- [ ] Close [the feature issue][main-issue] to indicate the feature will be released in the current milestone.
- [ ] Set the next milestone to this rollout issue for scheduling [the flag removal](#release-the-feature).
- [ ] (Optional) You can [create a separate issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20Flag%20Cleanup) for scheduling the steps below to [Release the feature](#release-the-feature).
- [ ] Set the title to "[Feature flag] Cleanup `<feature-flag-name>`".
@ -155,7 +164,7 @@ You can either [create a follow-up issue for Feature Flag Cleanup](https://gitla
If the merge request was deployed before [the monthly release was tagged](https://about.gitlab.com/handbook/engineering/releases/#self-managed-releases-1),
the feature can be officially announced in a release blog post.
- [ ] `/chatops run release check <merge-request-url> <milestone>`
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature will be released in the current milestone.
- [ ] Close [the feature issue][main-issue] to indicate the feature will be released in the current milestone.
- [ ] If not already done, clean up the feature flag from all environments by running these chatops command in `#production` channel:
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
- [ ] Close this rollout issue.

View File

@ -1,96 +0,0 @@
/* eslint-disable func-names */
import Dropzone from 'dropzone';
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
import { visitUrl } from '../lib/utils/url_utility';
Dropzone.autoDiscover = false;
function toggleLoading($el, $icon, loading) {
if (loading) {
$el.disable();
$icon.removeClass(HIDDEN_CLASS);
} else {
$el.enable();
$icon.addClass(HIDDEN_CLASS);
}
}
export default class BlobFileDropzone {
constructor(form, method) {
const formDropzone = form.find('.dropzone');
const submitButton = form.find('#submit-all');
const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
const dropzoneMessage = form.find('.dz-message');
Dropzone.autoDiscover = false;
const dropzone = formDropzone.dropzone({
autoDiscover: false,
autoProcessQueue: false,
url: form.attr('action'),
// Rails uses a hidden input field for PUT
method,
clickable: true,
uploadMultiple: false,
paramName: 'file',
maxFilesize: gon.max_file_size || 10,
parallelUploads: 1,
maxFiles: 1,
addRemoveLinks: true,
previewsContainer: '.dropzone-previews',
headers: csrf.headers,
init() {
this.on('processing', function () {
this.options.url = form.attr('action');
});
this.on('addedfile', () => {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide();
});
this.on('removedfile', () => {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.removeClass(HIDDEN_CLASS);
});
this.on('success', (header, response) => {
$('#modal-upload-blob').modal('hide');
visitUrl(response.filePath);
});
this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file);
});
this.on('sending', (file, xhr, formData) => {
formData.append('branch_name', form.find('.js-branch-name').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val());
});
},
// Override behavior of adding error underneath preview
error(file, errorMessage) {
const stripped = $('<div/>').html(errorMessage).text();
$('.dropzone-alerts')
.html(sprintf(__('Error uploading file: %{stripped}'), { stripped }))
.show();
this.removeFile(file);
},
});
submitButton.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
alert(__('Please select a file'));
return false;
}
toggleLoading(submitButton, submitButtonLoadingIcon, true);
dropzone[0].dropzone.processQueue();
return false;
});
}
}

View File

@ -3,9 +3,8 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { createAlert } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import { setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import NewCommitForm from '../new_commit_form';
const initPopovers = () => {
@ -38,21 +37,8 @@ const initPopovers = () => {
}
};
export const initUploadForm = () => {
const uploadBlobForm = $('.js-upload-blob-form');
if (uploadBlobForm.length) {
const method = uploadBlobForm.data('method');
new BlobFileDropzone(uploadBlobForm, method);
new NewCommitForm(uploadBlobForm);
disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file');
}
};
export default () => {
const editBlobForm = $('.js-edit-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
@ -99,10 +85,4 @@ export default () => {
// returning here blocks page navigation
window.onbeforeunload = () => '';
}
initUploadForm();
if (deleteBlobForm.length) {
new NewCommitForm(deleteBlobForm);
}
};

View File

@ -43,7 +43,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [boardCardInner],
inject: ['rootPath', 'scopedLabelsAvailable'],
inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard'],
props: {
item: {
type: Object,
@ -78,7 +78,7 @@ export default {
},
computed: {
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard', 'isProjectBoard']),
...mapGetters(['isProjectBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness

View File

@ -7,12 +7,7 @@ import { s__ } from '~/locale';
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants';
import {
DraggableItemTypes,
issuableTypes,
BoardType,
listsQuery,
} from 'ee_else_ce/boards/constants';
import { DraggableItemTypes, BoardType, listsQuery } from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue';
export default {
@ -31,7 +26,15 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
inject: ['canAdminList', 'boardType', 'fullPath', 'issuableType', 'isApolloBoard'],
inject: [
'canAdminList',
'boardType',
'fullPath',
'issuableType',
'isIssueBoard',
'isEpicBoard',
'isApolloBoard',
],
props: {
disabled: {
type: Boolean,
@ -78,12 +81,6 @@ export default {
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn']),
isIssueBoard() {
return this.issuableType === issuableTypes.issue;
},
isEpicBoard() {
return this.issuableType === issuableTypes.epic;
},
addColumnFormVisible() {
return this.addColumnForm?.visible;
},

View File

@ -84,7 +84,7 @@ export default {
},
computed: {
...mapState(['error']),
...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']),
...mapGetters(['isGroupBoard', 'isProjectBoard']),
isNewForm() {
return this.currentPage === formType.new;
},

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import { defaultSortableOptions } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
@ -31,6 +31,7 @@ export default {
BoardCardMoveToPosition,
},
mixins: [Tracking.mixin()],
inject: ['isEpicBoard'],
props: {
disabled: {
type: Boolean,
@ -69,7 +70,6 @@ export default {
},
computed: {
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']),
...mapGetters(['isEpicBoard']),
listItemsCount() {
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
},

View File

@ -57,6 +57,9 @@ export default {
canCreateEpic: {
default: false,
},
isEpicBoard: {
default: false,
},
},
props: {
list: {
@ -76,7 +79,7 @@ export default {
},
computed: {
...mapState(['activeId', 'filterParams', 'boardId']),
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
...mapGetters(['isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
},

View File

@ -31,7 +31,7 @@ export default {
GlModal: GlModalDirective,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
inject: ['canAdminList', 'scopedLabelsAvailable'],
inject: ['canAdminList', 'scopedLabelsAvailable', 'isIssueBoard'],
inheritAttrs: false,
data() {
return {
@ -40,10 +40,10 @@ export default {
},
modalId: 'board-settings-sidebar-modal',
computed: {
...mapGetters(['isSidebarOpen', 'isEpicBoard']),
...mapGetters(['isSidebarOpen']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
return this.glFeatures.wipLimits && !this.isEpicBoard;
return this.glFeatures.wipLimits && this.isIssueBoard;
},
activeList() {
return this.boardLists[this.activeId] || {};

View File

@ -1,5 +1,4 @@
<script>
import { mapGetters } from 'vuex';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
@ -20,10 +19,7 @@ export default {
EpicBoardFilteredSearch: () =>
import('ee_component/boards/components/epic_filtered_search.vue'),
},
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn'],
computed: {
...mapGetters(['isEpicBoard']),
},
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn', 'isIssueBoard'],
};
</script>
@ -37,8 +33,8 @@ export default {
>
<boards-selector />
<new-board-button />
<epic-board-filtered-search v-if="isEpicBoard" />
<issue-board-filtered-search v-else />
<issue-board-filtered-search v-if="isIssueBoard" />
<epic-board-filtered-search v-else />
</div>
<div
class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"

View File

@ -72,6 +72,8 @@ function mountBoardApp(el) {
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
isIssueBoard: true,
isEpicBoard: false,
// Permissions
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),

View File

@ -10,14 +10,6 @@ import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
// Project show page loads different overview content based on user preferences
if (document.querySelector('.js-upload-blob-form')) {
import(/* webpackChunkName: 'blobBundle' */ '~/blob_edit/blob_bundle')
.then(({ initUploadForm }) => {
initUploadForm();
})
.catch(() => {});
}
if (document.getElementById('js-tree-list')) {
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
.then(({ default: initTree }) => {

View File

@ -23,7 +23,6 @@ class Projects::LearnGitlabController < Projects::ApplicationController
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
e.candidate {}
e.publish_to_database
end
end

View File

@ -338,7 +338,6 @@ class Projects::PipelinesController < Projects::ApplicationController
experiment(:runners_availability_section, namespace: project.root_ancestor) do |e|
e.candidate {}
e.publish_to_database
end
end

View File

@ -3,25 +3,6 @@
class ApplicationExperiment < Gitlab::Experiment
control { nil } # provide a default control for anonymous experiments
# Documented in:
# https://gitlab.com/gitlab-org/gitlab/-/issues/357904
# https://gitlab.com/gitlab-org/gitlab/-/issues/345932
#
# @deprecated
def publish_to_database
ActiveSupport::Deprecation.warn('publish_to_database is deprecated and should not be used for reporting anymore')
return unless should_track?
# if the context contains a namespace, group, project, user, or actor
value = context.value
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
return unless ExperimentSubject.valid_subject?(subject)
variant_name = :experimental if variant&.name != 'control'
Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
end
def control_behavior
# define a default nil control behavior so we can omit it when not needed
end

View File

@ -12,18 +12,8 @@ class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment
run
end
def record_conversion(namespace)
return unless should_track?
Experiment.by_name(name).record_conversion_event_for_subject(subject, namespace_id: namespace.id)
end
private
def subject
context.value[:user]
end
def existing_user
return false unless user_or_actor

View File

@ -3,10 +3,4 @@
class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment
control {}
candidate {}
def publish(_result = nil)
super
publish_to_database
end
end

View File

@ -306,8 +306,8 @@ class Deployment < ApplicationRecord
last_deployment_id = environment.last_deployment&.id
return false unless last_deployment_id.present?
return false if self.id == last_deployment_id
return false if self.sha == environment.last_deployment&.sha
self.id < last_deployment_id
end

View File

@ -452,6 +452,14 @@ class User < ApplicationRecord
after_transition banned: :active do |user|
user.banned_user&.destroy
end
after_transition any => :active do |user|
user.starred_projects.update_counters(star_count: 1)
end
after_transition active: any do |user|
user.starred_projects.update_counters(star_count: -1)
end
end
# Scopes

View File

@ -3,7 +3,7 @@
class UsersStarProject < ApplicationRecord
include Sortable
belongs_to :project, counter_cache: :star_count
belongs_to :project
belongs_to :user
validates :user, presence: true
@ -12,7 +12,10 @@ class UsersStarProject < ApplicationRecord
alias_attribute :starred_since, :created_at
scope :with_active_user, -> { joins(:user).merge(User.active) }
after_create :increment_project_star_count
after_destroy :decrement_project_star_count
scope :with_active_user, -> { joins(:user).merge(User.with_state(:active)) }
scope :order_user_name_asc, -> { joins(:user).merge(User.order_name_asc) }
scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) }
scope :by_project, -> (project) { where(project_id: project.id) }
@ -36,4 +39,14 @@ class UsersStarProject < ApplicationRecord
joins(:user).merge(User.search(query, use_minimum_char_limit: false))
end
end
private
def increment_project_star_count
Project.update_counters(project, star_count: 1) if user.active?
end
def decrement_project_star_count
Project.update_counters(project, star_count: -1) if user.active?
end
end

View File

@ -851,6 +851,10 @@ class ProjectPolicy < BasePolicy
enable :read_incident_management_timeline_event_tag
end
rule { can?(:download_code) }.policy do
enable :read_code
end
private
def user_is_user?

View File

@ -12,8 +12,8 @@ module Projects
Project.transaction do
user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects
Project.reset_counters source_project.id, :users_star_projects
@project.update(star_count: @project.starrers.with_state(:active).size)
source_project.update(star_count: source_project.starrers.with_state(:active).size)
success
end

View File

@ -2,7 +2,7 @@ name: policy_project_updated
description: "This event is triggered whenever the security policy project is updated for a project."
introduced_by_issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/377877"
introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154"
milestone: 15.6
milestone: "15.6"
group: "govern::security policies"
saved_to_database: true
streamed: false

View File

@ -0,0 +1,13 @@
- name: "`gitlab-runner register` command" # (required) The name of the feature to be deprecated
announcement_milestone: "15.6" # (required) The milestone when this feature was first announced as deprecated.
announcement_date: "2022-11-22" # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
removal_date: "2023-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
reporter: pedropombeiro # (required) GitLab username of the person reporting the deprecation
stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/380872 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/), which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
end_of_support_milestone: "16.0" # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
end_of_support_date: "2023-05-22" # (optional) The date of the milestone release when support for this feature will end.

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class QueuePopulateProjectsStarCount < Gitlab::Database::Migration[2.0]
MIGRATION = 'PopulateProjectsStarCount'
DELAY_INTERVAL = 2.minutes
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:projects,
:id,
job_interval: DELAY_INTERVAL,
sub_batch_size: 50
)
end
def down
delete_batched_background_migration(MIGRATION, :projects, :id, [])
end
end

View File

@ -0,0 +1 @@
186e7df4e7e81913981595a069c5c8b5fbb600ee5dcebf333bfff728c5019ab2

View File

@ -4,7 +4,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/product/ux/technical-writing/#assignments
---
# Award emoji API **(FREE)**
# Award emojis API **(FREE)**
An [awarded emoji](../user/award_emojis.md) tells a thousand words.
@ -15,11 +15,11 @@ We call GitLab objects on which you can award an emoji "awardables". You can awa
- [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)).
- [Snippets](../user/snippets.md) ([API](snippets.md)).
Emojis can also [be awarded](../user/award_emojis.md#award-emoji-for-comments) on comments (also known as notes). See also [Notes API](notes.md).
Emojis can also [be awarded](../user/award_emojis.md#award-emojis-for-comments) on comments (also known as notes). See also [Notes API](notes.md).
## Issues, merge requests, and snippets
See [Award Emoji on Comments](#award-emoji-on-comments) for information on using these endpoints with comments.
See [Award emojis on comments](#award-emojis-on-comments) for information on using these endpoints with comments.
### List an awardable's award emojis
@ -201,7 +201,7 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344"
```
## Award Emoji on Comments
## Award emojis on comments
Comments (also known as notes) are a sub-resource of issues, merge requests, and snippets.
@ -366,7 +366,7 @@ Parameters:
| `id` | integer/string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
| `issue_iid` | integer | yes | Internal ID of an issue. |
| `note_id` | integer | yes | ID of a comment (note). |
| `award_id` | integer | yes | ID of an award_emoji. |
| `award_id` | integer | yes | ID of an award emoji. |
Example request:

View File

@ -6,17 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Use custom emojis with GraphQL **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - Enabled on GitLab.com.
> - Recommended for production use.
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-custom-emoji-api). **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6 [with a flag](../../administration/feature_flags.md) named `custom_emoji`. Disabled by default.
> - Enabled on GitLab.com in GitLab 14.0.
This in-development feature might not be available for your use. There can be
[risks when enabling features still in development](../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
Refer to this feature's version history for more details.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `custom_emoji`.
On GitLab.com, this feature is available.
This feature is ready for production use.
To use custom emoji in comments and descriptions, you can add them to a group using the GraphQL API.
To use custom emojis in comments and descriptions, you can add them to a group using the GraphQL API.
Parameters:
@ -40,7 +38,7 @@ mutation CreateCustomEmoji($groupPath: ID!) {
}
```
After adding custom emoji to the group, members can use it in the same way as other emoji in the comments.
After adding a custom emoji to the group, members can use it in the same way as other emojis in the comments.
## Get custom emoji for a group
@ -92,22 +90,3 @@ For more information on:
- GraphQL specific entities, such as Fragments and Interfaces, see the official
[GraphQL documentation](https://graphql.org/learn/).
- Individual attributes, see the [GraphQL API Resources](reference/index.md).
## Enable or disable custom emoji API **(FREE SELF)**
Custom emoji is under development but ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:custom_emoji)
```
To disable it:
```ruby
Feature.disable(:custom_emoji)
```

View File

@ -651,8 +651,8 @@ Supported attributes:
| `merge_commit_sha` | string | SHA of the merge request commit (set once merged). |
| `merge_error` | string | Error message due to a merge error. |
| `merge_user` | object | User who merged this merge request or set it to merge when pipeline succeeds. |
| `merge_status` | string | Status of the merge request. Can be `unchecked`, `checking`, `can_be_merged`, `cannot_be_merged` or `cannot_be_merged_recheck`. |
| `merge_when_pipeline_succeeds` | boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS). |
| `merge_status` | string | Status of the merge request. Can be `unchecked`, `checking`, `can_be_merged`, `cannot_be_merged` or `cannot_be_merged_recheck`. [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204) in GitLab 15.6. Use `detailed_merge_status` instead. |
| `merge_when_pipeline_succeeds` | boolean | Indicates if the merge has been set to be merged when its pipeline succeeds. |
| `merged_at` | datetime | Timestamp of when the merge request was merged. |
| `merged_by` | object | Deprecated: Use `merge_user` instead. User who merged this merge request or set it to merge when pipeline succeeds. |
| `milestone` | object | Milestone of the merge request. |
@ -848,14 +848,11 @@ the `approvals_before_merge` parameter:
### Merge status
> The `detailed_merge_status` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6.
> - The `detailed_merge_status` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6.
> - The `merge_status` field was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204) in GitLab 15.6.
Use `detailed_merge_status` instead of `merge_status` to account for all potential statuses.
- The `merge_status` field may hold one of the following values:
- `unchecked`: This merge request has not yet been checked.
- `checking`: This merge request is currently being checked to see if it can be merged.
- `can_be_merged`: This merge request can be merged without conflict.
- `cannot_be_merged`: There are merge conflicts between the source and target branches.
- `cannot_be_merged_recheck`: Currently unchecked. Before the current changes, there were conflicts.
- The `detailed_merge_status` field may hold one of the following values:
- `blocked_status`: Merge request is blocked by another merge request.
- `broken_status`: Can not merge the source into the target branch, potential conflict.

View File

@ -1,50 +1,41 @@
---
stage: Verify
group: Pipeline Execution
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: reference
---
# Get started with GitLab CI/CD **(FREE)**
# Tutorial: Create and run your first GitLab CI/CD pipeline **(FREE)**
Use this document to get started with [GitLab CI/CD](../index.md).
This tutorial shows you how to configure and run your first CI/CD pipeline in GitLab.
## Prerequisites
Before you start, make sure you have:
- A project in GitLab that you would like to use CI/CD for.
- The Maintainer or Owner role for the project.
If you are migrating from another CI/CD tool, view this documentation:
If you don't have a project, you can create a public project for free on <https://gitlab.com>.
- [Migrate from CircleCI](../migration/circleci.md).
- [Migrate from Jenkins](../migration/jenkins.md).
## Steps
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [First time GitLab & CI/CD](https://www.youtube.com/watch?v=kTNfi5z6Uvk&t=553s). This includes a quick introduction to GitLab, the first steps with CI/CD, building a Go project, running tests, using the CI/CD pipeline editor, detecting secrets and security vulnerabilities and offers more exercises for asynchronous practice.
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [Intro to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=358s). This workshop uses the Web IDE to quickly get going with building source code using CI/CD, and run unit tests.
## CI/CD process overview
To use GitLab CI/CD:
To create and run your first pipeline:
1. [Ensure you have runners available](#ensure-you-have-runners-available) to run your jobs.
GitLab SaaS provides runners, so if you're using GitLab.com, you can skip this step.
If you don't have a runner, [install GitLab Runner](https://docs.gitlab.com/runner/install/)
and [register a runner](https://docs.gitlab.com/runner/register/) for your instance, project, or group.
If you're using GitLab.com, you can skip this step. GitLab.com provides shared runners for you.
1. [Create a `.gitlab-ci.yml` file](#create-a-gitlab-ciyml-file)
at the root of your repository. This file is where you define your CI/CD jobs.
at the root of your repository. This file is where you define the CI/CD jobs.
When you commit the file to your repository, the runner runs your jobs.
The job results [are displayed in a pipeline](#view-the-status-of-your-pipeline-and-jobs).
### Ensure you have runners available
## Ensure you have runners available
In GitLab, runners are agents that run your CI/CD jobs.
You might already have runners available for your project, including
[shared runners](../runners/runners_scope.md), which are
available to all projects in your GitLab instance.
To view available runners:
- Go to **Settings > CI/CD** and expand **Runners**.
@ -52,34 +43,32 @@ To view available runners:
As long as you have at least one runner that's active, with a green circle next to it,
you have a runner available to process your jobs.
If no runners are listed on the **Runners** page in the UI, you or an administrator
must [install GitLab Runner](https://docs.gitlab.com/runner/install/) and
[register](https://docs.gitlab.com/runner/register/) at least one runner.
### If you don't have a runner
If you are testing CI/CD, you can install GitLab Runner and register runners on your local machine.
When your CI/CD jobs run, they run on your local machine.
If you don't have a runner:
### Create a `.gitlab-ci.yml` file
1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/) on your local machine.
1. [Register the runner](https://docs.gitlab.com/runner/register/) for your project.
Choose the `shell` executor.
The `.gitlab-ci.yml` file is a [YAML](https://en.wikipedia.org/wiki/YAML) file where
you configure specific instructions for GitLab CI/CD.
When your CI/CD jobs run, in a later step, they will run on your local machine.
## Create a `.gitlab-ci.yml` file
Now create a `.gitlab-ci.yml` file. It is a [YAML](https://en.wikipedia.org/wiki/YAML) file where
you specify instructions for GitLab CI/CD.
In this file, you define:
- The structure and order of jobs that the runner should execute.
- The decisions the runner should make when specific conditions are encountered.
For example, you might want to run a suite of tests when you commit to
any branch except the default branch. When you commit to the default branch, you want
to run the same suite, but also publish your application.
All of this is defined in the `.gitlab-ci.yml` file.
To create a `.gitlab-ci.yml` file:
1. On the left sidebar, select **Project information > Details**.
1. Above the file list, select the branch you want to commit to,
select the plus icon, then select **New file**:
1. On the left sidebar, select **Repository > Files**.
1. Above the file list, select the branch you want to commit to.
If you're not sure, leave `master` or `main`.
Then select the plus icon (**{plus}**) and **New file**:
![New file](img/new_file_v13_6.png)
@ -112,46 +101,50 @@ To create a `.gitlab-ci.yml` file:
environment: production
```
`$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are
[predefined variables](../variables/predefined_variables.md)
that populate when the job runs.
This example shows four jobs: `build-job`, `test-job1`, `test-job2`, and `deploy-prod`.
The comments listed in the `echo` commands are displayed in the UI when you view the jobs.
The values for the [predefined variables](../variables/predefined_variables.md)
`$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are populated when the jobs run.
1. Select **Commit changes**.
The pipeline starts when the commit is committed.
The pipeline starts and runs the jobs you defined in the `.gitlab-ci.yml` file.
#### `.gitlab-ci.yml` tips
## View the status of your pipeline and jobs
- After you create your first `.gitlab-ci.yml` file, use the [pipeline editor](../pipeline_editor/index.md)
for all future edits to the file. With the pipeline editor, you can:
- Edit the pipeline configuration with automatic syntax highlighting and validation.
- View the [CI/CD configuration visualization](../pipeline_editor/index.md#visualize-ci-configuration),
a graphical representation of your `.gitlab-ci.yml` file.
- If you want the runner to [use a Docker container to run the jobs](../docker/using_docker_images.md),
edit the `.gitlab-ci.yml` file
to include an image name:
Now take a look at your pipeline and the jobs within.
```yaml
default:
image: ruby:2.7.5
```
1. Go to **CI/CD > Pipelines**. A pipeline with three stages should be displayed:
This command tells the runner to use a Ruby image from Docker Hub
and to run the jobs in a container that's generated from the image.
![Three stages](img/three_stages_v13_6.png)
This process is different than
[building an application as a Docker container](../docker/using_docker_build.md).
Your application does not need to be built as a Docker container to
run CI/CD jobs in Docker containers.
1. View a visual representation of your pipeline by selecting the pipeline ID:
- Each job contains scripts and stages:
![Pipeline graph](img/pipeline_graph_v13_6.png)
1. View details of a job by selecting the job name. For example, `deploy-prod`:
![Job details](img/job_details_v13_6.png)
You have successfully created your first CI/CD pipeline in GitLab. Congratulations!
Now you can get started customizing your `.gitlab-ci.yml` and defining more advanced jobs.
## `.gitlab-ci.yml` tips
Here are some tips to get started working with the `.gitlab-ci.yml` file.
For the complete `.gitlab-ci.yml` syntax, see [the full `.gitlab-ci.yml` keyword reference](../yaml/index.md).
- Use the [pipeline editor](../pipeline_editor/index.md) to edit your `.gitlab-ci.yml` file.
- Each job contains a script section and belongs to a stage:
- The [`default`](../yaml/index.md#default) keyword is for
custom defaults, for example with [`before_script`](../yaml/index.md#before_script)
and [`after_script`](../yaml/index.md#after_script).
- [`stage`](../yaml/index.md#stage) describes the sequential execution of jobs.
Jobs in a single stage run in parallel as long as there are available runners.
- Use [Directed Acyclic Graphs (DAG)](../directed_acyclic_graph/index.md) keywords
to run jobs out of stage order.
- Use the [`needs` keyword](../yaml/index.md#needs) to run jobs out of stage order.
This creates a [Directed Acyclic Graph (DAG)](../directed_acyclic_graph/index.md).
- You can set additional configuration to customize how your jobs and stages perform:
- Use the [`rules`](../yaml/index.md#rules) keyword to specify when to run or skip jobs.
The `only` and `except` legacy keywords are still supported, but can't be used
@ -159,26 +152,10 @@ The pipeline starts when the commit is committed.
- Keep information across jobs and stages persistent in a pipeline with [`cache`](../yaml/index.md#cache)
and [`artifacts`](../yaml/index.md#artifacts). These keywords are ways to store
dependencies and job output, even when using ephemeral runners for each job.
- For the complete `.gitlab-ci.yml` syntax, see [the full `.gitlab-ci.yml` reference topic](../yaml/index.md).
### View the status of your pipeline and jobs
## Related topics
When you committed your changes, a pipeline started.
To view your pipeline:
- Go to **CI/CD > Pipelines**.
A pipeline with three stages should be displayed:
![Three stages](img/three_stages_v13_6.png)
- To view a visual representation of your pipeline, select the pipeline ID.
![Pipeline graph](img/pipeline_graph_v13_6.png)
- To view details of a job, select the job name, for example, `deploy-prod`.
![Job details](img/job_details_v13_6.png)
If the job status is `stuck`, check to ensure a runner is properly configured for the project.
- [Follow this guide to migrate from CircleCI](../migration/circleci.md).
- [Follow this guide to migrate from Jenkins](../migration/jenkins.md).
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [First time GitLab & CI/CD](https://www.youtube.com/watch?v=kTNfi5z6Uvk&t=553s). This includes a quick introduction to GitLab, the first steps with CI/CD, building a Go project, running tests, using the CI/CD pipeline editor, detecting secrets and security vulnerabilities and offers more exercises for asynchronous practice.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [Intro to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=358s). This workshop uses the Web IDE to quickly get going with building source code using CI/CD, and run unit tests.

View File

@ -47,12 +47,11 @@ If you want to know the in-depth details, here's what's really happening:
1. The preview URL is shown both at the job output and in the merge request
widget. You also get the link to the remote pipeline.
1. In the `gitlab-org/gitlab-docs` project, the pipeline is created and it
[skips the test jobs](https://gitlab.com/gitlab-org/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
[skips most test jobs](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/d41ca9323f762132780d2d072f845d28817a5383/.gitlab/ci/rules.gitlab-ci.yml#L101-103)
to lower the build time.
1. Once the docs site is built, the HTML files are uploaded as artifacts.
1. A specific runner tied only to the docs project, runs the Review App job
that downloads the artifacts and uses `rsync` to transfer the files over
to a location where NGINX serves them.
1. After the docs site is built, the HTML files are uploaded as artifacts to
a GCP bucket (see [issue `gitlab-com/gl-infra/reliability#11021`](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/11021)
for the implementation details).
The following GitLab features are used among others:
@ -60,42 +59,26 @@ The following GitLab features are used among others:
- [Multi project pipelines](../../ci/pipelines/downstream_pipelines.md#multi-project-pipelines)
- [Review Apps](../../ci/review_apps/index.md)
- [Artifacts](../../ci/yaml/index.md#artifacts)
- [Specific runner](../../ci/runners/runners_scope.md#prevent-a-specific-runner-from-being-enabled-for-other-projects)
- [Merge request pipelines](../../ci/pipelines/merge_request_pipelines.md)
## Troubleshooting review apps
### Review app returns a 404 error
### `NoSuchKey The specified key does not exist`
If the review app URL returns a 404 error, either the site is not
yet deployed, or something went wrong with the remote pipeline. You can:
If you see the following message in a review app, either the site is not
yet deployed, or something went wrong with the downstream pipeline in `gitlab-docs`.
- Wait a few minutes and it should appear online.
- Check the manual job's log and verify the URL. If the URL is different, try the
one from the job log.
```plaintext
NoSuchKeyThe specified key does not exist.No such object: <URL>
```
In that case, you can:
- Wait a few minutes and the review app should appear online.
- Check the `review-docs-deploy` job's log and verify the URL. If the URL shown in the merge
request UI is different than the job log, try the one from the job log.
- Check the status of the remote pipeline from the link in the merge request's job output.
If the pipeline failed or got stuck, GitLab team members can ask for help in the `#docs`
chat channel. Contributors can ping a technical writer in the merge request.
### Not enough disk space
Sometimes the review app server is full and there is no more disk space. Each review
app takes about 570MB of disk space.
A cron job to remove review apps older than 20 days runs hourly,
but the disk space still occasionally fills up. To manually free up more space,
a GitLab technical writing team member can:
1. Navigate to the [`gitlab-docs` schedules page](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules).
1. Select the play button for the `Remove old review apps from review app server`
schedule. By default, this cleans up review apps older than 14 days.
1. Navigate to the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines)
and start the manual job called `clean-pages`.
If the job says no review apps were found in that period, edit the `CLEAN_REVIEW_APPS_DAYS`
variable in the schedule, and repeat the process above. Gradually decrease the variable
until the free disk space reaches an acceptable amount (for example, 3GB).
Remember to set it to 14 again when you're done.
There's an issue to [migrate from the DigitalOcean server to GCP buckets](https://gitlab.com/gitlab-org/gitlab-docs/-/issues/735)),
which should solve the disk space problem.
internal Slack channel. Contributors can ping a
[technical writer](https://about.gitlab.com/handbook/product/ux/technical-writing/#designated-technical-writers)
in the merge request.

View File

@ -21,7 +21,7 @@ but that repository is no longer used for development.
## Install Workhorse
To install GitLab Workhorse you need [Go 1.15 or newer](https://go.dev/dl) and
To install GitLab Workhorse you need [Go 1.18 or newer](https://go.dev/dl) and
[GNU Make](https://www.gnu.org/software/make/).
To install into `/usr/local/bin` run `make install`.

View File

@ -53,7 +53,7 @@ CI/CD pipelines are used to automatically build, test, and deploy your code.
| Topic | Description | Good for beginners |
|-------|-------------|--------------------|
| [Get started: Create a pipeline](../ci/quick_start/index.md) | Create a `.gitlab-ci.yml` file and start a pipeline. | **{star}** |
| [Tutorial: Create and run your first GitLab CI/CD pipeline](../ci/quick_start/index.md) | Create a `.gitlab-ci.yml` file and start a pipeline. | **{star}** |
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Get started: Learn about CI/CD](https://www.youtube.com/watch?v=sIegJaLy2ug) (9m 02s) | Learn about the `.gitlab-ci.yml` file and how it's used. | **{star}** |
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CI deep dive](https://www.youtube.com/watch?v=ZVUbmVac-m8&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=27) (22m 51s) | Take a closer look at pipelines and continuous integration concepts. | |
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CD deep dive](https://www.youtube.com/watch?v=Cn0rzND-Yjw&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=10) (47m 54s) | Learn about deploying in GitLab. | |

View File

@ -37,11 +37,11 @@ Each time you push a change, Git records it as a unique *commit*. These commits
the history of when and how a file changed, and who changed it.
```mermaid
graph TB
graph LR
subgraph Repository commit history
A(Author: Alex<br>Date: 3 Jan at 1PM<br>Commit message: Added sales figures for January<br> Commit ID: 123abc12) ---> B
B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed outdated marketing information<br> Commit ID: aabb1122) ---> C
C(Author: Zhang<br>Date: 5 Jan at 3PM<br>Commit message: Added a new 'Invoices' file<br> Commit ID: ddee4455)
A(Author: Alex<br>Date: 3 Jan at 1PM<br>Commit message: Added sales figures<br> Commit ID: 123abc12) ---> B
B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed old info<br> Commit ID: aabb1122) ---> C
C(Author: Zhang<br>Date: 5 Jan at 3PM<br>Commit message: Added invoices<br> Commit ID: ddee4455)
end
```
@ -54,15 +54,14 @@ of a repository are in a default branch. To make changes, you:
1. When you're ready, *merge* your branch into the default branch.
```mermaid
flowchart TB
flowchart LR
subgraph Default branch
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
end
subgraph My branch
B --1. Create my branch--> E(Commit)
E --2. Add my commit--> F(Commit)
F --2. Add my commit--> G(Commit)
G --3. Merge my branch to default--> D
F --3. Merge my branch to default--> D
end
```

View File

@ -66,6 +66,21 @@ and method in GitLab 16.0, and introduce a new
This new architecture introduces a new method for registering runners and eliminates the legacy
[runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
</div>
<div class="deprecation removal-160 breaking-change">
### `gitlab-runner register` command
End of Support: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)<br />
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/), which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
</div>
</div>

View File

@ -4,22 +4,22 @@ group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Award emoji **(FREE)**
# Award emojis **(FREE)**
When you're collaborating online, you get fewer opportunities for high-fives
and thumbs-ups. Emoji can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md),
and thumbs-ups. Emojis can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md),
[snippets](snippets.md), and anywhere you can have a thread.
![Award emoji](img/award_emoji_select_v14_6.png)
Award emoji make it much easier to give and receive feedback without a long
Award emojis make it much easier to give and receive feedback without a long
comment thread.
For information on the relevant API, see [Award Emoji API](../api/award_emoji.md).
## Sort issues and merge requests on vote count
You can quickly sort issues and merge requests by the number of votes they
You can quickly sort issues and merge requests by the number of votes ("thumbs up" and "thumbs down" emoji) they
have received. The sort options can be found in the dropdown list as "Most
popular" and "Least popular".
@ -29,9 +29,9 @@ The total number of votes is not summed up. An issue with 18 upvotes and 5
downvotes is considered more popular than an issue with 17 upvotes and no
downvotes.
## Award emoji for comments
## Award emojis for comments
Award emoji can also be applied to individual comments when you want to
Award emojis can also be applied to individual comments when you want to
celebrate an accomplishment or agree with an opinion.
To add an award emoji:
@ -40,3 +40,11 @@ To add an award emoji:
1. Select an emoji from the dropdown list.
To remove an award emoji, select the emoji again.
## Custom emojis
You can upload custom emojis to a GitLab instance with the GraphQL API.
For more, visit [Use custom emojis with GraphQL](../api/graphql/custom_emoji.md).
For the list of custom emojis available for GitLab.com, visit
[the `custom_emoji` project](https://gitlab.com/custom_emoji/custom_emoji/-/tree/main/img).

View File

@ -1019,7 +1019,7 @@ Payload example:
```
NOTE:
The fields `assignee_id`, and `state` are deprecated.
The fields `assignee_id`, `state`, `merge_status` are deprecated.
## Wiki page events

View File

@ -6,47 +6,57 @@ module API
include ActionView::Helpers::NumberHelper
include CountHelper
expose :forks do |counts|
expose :forks,
documentation: { type: 'integer', example: 6, desc: 'Approximate number of repo forks' } do |counts|
approximate_fork_count_with_delimiters(counts)
end
expose :issues do |counts|
expose :issues,
documentation: { type: 'integer', example: 121, desc: 'Approximate number of issues' } do |counts|
approximate_count_with_delimiters(counts, ::Issue)
end
expose :merge_requests do |counts|
expose :merge_requests,
documentation: { type: 'integer', example: 49, desc: 'Approximate number of merge requests' } do |counts|
approximate_count_with_delimiters(counts, ::MergeRequest)
end
expose :notes do |counts|
expose :notes,
documentation: { type: 'integer', example: 6, desc: 'Approximate number of notes' } do |counts|
approximate_count_with_delimiters(counts, ::Note)
end
expose :snippets do |counts|
expose :snippets,
documentation: { type: 'integer', example: 4, desc: 'Approximate number of snippets' } do |counts|
approximate_count_with_delimiters(counts, ::Snippet)
end
expose :ssh_keys do |counts|
expose :ssh_keys,
documentation: { type: 'integer', example: 11, desc: 'Approximate number of SSH keys' } do |counts|
approximate_count_with_delimiters(counts, ::Key)
end
expose :milestones do |counts|
expose :milestones,
documentation: { type: 'integer', example: 3, desc: 'Approximate number of milestones' } do |counts|
approximate_count_with_delimiters(counts, ::Milestone)
end
expose :users do |counts|
expose :users, documentation: { type: 'integer', example: 22, desc: 'Approximate number of users' } do |counts|
approximate_count_with_delimiters(counts, ::User)
end
expose :projects do |counts|
expose :projects,
documentation: { type: 'integer', example: 4, desc: 'Approximate number of projects' } do |counts|
approximate_count_with_delimiters(counts, ::Project)
end
expose :groups do |counts|
expose :groups,
documentation: { type: 'integer', example: 1, desc: 'Approximate number of projects' } do |counts|
approximate_count_with_delimiters(counts, ::Group)
end
expose :active_users do |_|
expose :active_users,
documentation: { type: 'integer', example: 21, desc: 'Number of active users' } do |_|
number_with_delimiter(::User.active.count)
end
end

View File

@ -10,7 +10,7 @@ module API
MergeRequest, Note, Snippet, Key, Milestone].freeze
desc 'Get the current application statistics' do
success Entities::ApplicationStatistics
success code: 200, model: Entities::ApplicationStatistics
end
get "application/statistics", urgency: :low do
counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# The class to populates the star counter of projects
class PopulateProjectsStarCount < BatchedMigrationJob
MAX_UPDATE_RETRIES = 3
operation_name :update_all
def perform
each_sub_batch do |sub_batch|
update_with_retry(sub_batch)
end
end
private
# rubocop:disable Database/RescueQueryCanceled
# rubocop:disable Database/RescueStatementTimeout
def update_with_retry(sub_batch)
update_attempt = 1
begin
update_batch(sub_batch)
rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => e
update_attempt += 1
if update_attempt <= MAX_UPDATE_RETRIES
sleep(5)
retry
end
raise e
end
end
# rubocop:enable Database/RescueQueryCanceled
# rubocop:enable Database/RescueStatementTimeout
def update_batch(sub_batch)
ApplicationRecord.connection.execute <<~SQL
WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{sub_batch.select(:id).to_sql})
UPDATE projects
SET star_count = (
SELECT COUNT(*)
FROM users_star_projects
INNER JOIN users
ON users_star_projects.user_id = users.id
WHERE users_star_projects.project_id = batched_relation.id
AND users.state = 'active'
)
FROM batched_relation
WHERE projects.id = batched_relation.id
SQL
end
end
end
end

View File

@ -15694,9 +15694,6 @@ msgstr ""
msgid "Error uploading file. Please try again."
msgstr ""
msgid "Error uploading file: %{stripped}"
msgstr ""
msgid "Error while loading the merge request. Please try again."
msgstr ""
@ -30557,9 +30554,6 @@ msgstr ""
msgid "Please select a country"
msgstr ""
msgid "Please select a file"
msgstr ""
msgid "Please select a group"
msgstr ""

View File

@ -10,7 +10,7 @@ module QA
include Helpers
QA_PATTERN = %r{^qa/}.freeze
SPEC_PATTERN = %r{^qa/qa/specs/features/}.freeze
SPEC_PATTERN = %r{^qa/qa/specs/features/\S+_spec\.rb}.freeze
DEPENDENCY_PATTERN = Regexp.union(
/_VERSION/,
/Gemfile\.lock/,

View File

@ -49,6 +49,14 @@ RSpec.describe QA::Tools::Ci::QaChanges do
end
end
context "with shared example changes" do
let(:mr_diff) { [{ path: "qa/qa/specs/features/shared_context/some_context.rb", diff: "" }] }
it ".qa_tests do not return specific specs" do
expect(qa_changes.qa_tests).to be_nil
end
end
context "with non qa changes" do
let(:mr_diff) { [{ path: "Gemfile" }] }

View File

@ -34,8 +34,15 @@ RSpec.describe Projects::LearnGitlabController do
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it_behaves_like 'tracks assignment and records the subject', :invite_for_help_continuous_onboarding, :namespace do
subject { project.namespace }
context 'with invite_for_help_continuous_onboarding experiment' do
it 'tracks the assignment', :experiment do
stub_experiments(invite_for_help_continuous_onboarding: true)
expect(experiment(:invite_for_help_continuous_onboarding))
.to track(:assignment).with_context(namespace: project.namespace).on_next_instance
action
end
end
end
end

View File

@ -299,14 +299,15 @@ RSpec.describe Projects::PipelinesController do
stub_application_setting(auto_devops_enabled: false)
end
def action
get :index, params: { namespace_id: project.namespace, project_id: project }
end
context 'with runners_availability_section experiment' do
it 'tracks the assignment', :experiment do
stub_experiments(runners_availability_section: true)
subject { project.namespace }
expect(experiment(:runners_availability_section))
.to track(:assignment).with_context(namespace: project.namespace).on_next_instance
context 'runners_availability_section experiment' do
it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace
get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
end

View File

@ -43,72 +43,6 @@ RSpec.describe ApplicationExperiment, :experiment do
variant: 'control'
)
end
describe '#publish_to_database' do
using RSpec::Parameterized::TableSyntax
let(:publish_to_database) { ActiveSupport::Deprecation.silence { application_experiment.publish_to_database } }
shared_examples 'does not record to the database' do
it 'does not create an experiment record' do
expect { publish_to_database }.not_to change(Experiment, :count)
end
it 'does not create an experiment subject record' do
expect { publish_to_database }.not_to change(ExperimentSubject, :count)
end
end
context 'when there is a usable subject' do
let(:context) { { context_key => context_value } }
where(:context_key, :context_value, :object_type) do
:namespace | build(:namespace, id: non_existing_record_id) | :namespace
:group | build(:namespace, id: non_existing_record_id) | :namespace
:project | build(:project, id: non_existing_record_id) | :project
:user | build(:user, id: non_existing_record_id) | :user
:actor | build(:user, id: non_existing_record_id) | :user
end
with_them do
it 'creates an experiment and experiment subject record' do
expect { publish_to_database }.to change(Experiment, :count).by(1)
expect(Experiment.last.name).to eq('namespaced/stub')
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
end
end
end
context "when experiment hasn't ran" do
let(:context) { { user: create(:user) } }
it 'sets a variant on the experiment subject' do
publish_to_database
expect(ExperimentSubject.last.variant).to eq('control')
end
end
context 'when there is not a usable subject' do
let(:context) { { context_key => context_value } }
where(:context_key, :context_value) do
:namespace | nil
:foo | :bar
end
with_them do
include_examples 'does not record to the database'
end
end
context 'but we should not track' do
let(:should_track) { false }
include_examples 'does not record to the database'
end
end
end
describe "#track", :snowplow do

View File

@ -30,34 +30,6 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
end
end
describe '#record_conversion' do
let_it_be(:namespace) { create(:namespace) }
context 'when should_track? is false' do
before do
allow(experiment).to receive(:should_track?).and_return(false)
end
it 'does not record a conversion event' do
expect(experiment.publish_to_database).to be_nil
expect(experiment.record_conversion(namespace)).to be_nil
end
end
context 'when should_track? is true' do
before do
allow(experiment).to receive(:should_track?).and_return(true)
end
it 'records a conversion event' do
experiment_subject = experiment.publish_to_database
expect { experiment.record_conversion(namespace) }.to change { experiment_subject.reload.converted_at }.from(nil)
.and change { experiment_subject.context }.to include('namespace_id' => namespace.id)
end
end
end
describe 'exclusions' do
context 'when user is new' do
it 'is not excluded' do

View File

@ -6,10 +6,4 @@ RSpec.describe SecurityReportsMrWidgetPromptExperiment do
it "defines a control and candidate" do
expect(subject.behaviors.keys).to match_array(%w[control candidate])
end
it "publishes to the database" do
expect(subject).to receive(:publish_to_database)
subject.publish
end
end

View File

@ -86,7 +86,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
context 'creating an issue for threads' do
context 'creating an issue for threads', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/381729' do
before do
page.within '.mr-state-widget' do
page.click_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)

View File

@ -52,7 +52,7 @@ describe('Board card component', () => {
const performSearchMock = jest.fn();
const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
const createStore = ({ isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
actions: {
@ -65,13 +65,12 @@ describe('Board card component', () => {
},
getters: {
isGroupBoard: () => true,
isEpicBoard: () => isEpicBoard,
isProjectBoard: () => isProjectBoard,
},
});
};
const createWrapper = (props = {}) => {
const createWrapper = ({ props = {}, isEpicBoard = false } = {}) => {
wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
@ -97,6 +96,7 @@ describe('Board card component', () => {
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard,
},
});
};
@ -111,7 +111,7 @@ describe('Board card component', () => {
};
createStore();
createWrapper({ item: issue, list });
createWrapper({ props: { item: issue, list } });
});
afterEach(() => {
@ -146,7 +146,7 @@ describe('Board card component', () => {
});
it('renders the work type icon when props is passed', () => {
createWrapper({ item: issue, list, showWorkItemTypeIcon: true });
createWrapper({ props: { item: issue, list, showWorkItemTypeIcon: true } });
expect(findWorkItemIcon().exists()).toBe(true);
expect(findWorkItemIcon().props('workItemType')).toBe(issue.type);
});
@ -177,9 +177,11 @@ describe('Board card component', () => {
describe('blocked', () => {
it('renders blocked icon if issue is blocked', async () => {
createWrapper({
item: {
...issue,
blocked: true,
props: {
item: {
...issue,
blocked: true,
},
},
});
@ -188,9 +190,11 @@ describe('Board card component', () => {
it('does not show blocked icon if issue is not blocked', () => {
createWrapper({
item: {
...issue,
blocked: false,
props: {
item: {
...issue,
blocked: false,
},
},
});
@ -201,9 +205,11 @@ describe('Board card component', () => {
describe('confidential issue', () => {
beforeEach(() => {
createWrapper({
item: {
...wrapper.props('item'),
confidential: true,
props: {
item: {
...wrapper.props('item'),
confidential: true,
},
},
});
});
@ -216,9 +222,11 @@ describe('Board card component', () => {
describe('hidden issue', () => {
beforeEach(() => {
createWrapper({
item: {
...wrapper.props('item'),
hidden: true,
props: {
item: {
...wrapper.props('item'),
hidden: true,
},
},
});
});
@ -241,11 +249,13 @@ describe('Board card component', () => {
describe('with avatar', () => {
beforeEach(() => {
createWrapper({
item: {
...wrapper.props('item'),
assignees: [user],
updateData(newData) {
Object.assign(this, newData);
props: {
item: {
...wrapper.props('item'),
assignees: [user],
updateData(newData) {
Object.assign(this, newData);
},
},
},
});
@ -294,15 +304,17 @@ describe('Board card component', () => {
global.gon.default_avatar_url = 'default_avatar';
createWrapper({
item: {
...wrapper.props('item'),
assignees: [
{
id: 1,
name: 'testing 123',
username: 'test',
},
],
props: {
item: {
...wrapper.props('item'),
assignees: [
{
id: 1,
name: 'testing 123',
username: 'test',
},
],
},
},
});
});
@ -323,28 +335,30 @@ describe('Board card component', () => {
describe('multiple assignees', () => {
beforeEach(() => {
createWrapper({
item: {
...wrapper.props('item'),
assignees: [
{
id: 2,
name: 'user2',
username: 'user2',
avatarUrl: 'test_image',
},
{
id: 3,
name: 'user3',
username: 'user3',
avatarUrl: 'test_image',
},
{
id: 4,
name: 'user4',
username: 'user4',
avatarUrl: 'test_image',
},
],
props: {
item: {
...wrapper.props('item'),
assignees: [
{
id: 2,
name: 'user2',
username: 'user2',
avatarUrl: 'test_image',
},
{
id: 3,
name: 'user3',
username: 'user3',
avatarUrl: 'test_image',
},
{
id: 4,
name: 'user4',
username: 'user4',
avatarUrl: 'test_image',
},
],
},
},
});
});
@ -364,9 +378,11 @@ describe('Board card component', () => {
});
createWrapper({
item: {
...wrapper.props('item'),
assignees,
props: {
item: {
...wrapper.props('item'),
assignees,
},
},
});
});
@ -390,9 +406,11 @@ describe('Board card component', () => {
})),
];
createWrapper({
item: {
...wrapper.props('item'),
assignees,
props: {
item: {
...wrapper.props('item'),
assignees,
},
},
});
@ -405,7 +423,7 @@ describe('Board card component', () => {
describe('labels', () => {
beforeEach(() => {
createWrapper({ item: { ...issue, labels: [list.label, label1] } });
createWrapper({ props: { item: { ...issue, labels: [list.label, label1] } } });
});
it('does not render list label but renders all other labels', () => {
@ -417,7 +435,7 @@ describe('Board card component', () => {
});
it('does not render label if label does not have an ID', async () => {
createWrapper({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
createWrapper({ props: { item: { ...issue, labels: [label1, { title: 'closed' }] } } });
await nextTick();
@ -429,11 +447,13 @@ describe('Board card component', () => {
describe('filterByLabel method', () => {
beforeEach(() => {
createWrapper({
item: {
...issue,
labels: [label1],
props: {
item: {
...issue,
labels: [label1],
},
updateFilters: true,
},
updateFilters: true,
});
});
@ -480,9 +500,11 @@ describe('Board card component', () => {
describe('loading', () => {
it('renders loading icon', async () => {
createWrapper({
item: {
...issue,
isLoading: true,
props: {
item: {
...issue,
isLoading: true,
},
},
});
@ -504,17 +526,20 @@ describe('Board card component', () => {
};
beforeEach(() => {
createStore({ isEpicBoard: true });
createStore();
});
it('should render if the item has issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: true,
props: {
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: true,
},
},
isEpicBoard: true,
});
expect(findEpicCountables().exists()).toBe(true);
@ -535,18 +560,21 @@ describe('Board card component', () => {
it('shows render item countBadge, weights, and progress correctly', () => {
createWrapper({
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
props: {
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
},
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
},
hasIssues: true,
},
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
},
hasIssues: true,
},
isEpicBoard: true,
});
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
@ -556,15 +584,18 @@ describe('Board card component', () => {
it('does not render progress when weight is zero', () => {
createWrapper({
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
props: {
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
},
descendantWeightSum,
hasIssues: true,
},
descendantWeightSum,
hasIssues: true,
},
isEpicBoard: true,
});
expect(findEpicBadgeProgress().exists()).toBe(false);
@ -572,15 +603,18 @@ describe('Board card component', () => {
it('renders the tooltip with the correct data', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
props: {
item: {
...issue,
descendantCounts,
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
},
hasIssues: true,
},
hasIssues: true,
},
isEpicBoard: true,
});
const tooltip = findEpicCountablesTotalTooltip();

View File

@ -101,6 +101,8 @@ export default function createComponent({
weightFeatureAvailable: false,
boardWeight: null,
canAdminList: true,
isIssueBoard: true,
isEpicBoard: false,
...provide,
},
stubs,

View File

@ -53,11 +53,11 @@ describe('Board card layout', () => {
state: {
labels,
labelsLoading: false,
isEpicBoard: false,
},
}),
provide: {
scopedLabelsAvailable: true,
isEpicBoard: false,
},
}),
);

View File

@ -30,7 +30,6 @@ describe('Board card', () => {
},
actions: mockActions,
getters: {
isEpicBoard: () => false,
isProjectBoard: () => false,
},
});
@ -61,6 +60,7 @@ describe('Board card', () => {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard: false,
...provide,
},
});

View File

@ -47,6 +47,8 @@ describe('BoardContent', () => {
canAdminList = true,
isApolloBoard = false,
issuableType = 'issue',
isIssueBoard = true,
isEpicBoard = false,
boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
} = {}) => {
fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
@ -67,6 +69,8 @@ describe('BoardContent', () => {
boardType: 'group',
fullPath: 'gitlab-org/gitlab',
issuableType,
isIssueBoard,
isEpicBoard,
isApolloBoard,
},
store,
@ -133,7 +137,7 @@ describe('BoardContent', () => {
describe('when issuableType is not issue', () => {
beforeEach(() => {
createComponent({ issuableType: 'foo' });
createComponent({ issuableType: 'foo', isIssueBoard: false });
});
it('does not render BoardContentSidebar', () => {

View File

@ -59,7 +59,6 @@ describe('Board List Header Component', () => {
store = new Vuex.Store({
state: {},
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
getters: { isEpicBoard: () => false },
});
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
@ -76,6 +75,7 @@ describe('Board List Header Component', () => {
boardId,
weightFeatureAvailable: false,
currentUserId,
isEpicBoard: false,
},
}),
);

View File

@ -45,6 +45,7 @@ describe('BoardSettingsSidebar', () => {
provide: {
canAdminList,
scopedLabelsAvailable: false,
isIssueBoard: true,
},
directives: {
GlModal: createMockDirective(),

View File

@ -15,18 +15,14 @@ describe('BoardTopBar', () => {
Vue.use(Vuex);
const createStore = ({ mockGetters = {} } = {}) => {
const createStore = () => {
return new Vuex.Store({
state: {},
getters: {
isEpicBoard: () => false,
...mockGetters,
},
});
};
const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
const store = createStore({ mockGetters });
const createComponent = ({ provide = {} } = {}) => {
const store = createStore();
wrapper = shallowMount(BoardTopBar, {
store,
provide: {
@ -36,6 +32,7 @@ describe('BoardTopBar', () => {
fullPath: 'gitlab-org',
boardType: 'group',
releasesFetchPath: '/releases',
isIssueBoard: true,
...provide,
},
stubs: { IssueBoardFilteredSearch },

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::PopulateProjectsStarCount, schema: 20221019105041 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
let(:users_star_projects) { table(:users_star_projects) }
let(:namespace1) { namespaces.create!(name: 'namespace 1', path: 'namespace1') }
let(:namespace2) { namespaces.create!(name: 'namespace 2', path: 'namespace2') }
let(:namespace3) { namespaces.create!(name: 'namespace 3', path: 'namespace3') }
let(:namespace4) { namespaces.create!(name: 'namespace 4', path: 'namespace4') }
let(:namespace5) { namespaces.create!(name: 'namespace 5', path: 'namespace5') }
let(:project1) { projects.create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id) }
let(:project2) { projects.create!(namespace_id: namespace2.id, project_namespace_id: namespace2.id) }
let(:project3) { projects.create!(namespace_id: namespace3.id, project_namespace_id: namespace3.id) }
let(:project4) { projects.create!(namespace_id: namespace4.id, project_namespace_id: namespace4.id) }
let(:project5) { projects.create!(namespace_id: namespace5.id, project_namespace_id: namespace5.id) }
let(:user_active) { users.create!(state: 'active', email: 'test1@example.com', projects_limit: 5) }
let(:user_blocked) { users.create!(state: 'blocked', email: 'test2@example.com', projects_limit: 5) }
let(:migration) do
described_class.new(
start_id: project1.id,
end_id: project4.id,
batch_table: :projects,
batch_column: :id,
sub_batch_size: 2,
pause_ms: 2,
connection: ApplicationRecord.connection
)
end
subject(:perform_migration) { migration.perform }
it 'correctly populates the star counters' do
users_star_projects.create!(project_id: project1.id, user_id: user_active.id)
users_star_projects.create!(project_id: project2.id, user_id: user_blocked.id)
users_star_projects.create!(project_id: project4.id, user_id: user_active.id)
users_star_projects.create!(project_id: project4.id, user_id: user_blocked.id)
users_star_projects.create!(project_id: project5.id, user_id: user_active.id)
perform_migration
expect(project1.reload.star_count).to eq(1)
expect(project2.reload.star_count).to eq(0)
expect(project3.reload.star_count).to eq(0)
expect(project4.reload.star_count).to eq(1)
expect(project5.reload.star_count).to eq(0)
end
context 'when database timeouts' do
using RSpec::Parameterized::TableSyntax
where(error_class: [ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled])
with_them do
it 'retries on timeout error' do
expect(migration).to receive(:update_batch).exactly(3).times.and_raise(error_class)
expect(migration).to receive(:sleep).with(5).twice
expect do
perform_migration
end.to raise_error(error_class)
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueuePopulateProjectsStarCount do
let_it_be(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :projects,
column_name: :id,
interval: described_class::DELAY_INTERVAL
)
}
end
end
end

View File

@ -388,15 +388,30 @@ RSpec.describe Deployment do
end
context 'when deployment is behind current deployment' do
let_it_be(:commits) { project.repository.commits('master', limit: 2) }
let!(:deployment) do
create(:deployment, :success, project: project, environment: environment,
finished_at: 1.year.ago, sha: commits[0].sha)
end
let!(:last_deployment) do
create(:deployment, :success, project: project, environment: environment, sha: commits[1].sha)
end
it { is_expected.to be_truthy }
end
context 'when deployment is the same sha as the current deployment' do
let!(:deployment) do
create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
end
let!(:last_deployment) do
create(:deployment, :success, project: project, environment: environment)
create(:deployment, :success, project: project, environment: environment, sha: deployment.sha)
end
it { is_expected.to be_truthy }
it { is_expected.to be_falsey }
end
end

View File

@ -1658,6 +1658,33 @@ RSpec.describe Project, factory_default: :keep do
expect(project.reload.star_count).to eq(0)
end
it 'does not count stars from blocked users' do
user1 = create(:user)
user2 = create(:user)
project = create(:project, :public)
expect(project.star_count).to eq(0)
user1.toggle_star(project)
expect(project.reload.star_count).to eq(1)
user2.toggle_star(project)
project.reload
expect(project.reload.star_count).to eq(2)
user1.block
project.reload
expect(project.reload.star_count).to eq(1)
user2.block
project.reload
expect(project.reload.star_count).to eq(0)
user1.activate
project.reload
expect(project.reload.star_count).to eq(1)
end
it 'counts stars on the right project' do
user = create(:user)
project1 = create(:project, :public)

View File

@ -2482,6 +2482,30 @@ RSpec.describe User do
end
end
describe 'starred_projects' do
let_it_be(:project) { create(:project) }
before do
user.toggle_star(project)
end
context 'when blocking a user' do
let_it_be(:user) { create(:user) }
it 'decrements star count of project' do
expect { user.block }.to change { project.reload.star_count }.by(-1)
end
end
context 'when activating a user' do
let_it_be(:user) { create(:user, :blocked) }
it 'increments star count of project' do
expect { user.activate }.to change { project.reload.star_count }.by(1)
end
end
end
describe '.instance_access_request_approvers_to_be_notified' do
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }

View File

@ -3,14 +3,14 @@
require 'spec_helper'
RSpec.describe UsersStarProject, type: :model do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:user_active) { create(:user, state: 'active', name: 'user2', private_profile: true) }
let_it_be(:user_blocked) { create(:user, state: 'blocked', name: 'user1') }
it { is_expected.to belong_to(:project).touch(false) }
describe 'scopes' do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:user_active) { create(:user, state: 'active', name: 'user2', private_profile: true) }
let_it_be(:user_blocked) { create(:user, state: 'blocked', name: 'user1') }
let_it_be(:users_star_project1) { create(:users_star_project, project: project1, user: user_active) }
let_it_be(:users_star_project2) { create(:users_star_project, project: project2, user: user_blocked) }
@ -50,4 +50,38 @@ RSpec.describe UsersStarProject, type: :model do
end
end
end
describe 'star count hooks' do
context 'on after_create' do
context 'if user is active' do
it 'increments star count of project' do
expect { user_active.toggle_star(project1) }.to change { project1.reload.star_count }.by(1)
end
end
context 'if user is not active' do
it 'does not increment star count of project' do
expect { user_blocked.toggle_star(project1) }.not_to change { project1.reload.star_count }
end
end
end
context 'on after_destory' do
context 'if user is active' do
let_it_be(:users_star_project) { create(:users_star_project, project: project2, user: user_active) }
it 'decrements star count of project' do
expect { users_star_project.destroy! }.to change { project2.reload.star_count }.by(-1)
end
end
context 'if user is not active' do
let_it_be(:users_star_project) { create(:users_star_project, project: project2, user: user_blocked) }
it 'does not decrement star count of project' do
expect { users_star_project.destroy! }.not_to change { project2.reload.star_count }
end
end
end
end
end

View File

@ -2884,6 +2884,27 @@ RSpec.describe ProjectPolicy do
end
end
describe 'read_code' do
let(:current_user) { create(:user) }
before do
allow(subject).to receive(:allowed?).and_call_original
allow(subject).to receive(:allowed?).with(:download_code).and_return(can_download_code)
end
context 'when the current_user can download_code' do
let(:can_download_code) { true }
it { expect_allowed(:read_code) }
end
context 'when the current_user cannot download_code' do
let(:can_download_code) { false }
it { expect_disallowed(:read_code) }
end
end
private
def project_subject(project_type)

View File

@ -15,6 +15,9 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
end
it 'moves the user\'s stars from one project to another' do
project_with_stars.reload
target_project.reload
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
@ -34,6 +37,8 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
project_with_stars.reload
target_project.reload
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2

View File

@ -57,7 +57,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
context "race conditions" do
context "when #{record_class_name} migration fails and is rolled back" do
before do
expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
allow_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
end
@ -68,6 +68,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
end
it "doesn't unblock a previously-blocked user" do
expect(user.starred_projects).to receive(:update_all).and_call_original
user.block
expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'tracks assignment and records the subject' do |experiment, subject_type|
before do
stub_experiments(experiment => true)
end
it 'tracks the assignment', :experiment do
expect(experiment(experiment))
.to track(:assignment)
.with_context(subject_type => subject)
.on_next_instance
action
end
it 'records the subject' do
expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: anything, subject: subject)
action
end
end