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 --> <!-- 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 ## 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. that is currently behind the `<feature-flag-name>` feature flag.
<!-- Short description of what the feature is about and link to relevant other issues. --> <!-- 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. - [ ] 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. 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)). - [ ] 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. - [ ] 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)). - [ ] 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` - [ ] `/chatops run feature set <feature-flag-name> <rollout-percentage> --random`
- Enable the feature globally on production environment. - Enable the feature globally on production environment.
- [ ] `/chatops run feature set <feature-flag-name> true` - [ ] `/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). - [ ] 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 ### (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>` - [ ] `/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. - [ ] 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` - [ ] `/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). - [ ] 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). - [ ] (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>`". - [ ] 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), 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. the feature can be officially announced in a release blog post.
- [ ] `/chatops run release check <merge-request-url> <milestone>` - [ ] `/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: - [ ] 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` - [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
- [ ] Close this rollout issue. - [ ] 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 $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml'; import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { createAlert } from '~/flash'; import { createAlert } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import { setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import NewCommitForm from '../new_commit_form'; import NewCommitForm from '../new_commit_form';
const initPopovers = () => { 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 () => { export default () => {
const editBlobForm = $('.js-edit-blob-form'); const editBlobForm = $('.js-edit-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
@ -99,10 +85,4 @@ export default () => {
// returning here blocks page navigation // returning here blocks page navigation
window.onbeforeunload = () => ''; window.onbeforeunload = () => '';
} }
initUploadForm();
if (deleteBlobForm.length) {
new NewCommitForm(deleteBlobForm);
}
}; };

View File

@ -43,7 +43,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [boardCardInner], mixins: [boardCardInner],
inject: ['rootPath', 'scopedLabelsAvailable'], inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard'],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -78,7 +78,7 @@ export default {
}, },
computed: { computed: {
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']), ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard', 'isProjectBoard']), ...mapGetters(['isProjectBoard']),
cappedAssignees() { cappedAssignees() {
// e.g. maxRender is 4, // e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness // 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 { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants'; import { defaultSortableOptions } from '~/sortable/constants';
import { import { DraggableItemTypes, BoardType, listsQuery } from 'ee_else_ce/boards/constants';
DraggableItemTypes,
issuableTypes,
BoardType,
listsQuery,
} from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue'; import BoardColumn from './board_column.vue';
export default { export default {
@ -31,7 +26,15 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert, GlAlert,
}, },
inject: ['canAdminList', 'boardType', 'fullPath', 'issuableType', 'isApolloBoard'], inject: [
'canAdminList',
'boardType',
'fullPath',
'issuableType',
'isIssueBoard',
'isEpicBoard',
'isApolloBoard',
],
props: { props: {
disabled: { disabled: {
type: Boolean, type: Boolean,
@ -78,12 +81,6 @@ export default {
computed: { computed: {
...mapState(['boardLists', 'error', 'addColumnForm']), ...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn']), ...mapGetters(['isSwimlanesOn']),
isIssueBoard() {
return this.issuableType === issuableTypes.issue;
},
isEpicBoard() {
return this.issuableType === issuableTypes.epic;
},
addColumnFormVisible() { addColumnFormVisible() {
return this.addColumnForm?.visible; return this.addColumnForm?.visible;
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,14 +10,6 @@ import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more'; import initReadMore from '~/read_more';
// Project show page loads different overview content based on user preferences // 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')) { if (document.getElementById('js-tree-list')) {
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository') import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
.then(({ default: initTree }) => { .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| experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
e.candidate {} e.candidate {}
e.publish_to_database
end end
end end

View File

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

View File

@ -3,25 +3,6 @@
class ApplicationExperiment < Gitlab::Experiment class ApplicationExperiment < Gitlab::Experiment
control { nil } # provide a default control for anonymous experiments 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 def control_behavior
# define a default nil control behavior so we can omit it when not needed # define a default nil control behavior so we can omit it when not needed
end end

View File

@ -12,18 +12,8 @@ class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment
run run
end 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 private
def subject
context.value[:user]
end
def existing_user def existing_user
return false unless user_or_actor return false unless user_or_actor

View File

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

View File

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

View File

@ -452,6 +452,14 @@ class User < ApplicationRecord
after_transition banned: :active do |user| after_transition banned: :active do |user|
user.banned_user&.destroy user.banned_user&.destroy
end 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 end
# Scopes # Scopes

View File

@ -3,7 +3,7 @@
class UsersStarProject < ApplicationRecord class UsersStarProject < ApplicationRecord
include Sortable include Sortable
belongs_to :project, counter_cache: :star_count belongs_to :project
belongs_to :user belongs_to :user
validates :user, presence: true validates :user, presence: true
@ -12,7 +12,10 @@ class UsersStarProject < ApplicationRecord
alias_attribute :starred_since, :created_at 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_asc, -> { joins(:user).merge(User.order_name_asc) }
scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) } scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) }
scope :by_project, -> (project) { where(project_id: project.id) } 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)) joins(:user).merge(User.search(query, use_minimum_char_limit: false))
end end
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 end

View File

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

View File

@ -12,8 +12,8 @@ module Projects
Project.transaction do Project.transaction do
user_stars.update_all(project_id: @project.id) user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects @project.update(star_count: @project.starrers.with_state(:active).size)
Project.reset_counters source_project.id, :users_star_projects source_project.update(star_count: source_project.starrers.with_state(:active).size)
success success
end 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." 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_issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/377877"
introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154" introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154"
milestone: 15.6 milestone: "15.6"
group: "govern::security policies" group: "govern::security policies"
saved_to_database: true saved_to_database: true
streamed: false 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 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. 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)). - [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)).
- [Snippets](../user/snippets.md) ([API](snippets.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 ## 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 ### 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" 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. 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). | | `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. | | `issue_iid` | integer | yes | Internal ID of an issue. |
| `note_id` | integer | yes | ID of a comment (note). | | `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: 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)** # Use custom emojis with GraphQL **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6 > - [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.
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default. > - Enabled on GitLab.com in GitLab 14.0.
> - 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)**
This in-development feature might not be available for your use. There can be FLAG:
[risks when enabling features still in development](../../administration/feature_flags.md#risks-when-enabling-features-still-in-development). 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`.
Refer to this feature's version history for more details. 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: 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 ## 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 specific entities, such as Fragments and Interfaces, see the official
[GraphQL documentation](https://graphql.org/learn/). [GraphQL documentation](https://graphql.org/learn/).
- Individual attributes, see the [GraphQL API Resources](reference/index.md). - 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_commit_sha` | string | SHA of the merge request commit (set once merged). |
| `merge_error` | string | Error message due to a merge error. | | `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_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_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 (MWPS). | | `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_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. | | `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. | | `milestone` | object | Milestone of the merge request. |
@ -848,14 +848,11 @@ the `approvals_before_merge` parameter:
### Merge status ### 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: - The `detailed_merge_status` field may hold one of the following values:
- `blocked_status`: Merge request is blocked by another merge request. - `blocked_status`: Merge request is blocked by another merge request.
- `broken_status`: Can not merge the source into the target branch, potential conflict. - `broken_status`: Can not merge the source into the target branch, potential conflict.

View File

@ -1,50 +1,41 @@
--- ---
stage: Verify 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 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 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: Before you start, make sure you have:
- A project in GitLab that you would like to use CI/CD for. - A project in GitLab that you would like to use CI/CD for.
- The Maintainer or Owner role for the project. - 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). ## Steps
- [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. To create and run your first pipeline:
> - <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:
1. [Ensure you have runners available](#ensure-you-have-runners-available) to run your jobs. 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/) If you're using GitLab.com, you can skip this step. GitLab.com provides shared runners for you.
and [register a runner](https://docs.gitlab.com/runner/register/) for your instance, project, or group.
1. [Create a `.gitlab-ci.yml` file](#create-a-gitlab-ciyml-file) 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. 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). 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. 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: To view available runners:
- Go to **Settings > CI/CD** and expand **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, 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. 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 ### If you don't have a runner
must [install GitLab Runner](https://docs.gitlab.com/runner/install/) and
[register](https://docs.gitlab.com/runner/register/) at least one runner.
If you are testing CI/CD, you can install GitLab Runner and register runners on your local machine. If you don't have a runner:
When your CI/CD jobs run, they run on your local machine.
### 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 When your CI/CD jobs run, in a later step, they will run on your local machine.
you configure specific instructions for GitLab CI/CD.
## 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: In this file, you define:
- The structure and order of jobs that the runner should execute. - The structure and order of jobs that the runner should execute.
- The decisions the runner should make when specific conditions are encountered. - 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: To create a `.gitlab-ci.yml` file:
1. On the left sidebar, select **Project information > Details**. 1. On the left sidebar, select **Repository > Files**.
1. Above the file list, select the branch you want to commit to, 1. Above the file list, select the branch you want to commit to.
select the plus icon, then select **New file**: 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) ![New file](img/new_file_v13_6.png)
@ -112,46 +101,50 @@ To create a `.gitlab-ci.yml` file:
environment: production environment: production
``` ```
`$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are This example shows four jobs: `build-job`, `test-job1`, `test-job2`, and `deploy-prod`.
[predefined variables](../variables/predefined_variables.md) The comments listed in the `echo` commands are displayed in the UI when you view the jobs.
that populate when the job runs. 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**. 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) Now take a look at your pipeline and the jobs within.
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:
```yaml 1. Go to **CI/CD > Pipelines**. A pipeline with three stages should be displayed:
default:
image: ruby:2.7.5
```
This command tells the runner to use a Ruby image from Docker Hub ![Three stages](img/three_stages_v13_6.png)
and to run the jobs in a container that's generated from the image.
This process is different than 1. View a visual representation of your pipeline by selecting the pipeline ID:
[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.
- 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 - The [`default`](../yaml/index.md#default) keyword is for
custom defaults, for example with [`before_script`](../yaml/index.md#before_script) custom defaults, for example with [`before_script`](../yaml/index.md#before_script)
and [`after_script`](../yaml/index.md#after_script). and [`after_script`](../yaml/index.md#after_script).
- [`stage`](../yaml/index.md#stage) describes the sequential execution of jobs. - [`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. 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 - Use the [`needs` keyword](../yaml/index.md#needs) to run jobs out of stage order.
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: - 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. - 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 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) - 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 and [`artifacts`](../yaml/index.md#artifacts). These keywords are ways to store
dependencies and job output, even when using ephemeral runners for each job. 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. - [Follow this guide to migrate from CircleCI](../migration/circleci.md).
- [Follow this guide to migrate from Jenkins](../migration/jenkins.md).
To view your pipeline: - <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.
- 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.

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 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. widget. You also get the link to the remote pipeline.
1. In the `gitlab-org/gitlab-docs` project, the pipeline is created and it 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. to lower the build time.
1. Once the docs site is built, the HTML files are uploaded as artifacts. 1. After the docs site is built, the HTML files are uploaded as artifacts to
1. A specific runner tied only to the docs project, runs the Review App job a GCP bucket (see [issue `gitlab-com/gl-infra/reliability#11021`](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/11021)
that downloads the artifacts and uses `rsync` to transfer the files over for the implementation details).
to a location where NGINX serves them.
The following GitLab features are used among others: 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) - [Multi project pipelines](../../ci/pipelines/downstream_pipelines.md#multi-project-pipelines)
- [Review Apps](../../ci/review_apps/index.md) - [Review Apps](../../ci/review_apps/index.md)
- [Artifacts](../../ci/yaml/index.md#artifacts) - [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) - [Merge request pipelines](../../ci/pipelines/merge_request_pipelines.md)
## Troubleshooting review apps ## 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 If you see the following message in a review app, either the site is not
yet deployed, or something went wrong with the remote pipeline. You can: yet deployed, or something went wrong with the downstream pipeline in `gitlab-docs`.
- Wait a few minutes and it should appear online. ```plaintext
- Check the manual job's log and verify the URL. If the URL is different, try the NoSuchKeyThe specified key does not exist.No such object: <URL>
one from the job log. ```
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. - 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` 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. internal Slack channel. Contributors can ping a
[technical writer](https://about.gitlab.com/handbook/product/ux/technical-writing/#designated-technical-writers)
### Not enough disk space in the merge request.
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.

View File

@ -21,7 +21,7 @@ but that repository is no longer used for development.
## Install Workhorse ## 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/). [GNU Make](https://www.gnu.org/software/make/).
To install into `/usr/local/bin` run `make install`. 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 | | 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> [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> [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. | | | <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. the history of when and how a file changed, and who changed it.
```mermaid ```mermaid
graph TB graph LR
subgraph Repository commit history 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 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 outdated marketing information<br> Commit ID: aabb1122) ---> C 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 a new 'Invoices' file<br> Commit ID: ddee4455) C(Author: Zhang<br>Date: 5 Jan at 3PM<br>Commit message: Added invoices<br> Commit ID: ddee4455)
end 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. 1. When you're ready, *merge* your branch into the default branch.
```mermaid ```mermaid
flowchart TB flowchart LR
subgraph Default branch subgraph Default branch
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit] A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
end end
subgraph My branch subgraph My branch
B --1. Create my branch--> E(Commit) B --1. Create my branch--> E(Commit)
E --2. Add my commit--> F(Commit) E --2. Add my commit--> F(Commit)
F --2. Add my commit--> G(Commit) F --3. Merge my branch to default--> D
G --3. Merge my branch to default--> D
end 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 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). [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>
</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 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 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. [snippets](snippets.md), and anywhere you can have a thread.
![Award emoji](img/award_emoji_select_v14_6.png) ![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. comment thread.
For information on the relevant API, see [Award Emoji API](../api/award_emoji.md). For information on the relevant API, see [Award Emoji API](../api/award_emoji.md).
## Sort issues and merge requests on vote count ## 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 have received. The sort options can be found in the dropdown list as "Most
popular" and "Least popular". 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 is considered more popular than an issue with 17 upvotes and no
downvotes. 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. celebrate an accomplishment or agree with an opinion.
To add an award emoji: To add an award emoji:
@ -40,3 +40,11 @@ To add an award emoji:
1. Select an emoji from the dropdown list. 1. Select an emoji from the dropdown list.
To remove an award emoji, select the emoji again. 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: NOTE:
The fields `assignee_id`, and `state` are deprecated. The fields `assignee_id`, `state`, `merge_status` are deprecated.
## Wiki page events ## Wiki page events

View File

@ -6,47 +6,57 @@ module API
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include CountHelper 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) approximate_fork_count_with_delimiters(counts)
end 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) approximate_count_with_delimiters(counts, ::Issue)
end 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) approximate_count_with_delimiters(counts, ::MergeRequest)
end 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) approximate_count_with_delimiters(counts, ::Note)
end 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) approximate_count_with_delimiters(counts, ::Snippet)
end 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) approximate_count_with_delimiters(counts, ::Key)
end 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) approximate_count_with_delimiters(counts, ::Milestone)
end 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) approximate_count_with_delimiters(counts, ::User)
end 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) approximate_count_with_delimiters(counts, ::Project)
end 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) approximate_count_with_delimiters(counts, ::Group)
end 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) number_with_delimiter(::User.active.count)
end end
end end

View File

@ -10,7 +10,7 @@ module API
MergeRequest, Note, Snippet, Key, Milestone].freeze MergeRequest, Note, Snippet, Key, Milestone].freeze
desc 'Get the current application statistics' do desc 'Get the current application statistics' do
success Entities::ApplicationStatistics success code: 200, model: Entities::ApplicationStatistics
end end
get "application/statistics", urgency: :low do get "application/statistics", urgency: :low do
counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS) 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." msgid "Error uploading file. Please try again."
msgstr "" msgstr ""
msgid "Error uploading file: %{stripped}"
msgstr ""
msgid "Error while loading the merge request. Please try again." msgid "Error while loading the merge request. Please try again."
msgstr "" msgstr ""
@ -30557,9 +30554,6 @@ msgstr ""
msgid "Please select a country" msgid "Please select a country"
msgstr "" msgstr ""
msgid "Please select a file"
msgstr ""
msgid "Please select a group" msgid "Please select a group"
msgstr "" msgstr ""

View File

@ -10,7 +10,7 @@ module QA
include Helpers include Helpers
QA_PATTERN = %r{^qa/}.freeze 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( DEPENDENCY_PATTERN = Regexp.union(
/_VERSION/, /_VERSION/,
/Gemfile\.lock/, /Gemfile\.lock/,

View File

@ -49,6 +49,14 @@ RSpec.describe QA::Tools::Ci::QaChanges do
end end
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 context "with non qa changes" do
let(:mr_diff) { [{ path: "Gemfile" }] } 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) } it { is_expected.to have_gitlab_http_status(:not_found) }
end end
it_behaves_like 'tracks assignment and records the subject', :invite_for_help_continuous_onboarding, :namespace do context 'with invite_for_help_continuous_onboarding experiment' do
subject { project.namespace } 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 end
end end

View File

@ -299,14 +299,15 @@ RSpec.describe Projects::PipelinesController do
stub_application_setting(auto_devops_enabled: false) stub_application_setting(auto_devops_enabled: false)
end end
def action context 'with runners_availability_section experiment' do
get :index, params: { namespace_id: project.namespace, project_id: project } it 'tracks the assignment', :experiment do
end 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 get :index, params: { namespace_id: project.namespace, project_id: project }
it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace end
end end
end end

View File

@ -43,72 +43,6 @@ RSpec.describe ApplicationExperiment, :experiment do
variant: 'control' variant: 'control'
) )
end 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 end
describe "#track", :snowplow do describe "#track", :snowplow do

View File

@ -30,34 +30,6 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
end end
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 describe 'exclusions' do
context 'when user is new' do context 'when user is new' do
it 'is not excluded' do it 'is not excluded' do

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,18 +15,14 @@ describe('BoardTopBar', () => {
Vue.use(Vuex); Vue.use(Vuex);
const createStore = ({ mockGetters = {} } = {}) => { const createStore = () => {
return new Vuex.Store({ return new Vuex.Store({
state: {}, state: {},
getters: {
isEpicBoard: () => false,
...mockGetters,
},
}); });
}; };
const createComponent = ({ provide = {}, mockGetters = {} } = {}) => { const createComponent = ({ provide = {} } = {}) => {
const store = createStore({ mockGetters }); const store = createStore();
wrapper = shallowMount(BoardTopBar, { wrapper = shallowMount(BoardTopBar, {
store, store,
provide: { provide: {
@ -36,6 +32,7 @@ describe('BoardTopBar', () => {
fullPath: 'gitlab-org', fullPath: 'gitlab-org',
boardType: 'group', boardType: 'group',
releasesFetchPath: '/releases', releasesFetchPath: '/releases',
isIssueBoard: true,
...provide, ...provide,
}, },
stubs: { IssueBoardFilteredSearch }, 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 end
context 'when deployment is behind current deployment' do 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 let!(:deployment) do
create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago) create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
end end
let!(:last_deployment) do let!(:last_deployment) do
create(:deployment, :success, project: project, environment: environment) create(:deployment, :success, project: project, environment: environment, sha: deployment.sha)
end end
it { is_expected.to be_truthy } it { is_expected.to be_falsey }
end end
end end

View File

@ -1658,6 +1658,33 @@ RSpec.describe Project, factory_default: :keep do
expect(project.reload.star_count).to eq(0) expect(project.reload.star_count).to eq(0)
end 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 it 'counts stars on the right project' do
user = create(:user) user = create(:user)
project1 = create(:project, :public) project1 = create(:project, :public)

View File

@ -2482,6 +2482,30 @@ RSpec.describe User do
end end
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 describe '.instance_access_request_approvers_to_be_notified' do
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) } let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }

View File

@ -3,14 +3,14 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe UsersStarProject, type: :model do 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) } it { is_expected.to belong_to(:project).touch(false) }
describe 'scopes' do 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_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) } 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 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 end

View File

@ -2884,6 +2884,27 @@ RSpec.describe ProjectPolicy do
end end
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 private
def project_subject(project_type) def project_subject(project_type)

View File

@ -15,6 +15,9 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
end end
it 'moves the user\'s stars from one project to another' do 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.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2 expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0 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) allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_stars) }.to raise_error(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.users_star_projects.count).to eq 2
expect(project_with_stars.star_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 "race conditions" do
context "when #{record_class_name} migration fails and is rolled back" do context "when #{record_class_name} migration fails and is rolled back" do
before 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) .to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
end end
@ -68,6 +68,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
end end
it "doesn't unblock a previously-blocked user" do it "doesn't unblock a previously-blocked user" do
expect(user.starred_projects).to receive(:update_all).and_call_original
user.block user.block
expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout) 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