Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-23 18:08:14 +00:00
parent 3e53902ee1
commit 5e555ebcf6
49 changed files with 555 additions and 399 deletions

View File

@ -456,12 +456,7 @@ export default {
<p class="gl-mb-5">
{{ $options.i18n.autoCollapsed }}
</p>
<gl-button
data-testid="expand-button"
category="secondary"
variant="warning"
@click.prevent="handleToggle"
>
<gl-button data-testid="expand-button" @click.prevent="handleToggle">
{{ $options.i18n.expand }}
</gl-button>
</div>

View File

@ -13,8 +13,6 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -160,9 +158,7 @@ export default {
},
apollo: {
issues: {
query() {
return this.hasCrmParameter ? getIssuesQuery : getIssuesWithoutCrmQuery;
},
query: getIssuesQuery,
variables() {
return this.queryVariables;
},
@ -186,9 +182,7 @@ export default {
debounce: 200,
},
issuesCounts: {
query() {
return this.hasCrmParameter ? getIssuesCountsQuery : getIssuesCountsWithoutCrmQuery;
},
query: getIssuesCountsQuery,
variables() {
return this.queryVariables;
},
@ -403,12 +397,6 @@ export default {
page_before: this.pageParams.beforeCursor,
};
},
hasCrmParameter() {
return (
window.location.search.includes('crm_contact_id=') ||
window.location.search.includes('crm_organization_id=')
);
},
},
watch: {
$route(newValue, oldValue) {

View File

@ -1,136 +0,0 @@
query getIssuesCountWithoutCrm(
$isProject: Boolean = false
$fullPath: ID!
$iid: String
$search: String
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
$confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
$myReactionEmoji: String
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
openedIssues: issues(
includeSubgroups: true
state: opened
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
count
}
closedIssues: issues(
includeSubgroups: true
state: closed
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
count
}
allIssues: issues(
includeSubgroups: true
state: all
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
) {
count
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
id
openedIssues: issues(
state: opened
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
count
}
closedIssues: issues(
state: closed
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
count
}
allIssues: issues(
state: all
iid: $iid
search: $search
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
) {
count
}
}
}

View File

@ -1,94 +0,0 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "./issue.fragment.graphql"
query getIssuesWithoutCrm(
$hideUsers: Boolean = false
$isProject: Boolean = false
$isSignedIn: Boolean = false
$fullPath: ID!
$iid: String
$search: String
$sort: IssueSort
$state: IssuableState
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
$confidential: Boolean
$labelName: [String]
$milestoneTitle: [String]
$milestoneWildcardId: MilestoneWildcardId
$myReactionEmoji: String
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
issues(
includeSubgroups: true
iid: $iid
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
types: $types
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
reference(full: true)
}
}
}
project(fullPath: $fullPath) @include(if: $isProject) {
id
issues(
iid: $iid
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
confidential: $confidential
labelName: $labelName
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
myReactionEmoji: $myReactionEmoji
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
pageInfo {
...PageInfo
}
nodes {
...IssueFragment
}
}
}
}

View File

@ -12,6 +12,7 @@ import Vue from 'vue';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import createFlash from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
@ -66,7 +67,7 @@ export default {
issuableType: {
type: String,
required: false,
default: 'issue',
default: IssuableType.Issue,
},
updateUrl: {
type: String,
@ -177,7 +178,9 @@ export default {
onError: this.taskListUpdateError.bind(this),
});
this.renderSortableLists();
if (this.issuableType === IssuableType.Issue) {
this.renderSortableLists();
}
}
},
renderSortableLists() {
@ -185,6 +188,10 @@ export default {
const lists = document.querySelectorAll('.description ul, .description ol');
lists.forEach((list) => {
if (list.children.length <= 1) {
return;
}
Array.from(list.children).forEach((listItem) => {
listItem.prepend(this.createDragIconElement());
this.addPointerEventListeners(listItem);
@ -211,13 +218,18 @@ export default {
},
addPointerEventListeners(listItem) {
const pointeroverListener = (event) => {
if (isDragging() || this.isUpdating) {
const dragIcon = event.target.closest('li').querySelector('.drag-icon');
if (!dragIcon || isDragging() || this.isUpdating) {
return;
}
event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign
dragIcon.style.visibility = 'visible';
};
const pointeroutListener = (event) => {
event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign
const dragIcon = event.target.closest('li').querySelector('.drag-icon');
if (!dragIcon) {
return;
}
dragIcon.style.visibility = 'hidden';
};
// We use pointerover/pointerout instead of CSS so that when we hover over a

View File

@ -1,39 +1,35 @@
import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility';
/**
* Get the index from sourcepos that represents the line of
* the description when the description is split by newline.
* Returns the start and end `sourcepos` rows, converted to zero-based numbering.
*
* @param {String} sourcepos Source position in format `23:3-23:14`
* @returns {Number} Index of description split by newline
* @returns {Array<Number>} Start and end `sourcepos` rows, zero-based numbered
*/
const getDescriptionIndex = (sourcepos) => {
const [startRange] = sourcepos.split(HYPHEN);
const getSourceposRows = (sourcepos) => {
const [startRange, endRange] = sourcepos.split(HYPHEN);
const [startRow] = startRange.split(COLON);
return startRow - 1;
const [endRow] = endRange.split(COLON);
return [startRow - 1, endRow - 1];
};
/**
* Given a `ul` or `ol` element containing a new sort order, this function performs
* a depth-first search to get the new sort order in the form of sourcepos indices.
* Given a `ul` or `ol` element containing a new sort order, this function returns
* an array of this new order which is derived from its list items' sourcepos values.
*
* @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
* @returns {Array<Number>} An array representing the new order of the list
* @returns {Array<Number>} A numerical array representing the new order of the list.
* The numbers represent the rows of the original markdown source.
*/
const getNewSourcePositions = (list) => {
const newSourcePositions = [];
function pushPositionOfChildListItems(el) {
if (!el) {
return;
Array.from(list.children).forEach((listItem) => {
const [start, end] = getSourceposRows(listItem.dataset.sourcepos);
for (let i = start; i <= end; i += 1) {
newSourcePositions.push(i);
}
if (el.tagName === 'LI') {
newSourcePositions.push(getDescriptionIndex(el.dataset.sourcepos));
}
Array.from(el.children).forEach(pushPositionOfChildListItems);
}
pushPositionOfChildListItems(list);
});
return newSourcePositions;
};
@ -56,17 +52,17 @@ const getNewSourcePositions = (list) => {
* And a reordered list (due to dragging Item 2 into Item 1's position) like:
*
* <pre>
* <ul data-sourcepos="3:1-8:0">
* <li data-sourcepos="4:1-4:8">
* <ul data-sourcepos="3:1-7:8">
* <li data-sourcepos="4:1-6:10">
* Item 2
* <ul data-sourcepos="5:1-6:10">
* <li data-sourcepos="5:1-5:10">Item 3</li>
* <li data-sourcepos="6:1-6:10">Item 4</li>
* <ul data-sourcepos="5:3-6:10">
* <li data-sourcepos="5:3-5:10">Item 3</li>
* <li data-sourcepos="6:3-6:10">Item 4</li>
* </ul>
* </li>
* <li data-sourcepos="3:1-3:8">Item 1</li>
* <li data-sourcepos="7:1-8:0">Item 5</li>
* <ul>
* <li data-sourcepos="7:1-7:8">Item 5</li>
* </ul>
* </pre>
*
* This function returns:
@ -87,7 +83,7 @@ const getNewSourcePositions = (list) => {
*/
export const convertDescriptionWithNewSort = (description, list) => {
const descriptionLines = description.split(NEWLINE);
const startIndexOfList = getDescriptionIndex(list.dataset.sourcepos);
const [startIndexOfList] = getSourceposRows(list.dataset.sourcepos);
getNewSourcePositions(list)
.map((lineIndex) => descriptionLines[lineIndex])

View File

@ -137,9 +137,8 @@ export default {
class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>#{{ pipeline[pipelineKey] }}</gl-link
>
#{{ pipeline[pipelineKey] }}
</gl-link>
<!--Commit row-->
<div class="icon-container gl-display-inline-block gl-mr-1">
<gl-icon

View File

@ -1,6 +1,7 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import AssigneeAvatar from './assignee_avatar.vue';
@ -94,6 +95,9 @@ export default {
assigneeUrl() {
return this.user.web_url || this.user.webUrl;
},
assigneeId() {
return isGid(this.user.id) ? getIdFromGraphQLId(this.user.id) : this.user.id;
},
},
};
</script>
@ -103,7 +107,7 @@ export default {
<gl-link
:href="assigneeUrl"
:title="tooltipTitle"
:data-user-id="user.id"
:data-user-id="assigneeId"
data-placement="left"
class="gl-display-inline-block js-user-link"
>

View File

@ -134,3 +134,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
export const BIDI_CHAR_TOOLTIP = __(
'Potentially unwanted character detected: Unicode BiDi Control',
);
export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';

View File

@ -0,0 +1,13 @@
import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
import wrapComments from './wrap_comments';
/**
* Registers our plugins for Highlight.js
*
* Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
*
* @param {Object} hljs - the Highlight.js instance.
*/
export const registerPlugins = (hljs) => {
hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
};

View File

@ -0,0 +1,38 @@
import { HLJS_COMMENT_SELECTOR } from '../constants';
const createWrapper = (content) => {
const span = document.createElement('span');
span.className = HLJS_COMMENT_SELECTOR;
span.innerHTML = content;
return span.outerHTML;
};
/**
* Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
* This ensures that multi-line comments are rendered correctly in the GitLab UI.
*
* Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
*
* @param {Object} Result - an object that represents the highlighted result from Highlight.js
*/
export default (result) => {
if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
let wrapComment = false;
// eslint-disable-next-line no-param-reassign
result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
.split('\n')
.map((lineContent) => {
if (lineContent.includes(HLJS_COMMENT_SELECTOR)) {
wrapComment = true;
return lineContent;
}
const line = wrapComment ? createWrapper(lineContent) : lineContent;
if (lineContent.includes('</span>')) {
wrapComment = false;
}
return line;
})
.join('\n');
};

View File

@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants';
import Chunk from './components/chunk.vue';
import { registerPlugins } from './plugins/index';
/*
* This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
@ -111,6 +112,7 @@ export default {
let detectedLanguage = language;
let highlightedContent;
if (this.hljs) {
registerPlugins(this.hljs);
if (!detectedLanguage) {
const hljsHighlightAuto = this.hljs.highlightAuto(content);
highlightedContent = hljsHighlightAuto.value;

View File

@ -3,8 +3,8 @@
.description {
ul,
ol {
/* We're changing list-style-position to inside because the default of outside
* doesn't move the negative margin to the left of the bullet. */
/* We're changing list-style-position to inside because the default of
* outside doesn't move negative margin to the left of the bullet. */
list-style-position: inside;
}
@ -21,6 +21,26 @@
inset-block-start: 0.3rem;
inset-inline-start: 1rem;
}
/* The inside bullet aligns itself to the bottom, which we see when text to its right wraps.
* We fix this by aligning it to the top. Targeting ::marker doesn't seem to work. */
> * {
vertical-align: top;
}
/* The inside bullet is treated like an element inside the li element, so when we have a
* multi-paragraph list item, the text doesn't start on the right of the bullet because
* it is a block level p element. We make it inline to fix this. */
> p:first-of-type {
display: inline-block;
max-width: calc(100% - 1.5rem);
}
/* We fix the other paragraphs not indenting to the
* right of the bullet due to the inside bullet. */
> :not(p:first-of-type):not(.drag-icon):not(.task-list-item-checkbox):not(.gfm-issue):not(.js-add-task) {
margin-inline-start: 1rem;
}
}
ul.task-list {

View File

@ -55,15 +55,6 @@
box-shadow ease-in-out 0.15s;
background-color: $white;
&.is-focused {
@include gl-focus;
.comment-toolbar,
.nav-links {
border-color: $blue-300;
}
}
&.is-dropzone-hover {
border-color: $green-500;
box-shadow: 0 0 2px $black-transparent,
@ -80,6 +71,10 @@
@include gl-shadow-none;
}
}
.comment-warning-wrapper:focus-within {
@include gl-focus;
}
}
.md-header .nav-links {

View File

@ -553,7 +553,7 @@ class Namespace < ApplicationRecord
private
def cluster_enabled_granted?
root_ancestor.cluster_enabled_grant.present? && (Gitlab.com? || Gitlab.dev_or_test_env?)
(Gitlab.com? || Gitlab.dev_or_test_env?) && root_ancestor.cluster_enabled_grant.present?
end
def certificate_based_clusters_enabled_ff?

View File

@ -3,6 +3,10 @@
class IssueBoardEntity < Grape::Entity
include RequestAwareEntity
format_with(:upcase) do |item|
item.try(:upcase)
end
expose :id
expose :iid
expose :title
@ -51,6 +55,11 @@ class IssueBoardEntity < Grape::Entity
expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue|
project_labels_path(issue.project, format: :json, include_ancestor_groups: true)
end
expose :issue_type,
as: :type,
format_with: :upcase,
documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
end
IssueBoardEntity.prepend_mod_with('IssueBoardEntity')

View File

@ -3,6 +3,10 @@
class IssueEntity < IssuableEntity
include TimeTrackableEntity
format_with(:upcase) do |item|
item.try(:upcase)
end
expose :state
expose :milestone_id
expose :updated_by_id
@ -75,6 +79,11 @@ class IssueEntity < IssuableEntity
expose :issue_email_participants do |issue|
issue.issue_email_participants.map { |x| { email: x.email } }
end
expose :issue_type,
as: :type,
format_with: :upcase,
documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
end
IssueEntity.prepend_mod_with('IssueEntity')

View File

@ -39,6 +39,7 @@ module Git
def enqueue_update_mrs
return if params[:merge_request_branches]&.exclude?(branch_name)
# TODO: pass params[:push_options] to worker
UpdateMergeRequestsWorker.perform_async(
project.id,
current_user.id,

View File

@ -185,9 +185,12 @@ module MergeRequests
def create_pipeline_for(merge_request, user, async: false)
if async
# TODO: pass push_options to worker
MergeRequests::CreatePipelineWorker.perform_async(project.id, user.id, merge_request.id)
else
MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
MergeRequests::CreatePipelineService
.new(project: project, current_user: user, params: params.slice(:push_options))
.execute(merge_request)
end
end

View File

@ -9,9 +9,11 @@ module MergeRequests
end
def create_detached_merge_request_pipeline(merge_request)
Ci::CreatePipelineService.new(pipeline_project(merge_request),
current_user,
ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request))
Ci::CreatePipelineService
.new(pipeline_project(merge_request),
current_user,
ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request),
push_options: params[:push_options])
.execute(:merge_request_event, merge_request: merge_request)
end

View File

@ -15,7 +15,7 @@ module MergeRequests
worker_resource_boundary :cpu
idempotent!
def perform(project_id, user_id, merge_request_id)
def perform(project_id, user_id, merge_request_id, params = {})
project = Project.find_by_id(project_id)
return unless project
@ -25,7 +25,12 @@ module MergeRequests
merge_request = MergeRequest.find_by_id(merge_request_id)
return unless merge_request
MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
push_options = params.with_indifferent_access[:push_options]
MergeRequests::CreatePipelineService
.new(project: project, current_user: user, params: { push_options: push_options })
.execute(merge_request)
merge_request.update_head_pipeline
end
end

View File

@ -13,13 +13,17 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
weight 3
loggable_arguments 2, 3, 4
def perform(project_id, user_id, oldrev, newrev, ref)
def perform(project_id, user_id, oldrev, newrev, ref, params = {})
project = Project.find_by_id(project_id)
return unless project
user = User.find_by_id(user_id)
return unless user
MergeRequests::RefreshService.new(project: project, current_user: user).execute(oldrev, newrev, ref)
push_options = params.with_indifferent_access[:push_options]
MergeRequests::RefreshService
.new(project: project, current_user: user, params: { push_options: push_options })
.execute(oldrev, newrev, ref)
end
end

View File

@ -32,7 +32,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware,
Gitlab::Metrics::RackMiddleware
config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware,
Gitlab::Middleware::RailsQueueDuration
config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware)
end

View File

@ -47,6 +47,8 @@
- 1
- - audit_events_user_impersonation_event_create
- 1
- - auth_saml_group_sync
- 1
- - authorized_keys
- 2
- - authorized_project_update

View File

@ -158,9 +158,11 @@ the destination's value when [listing streaming destinations](#list-streaming-de
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.9 [with a flag](../administration/feature_flags.md) named `audit_event_streaming_git_operations`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/357211) in GitLab 15.0.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/357211) in GitLab 15.1 by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`. On GitLab.com, this feature is available.
On self-managed GitLab, by default this feature is available. To hide the
feature, ask an administrator to [disable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`.
Streaming audit events can be sent when signed-in users push or pull a project's remote Git repositories:

View File

@ -15,19 +15,13 @@ projects.
## View packages
To view packages within your project or group:
To view packages within your project:
1. Go to the project or group.
1. Go to the project.
1. Go to **Packages & Registries > Infrastructure Registry**.
You can search, sort, and filter packages on this page.
When you view packages in a group:
- All packages published to the group and its projects are displayed.
- Only the projects you can access are displayed.
- If a project is private, or you are not a member of the project, it is not displayed.
For information on how to create and upload a package, view the GitLab
documentation for your package type:
@ -68,7 +62,7 @@ To delete a package, you must have suitable [permissions](../../permissions.md).
You can delete packages by using [the API](../../../api/packages.md#delete-a-project-package) or the UI.
To delete a package in the UI, from your group or project:
To delete a package in the UI, from your project:
1. Go to **Packages & Registries > Infrastructure Registry**.
1. Find the name of the package you want to delete.

View File

@ -148,14 +148,6 @@ module Gitlab
'your migration class.'
end
database_name = Gitlab::Database.db_config_name(connection)
unless ActiveRecord::Base.configurations.primary?(database_name)
raise 'The `#finalize_background_migration` is currently not supported when running in decomposed database, ' \
'and this database is not `main:`. For more information visit: ' \
'https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html'
end
Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(
@ -163,7 +155,14 @@ module Gitlab
raise 'Could not find batched background migration' if migration.nil?
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection)
with_restored_connection_stack do |restored_connection|
Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(
job_class_name, table_name,
column_name, job_arguments,
connection: restored_connection)
end
end
end
# Deletes batched background migration for the given configuration.

View File

@ -17,7 +17,9 @@ module Gitlab
original_handler = ActiveRecord::Base.connection_handler
original_db_config = ActiveRecord::Base.connection_db_config
return yield if ActiveRecord::Base.configurations.primary?(original_db_config.name)
if ActiveRecord::Base.configurations.primary?(original_db_config.name)
return yield(ActiveRecord::Base.connection)
end
# If the `ActiveRecord::Base` connection is different than `:main`
# re-establish and configure `SharedModel` context accordingly
@ -43,7 +45,7 @@ module Gitlab
ActiveRecord::Base.establish_connection :main # rubocop:disable Database/EstablishConnection
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
yield
yield(base_model.connection)
end
ensure
ActiveRecord::Base.connection_handler = original_handler

View File

@ -6,21 +6,12 @@ module Gitlab
module GrapeLogging
module Loggers
class QueueDurationLogger < ::GrapeLogging::Loggers::Base
attr_accessor :start_time
def before
@start_time = Time.now
end
def parameters(request, _)
proxy_start = request.env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
duration_s = request.env[Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY].presence
return {} unless proxy_start && start_time
return {} unless duration_s
# Time in milliseconds since gitlab-workhorse started the request
duration = start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000
{ 'queue_duration_s': Gitlab::Utils.ms_to_round_sec(duration) }
{ 'queue_duration_s': duration_s }
end
end
end

View File

@ -145,13 +145,18 @@ RSpec.describe Projects::IssuesController do
project.add_developer(user)
end
it "returns issue_email_participants" do
it "returns issue attributes" do
participants = create_list(:issue_email_participant, 2, issue: issue)
get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issue_email_participants']).to contain_exactly({ "email" => participants[0].email }, { "email" => participants[1].email })
expect(json_response).to include(
'issue_email_participants' => contain_exactly(
{ "email" => participants[0].email }, { "email" => participants[1].email }
),
'type' => 'ISSUE'
)
end
end

View File

@ -3,6 +3,7 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"type": { "type": "string" },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"lock_version": { "type": ["integer", "null"] },

View File

@ -3,6 +3,7 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"type": { "type": "string" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"closed": { "type": "boolean" },

View File

@ -8,8 +8,6 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@ -121,8 +119,6 @@ describe('CE IssuesListApp component', () => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
[getIssuesWithoutCrmQuery, issuesQueryResponse],
[getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
];

View File

@ -2,7 +2,7 @@ import { convertDescriptionWithNewSort } from '~/issues/show/utils';
describe('app/assets/javascripts/issues/show/utils.js', () => {
describe('convertDescriptionWithNewSort', () => {
it('converts markdown description with new list sort order', () => {
it('converts markdown description with nested lists with new list sort order', () => {
const description = `I am text
- Item 1
@ -12,17 +12,17 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
- Item 5`;
// Drag Item 2 + children to Item 1's position
const html = `<ul data-sourcepos="3:1-8:0">
<li data-sourcepos="4:1-4:8">
const html = `<ul data-sourcepos="3:1-7:8">
<li data-sourcepos="4:1-6:10">
Item 2
<ul data-sourcepos="5:1-6:10">
<li data-sourcepos="5:1-5:10">Item 3</li>
<li data-sourcepos="6:1-6:10">Item 4</li>
<ul data-sourcepos="5:3-6:10">
<li data-sourcepos="5:3-5:10">Item 3</li>
<li data-sourcepos="6:3-6:10">Item 4</li>
</ul>
</li>
<li data-sourcepos="3:1-3:8">Item 1</li>
<li data-sourcepos="7:1-8:0">Item 5</li>
<ul>`;
<li data-sourcepos="7:1-7:8">Item 5</li>
</ul>`;
const list = document.createElement('div');
list.innerHTML = html;
@ -36,5 +36,105 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
});
it('converts markdown description with multi-line list items with new list sort order', () => {
const description = `Labore ea omnis et officia excepturi.
1. Item 1
Item 1 part 2
1. Item 2
- Item 2.1
- Item 2.1.1
- Item 2.1.2
- Item 2.2
- Item 2.3
1. Item 3
1. Item 4
\`\`\`
const variable = 'string';
\`\`\`
![iii](img.jpg)
last paragraph
1. Item 5
1. Item 6`;
// Drag Item 2 + children to Item 5's position
const html = `<ol data-sourcepos="3:1-25:7">
<li data-sourcepos="3:1-6:0">
<p data-sourcepos="3:4-3:7">Item 1</p>
<p data-sourcepos="5:4-5:8">Item 1 part 2</p>
</li>
<li data-sourcepos="13:1-13:7">
<p data-sourcepos="13:4-13:7">Item 3</p>
</li>
<li data-sourcepos="14:1-23:0">
<p data-sourcepos="14:4-14:7">Item 4</p>
<div>
<pre data-sourcepos="16:4-18:6">
<code><span lang="plaintext">const variabl = 'string';</span></code>
</pre>
</div>
<p data-sourcepos="20:4-20:32">
<a href="href"><img src="img.jpg" alt="description" /></a>
</p>
<p data-sourcepos="22:4-22:17">last paragraph</p>
</li>
<li data-sourcepos="24:1-24:7">
<p data-sourcepos="24:4-24:7">Item 5</p>
</li>
<li data-sourcepos="7:1-12:10">
<p data-sourcepos="7:4-7:7">Item 2</p>
<ul data-sourcepos="8:4-12:10">
<li data-sourcepos="8:4-10:15">Item 2.1
<ul data-sourcepos="9:6-10:15">
<li data-sourcepos="9:6-9:12">Item 2.1.1</li>
<li data-sourcepos="10:6-10:15">Item 2.1.2</li>
</ul>
</li>
<li data-sourcepos="11:4-11:10">Item 2.2</li>
<li data-sourcepos="12:4-12:10">Item 2.3</li>
</ul>
</li>
<li data-sourcepos="25:1-25:7">
<p data-sourcepos="25:4-25:7">Item 6</p>
</li>
</ol>`;
const list = document.createElement('div');
list.innerHTML = html;
const expected = `Labore ea omnis et officia excepturi.
1. Item 1
Item 1 part 2
1. Item 3
1. Item 4
\`\`\`
const variable = 'string';
\`\`\`
![iii](img.jpg)
last paragraph
1. Item 5
1. Item 2
- Item 2.1
- Item 2.1.1
- Item 2.1.2
- Item 2.2
- Item 2.3
1. Item 6`;
expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
});
});
});

View File

@ -1,5 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import userDataMock from '../../user_data_mock';
@ -32,6 +35,7 @@ describe('AssigneeAvatarLink component', () => {
});
const findTooltipText = () => wrapper.attributes('title');
const findUserLink = () => wrapper.findComponent(GlLink);
it('has the root url present in the assigneeUrl method', () => {
createComponent();
@ -112,4 +116,24 @@ describe('AssigneeAvatarLink component', () => {
});
},
);
it('passes the correct user id for REST API', () => {
createComponent({
tooltipHasName: true,
user: userDataMock(),
});
expect(findUserLink().attributes('data-user-id')).toBe(String(userDataMock().id));
});
it('passes the correct user id for GraphQL API', () => {
const userId = userDataMock().id;
createComponent({
tooltipHasName: true,
user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) },
});
expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
});
});

View File

@ -0,0 +1,14 @@
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants';
import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments');
const hljsMock = { addPlugin: jest.fn() };
describe('Highlight.js plugin registration', () => {
beforeEach(() => registerPlugins(hljsMock));
it('registers our plugins', () => {
expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
});
});

View File

@ -0,0 +1,20 @@
import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants';
import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
describe('Highlight.js plugin for wrapping comments', () => {
it('mutates the input value by wrapping each line in a span tag', () => {
const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n* Line 2 \n*/</span>`;
const outputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n<span class="${HLJS_COMMENT_SELECTOR}">* Line 2 </span>\n<span class="${HLJS_COMMENT_SELECTOR}">*/</span>`;
const hljsResultMock = { value: inputValue };
wrapComments(hljsResultMock);
expect(hljsResultMock.value).toBe(outputValue);
});
it('does not mutate the input value if the hljs comment selector is not present', () => {
const inputValue = '<span class="hljs-keyword">const</span>';
const hljsResultMock = { value: inputValue };
expect(hljsResultMock.value).toBe(inputValue);
});
});

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
@ -11,6 +12,7 @@ import eventHub from '~/notes/event_hub';
jest.mock('~/blob/line_highlighter');
jest.mock('highlight.js/lib/core');
jest.mock('~/vue_shared/components/source_viewer/plugins/index');
Vue.use(VueRouter);
const router = new VueRouter();
@ -59,6 +61,10 @@ describe('Source Viewer component', () => {
describe('highlight.js', () => {
beforeEach(() => createComponent({ language: mappedLanguage }));
it('registers our plugins for Highlight.js', () => {
expect(registerPlugins).toHaveBeenCalledWith(hljs);
});
it('registers the language definition', async () => {
const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);

View File

@ -444,7 +444,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
it 'does restore connection hierarchy' do
expect_next_instances_of(job_class, 1..) do |job|
expect(job).to receive(:perform) do
validate_connections!
validate_connections_stack!
end
end

View File

@ -3,8 +3,14 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers do
let(:migration_class) do
Class.new(ActiveRecord::Migration[6.1])
.include(described_class)
.include(Gitlab::Database::Migrations::ReestablishedConnectionStack)
end
let(:migration) do
ActiveRecord::Migration.new.extend(described_class)
migration_class.new
end
describe '#queue_batched_background_migration' do
@ -221,24 +227,64 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
end
end
context 'when uses a CI connection', :reestablished_active_record_base do
context 'when within transaction' do
before do
skip_if_multiple_databases_not_setup
ActiveRecord::Base.establish_connection(:ci) # rubocop:disable Database/EstablishConnection
allow(migration).to receive(:transaction_open?).and_return(true)
end
it 'raises an exception' do
ci_migration = create(:batched_background_migration, :active)
it 'does raise an exception' do
expect { migration.finalize_batched_background_migration(job_class_name: 'MyJobClass', table_name: :projects, column_name: :id, job_arguments: []) }
.to raise_error /`finalize_batched_background_migration` cannot be run inside a transaction./
end
end
expect do
migration.finalize_batched_background_migration(
job_class_name: ci_migration.job_class_name,
table_name: ci_migration.table_name,
column_name: ci_migration.column_name,
job_arguments: ci_migration.job_arguments
)
end.to raise_error /is currently not supported when running in decomposed/
context 'when running migration in reconfigured ActiveRecord::Base context' do
it_behaves_like 'reconfigures connection stack', 'ci' do
before do
create(:batched_background_migration,
job_class_name: 'Ci::MyClass',
table_name: :ci_builds,
column_name: :id,
job_arguments: [],
gitlab_schema: :gitlab_ci)
end
context 'when restrict_gitlab_migration is set to gitlab_ci' do
it 'finalizes the migration' do
migration_class.include(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema)
migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_ci
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
expect(runner).to receive(:finalize).with('Ci::MyClass', :ci_builds, :id, []) do
validate_connections_stack!
end
end
migration.finalize_batched_background_migration(
job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: [])
end
end
context 'when restrict_gitlab_migration is set to gitlab_main' do
it 'does not find any migrations' do
migration_class.include(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema)
migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_main
expect do
migration.finalize_batched_background_migration(
job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: [])
end.to raise_error /Could not find batched background migration/
end
end
context 'when no restrict is set' do
it 'does not find any migrations' do
expect do
migration.finalize_batched_background_migration(
job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: [])
end.to raise_error /Could not find batched background migration/
end
end
end
end

View File

@ -16,7 +16,7 @@ RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do
it_behaves_like "reconfigures connection stack", db_config_name do
it 'does restore connection hierarchy' do
model.with_restored_connection_stack do
validate_connections!
validate_connections_stack!
end
end

View File

@ -8,7 +8,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
describe ".parameters" do
let(:start_time) { Time.new(2018, 01, 01) }
describe 'when no proxy time is available' do
describe 'when no proxy duration is available' do
let(:mock_request) { double('env', env: {}) }
it 'returns an empty hash' do
@ -16,20 +16,18 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
end
end
describe 'when a proxy time is available' do
describe 'when a proxy duration is available' do
let(:mock_request) do
double('env',
env: {
'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9)
'GITLAB_RAILS_QUEUE_DURATION' => 2.seconds
}
)
end
it 'returns the correct duration in seconds' do
it 'adds the duration to log parameters' do
travel_to(start_time) do
subject.before
expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 1.hour.to_f })
expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 2.seconds.to_f })
end
end
end

View File

@ -43,6 +43,12 @@ RSpec.describe IssueBoardEntity do
expect(subject).to include(labels: array_including(hash_including(:id, :title, :color, :description, :text_color, :priority)))
end
describe 'type' do
it 'has an issue type' do
expect(subject[:type]).to eq('ISSUE')
end
end
describe 'real_path' do
it 'has an issue path' do
expect(subject[:real_path]).to eq(project_issue_path(project, resource.iid))

View File

@ -24,6 +24,12 @@ RSpec.describe IssueEntity do
end
end
describe 'type' do
it 'has an issue type' do
expect(subject[:type]).to eq('ISSUE')
end
end
it 'has Issuable attributes' do
expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
:title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)

View File

@ -50,6 +50,19 @@ RSpec.describe MergeRequests::CreatePipelineService do
expect(response.payload.source).to eq('merge_request_event')
end
context 'when push options contain ci.skip' do
let(:params) { { push_options: { ci: { skip: true } } } }
it 'creates a skipped pipeline' do
expect { response }.to change { Ci::Pipeline.count }.by(1)
expect(response).to be_success
expect(response.payload).to be_persisted
expect(response.payload.builds).to be_empty
expect(response.payload).to be_skipped
end
end
context 'with fork merge request' do
let_it_be(:forked_project) { fork_project(project, nil, repository: true, target_project: create(:project, :private, :repository)) }

View File

@ -228,6 +228,21 @@ RSpec.describe MergeRequests::RefreshService do
expect(@another_merge_request.has_commits?).to be_falsy
end
context 'when "push_options: nil" is passed' do
let(:service_instance) { service.new(project: project, current_user: @user, params: { push_options: nil }) }
subject { service_instance.execute(@oldrev, @newrev, ref) }
it 'creates a detached merge request pipeline with commits' do
expect { subject }
.to change { @merge_request.pipelines_for_merge_request.count }.by(1)
.and change { @another_merge_request.pipelines_for_merge_request.count }.by(0)
expect(@merge_request.has_commits?).to be_truthy
expect(@another_merge_request.has_commits?).to be_falsy
end
end
it 'does not create detached merge request pipeline for forked project' do
expect { subject }
.not_to change { @fork_merge_request.pipelines_for_merge_request.count }

View File

@ -22,7 +22,7 @@ RSpec.shared_context 'reconfigures connection stack' do |db_config_name|
end
end
def validate_connections!
def validate_connections_stack!
model_connections = Gitlab::Database.database_base_models.to_h do |db_config_name, model_class|
[model_class, Gitlab::Database.db_config_name(model_class.connection)]
end

View File

@ -3,24 +3,50 @@
require 'spec_helper'
RSpec.describe MergeRequests::CreatePipelineWorker do
subject(:worker) { described_class.new }
describe '#perform' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request) }
let(:worker) { described_class.new }
subject { worker.perform(project.id, user.id, merge_request.id) }
context 'when the objects exist' do
it 'calls the merge request create pipeline service and calls update head pipeline' do
aggregate_failures do
expect_next_instance_of(MergeRequests::CreatePipelineService, project: project, current_user: user) do |service|
expect_next_instance_of(MergeRequests::CreatePipelineService,
project: project,
current_user: user,
params: { push_options: nil }) do |service|
expect(service).to receive(:execute).with(merge_request)
end
expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request)
expect(merge_request).to receive(:update_head_pipeline)
subject.perform(project.id, user.id, merge_request.id)
subject
end
end
context 'when push options are passed as Hash to the worker' do
let(:extra_params) { { 'push_options' => { 'ci' => { 'skip' => true } } } }
subject { worker.perform(project.id, user.id, merge_request.id, extra_params) }
it 'calls the merge request create pipeline service and calls update head pipeline' do
aggregate_failures do
expect_next_instance_of(MergeRequests::CreatePipelineService,
project: project,
current_user: user,
params: { push_options: { ci: { skip: true } } }) do |service|
expect(service).to receive(:execute).with(merge_request)
end
expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request)
expect(merge_request).to receive(:update_head_pipeline)
subject
end
end
end
end
@ -29,8 +55,7 @@ RSpec.describe MergeRequests::CreatePipelineWorker do
it 'does not call the create pipeline service' do
expect(MergeRequests::CreatePipelineService).not_to receive(:new)
expect { subject.perform(project.id, user.id, merge_request.id) }
.not_to raise_exception
expect { subject }.not_to raise_exception
end
end

View File

@ -3,28 +3,47 @@
require 'spec_helper'
RSpec.describe UpdateMergeRequestsWorker do
include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:oldrev) { "123456" }
let_it_be(:newrev) { "789012" }
let_it_be(:ref) { "refs/heads/test" }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
subject { described_class.new }
let(:worker) { described_class.new }
describe '#perform' do
let(:oldrev) { "123456" }
let(:newrev) { "789012" }
let(:ref) { "refs/heads/test" }
def perform
subject.perform(project.id, user.id, oldrev, newrev, ref)
end
subject { worker.perform(project.id, user.id, oldrev, newrev, ref) }
it 'executes MergeRequests::RefreshService with expected values' do
expect_next_instance_of(MergeRequests::RefreshService, project: project, current_user: user) do |refresh_service|
expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
expect_next_instance_of(MergeRequests::RefreshService,
project: project,
current_user: user,
params: { push_options: nil }) do |service|
expect(service)
.to receive(:execute)
.with(oldrev, newrev, ref)
end
perform
subject
end
context 'when push options are passed as Hash' do
let(:extra_params) { { 'push_options' => { 'ci' => { 'skip' => true } } } }
subject { worker.perform(project.id, user.id, oldrev, newrev, ref, extra_params) }
it 'executes MergeRequests::RefreshService with expected values' do
expect_next_instance_of(MergeRequests::RefreshService,
project: project,
current_user: user,
params: { push_options: { ci: { skip: true } } }) do |service|
expect(service)
.to receive(:execute)
.with(oldrev, newrev, ref)
end
subject
end
end
end
end