Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-23 18:10:19 +00:00
parent 9dbca64417
commit b8d021cb60
94 changed files with 1203 additions and 690 deletions

View File

@ -10,74 +10,6 @@ doc/api/graphql/reference/gitlab_schema.graphql
*.scss
*.md
## lovely-lovelace
app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
app/assets/javascripts/behaviors/markdown/paste_markdown_table.js
app/assets/javascripts/boards/components/sidebar/remove_issue.vue
app/assets/javascripts/diffs/components/diff_row.vue
app/assets/javascripts/diffs/store/getters.js
app/assets/javascripts/dropzone_input.js
app/assets/javascripts/feature_flags/components/strategy.vue
app/assets/javascripts/ide/lib/create_diff.js
app/assets/javascripts/ide/stores/modules/pipelines/getters.js
app/assets/javascripts/members/components/table/members_table.vue
app/assets/javascripts/members/store/utils.js
app/assets/javascripts/monitoring/stores/actions.js
app/assets/javascripts/monitoring/stores/getters.js
app/assets/javascripts/packages/list/utils.js
app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
app/assets/javascripts/pages/users/user_tabs.js
app/assets/javascripts/projects/settings/access_dropdown.js
## stoic-swirles
app/assets/javascripts/repository/log_tree.js
app/assets/javascripts/repository/utils/dom.js
app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
app/assets/javascripts/user_lists/store/utils.js
app/assets/javascripts/vue_shared/components/alert_details_table.vue
app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
app/assets/javascripts/vue_shared/constants.js
ee/app/assets/javascripts/analytics/cycle_analytics/utils.js
ee/app/assets/javascripts/analytics/repository_analytics/components/select_projects_dropdown.vue
ee/app/assets/javascripts/boards/stores/getters.js
ee/app/assets/javascripts/dependencies/store/modules/list/getters.js
ee/app/assets/javascripts/epic/store/getters.js
ee/app/assets/javascripts/insights/components/insights_page.vue
ee/app/assets/javascripts/pages/trial_registrations/new/username_suggester.js
ee/app/assets/javascripts/related_items_tree/store/mutations.js
ee/app/assets/javascripts/security_dashboard/components/project_vulnerabilities.vue
ee/spec/frontend/analytics/shared/components/groups_dropdown_filter_spec.js
ee/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
ee/spec/frontend/approvals/components/approvers_list_spec.js
ee/spec/frontend/approvals/components/rule_controls_spec.js
ee/spec/frontend/audit_events/components/audit_events_filter_spec.js
ee/spec/frontend/dependencies/components/dependencies_table_spec.js
ee/spec/frontend/geo_node_form/components/geo_node_form_capacities_spec.js
ee/spec/frontend/security_configuration/dast_profiles/graphql/cache_utils_spec.js
ee/spec/frontend/security_configuration/dast_site_profiles_form/components/dast_site_profile_form_spec.js
ee/spec/frontend/security_configuration/dast_site_validation/components/dast_site_validation_modal_spec.js
ee/spec/frontend/security_dashboard/components/vulnerability_list_spec.js
ee/spec/frontend/sidebar/components/status/status_spec.js
ee/spec/frontend/storage_counter/components/projects_table_spec.js
ee/spec/frontend/vulnerabilities/footer_spec.js
spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
## objective-swirles
spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
spec/frontend/clusters/stores/clusters_store_spec.js
spec/frontend/diffs/components/diff_file_header_spec.js
spec/frontend/diffs/components/hidden_files_warning_spec.js
spec/frontend/environments/environment_monitoring_spec.js
spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
spec/frontend/notes/components/discussion_filter_spec.js
spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
spec/frontend/snippets/components/edit_spec.js
spec/frontend/user_lists/store/show/mutations_spec.js
spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
spec/frontend_integration/ide/helpers/ide_helper.js
## boring-bohr
jest.config.base.js
jest.config.js

View File

@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.7.1 (2020-12-23)
### Fixed (1 change)
- Fix project transfer corrupting shared runners state. !47316
## 13.7.0 (2020-12-22)
### Security (1 change)

View File

@ -23,7 +23,7 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlerts from '~/graphql_shared/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import {
ALERTS_STATUS_TABS,

View File

@ -3,7 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { trackAlertStatusUpdateOptions } from '../constants';
import updateAlertStatusMutation from '../graphql/mutations/update_alert_status.mutation.graphql';
import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
export default {
i18n: {

View File

@ -1,5 +1,5 @@
#import "./list_item.fragment.graphql"
#import "./alert_note.fragment.graphql"
#import "~/graphql_shared/fragments/alert.fragment.graphql"
#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
fragment AlertDetailItem on AlertManagementAlert {
...AlertListItem

View File

@ -1,4 +1,4 @@
#import "../fragments/alert_note.fragment.graphql"
#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) {
alertSetAssignees(

View File

@ -243,7 +243,9 @@ export default {
});
},
editIntegration({ id }) {
const currentIntegration = this.integrations.list.find(integration => integration.id === id);
const currentIntegration = this.integrations.list.find(
(integration) => integration.id === id,
);
this.$apollo.mutate({
mutation: updateCurrentIntergrationMutation,
variables: {

View File

@ -1,4 +1,5 @@
const maxColumnWidth = (rows, columnIndex) => Math.max(...rows.map(row => row[columnIndex].length));
const maxColumnWidth = (rows, columnIndex) =>
Math.max(...rows.map((row) => row[columnIndex].length));
export default class PasteMarkdownTable {
constructor(clipboardData) {
@ -16,7 +17,7 @@ export default class PasteMarkdownTable {
this.calculateColumnWidths();
const markdownRows = this.rows.map(
row =>
(row) =>
// | Name | Title | Email Address |
// |--------------|-------|----------------|
// | Jane Atler | CEO | jane@acme.com |
@ -66,7 +67,7 @@ export default class PasteMarkdownTable {
return false;
}
this.rows = splitRows.map(row => row.split('\t'));
this.rows = splitRows.map((row) => row.split('\t'));
this.normalizeRows();
// Check that the max number of columns in the HTML matches the number of
@ -81,10 +82,10 @@ export default class PasteMarkdownTable {
// Ensure each row has the same number of columns
normalizeRows() {
const rowLengths = this.rows.map(row => row.length);
const rowLengths = this.rows.map((row) => row.length);
const maxLength = Math.max(...rowLengths);
this.rows.forEach(row => {
this.rows.forEach((row) => {
while (row.length < maxLength) {
row.push('');
}
@ -101,7 +102,7 @@ export default class PasteMarkdownTable {
const textColumnCount = this.rows[0].length;
let htmlColumnCount = 0;
this.doc.querySelectorAll('table tr').forEach(row => {
this.doc.querySelectorAll('table tr').forEach((row) => {
htmlColumnCount = Math.max(row.cells.length, htmlColumnCount);
});

View File

@ -42,13 +42,13 @@ export default {
axios.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach(list => {
lists.forEach((list) => {
list.addIssue(issue);
});
});
// Remove from the frontend store
lists.forEach(list => {
lists.forEach((list) => {
list.removeIssue(issue);
});
@ -58,9 +58,11 @@ export default {
* Build the default patch request.
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const listLabelIds = lists.map((list) => list.label.id);
const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
const labelIds = issue.labels
.map((label) => label.id)
.filter((id) => !listLabelIds.includes(id));
return {
label_ids: labelIds,

View File

@ -115,7 +115,9 @@ export default {
const table = line.closest('.diff-table');
table.classList.remove('left-side-selected', 'right-side-selected');
const [lineClass] = ['left-side', 'right-side'].filter(name => line.classList.contains(name));
const [lineClass] = ['left-side', 'right-side'].filter((name) =>
line.classList.contains(name),
);
if (lineClass) {
table.classList.add(`${lineClass}-selected`);

View File

@ -9,13 +9,13 @@ import {
export * from './getters_versions_dropdowns';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
export const isParallelView = (state) => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
export const isInlineView = (state) => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
export const whichCollapsedTypes = state => {
const automatic = state.diffFiles.some(file => file.viewer?.automaticallyCollapsed);
const manual = state.diffFiles.some(file => file.viewer?.manuallyCollapsed);
export const whichCollapsedTypes = (state) => {
const automatic = state.diffFiles.some((file) => file.viewer?.automaticallyCollapsed);
const manual = state.diffFiles.some((file) => file.viewer?.manuallyCollapsed);
return {
any: automatic || manual,
@ -24,18 +24,18 @@ export const whichCollapsedTypes = state => {
};
};
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
export const commitId = (state) => (state.commit && state.commit.id ? state.commit.id : null);
/**
* Checks if the diff has all discussions expanded
* @param {Object} diff
* @returns {Boolean}
*/
export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
export const diffHasAllExpandedDiscussions = (state, getters) => (diff) => {
const discussions = getters.getDiffFileDiscussions(diff);
return (
(discussions && discussions.length && discussions.every(discussion => discussion.expanded)) ||
(discussions && discussions.length && discussions.every((discussion) => discussion.expanded)) ||
false
);
};
@ -45,11 +45,13 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
* @param {Object} diff
* @returns {Boolean}
*/
export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
export const diffHasAllCollapsedDiscussions = (state, getters) => (diff) => {
const discussions = getters.getDiffFileDiscussions(diff);
return (
(discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) ||
(discussions &&
discussions.length &&
discussions.every((discussion) => !discussion.expanded)) ||
false
);
};
@ -59,9 +61,9 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
* @param {Object} diff
* @returns {Boolean}
*/
export const diffHasExpandedDiscussions = () => diff => {
return diff[INLINE_DIFF_LINES_KEY].filter(l => l.discussions.length >= 1).some(
l => l.discussionsExpanded,
export const diffHasExpandedDiscussions = () => (diff) => {
return diff[INLINE_DIFF_LINES_KEY].filter((l) => l.discussions.length >= 1).some(
(l) => l.discussionsExpanded,
);
};
@ -70,8 +72,8 @@ export const diffHasExpandedDiscussions = () => diff => {
* @param {Boolean} diff
* @returns {Boolean}
*/
export const diffHasDiscussions = () => diff => {
return diff[INLINE_DIFF_LINES_KEY].some(l => l.discussions.length >= 1);
export const diffHasDiscussions = () => (diff) => {
return diff[INLINE_DIFF_LINES_KEY].some((l) => l.discussions.length >= 1);
};
/**
@ -79,22 +81,22 @@ export const diffHasDiscussions = () => diff => {
* @param {Object} diff
* @returns {Array}
*/
export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff =>
export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => (diff) =>
rootGetters.discussions.filter(
discussion => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash,
(discussion) => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash,
) || [];
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
export const getDiffFileByHash = (state) => (fileHash) =>
state.diffFiles.find((file) => file.file_hash === fileHash);
export const flatBlobsList = state =>
Object.values(state.treeEntries).filter(f => f.type === 'blob');
export const flatBlobsList = (state) =>
Object.values(state.treeEntries).filter((f) => f.type === 'blob');
export const allBlobs = (state, getters) =>
getters.flatBlobsList.reduce((acc, file) => {
const { parentPath } = file;
if (parentPath && !acc.some(f => f.path === parentPath)) {
if (parentPath && !acc.some((f) => f.path === parentPath)) {
acc.push({
path: parentPath,
isHeader: true,
@ -102,13 +104,13 @@ export const allBlobs = (state, getters) =>
});
}
acc.find(f => f.path === parentPath).tree.push(file);
acc.find((f) => f.path === parentPath).tree.push(file);
return acc;
}, []);
export const getCommentFormForDiffFile = state => fileHash =>
state.commentForms.find(form => form.fileHash === fileHash);
export const getCommentFormForDiffFile = (state) => (fileHash) =>
state.commentForms.find((form) => form.fileHash === fileHash);
/**
* Returns the test coverage hits for a specific line of a given file
@ -116,7 +118,7 @@ export const getCommentFormForDiffFile = state => fileHash =>
* @param {number} line
* @returns {number}
*/
export const fileLineCoverage = state => (file, line) => {
export const fileLineCoverage = (state) => (file, line) => {
if (!state.coverageFiles.files) return {};
const fileCoverage = state.coverageFiles.files[file];
if (!fileCoverage) return {};
@ -137,13 +139,13 @@ export const fileLineCoverage = state => (file, line) => {
* Returns index of a currently selected diff in diffFiles
* @returns {number}
*/
export const currentDiffIndex = state =>
export const currentDiffIndex = (state) =>
Math.max(
0,
state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId),
state.diffFiles.findIndex((diff) => diff.file_hash === state.currentDiffFileId),
);
export const diffLines = state => (file, unifiedDiffComponents) => {
export const diffLines = (state) => (file, unifiedDiffComponents) => {
if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
return null;
}
@ -155,5 +157,5 @@ export const diffLines = state => (file, unifiedDiffComponents) => {
};
export function fileReviews(state) {
return state.diffFiles.map(file => isFileReviewed(state.mrReviews, file));
return state.diffFiles.map((file) => isFileReviewed(state.mrReviews, file));
}

View File

@ -46,7 +46,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
formTextarea.on('paste', event => handlePaste(event));
formTextarea.on('paste', (event) => handlePaste(event));
// Add dropzone area to the form.
const $mdArea = formTextarea.closest('.md-area');
@ -139,7 +139,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', e => {
$cancelButton.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
@ -149,7 +149,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
// clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', e => {
$retryLink.on('click', (e) => {
const dropzoneInstance = Dropzone.forElement(
e.target.closest('.js-main-target-form').querySelector('.div-dropzone'),
);
@ -161,7 +161,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
// uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true);
failedFiles.map(failedFile => {
failedFiles.map((failedFile) => {
const file = failedFile;
if (file.status === Dropzone.ERROR) {
@ -173,7 +173,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
});
});
// eslint-disable-next-line consistent-return
handlePaste = event => {
handlePaste = (event) => {
const pasteEvent = event.originalEvent;
const { clipboardData } = pasteEvent;
if (clipboardData && clipboardData.items) {
@ -198,7 +198,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
}
};
isImage = data => {
isImage = (data) => {
let i = 0;
while (i < data.clipboardData.items.length) {
const item = data.clipboardData.items[i];
@ -228,7 +228,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
return formTextarea.trigger('input');
};
addFileToForm = path => {
addFileToForm = (path) => {
$(form).append(`<input type="hidden" name="files[]" value="${escape(path)}">`);
};
@ -236,7 +236,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
const showError = message => {
const showError = (message) => {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
@ -269,15 +269,16 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
insertToTextArea(filename, md);
closeSpinner();
})
.catch(e => {
.catch((e) => {
showError(e.response.data.message);
closeSpinner();
});
};
updateAttachingMessage = (files, messageContainer) => {
const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued')
.length;
const filesCount = files.filter(
(file) => file.status === 'uploading' || file.status === 'queued',
).length;
const attachingMessage = n__('Attaching a file', 'Attaching %d files', filesCount);
messageContainer.text(`${attachingMessage} -`);

View File

@ -83,7 +83,7 @@ export default {
);
},
filteredEnvironments() {
return this.environments.filter(e => !e.shouldBeDestroyed);
return this.environments.filter((e) => !e.shouldBeDestroyed);
},
isPercentUserRollout() {
return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
@ -91,7 +91,9 @@ export default {
},
methods: {
addEnvironment(environment) {
const allEnvironmentsScope = this.environments.find(scope => scope.environmentScope === '*');
const allEnvironmentsScope = this.environments.find(
(scope) => scope.environmentScope === '*',
);
if (allEnvironmentsScope) {
allEnvironmentsScope.shouldBeDestroyed = true;
}
@ -113,7 +115,7 @@ export default {
if (isNumber(environment.id)) {
Vue.set(environment, 'shouldBeDestroyed', true);
} else {
this.environments = this.environments.filter(e => e !== environment);
this.environments = this.environments.filter((e) => e !== environment);
}
if (this.filteredEnvironments.length === 0) {
this.environments.push({ environmentScope: '*' });

View File

@ -1,4 +1,4 @@
#import "../fragments/alert_note.fragment.graphql"
#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {

View File

@ -1,4 +1,4 @@
#import "../fragments/list_item.fragment.graphql"
#import "~/graphql_shared/fragments/alert.fragment.graphql"
query getAlerts(
$projectPath: ID!

View File

@ -32,8 +32,8 @@ const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} })
// We need to clean "move" actions, because we can only support 100% similarity moves at the moment.
// This is because the previous file's content might not be loaded.
Object.values(changes)
.filter(change => change.action === commitActionTypes.move)
.forEach(change => {
.filter((change) => change.action === commitActionTypes.move)
.forEach((change) => {
const prev = changes[change.file.prevPath];
if (!prev) {
@ -51,14 +51,14 @@ const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} })
// Next, we need to add deleted directories by looking at the parents
Object.values(changes)
.filter(change => change.action === commitActionTypes.delete && change.file.parentPath)
.filter((change) => change.action === commitActionTypes.delete && change.file.parentPath)
.forEach(({ file }) => {
// Do nothing if we've already visited this directory.
if (changes[file.parentPath]) {
return;
}
getDeletedParents(entries, file).forEach(parent => {
getDeletedParents(entries, file).forEach((parent) => {
changes[parent.path] = { action: commitActionTypes.delete, file: parent };
});
});
@ -66,13 +66,15 @@ const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} })
return Object.values(changes);
};
const createDiff = state => {
const createDiff = (state) => {
const changes = filesWithChanges(state);
const toDelete = changes.filter(x => x.action === commitActionTypes.delete).map(x => x.file.path);
const toDelete = changes
.filter((x) => x.action === commitActionTypes.delete)
.map((x) => x.file.path);
const patch = changes
.filter(x => x.action !== commitActionTypes.delete)
.filter((x) => x.action !== commitActionTypes.delete)
.map(({ file, action }) => createFileDiff(file, action))
.join('');

View File

@ -1,22 +1,23 @@
import { states } from './constants';
export const hasLatestPipeline = state => !state.isLoadingPipeline && Boolean(state.latestPipeline);
export const hasLatestPipeline = (state) =>
!state.isLoadingPipeline && Boolean(state.latestPipeline);
export const pipelineFailed = state =>
export const pipelineFailed = (state) =>
state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
export const failedStages = state =>
export const failedStages = (state) =>
state.stages
.filter(stage => stage.status.text.toLowerCase() === states.failed)
.map(stage => ({
.filter((stage) => stage.status.text.toLowerCase() === states.failed)
.map((stage) => ({
...stage,
jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed),
jobs: stage.jobs.filter((job) => job.status.text.toLowerCase() === states.failed),
}));
export const failedJobsCount = state =>
export const failedJobsCount = (state) =>
state.stages.reduce(
(acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length,
(acc, stage) => acc + stage.jobs.filter((j) => j.status.text === states.failed).length,
0,
);
export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
export const jobsCount = (state) => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);

View File

@ -34,7 +34,9 @@ export default {
computed: {
...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId', 'sourceId']),
filteredFields() {
return FIELDS.filter(field => this.tableFields.includes(field.key) && this.showField(field));
return FIELDS.filter(
(field) => this.tableFields.includes(field.key) && this.showField(field),
);
},
userIsLoggedIn() {
return this.currentUserId !== null;
@ -56,7 +58,7 @@ export default {
return false;
}
return this.members.some(member => {
return this.members.some((member) => {
return (
canRemove(member, this.sourceId) ||
canResend(member) ||

View File

@ -1 +1,2 @@
export const findMember = (state, memberId) => state.members.find(member => member.id === memberId);
export const findMember = (state, memberId) =>
state.members.find((member) => member.id === memberId);

View File

@ -1,4 +1,5 @@
<script>
import produce from 'immer';
import { flattenDeep, isNumber } from 'lodash';
import { GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { roundOffFloat } from '~/lib/utils/common_utils';
@ -84,11 +85,13 @@ export default {
metricData() {
const originalMetricQuery = this.graphData.metrics[0];
const metricQuery = { ...originalMetricQuery };
metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
x,
y + this.yOffset,
]);
const metricQuery = produce(originalMetricQuery, draftQuery => {
// eslint-disable-next-line no-param-reassign
draftQuery.result[0].values = draftQuery.result[0].values.map(([x, y]) => [
x,
y + this.yOffset,
]);
});
return {
...this.graphData,
type: panelTypes.LINE_CHART,

View File

@ -114,7 +114,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
}
return getDashboard(state.dashboardEndpoint, params)
.then(response => {
.then((response) => {
dispatch('receiveMetricsDashboardSuccess', { response });
/**
* After the dashboard is fetched, there can be non-blocking invalid syntax
@ -125,7 +125,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
*/
dispatch('fetchDashboardValidationWarnings');
})
.catch(error => {
.catch((error) => {
Sentry.captureException(error);
commit(types.SET_ALL_DASHBOARDS, error.response?.data?.all_dashboards ?? []);
@ -185,9 +185,9 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
dispatch('fetchVariableMetricLabelValues', { defaultQueryParams });
const promises = [];
state.dashboard.panelGroups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
state.dashboard.panelGroups.forEach((group) => {
group.panels.forEach((panel) => {
panel.metrics.forEach((metric) => {
promises.push(dispatch('fetchPrometheusMetric', { metric, defaultQueryParams }));
});
});
@ -231,10 +231,10 @@ export const fetchPrometheusMetric = (
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
return getPrometheusQueryData(metric.prometheusEndpointPath, queryParams)
.then(data => {
.then((data) => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, data });
})
.catch(error => {
.catch((error) => {
Sentry.captureException(error);
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error });
@ -251,15 +251,15 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
}
return axios
.get(state.deploymentsEndpoint)
.then(resp => resp.data)
.then(response => {
.then((resp) => resp.data)
.then((response) => {
if (!response || !response.deployments) {
createFlash(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
}
dispatch('receiveDeploymentsDataSuccess', response.deployments);
})
.catch(error => {
.catch((error) => {
Sentry.captureException(error);
dispatch('receiveDeploymentsDataFailure');
createFlash(s__('Metrics|There was an error getting deployment information.'));
@ -285,10 +285,10 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
states: [ENVIRONMENT_AVAILABLE_STATE],
},
})
.then(resp =>
.then((resp) =>
parseEnvironmentsResponse(resp.data?.project?.data?.environments, state.projectPath),
)
.then(environments => {
.then((environments) => {
if (!environments) {
createFlash(
s__('Metrics|There was an error fetching the environments data, please try again'),
@ -297,7 +297,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
dispatch('receiveEnvironmentsDataSuccess', environments);
})
.catch(err => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveEnvironmentsDataFailure');
createFlash(s__('Metrics|There was an error getting environments information.'));
@ -326,16 +326,18 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
startingFrom: start,
},
})
.then(resp => resp.data?.project?.environments?.nodes?.[0].metricsDashboard?.annotations.nodes)
.then(
(resp) => resp.data?.project?.environments?.nodes?.[0].metricsDashboard?.annotations.nodes,
)
.then(parseAnnotationsResponse)
.then(annotations => {
.then((annotations) => {
if (!annotations) {
createFlash(s__('Metrics|There was an error fetching annotations. Please try again.'));
}
dispatch('receiveAnnotationsSuccess', annotations);
})
.catch(err => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveAnnotationsFailure');
createFlash(s__('Metrics|There was an error getting annotations information.'));
@ -363,7 +365,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) =
dashboardPath,
},
})
.then(resp => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard)
.then((resp) => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard)
.then(({ schemaValidationWarnings } = {}) => {
const hasWarnings = schemaValidationWarnings && schemaValidationWarnings.length !== 0;
/**
@ -372,7 +374,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) =
*/
dispatch('receiveDashboardValidationWarningsSuccess', hasWarnings || false);
})
.catch(err => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveDashboardValidationWarningsFailure');
createFlash(
@ -437,9 +439,9 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
return axios
.post(state.dashboardsEndpoint, params)
.then(response => response.data)
.then(data => data.dashboard)
.catch(error => {
.then((response) => response.data)
.then((data) => data.dashboard)
.catch((error) => {
Sentry.captureException(error);
const { response } = error;
@ -466,7 +468,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
const { start_time, end_time } = defaultQueryParams;
const optionsRequests = [];
state.variables.forEach(variable => {
state.variables.forEach((variable) => {
if (variable.type === VARIABLE_TYPES.metric_label_values) {
const { prometheusEndpointPath, label } = variable.options;
@ -474,7 +476,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
start_time,
end_time,
})
.then(data => {
.then((data) => {
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
.catch(() => {
@ -512,7 +514,7 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml)
dispatch('fetchPanelPreviewMetrics');
})
.catch(error => {
.catch((error) => {
commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, extractErrorMessage(error));
});
};
@ -535,10 +537,10 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => {
return getPrometheusQueryData(metric.prometheusEndpointPath, params, {
cancelToken: cancelTokenSource.token,
})
.then(data => {
.then((data) => {
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data });
})
.catch(error => {
.catch((error) => {
Sentry.captureException(error);
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error });

View File

@ -5,8 +5,10 @@ import {
normalizeCustomDashboardPath,
} from './utils';
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
const metricsIdsInPanel = (panel) =>
panel.metrics
.filter((metric) => metric.metricId && metric.result)
.map((metric) => metric.metricId);
/**
* Returns a reference to the currently selected dashboard
@ -17,8 +19,8 @@ const metricsIdsInPanel = panel =>
export const selectedDashboard = (state, getters) => {
const { allDashboards } = state;
return (
allDashboards.find(d => d.path === getters.fullDashboardPath) ||
allDashboards.find(d => d.default) ||
allDashboards.find((d) => d.path === getters.fullDashboardPath) ||
allDashboards.find((d) => d.default) ||
null
);
};
@ -32,15 +34,15 @@ export const selectedDashboard = (state, getters) => {
* @returns {Function} A function that returns an array of
* states in all the metric in the dashboard or group.
*/
export const getMetricStates = state => groupKey => {
export const getMetricStates = (state) => (groupKey) => {
let groups = state.dashboard.panelGroups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
groups = groups.filter((group) => group.key === groupKey);
}
const metricStates = groups.reduce((acc, group) => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
group.panels.forEach((panel) => {
panel.metrics.forEach((metric) => {
if (metric.state) {
acc.push(metric.state);
}
@ -64,15 +66,15 @@ export const getMetricStates = state => groupKey => {
* metrics in the dashboard that contain results, optionally
* filtered by group key.
*/
export const metricsWithData = state => groupKey => {
export const metricsWithData = (state) => (groupKey) => {
let groups = state.dashboard.panelGroups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
groups = groups.filter((group) => group.key === groupKey);
}
const res = [];
groups.forEach(group => {
group.panels.forEach(panel => {
groups.forEach((group) => {
group.panels.forEach((panel) => {
res.push(...metricsIdsInPanel(panel));
});
});
@ -89,7 +91,7 @@ export const metricsWithData = state => groupKey => {
* https://gitlab.com/gitlab-org/gitlab/-/issues/28241
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27447
*/
export const metricsSavedToDb = state => {
export const metricsSavedToDb = (state) => {
const metricIds = [];
state.dashboard.panelGroups.forEach(({ panels }) => {
panels.forEach(({ metrics }) => {
@ -111,8 +113,8 @@ export const metricsSavedToDb = state => {
* @param {Object} state
* @returns {Array} List of environments
*/
export const filteredEnvironments = state =>
state.environments.filter(env =>
export const filteredEnvironments = (state) =>
state.environments.filter((env) =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
);
@ -125,7 +127,7 @@ export const filteredEnvironments = state =>
* @param {Object} state
* @returns {Array} modified array of links
*/
export const linksWithMetadata = state => {
export const linksWithMetadata = (state) => {
const metadata = {
timeRange: state.timeRange,
};
@ -152,7 +154,7 @@ export const linksWithMetadata = state => {
* in the format of {variables[key1]=value1, variables[key2]=value2}
*/
export const getCustomVariablesParams = state =>
export const getCustomVariablesParams = (state) =>
state.variables.reduce((acc, variable) => {
const { name, value } = variable;
if (value !== null) {
@ -168,5 +170,5 @@ export const getCustomVariablesParams = state =>
* @param {Object} state
* @returns {String} full dashboard path
*/
export const fullDashboardPath = state =>
export const fullDashboardPath = (state) =>
normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath);

View File

@ -1,6 +1,7 @@
import { LIST_KEY_PROJECT, SORT_FIELDS } from './constants';
export default isGroupPage => SORT_FIELDS.filter(f => f.key !== LIST_KEY_PROJECT || isGroupPage);
export default (isGroupPage) =>
SORT_FIELDS.filter((f) => f.key !== LIST_KEY_PROJECT || isGroupPage);
/**
* A small util function that works out if the delete action has deleted the

View File

@ -31,7 +31,9 @@ export default {
},
computed: {
filteredNamespaces() {
return this.namespaces.filter(n => n.name.toLowerCase().includes(this.filter.toLowerCase()));
return this.namespaces.filter((n) =>
n.name.toLowerCase().includes(this.filter.toLowerCase()),
);
},
},
@ -43,7 +45,7 @@ export default {
loadGroups() {
axios
.get(this.endpoint)
.then(response => {
.then((response) => {
this.namespaces = response.data.namespaces;
})
.catch(() => createFlash(__('There was a problem fetching groups.')));

View File

@ -100,8 +100,8 @@ export default class UserTabs {
bindEvents() {
this.$parentEl
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event))
.on('click', '.gl-pagination a', event => this.changeProjectsPage(event));
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event))
.on('click', '.gl-pagination a', (event) => this.changeProjectsPage(event));
window.addEventListener('resize', () => this.onResize());
}
@ -212,17 +212,19 @@ export default class UserTabs {
const calendarPath = $calendarWrap.data('calendarPath');
AjaxCache.retrieve(calendarPath)
.then(data => UserTabs.renderActivityCalendar(data, $calendarWrap))
.then((data) => UserTabs.renderActivityCalendar(data, $calendarWrap))
.catch(() => {
const cWrap = $calendarWrap[0];
cWrap.querySelector('.spinner').classList.add('invisible');
cWrap.querySelector('.user-calendar-error').classList.remove('invisible');
cWrap.querySelector('.user-calendar-error .js-retry-load').addEventListener('click', e => {
e.preventDefault();
cWrap.querySelector('.user-calendar-error').classList.add('invisible');
cWrap.querySelector('.spinner').classList.remove('invisible');
this.loadActivityCalendar();
});
cWrap
.querySelector('.user-calendar-error .js-retry-load')
.addEventListener('click', (e) => {
e.preventDefault();
cWrap.querySelector('.user-calendar-error').classList.add('invisible');
cWrap.querySelector('.spinner').classList.remove('invisible');
this.loadActivityCalendar();
});
});
}

View File

@ -25,7 +25,7 @@ export default class AccessDropdown {
this.setSelectedItems([]);
this.persistPreselectedItems();
this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE);
this.noOneObj = this.accessLevelsData.find((level) => level.id === ACCESS_LEVEL_NONE);
this.initDropdown();
}
@ -45,7 +45,7 @@ export default class AccessDropdown {
onHide();
}
},
clicked: options => {
clicked: (options) => {
const { $el, e } = options;
const item = options.selectedObj;
const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
@ -56,7 +56,7 @@ export default class AccessDropdown {
// We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
// remove all preselected items before selecting this item
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
this.accessLevelsData.forEach(level => {
this.accessLevelsData.forEach((level) => {
this.removeSelectedItem(level);
});
}
@ -65,7 +65,7 @@ export default class AccessDropdown {
if (this.noOneObj) {
if (item.id === this.noOneObj.id && !fossWithMergeAccess) {
// remove all others selected items
this.accessLevelsData.forEach(level => {
this.accessLevelsData.forEach((level) => {
if (level.id !== item.id) {
this.removeSelectedItem(level);
}
@ -109,7 +109,7 @@ export default class AccessDropdown {
return;
}
const persistedItems = itemsToPreselect.map(item => {
const persistedItems = itemsToPreselect.map((item) => {
const persistedItem = { ...item };
persistedItem.persisted = true;
return persistedItem;
@ -123,7 +123,7 @@ export default class AccessDropdown {
}
getSelectedItems() {
return this.items.filter(item => !item._destroy);
return this.items.filter((item) => !item._destroy);
}
getAllSelectedItems() {
@ -134,7 +134,7 @@ export default class AccessDropdown {
getInputData() {
const selectedItems = this.getAllSelectedItems();
const accessLevels = selectedItems.map(item => {
const accessLevels = selectedItems.map((item) => {
const obj = {};
if (typeof item.id !== 'undefined') {
@ -288,12 +288,14 @@ export default class AccessDropdown {
$dropdownToggleText.removeClass('is-default');
if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
const roleData = this.accessLevelsData.find(data => data.id === currentItems[0].access_level);
const roleData = this.accessLevelsData.find(
(data) => data.id === currentItems[0].access_level,
);
return roleData.text;
}
const labelPieces = [];
const counts = countBy(currentItems, item => item.type);
const counts = countBy(currentItems, (item) => item.type);
if (counts[LEVEL_TYPES.ROLE] > 0) {
labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
@ -336,7 +338,7 @@ export default class AccessDropdown {
});
} else {
this.getDeployKeys(query)
.then(deployKeysResponse => callback(this.consolidateData(deployKeysResponse.data)))
.then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
.catch(() => createFlash({ message: __('Failed to load deploy keys.') }));
}
}
@ -365,7 +367,7 @@ export default class AccessDropdown {
/*
* Build roles
*/
const roles = this.accessLevelsData.map(level => {
const roles = this.accessLevelsData.map((level) => {
/* eslint-disable no-param-reassign */
// This re-assignment is intentional as
// level.type property is being used in removeSelectedItem()
@ -389,7 +391,7 @@ export default class AccessDropdown {
/*
* Build groups
*/
const groups = groupsResponse.map(group => ({
const groups = groupsResponse.map((group) => ({
...group,
type: LEVEL_TYPES.GROUP,
}));
@ -398,8 +400,8 @@ export default class AccessDropdown {
* Build users
*/
const users = selectedItems
.filter(item => item.type === LEVEL_TYPES.USER)
.map(item => {
.filter((item) => item.type === LEVEL_TYPES.USER)
.map((item) => {
// Save identifiers for easy-checking more later
map.push(LEVEL_TYPES.USER + item.user_id);
@ -414,7 +416,7 @@ export default class AccessDropdown {
// Has to be checked against server response
// because the selected item can be in filter results
usersResponse.forEach(response => {
usersResponse.forEach((response) => {
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
const user = { ...response };
@ -444,7 +446,7 @@ export default class AccessDropdown {
}
if (this.deployKeysOnProtectedBranchesEnabled) {
const deployKeys = deployKeysResponse.map(response => {
const deployKeys = deployKeysResponse.map((response) => {
const {
id,
fingerprint,

View File

@ -43,6 +43,11 @@ export default {
required: false,
default: false,
},
metadataLoading: {
type: Boolean,
required: false,
default: false,
},
},
loader: {
repeat: 10,
@ -92,7 +97,11 @@ export default {
</script>
<template>
<title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE" :info-messages="infoMessages">
<title-area
:title="$options.i18n.CONTAINER_REGISTRY_TITLE"
:info-messages="infoMessages"
:metadata-loading="metadataLoading"
>
<template #right-actions>
<slot name="commands"></slot>
</template>

View File

@ -242,6 +242,7 @@ export default {
<template v-else>
<registry-header
:metadata-loading="isLoading"
:images-count="containerRepositoriesCount"
:expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath"

View File

@ -9,7 +9,9 @@ const fetchpromises = {};
const resolvers = {};
export function resolveCommit(commits, path, { resolve, entry }) {
const commit = commits.find(c => c.filePath === `${path}/${entry.name}` && c.type === entry.type);
const commit = commits.find(
(c) => c.filePath === `${path}/${entry.name}` && c.type === entry.type,
);
if (commit) {
resolve(commit);
@ -42,7 +44,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
.then(({ data: newData, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
const sourceData = client.readQuery({ query: commitsQuery });
const data = produce(sourceData, draftState => {
const data = produce(sourceData, (draftState) => {
draftState.commits.push(...normalizeData(newData, path));
});
client.writeQuery({
@ -50,7 +52,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
data,
});
resolvers[path].forEach(r => resolveCommit(data.commits, path, r));
resolvers[path].forEach((r) => resolveCommit(data.commits, path, r));
delete fetchpromises[path];

View File

@ -1,7 +1,9 @@
import { joinPaths } from '~/lib/utils/url_utility';
export const updateElementsVisibility = (selector, isVisible) => {
document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible));
document
.querySelectorAll(selector)
.forEach((elem) => elem.classList.toggle('hidden', !isVisible));
};
export const updateFormAction = (selector, basePath, path) => {

View File

@ -50,9 +50,13 @@ export default {
$(this.$el).trigger('hidden.gl.dropdown');
},
getUpdateVariables(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id);
const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id);
const currentLabelIds = this.selectedLabels.map((label) => label.id);
const userAddedLabelIds = dropdownLabels
.filter((label) => label.set)
.map((label) => label.id);
const userRemovedLabelIds = dropdownLabels
.filter((label) => !label.set)
.map((label) => label.id);
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
@ -116,7 +120,7 @@ export default {
}
const issuableType = camelCase(this.issuableType);
this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map(label => ({
this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map((label) => ({
...label,
id: getIdFromGraphQLId(label.id),
}));

View File

@ -1,5 +1,6 @@
export const parseUserIds = userIds => userIds.split(/\s*,\s*/g);
export const parseUserIds = (userIds) => userIds.split(/\s*,\s*/g);
export const stringifyUserIds = userIds => userIds.join(',');
export const stringifyUserIds = (userIds) => userIds.join(',');
export const getErrorMessages = error => [].concat(error?.response?.data?.message ?? error.message);
export const getErrorMessages = (error) =>
[].concat(error?.response?.data?.message ?? error.message);

View File

@ -49,7 +49,8 @@ export default {
label: s__('AlertManagement|Key'),
thClass,
tdClass,
formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))),
formatter: (string) =>
capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))),
},
{
key: 'value',

View File

@ -1,5 +1,5 @@
<script>
import { GlAvatar, GlSprintf, GlLink } from '@gitlab/ui';
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
export default {
name: 'TitleArea',
@ -7,6 +7,7 @@ export default {
GlAvatar,
GlSprintf,
GlLink,
GlSkeletonLoader,
},
props: {
avatar: {
@ -24,6 +25,11 @@ export default {
default: () => [],
required: false,
},
metadataLoading: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -68,13 +74,23 @@ export default {
</div>
<div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
<div
v-for="(row, metadataIndex) in metadataSlots"
:key="metadataIndex"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<slot :name="row"></slot>
</div>
<template v-if="!metadataLoading">
<div
v-for="(row, metadataIndex) in metadataSlots"
:key="metadataIndex"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<slot :name="row"></slot>
</div>
</template>
<template v-else>
<div class="gl-w-full">
<gl-skeleton-loader :width="200" :height="16" preserve-aspect-ratio="xMinYMax meet">
<circle cx="6" cy="8" r="6" />
<rect x="16" y="4" width="200" height="8" rx="4" />
</gl-skeleton-loader>
</div>
</template>
</div>
</div>
<div v-if="$slots['right-actions']" class="gl-mt-3">

View File

@ -62,7 +62,9 @@ export default {
return files.every(this.isFileValid);
},
isValidDragDataType({ dataTransfer }) {
return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE));
return Boolean(
dataTransfer && dataTransfer.types.some((t) => t === VALID_DATA_TRANSFER_TYPE),
);
},
ondrop({ dataTransfer = {} }) {
this.dragCounter = 0;

View File

@ -54,5 +54,6 @@ export const timeRanges = [
},
];
export const defaultTimeRange = timeRanges.find(tr => tr.default);
export const getTimeWindow = timeWindowName => timeRanges.find(tr => tr.name === timeWindowName);
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
export const getTimeWindow = (timeWindowName) =>
timeRanges.find((tr) => tr.name === timeWindowName);

View File

@ -51,17 +51,17 @@ module Mutations
params = scalars.with_indifferent_access
release_result = ::Releases::UpdateService.new(project, current_user, params).execute
result = ::Releases::UpdateService.new(project, current_user, params).execute
if release_result[:status] == :success
if result[:status] == :success
{
release: release_result[:release],
release: result[:release],
errors: []
}
else
{
release: nil,
errors: [release_result[:message]]
errors: [result[:message]]
}
end
end

View File

@ -6,6 +6,7 @@ module Resolvers
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Projects::Services::JiraProjectType.connection_type, null: true
authorize :admin_project
argument :name,
GraphQL::STRING_TYPE,
@ -31,10 +32,6 @@ module Resolvers
end
end
def authorized_resource?(project)
Ability.allowed?(context[:current_user], :admin_project, project)
end
private
alias_method :jira_service, :object

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Resolvers
class ReleaseMilestonesResolver < BaseResolver
type Types::MilestoneType.connection_type, null: true
alias_method :release, :object
def resolve(**args)
offset_pagination(release.milestones.order_by_dates_and_title)
end
end
end

View File

@ -35,7 +35,8 @@ module Types
field :links, Types::ReleaseLinksType, null: true, method: :itself,
description: 'Links of the release'
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release'
description: 'Milestones associated to the release',
resolver: ::Resolvers::ReleaseMilestonesResolver
field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release'

View File

@ -33,6 +33,7 @@ class Milestone < ApplicationRecord
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }

View File

@ -82,7 +82,7 @@ class Release < ApplicationRecord
end
def milestone_titles
self.milestones.map {|m| m.title }.sort.join(", ")
self.milestones.order_by_dates_and_title.map {|m| m.title }.join(', ')
end
def to_hook_data(action)

View File

@ -0,0 +1,5 @@
---
title: Adjust container registry metadata during loading
merge_request: 50181
author:
type: changed

View File

@ -1,5 +0,0 @@
---
title: Fix project transfer corrupting shared runners state
merge_request: 47316
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Return release milestones in predictable order
merge_request: 47700
author:
type: fixed

View File

@ -9499,6 +9499,11 @@ type GeoNode {
"""
first: Int
"""
Global ID of a specific compliance framework to return.
"""
id: ComplianceManagementFrameworkID
"""
Returns the last _n_ elements from the list.
"""
@ -9715,7 +9720,7 @@ type Group {
): CodeCoverageActivityConnection
"""
Compliance frameworks available to projects in this namespace Available only
Compliance frameworks available to projects in this namespace. Available only
when feature flag `ff_custom_compliance_frameworks` is enabled.
"""
complianceFrameworks(
@ -15368,7 +15373,7 @@ type Namespace {
additionalPurchasedStorageSize: Float
"""
Compliance frameworks available to projects in this namespace Available only
Compliance frameworks available to projects in this namespace. Available only
when feature flag `ff_custom_compliance_frameworks` is enabled.
"""
complianceFrameworks(
@ -15387,6 +15392,11 @@ type Namespace {
"""
first: Int
"""
Global ID of a specific compliance framework to return.
"""
id: ComplianceManagementFrameworkID
"""
Returns the last _n_ elements from the list.
"""

View File

@ -26177,6 +26177,16 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "id",
"description": "Global ID of a specific compliance framework to return.",
"type": {
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
@ -26928,7 +26938,7 @@
},
{
"name": "complianceFrameworks",
"description": "Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled.",
"description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled.",
"args": [
{
"name": "after",
@ -45542,7 +45552,7 @@
},
{
"name": "complianceFrameworks",
"description": "Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled.",
"description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled.",
"args": [
{
"name": "after",
@ -45583,6 +45593,16 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "id",
"description": "Global ID of a specific compliance framework to return.",
"type": {
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"ofType": null
},
"defaultValue": null
}
],
"type": {

View File

@ -1579,7 +1579,7 @@ Represents an external issue.
| `board` | Board | A single board of the group |
| `boards` | BoardConnection | Boards of the group |
| `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled. |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled. |
| `containerRepositories` | ContainerRepositoryConnection | Container repositories of the group |
| `containerRepositoriesCount` | Int! | Number of container repositories in the group |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
@ -2329,7 +2329,7 @@ Contains statistics about a milestone.
| ----- | ---- | ----------- |
| `actualRepositorySizeLimit` | Float | Size limit for repositories in the namespace in bytes |
| `additionalPurchasedStorageSize` | Float | Additional storage purchased for the root namespace in bytes |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace Available only when feature flag `ff_custom_compliance_frameworks` is enabled. |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled. |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |

View File

@ -7,15 +7,42 @@ type: reference, api
# Merge requests API
> - `author_id`, `author_username`, and `assignee_id` were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5.
> - `my_reaction_emoji` was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0.
> - For the `scope` attribute, `created-by-me` and `assigned-to-me` were [deprecated](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18935) in favor of `created_by_me` and `assigned_to_me` in GitLab 11.0.
> - `with_labels_details` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) in GitLab 12.7.
> - `author_username` and `author_username` were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10.
> - `reference` was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20354) in GitLab 12.10 in favour of `references`.
> - `with_merge_status_recheck` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0.
Every API call to merge requests must be authenticated.
WARNING:
> `reference` attribute in response is deprecated in favour of `references`.
> Introduced [GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20354)
**Important notes:**
NOTE:
> `references.relative` is relative to the group / project that the merge request is being requested. When merge request is fetched from its project
> `relative` format would be the same as `short` format and when requested across groups / projects it is expected to be the same as `full` format.
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29984) in GitLab 12.8, the mergeability (`merge_status`)
of each merge request is checked asynchronously when a request is made to this endpoint. Poll this API endpoint
to get updated status. This affects the `has_conflicts` property as it is dependent on the `merge_status`. It returns
`false` unless `merge_status` is `cannot_be_merged`.
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0, listing merge requests may
not proactively update `merge_status` (which also affects the `has_conflicts`), as this can be an expensive operation.
If you need the value of these fields from this endpoint, set the `with_merge_status_recheck` parameter to
`true` in the query.
- `references.relative` is relative to the group or project that the merge request is being requested. When the merge request
is fetched from its project, `relative` format would be the same as `short` format, and when requested across groups or projects, it is expected to be the same as `full` format.
- If `approvals_before_merge` **(STARTER)** is not provided, it inherits the value from the target project. If provided, the following conditions must hold for it to take effect:
- The target project's `approvals_before_merge` must be greater than zero. A
value of zero disables approvals for that project.
- The provided value of `approvals_before_merge` must be greater than the
target project's `approvals_before_merge`.
This API returns `HTTP 201 Created` for a successful response.
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46190) in GitLab 13.6,
diffs associated with the set of changes have the same size limitations applied as other diffs
returned by the API or viewed via the UI. When these limits impact the results, the `overflow`
field contains a value of `true`. Diff data without these limits applied can be retrieved by
adding the `access_raw_diffs` parameter, but it is slower and more resource-intensive.
## List merge requests
@ -26,7 +53,7 @@ default it returns only merge requests created by the current user. To
get all merge requests, use parameter `scope=all`.
The `state` parameter can be used to get only merge requests with a
given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`). It should be noted that when searching by `locked` it will mostly return no results as it is a short-lived, transitional state.
given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`). It should be noted that when searching by `locked` it mostly returns no results as it is a short-lived, transitional state.
The pagination parameters `page` and `per_page` can be used to
restrict the list of merge requests.
@ -47,50 +74,35 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`. |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc`. |
| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. |
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. |
| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_ | |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`.
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
| `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji` |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `source_branch` | string | no | Return merge requests with the given source branch. |
| `target_branch` | string | no | Return merge requests with the given target branch. |
| `search` | string | no | Search merge requests against their `title` and `description`. |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description`. |
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests. |
| `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`. |
| `environment` | string | no | Returns merge requests deployed to the given environment. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `deployed_before` | datetime | no | Return merge requests deployed before the given date/time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `deployed_after` | datetime | no | Return merge requests deployed after the given date/time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
NOTE:
[Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890),
listing merge requests may not proactively update the `merge_status` field
(which also affects the `has_conflicts` field), as this can be an expensive
operation. If you are interested in the value of these fields from this
endpoint, set the `with_merge_status_recheck` parameter to `true` in the query.
NOTE:
[Starting in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/29984),
the mergeability (`merge_status`) of each merge request will be checked
asynchronously when a request is made to this endpoint. Poll this API endpoint
to get updated status. This affects the `has_conflicts` property as it is
dependent on the `merge_status`. It'll return `false` unless `merge_status` is
`cannot_be_merged`.
```json
[
{
@ -193,7 +205,7 @@ dependent on the `merge_status`. It'll return `false` unless `merge_status` is
]
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -224,43 +236,43 @@ GET /projects/:id/merge_requests?my_reaction_emoji=star
```
`project_id` represents the ID of the project where the MR resides.
`project_id` will always equal `target_project_id`.
`project_id` always equals `target_project_id`.
In the case of a merge request from the same project,
`source_project_id`, `target_project_id` and `project_id`
will be the same. In the case of a merge request from a fork,
`target_project_id` and `project_id` will be the same and
`source_project_id` will be the fork project's ID.
are the same. In the case of a merge request from a fork,
`target_project_id` and `project_id` are the same and
`source_project_id` is the fork project's ID.
Parameters:
| Attribute | Type | Required | Description |
| ------------------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a project |
| `iids[]` | integer array | no | Return the request having the given `iid` |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
| `id` | integer | yes | The ID of a project. |
| `iids[]` | integer array | no | Return the request having the given `iid`. |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`. |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc`. |
| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. |
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. |
| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5. [Changed to snake_case](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18935) in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_ |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me`, or `all`. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. |
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`.|
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `source_branch` | string | no | Return merge requests with the given source branch. |
| `target_branch` | string | no | Return merge requests with the given target branch. |
| `search` | string | no | Search merge requests against their `title` and `description`. |
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests. |
```json
[
@ -366,7 +378,7 @@ Parameters:
]
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -401,30 +413,30 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a group |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
| `id` | integer | yes | The ID of a group. |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. |
| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at`. |
| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc`. |
| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413)|
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_ |
| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413).|
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890). |
| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_.
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_. | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_. |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
| `non_archived` | boolean | no | Return merge requests from non archived projects only. Default is true. _(Introduced in [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23809))_ |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_. |
| `source_branch` | string | no | Return merge requests with the given source branch. |
| `target_branch` | string | no | Return merge requests with the given target branch. |
| `search` | string | no | Search merge requests against their `title` and `description`. |
| `non_archived` | boolean | no | Return merge requests from non archived projects only. Default is true. _(Introduced in [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23809))_. |
```json
[
@ -528,7 +540,7 @@ Parameters:
]
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -548,7 +560,7 @@ Shows information about a single merge request.
**Note**: the `changes_count` value in the response is a string, not an
integer. This is because when an MR has too many changes to display and store,
it will be capped at 1,000. In that case, the API will return the string
it is capped at 1,000. In that case, the API returns the string
`"1000+"` for the changes count.
```plaintext
@ -557,19 +569,11 @@ GET /projects/:id/merge_requests/:merge_request_iid
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
- `render_html` (optional) - If `true` response includes rendered HTML for title and description
- `include_diverged_commits_count` (optional) - If `true` response includes the commits behind the target branch
- `include_rebase_in_progress` (optional) - If `true` response includes whether a rebase operation is in progress
NOTE:
[Starting in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/29984),
the mergeability (`merge_status`) of a merge request will be checked
asynchronously when a request is made to this endpoint. Poll this API endpoint
to get updated status. This affects the `has_conflicts` property as it is
dependent on the `merge_status`. It'll return `false` unless `merge_status` is
`cannot_be_merged`.
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - The internal ID of the merge request.
- `render_html` (optional) - If `true` response includes rendered HTML for title and description.
- `include_diverged_commits_count` (optional) - If `true` response includes the commits behind the target branch.
- `include_rebase_in_progress` (optional) - If `true` response includes whether a rebase operation is in progress.
```json
{
@ -697,7 +701,7 @@ dependent on the `merge_status`. It'll return `false` unless `merge_status` is
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -719,8 +723,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/participants
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - The internal ID of the merge request.
```json
[
@ -753,8 +757,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/commits
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - The internal ID of the merge request.
```json
[
@ -787,12 +791,6 @@ Shows information about the merge request including its files and changes.
GET /projects/:id/merge_requests/:merge_request_iid/changes
```
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46190) in GitLab 13.6,
diffs associated with the set of changes will have the same size limitations applied as other diffs
returned by the API or viewed via the UI. When these limits impact the results, the `overflow`
field will contain a value of `true`. Diff data without these limits applied can be retrieved by
adding the `access_raw_diffs` parameter, however, it will be slower and more resource-intensive.
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
@ -898,7 +896,7 @@ Parameters:
## List MR pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15454) in GitLab 10.5.0.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15454) in GitLab 10.5.
Get a list of merge request pipelines.
@ -908,8 +906,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/pipelines
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - The internal ID of the merge request.
```json
[
@ -926,7 +924,9 @@ Parameters:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/31722) in GitLab 12.3.
Create a new [pipeline for a merge request](../ci/merge_request_pipelines/index.md). A pipeline created via this endpoint will not run a regular branch/tag pipeline, it requires `.gitlab-ci.yml` to be configured with `only: [merge_requests]` to create jobs.
Create a new [pipeline for a merge request](../ci/merge_request_pipelines/index.md).
A pipeline created via this endpoint doesn't run a regular branch/tag pipeline.
It requires `.gitlab-ci.yml` to be configured with `only: [merge_requests]` to create jobs.
The new pipeline can be:
@ -940,8 +940,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/pipelines
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `merge_request_iid` (required) - The internal ID of the merge request
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding).
- `merge_request_iid` (required) - The internal ID of the merge request.
```json
{
@ -993,29 +993,19 @@ POST /projects/:id/merge_requests
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `source_branch` | string | yes | The source branch |
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
| `source_branch` | string | yes | The source branch. |
| `target_branch` | string | yes | The target branch. |
| `title` | string | yes | Title of MR. |
| `assignee_id` | integer | no | Assignee user ID. |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR. Limited to 1,048,576 characters. |
| `target_project_id` | integer | no | The target project (numeric ID) |
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The global ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch |
| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration |
| `squash` | boolean | no | Squash commits into a single commit when merging |
If `approvals_before_merge` **(STARTER)** is not provided, it inherits the value from the
target project. If it is provided, then the following conditions must hold in
order for it to take effect:
1. The target project's `approvals_before_merge` must be greater than zero. A
value of zero disables approvals for that project.
1. The provided value of `approvals_before_merge` must be greater than the
target project's `approvals_before_merge`.
1. This API returns 201 (created) for a successful response.
| `target_project_id` | integer | no | The target project (numeric ID). |
| `labels` | string | no | Labels for MR as a comma-separated list. |
| `milestone_id` | integer | no | The global ID of a milestone. |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging. |
| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch. |
| `allow_maintainer_to_push` | boolean | no | Deprecated, see `allow_collaboration`. |
| `squash` | boolean | no | Squash commits into a single commit when merging. |
```json
{
@ -1128,7 +1118,7 @@ order for it to take effect:
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -1150,10 +1140,10 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The ID of a merge request |
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The ID of a merge request. |
| `target_branch` | string | no | The target branch. |
| `title` | string | no | Title of MR. |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
@ -1161,12 +1151,12 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `add_labels` | string | no | Comma-separated label names to add to a merge request. |
| `remove_labels` | string | no | Comma-separated label names to remove from a merge request. |
| `description` | string | no | Description of MR. Limited to 1,048,576 characters. |
| `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `squash` | boolean | no | Squash commits into a single commit when merging |
| `state_event` | string | no | New state (close/reopen). |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging. |
| `squash` | boolean | no | Squash commits into a single commit when merging. |
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch |
| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration |
| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch. |
| `allow_maintainer_to_push` | boolean | no | Deprecated, see `allow_collaboration`. |
Must include at least one non-required attribute from above.
@ -1289,7 +1279,7 @@ Must include at least one non-required attribute from above.
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -1303,7 +1293,7 @@ the `approvals_before_merge` parameter:
## Delete a merge request
Only for admins and project owners. Deletes the merge request in question.
Only for administrators and project owners. Deletes the merge request in question.
```plaintext
DELETE /projects/:id/merge_requests/:merge_request_iid
@ -1311,8 +1301,8 @@ DELETE /projects/:id/merge_requests/:merge_request_iid
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/merge_requests/85"
@ -1322,13 +1312,13 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Merge changes submitted with MR using this API.
If merge request is unable to be accepted (such as Draft, Closed, Pipeline Pending Completion, or Failed while requiring Success) - you'll get a `405` and the error message 'Method Not Allowed'
If a merge request is unable to be accepted (such as Draft, Closed, Pipeline Pending Completion, or Failed while requiring Success) - you receive a `405` and the error message 'Method Not Allowed'
If it has some conflicts and can not be merged - you'll get a `406` and the error message 'Branch cannot be merged'
If it has some conflicts and can not be merged - you receive a `406` and the error message 'Branch cannot be merged'
If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a `409` and the error message 'SHA does not match HEAD of source branch'
If the `sha` parameter is passed and does not match the HEAD of the source - you receive a `409` and the error message 'SHA does not match HEAD of source branch'
If you don't have permissions to accept this merge request - you'll get a `401`
If you don't have permissions to accept this merge request - you receive a `401`
```plaintext
PUT /projects/:id/merge_requests/:merge_request_iid/merge
@ -1336,14 +1326,14 @@ PUT /projects/:id/merge_requests/:merge_request_iid/merge
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
- `merge_commit_message` (optional) - Custom merge commit message
- `squash_commit_message` (optional) - Custom squash commit message
- `squash` (optional) - if `true` the commits will be squashed into a single commit on merge
- `should_remove_source_branch` (optional) - if `true` removes the source branch
- `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds
- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - Internal ID of MR.
- `merge_commit_message` (optional) - Custom merge commit message.
- `squash_commit_message` (optional) - Custom squash commit message.
- `squash` (optional) - if `true` the commits are squashed into a single commit on merge.
- `should_remove_source_branch` (optional) - if `true` removes the source branch.
- `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds.
- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge fails.
```json
{
@ -1464,7 +1454,7 @@ Parameters:
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -1479,15 +1469,15 @@ the `approvals_before_merge` parameter:
## Merge to default merge ref path
Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge`
ref, of the target project repository, if possible. This ref will have the state the target branch would have if
ref, of the target project repository, if possible. This ref has the state the target branch would have if
a regular merge action was taken.
This is not a regular merge action given it doesn't change the merge request target branch state in any manner.
This ref (`refs/merge-requests/:iid/merge`) isn't necessarily overwritten when submitting
requests to this API, though it'll make sure the ref has the latest possible state.
requests to this API, though it makes sure the ref has the latest possible state.
If the merge request has conflicts, is empty or already merged, you'll get a `400` and a descriptive error message.
If the merge request has conflicts, is empty or already merged, you receive a `400` and a descriptive error message.
It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in case of `200`.
@ -1497,8 +1487,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/merge_ref
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - Internal ID of MR.
```json
{
@ -1508,11 +1498,9 @@ Parameters:
## Cancel Merge When Pipeline Succeeds
If you don't have permissions to accept this merge request - you'll get a `401`
If the merge request is already merged or closed - you get `405` and error message 'Method Not Allowed'
In case the merge request is not set to be merged when the pipeline succeeds, you'll also get a `406` error.
- If you don't have permissions to accept this merge request - you receive a `HTTP 401 Unauthorized`.
- If the merge request is already merged or closed - you receive a `HTTP 405 Method Not Allowed` and the error message 'Method Not Allowed'.
- In case the merge request is not set to be merged when the pipeline succeeds, you also receive a `HTTP 406 Not Acceptable` error.
```plaintext
POST /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds
@ -1520,8 +1508,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.
- `merge_request_iid` (required) - Internal ID of the merge request.
```json
{
@ -1642,7 +1630,7 @@ Parameters:
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -1660,7 +1648,7 @@ Automatically rebase the `source_branch` of the merge request against its
`target_branch`.
If you don't have permissions to push to the merge request's source branch -
you'll get a `403 Forbidden` response.
you receive a `403 Forbidden` response.
```plaintext
PUT /projects/:id/merge_requests/:merge_request_iid/rebase
@ -1668,15 +1656,15 @@ PUT /projects/:id/merge_requests/:merge_request_iid/rebase
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `skip_ci` | boolean | no | Set to `true` to skip creating a CI pipeline |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
| `skip_ci` | boolean | no | Set to `true` to skip creating a CI pipeline. |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase"
```
This is an asynchronous request. The API will return a `202 Accepted` response
This is an asynchronous request. The API returns a `HTTP 202 Accepted` response
if the request is enqueued successfully, with a response containing:
```json
@ -1689,7 +1677,7 @@ You can poll the [Get single MR](#get-single-mr) endpoint with the
`include_rebase_in_progress` parameter to check the status of the
asynchronous request.
If the rebase operation is ongoing, the response will include the following:
If the rebase operation is ongoing, the response includes the following:
```json
{
@ -1698,7 +1686,7 @@ If the rebase operation is ongoing, the response will include the following:
}
```
Once the rebase operation has completed successfully, the response will include
After the rebase operation has completed successfully, the response includes
the following:
```json
@ -1708,7 +1696,7 @@ the following:
}
```
If the rebase operation fails, the response will include the following:
If the rebase operation fails, the response includes the following:
```json
{
@ -1721,7 +1709,7 @@ If the rebase operation fails, the response will include the following:
Comments are done via the [notes](notes.md) resource.
## List issues that will close on merge
## List issues that close on merge
Get all the issues that would be closed by merging the provided merge request.
@ -1731,8 +1719,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/closes_issues
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/76/merge_requests/1/closes_issues"
@ -1785,7 +1773,7 @@ Example response when the GitLab issue tracker is used:
]
```
Example response when an external issue tracker (e.g. Jira) is used:
Example response when an external issue tracker (for example, Jira) is used:
```json
[
@ -1799,7 +1787,7 @@ Example response when an external issue tracker (e.g. Jira) is used:
## Subscribe to a merge request
Subscribes the authenticated user to a merge request to receive notification. If the user is already subscribed to the merge request, the
status code `304` is returned.
status code `HTTP 304 Not Modified` is returned.
```plaintext
POST /projects/:id/merge_requests/:merge_request_iid/subscribe
@ -1807,8 +1795,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/subscribe
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/17/subscribe"
@ -1934,7 +1922,7 @@ Example response:
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -1950,7 +1938,7 @@ the `approvals_before_merge` parameter:
Unsubscribes the authenticated user from a merge request to not receive
notifications from that merge request. If the user is
not subscribed to the merge request, the status code `304` is returned.
not subscribed to the merge request, the status code `HTTP 304 Not Modified` is returned.
```plaintext
POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe
@ -1958,8 +1946,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/17/unsubscribe"
@ -2085,7 +2073,7 @@ Example response:
}
```
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) also see
the `approvals_before_merge` parameter:
```json
@ -2101,7 +2089,7 @@ the `approvals_before_merge` parameter:
Manually creates a to-do item for the current user on a merge request.
If there already exists a to-do item for the user on that merge request,
status code `304` is returned.
status code `HTTP 304 Not Modified` is returned.
```plaintext
POST /projects/:id/merge_requests/:merge_request_iid/todo
@ -2109,8 +2097,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/todo
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/27/todo"
@ -2226,8 +2214,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/versions
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | String | yes | The ID of the project |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | String | yes | The ID of the project. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions"
@ -2267,9 +2255,9 @@ GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | String | yes | The ID of the project |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `version_id` | integer | yes | The ID of the merge request diff version |
| `id` | String | yes | The ID of the project. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
| `version_id` | integer | yes | The ID of the merge request diff version. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions/1"
@ -2335,9 +2323,9 @@ POST /projects/:id/merge_requests/:merge_request_iid/time_estimate
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
| `duration` | string | yes | The duration in human format, such as `3h30m`. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_estimate?duration=3h30m"
@ -2364,8 +2352,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/reset_time_estimate
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_time_estimate"
@ -2384,7 +2372,7 @@ Example response:
## Add spent time for a merge request
Adds spent time for this merge request
Adds spent time for this merge request.
```plaintext
POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time
@ -2392,9 +2380,9 @@ POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
| `duration` | string | yes | The duration in human format, such as `3h30m` |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/add_spent_time?duration=1h"
@ -2421,8 +2409,8 @@ POST /projects/:id/merge_requests/:merge_request_iid/reset_spent_time
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_spent_time"
@ -2447,8 +2435,8 @@ GET /projects/:id/merge_requests/:merge_request_iid/time_stats
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `merge_request_iid` | integer | yes | The internal ID of the merge request |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_stats"

View File

@ -20,32 +20,32 @@ Examples are available in several forms. As a collection of:
## CI/CD examples
The following table lists examples with step-by-step tutorials that are contained in this section.
The following table lists examples with step-by-step tutorials that are contained in this section:
| Use case | Resource |
|:------------------------------|:---------------------------------------------------------------------------------------------------------------------------|
| Use case | Resource |
|:------------------------------|:---------|
| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](../../user/project/merge_requests/browser_performance_testing.md). |
| Load performance testing | [Load Performance Testing with the k6 container](../../user/project/merge_requests/load_performance_testing.md). |
| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
| Deployment with Dpl | [Using `dpl` as deployment tool](deployment/README.md). |
| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
| End-to-end testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). |
| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
| Deployment with Dpl | [Using `dpl` as deployment tool](deployment/README.md). |
| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example of deploying a static site. |
| Java with Spring Boot | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). |
| Java with Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). |
| PHP with PHPunit, atoum | [Testing PHP projects](php.md). |
| PHP with NPM, SCP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). |
| PHP with Laravel, Envoy | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
| Python on Heroku | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). |
| Ruby on Heroku | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). |
| Scala on Heroku | [Test and deploy a Scala application to Heroku](test-scala-application.md). |
| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
| End-to-end testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). |
| Java with Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). |
| Java with Spring Boot | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). |
| Load performance testing | [Load Performance Testing with the k6 container](../../user/project/merge_requests/load_performance_testing.md). |
| Multi project pipeline | [Build, test deploy using multi project pipeline](https://gitlab.com/gitlab-examples/upstream-project). |
| NPM with semantic-release | [Publish NPM packages to the GitLab Package Registry using semantic-release](semantic-release.md). |
| PHP with Laravel, Envoy | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
| PHP with NPM, SCP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). |
| PHP with PHPunit, atoum | [Testing PHP projects](php.md). |
| Parallel testing Ruby & JS | [GitLab CI/CD parallel jobs testing for Ruby & JavaScript projects](https://docs.knapsackpro.com/2019/how-to-run-parallel-jobs-for-rspec-tests-on-gitlab-ci-pipeline-and-speed-up-ruby-javascript-testing). |
| Secrets management with Vault | [Authenticating and Reading Secrets With Hashicorp Vault](authenticating-with-hashicorp-vault/index.md). |
| Multi project pipeline | [Build, test deploy using multi project pipeline](https://gitlab.com/gitlab-examples/upstream-project). |
| NPM with semantic-release | [Publish NPM packages to the GitLab Package Registry using semantic-release](semantic-release.md). |
| Python on Heroku | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). |
| Ruby on Heroku | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). |
| Scala on Heroku | [Test and deploy a Scala application to Heroku](test-scala-application.md). |
| Secrets management with Vault | [Authenticating and Reading Secrets With Hashicorp Vault](authenticating-with-hashicorp-vault/index.md). |
### Contributing examples
### How to contributing examples
Contributions are welcome! You can help your favorite programming
language users and GitLab by sending a merge request with a guide for that language.

View File

@ -3,9 +3,14 @@ stage: Verify
group: Continuous Integration
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
disqus_identifier: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html'
author: Fabio Busatto
author_gitlab: bikebilly
type: tutorial
date: 2017-08-15
---
<!-- vale off -->
# How to deploy Maven projects to Artifactory with GitLab CI/CD
## Introduction

View File

@ -2,9 +2,15 @@
stage: Release
group: Release
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
author: Dylan Griffith
author_gitlab: DylanGriffith
type: tutorial
date: 2018-06-07
description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD"
---
<!-- vale off -->
# Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD
## Introduction

View File

@ -2,9 +2,14 @@
stage: Verify
group: Continuous Integration
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
author: Ryan Hall
author_gitlab: blitzgren
type: tutorial
date: 2018-03-07
---
<!-- vale off -->
# DevOps and Game Dev with GitLab CI/CD
With advances in WebGL and WebSockets, browsers are extremely viable as game development

View File

@ -2,9 +2,15 @@
stage: Verify
group: Testing
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
author: Vincent Tunru
author_gitlab: Vinnl
type: tutorial
date: 2019-02-18
description: 'Confidence checking your entire app every time a new feature is added can quickly become repetitive. Learn how to automate it with GitLab CI/CD.'
---
<!-- vale off -->
# End-to-end testing with GitLab CI/CD and WebdriverIO
[Review Apps](../../review_apps/index.md) are great: for every merge request

View File

@ -2,9 +2,15 @@
stage: Verify
group: Continuous Integration
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
disqus_identifier: 'https://docs.gitlab.com/ee/articles/laravel_with_gitlab_and_envoy/index.html'
author: Mehran Rasulian
author_gitlab: mehranrasulian
type: tutorial
date: 2017-08-31
---
<!-- vale off -->
# Test and deploy Laravel applications with GitLab CI/CD and Envoy
## Introduction

View File

@ -2,9 +2,14 @@
stage: Verify
group: Continuous Integration
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
author: Alexandre S Hostert
author_gitlab: Hostert
type: tutorial
date: 2018-02-20
---
<!-- vale off -->
# Testing a Phoenix application with GitLab CI/CD
[Phoenix](https://www.phoenixframework.org/) is a web development framework written in [Elixir](https://elixir-lang.org), which is a

View File

@ -743,6 +743,8 @@ available in the Admin UI.
#### Releasing the feature
1. In `ee/config/feature_flags/development/geo_widget_replication.yml`, set `default_enabled: true`
1. In `ee/app/replicators/geo/widget_replicator.rb`, delete the `self.replication_enabled_by_default?` method:
```ruby
@ -770,3 +772,260 @@ available in the Admin UI.
description: 'Find widget registries on this Geo node',
feature_flag: :geo_widget_replication # REMOVE THIS LINE
```
### Repository Replicator Strategy
Models that refer to any repository on the disk
can be easily supported by Geo with the `Geo::RepositoryReplicatorStrategy` module.
For example, to add support for files referenced by a `Gizmos` model with a
`gizmos` table, you would perform the following steps.
#### Replication
1. Include `Gitlab::Geo::ReplicableModel` in the `Gizmo` class, and specify
the Replicator class `with_replicator Geo::GizmoReplicator`.
At this point the `Gizmo` class should look like this:
```ruby
# frozen_string_literal: true
class Gizmo < ApplicationRecord
include ::Gitlab::Geo::ReplicableModel
with_replicator Geo::GizmoReplicator
# @param primary_key_in [Range, Gizmo] arg to pass to primary_key_in scope
# @return [ActiveRecord::Relation<Gizmo>] everything that should be synced to this node, restricted by primary key
def self.replicables_for_current_secondary(primary_key_in)
# Should be implemented. The idea of the method is to restrict
# the set of synced items depending on synchronization settings
end
# Geo checks this method in FrameworkRepositorySyncService to avoid
# snapshotting repositories using object pools
def pool_repository
nil
end
...
end
```
Pay some attention to method `pool_repository`. Not every repository type uses
repository pooling. As Geo prefers to use repository snapshotting, it can lead to data loss.
Make sure to overwrite `pool_repository` so it returns nil for repositories that do not
have pools.
If there is a common constraint for records to be available for replication,
make sure to also overwrite the `available_replicables` scope.
1. Create `ee/app/replicators/geo/gizmo_replicator.rb`. Implement the
`#repository` method which should return a `<Repository>` instance,
and implement the class method `.model` to return the `Gizmo` class:
```ruby
# frozen_string_literal: true
module Geo
class GizmoReplicator < Gitlab::Geo::Replicator
include ::Geo::RepositoryReplicatorStrategy
def self.model
::Gizmo
end
def repository
model_record.repository
end
def self.git_access_class
::Gitlab::GitAccessGizmo
end
# The feature flag follows the format `geo_#{replicable_name}_replication`,
# so here it would be `geo_gizmo_replication`
def self.replication_enabled_by_default?
false
end
end
end
```
1. Generate the feature flag definition file by running the feature flag command
and running through the steps:
```shell
bin/feature-flag --ee geo_gizmo_replication --type development --group 'group::geo'
```
1. Make sure Geo push events are created. Usually it needs some
change in the `app/workers/post_receive.rb` file. Example:
```ruby
def replicate_gizmo_changes(gizmo)
if ::Gitlab::Geo.primary?
gizmo.replicator.handle_after_update if gizmo
end
end
```
See `app/workers/post_receive.rb` for more examples.
1. Make sure the repository removal is also handled. You may need to add something
like the following in the destroy service of the repository:
```ruby
gizmo.replicator.handle_after_destroy if gizmo.repository
```
1. Add this replicator class to the method `replicator_classes` in
`ee/lib/gitlab/geo.rb`:
```ruby
REPLICATOR_CLASSES = [
...
::Geo::PackageFileReplicator,
::Geo::GizmoReplicator
]
end
```
1. Create `ee/spec/replicators/geo/gizmo_replicator_spec.rb` and perform
the necessary setup to define the `model_record` variable for the shared
examples:
```ruby
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Geo::GizmoReplicator do
let(:model_record) { build(:gizmo) }
include_examples 'a repository replicator'
end
```
1. Create the `gizmo_registry` table, with columns ordered according to [our guidelines](../ordering_table_columns.md) so Geo secondaries can track the sync and
verification state of each Gizmo. This migration belongs in `ee/db/geo/migrate`:
```ruby
# frozen_string_literal: true
class CreateGizmoRegistry < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :gizmo_registry, id: :bigserial, force: :cascade do |t|
t.datetime_with_timezone :retry_at
t.datetime_with_timezone :last_synced_at
t.datetime_with_timezone :created_at, null: false
t.bigint :gizmo_id, null: false
t.integer :state, default: 0, null: false, limit: 2
t.integer :retry_count, default: 0, limit: 2
t.text :last_sync_failure
t.boolean :force_to_redownload
t.boolean :missing_on_primary
t.index :gizmo_id, name: :index_gizmo_registry_on_gizmo_id, unique: true
t.index :retry_at
t.index :state
end
add_text_limit :gizmo_registry, :last_sync_failure, 255
end
def down
drop_table :gizmo_registry
end
end
```
1. Create `ee/app/models/geo/gizmo_registry.rb`:
```ruby
# frozen_string_literal: true
class Geo::GizmoRegistry < Geo::BaseRegistry
include Geo::ReplicableRegistry
MODEL_CLASS = ::Gizmo
MODEL_FOREIGN_KEY = :gizmo_id
belongs_to :gizmo, class_name: 'Gizmo'
end
```
1. Update `REGISTRY_CLASSES` in `ee/app/workers/geo/secondary/registry_consistency_worker.rb`.
1. Add `gizmo_registry` to `ActiveSupport::Inflector.inflections` in `config/initializers_before_autoloader/000_inflections.rb`.
1. Create `ee/spec/factories/geo/gizmo_registry.rb`:
```ruby
# frozen_string_literal: true
FactoryBot.define do
factory :geo_gizmo_registry, class: 'Geo::GizmoRegistry' do
gizmo
state { Geo::GizmoRegistry.state_value(:pending) }
trait :synced do
state { Geo::GizmoRegistry.state_value(:synced) }
last_synced_at { 5.days.ago }
end
trait :failed do
state { Geo::GizmoRegistry.state_value(:failed) }
last_synced_at { 1.day.ago }
retry_count { 2 }
last_sync_failure { 'Random error' }
end
trait :started do
state { Geo::GizmoRegistry.state_value(:started) }
last_synced_at { 1.day.ago }
retry_count { 0 }
end
end
end
```
1. Create `ee/spec/models/geo/gizmo_registry_spec.rb`:
```ruby
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Geo::GizmoRegistry, :geo, type: :model do
let_it_be(:registry) { create(:geo_gizmo_registry) }
specify 'factory is valid' do
expect(registry).to be_valid
end
include_examples 'a Geo framework registry'
end
```
1. Make sure the newly added repository type can be accessed by a secondary.
You may need to make some changes to one of the Git access classes.
Gizmos should now be replicated by Geo.
#### Metrics
You need to make the same changes as for Blob Replicator Strategy.
You need to make the same changes for the [metrics as in the Blob Replicator Strategy](#metrics).
#### GraphQL API
You need to make the same changes for the GraphQL API [as in the Blob Replicator Strategy](#graphql-api).
#### Releasing the feature
You need to make the same changes for [releasing the feature as in the Blob Replicator Strategy](#releasing-the-feature).

View File

@ -167,14 +167,13 @@ original repository if you'd like.
### Download vs clone
To create a copy of a remote repository files on your computer, you can either
**download** or **clone** it. If you download it, you cannot sync it with the
To create a copy of a remote repository's files on your computer, you can either
**download** or **clone**. If you download, you cannot sync it with the
remote repository on GitLab.
On the other hand, by cloning a repository, you'll download a copy of its
files to your local computer, but preserve the Git connection with the remote
repository, so that you can work on the its files on your computer and then
upload the changes to GitLab.
Cloning a repository is the same as downloading, except it preserves the Git connection
with the remote repository. This allows you to modify the files locally and
upload the changes to the remote repository on GitLab.
### Pull and push

View File

@ -47,7 +47,7 @@ Conan version 1.20.5
### Install CMake
When you develop with C++ and Conan, you can select from many available
compilers. This example uses the CMake compiler.
compilers. This example uses the CMake build system generator.
To install CMake:

View File

@ -136,7 +136,7 @@ use these placeholders in the email:
You can customize the email display name. Emails sent from Service Desk have
this name in the `From` header. The default display name is `GitLab Support Bot`.
### Using custom email address **(CORE ONLY)**
### Using custom email address
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2201) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0.
> - It was [deployed behind a feature flag](../feature_flags.md), disabled by default.

View File

@ -16,7 +16,12 @@ module API
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
expose :upcoming_release?, as: :upcoming_release
expose :milestones, using: Entities::MilestoneWithStats, if: -> (release, _) { release.milestones.present? && can_read_milestone? }
expose :milestones,
using: Entities::MilestoneWithStats,
if: -> (release, _) { release.milestones.present? && can_read_milestone? } do |release, _|
release.milestones.order_by_dates_and_title
end
expose :commit_path, expose_nil: false
expose :tag_path, expose_nil: false

View File

@ -3,7 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
import AlertManagementStatus from '~/alert_management/components/alert_status.vue';
import updateAlertStatusMutation from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql';
import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
import Tracking from '~/tracking';
import mockAlerts from '../mocks/alerts.json';

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue';
import updateAlertStatusMutation from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql';
import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
import Tracking from '~/tracking';
import mockAlerts from '../../mocks/alerts.json';

View File

@ -49,7 +49,7 @@ describe('BalsamiqViewer', () => {
);
});
it('should call `renderFile` on request success', done => {
it('should call `renderFile` on request success', (done) => {
jest.spyOn(axios, 'get').mockReturnValue(requestSuccess);
jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
@ -61,7 +61,7 @@ describe('BalsamiqViewer', () => {
.catch(done.fail);
});
it('should not call `renderFile` on request failure', done => {
it('should not call `renderFile` on request failure', (done) => {
jest.spyOn(axios, 'get').mockReturnValue(Promise.reject());
jest.spyOn(bv, 'renderFile').mockImplementation(() => {});
@ -95,8 +95,8 @@ describe('BalsamiqViewer', () => {
balsamiqViewer.viewer = viewer;
balsamiqViewer.getPreviews.mockReturnValue(previews);
balsamiqViewer.renderPreview.mockImplementation(preview => preview);
viewer.appendChild.mockImplementation(containerElement => {
balsamiqViewer.renderPreview.mockImplementation((preview) => preview);
viewer.appendChild.mockImplementation((containerElement) => {
container = containerElement;
});
@ -177,7 +177,9 @@ describe('BalsamiqViewer', () => {
database,
};
jest.spyOn(BalsamiqViewer, 'parsePreview').mockImplementation(preview => preview.toString());
jest
.spyOn(BalsamiqViewer, 'parsePreview')
.mockImplementation((preview) => preview.toString());
database.exec.mockReturnValue(thumbnails);
getPreviews = BalsamiqViewer.prototype.getPreviews.call(balsamiqViewer);

View File

@ -10,8 +10,8 @@ import createFlash from '~/flash';
jest.mock('~/flash');
const TEST_LABELS_PAYLOAD = TEST_LABELS.map(label => ({ ...label, set: true }));
const TEST_LABELS_TITLES = TEST_LABELS.map(label => label.title);
const TEST_LABELS_PAYLOAD = TEST_LABELS.map((label) => ({ ...label, set: true }));
const TEST_LABELS_TITLES = TEST_LABELS.map((label) => label.title);
describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
let wrapper;
@ -44,7 +44,8 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
};
const findLabelsSelect = () => wrapper.find({ ref: 'labelsSelect' });
const findLabelsTitles = () => wrapper.findAll(GlLabel).wrappers.map(item => item.props('title'));
const findLabelsTitles = () =>
wrapper.findAll(GlLabel).wrappers.map((item) => item.props('title'));
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
it('renders "None" when no labels are selected', () => {
@ -76,7 +77,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({
addLabelIds: TEST_LABELS.map(label => label.id),
addLabelIds: TEST_LABELS.map((label) => label.id),
projectPath: 'gitlab-org/test-subgroup/gitlab-test',
removeLabelIds: [],
});

View File

@ -237,19 +237,22 @@ describe('Clusters Store', () => {
});
});
describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
describe.each(APPLICATION_INSTALLED_STATUSES)(
'given the current app status is %s',
(status) => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
mockResponseData.applications[runnerAppIndex].status = status;
mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData);
store.updateStateFromServer(mockResponseData);
expect(store.state.applications[RUNNER].installed).toBe(true);
});
});
expect(store.state.applications[RUNNER].installed).toBe(true);
});
},
);
it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData =

View File

@ -62,7 +62,7 @@ describe('DiffFileHeader component', () => {
diffHasDiscussionsResultMock,
diffHasExpandedDiscussionsResultMock,
...Object.values(mockStoreConfig.modules.diffs.actions),
].forEach(mock => mock.mockReset());
].forEach((mock) => mock.mockReset());
wrapper.destroy();
});
@ -80,7 +80,7 @@ describe('DiffFileHeader component', () => {
const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
const findEditButton = () => wrapper.find({ ref: 'editButton' });
const createComponent = props => {
const createComponent = (props) => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
const store = new Vuex.Store(mockStoreConfig);
@ -219,7 +219,7 @@ describe('DiffFileHeader component', () => {
});
describe('for any file', () => {
const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed');
const otherModes = Object.keys(diffViewerModes).filter((m) => m !== 'mode_changed');
it('for mode_changed file mode displays mode changes', () => {
createComponent({
@ -236,20 +236,23 @@ describe('DiffFileHeader component', () => {
expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
});
it.each(otherModes.map(m => [m]))('for %s file mode does not display mode changes', mode => {
createComponent({
diffFile: {
...diffFile,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
...diffFile.viewer,
name: diffViewerModes[mode],
it.each(otherModes.map((m) => [m]))(
'for %s file mode does not display mode changes',
(mode) => {
createComponent({
diffFile: {
...diffFile,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
...diffFile.viewer,
name: diffViewerModes[mode],
},
},
},
});
expect(findModeChangedLine().exists()).toBeFalsy();
});
});
expect(findModeChangedLine().exists()).toBeFalsy();
},
);
it('displays the LFS label for files stored in LFS', () => {
createComponent({

View File

@ -26,13 +26,15 @@ describe('HiddenFilesWarning', () => {
});
it('has a correct plain diff URL', () => {
const plainDiffLink = wrapper.findAll('a').wrappers.filter(x => x.text() === 'Plain diff')[0];
const plainDiffLink = wrapper.findAll('a').wrappers.filter((x) => x.text() === 'Plain diff')[0];
expect(plainDiffLink.attributes('href')).toBe(propsData.plainDiffPath);
});
it('has a correct email patch URL', () => {
const emailPatchLink = wrapper.findAll('a').wrappers.filter(x => x.text() === 'Email patch')[0];
const emailPatchLink = wrapper
.findAll('a')
.wrappers.filter((x) => x.text() === 'Email patch')[0];
expect(emailPatchLink.attributes('href')).toBe(propsData.emailPatchPath);
});

View File

@ -16,7 +16,8 @@ describe('Monitoring Component', () => {
};
const findButtons = () => wrapper.findAll(GlButton);
const findButtonsByIcon = icon => findButtons().filter(button => button.props('icon') === icon);
const findButtonsByIcon = (icon) =>
findButtons().filter((button) => button.props('icon') === icon);
beforeEach(() => {
createWrapper();

View File

@ -23,7 +23,7 @@ describe('Bulk import status poller', () => {
let clientMock;
const listQueryCacheCalls = () =>
clientMock.readQuery.mock.calls.filter(call => call[0].query === bulkImportSourceGroupsQuery);
clientMock.readQuery.mock.calls.filter((call) => call[0].query === bulkImportSourceGroupsQuery);
beforeEach(() => {
clientMock = createMockClient({
@ -142,9 +142,11 @@ describe('Bulk import status poller', () => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STARTED_GROUP_1, NOT_STARTED_GROUP, STARTED_GROUP_2].map(group =>
generateFakeEntry(group),
),
bulkImportSourceGroups: [
STARTED_GROUP_1,
NOT_STARTED_GROUP,
STARTED_GROUP_2,
].map((group) => generateFakeEntry(group)),
},
});
@ -155,9 +157,9 @@ describe('Bulk import status poller', () => {
await waitForPromises();
const [[doc]] = clientMock.query.mock.calls;
const { selections } = doc.query.definitions[0].selectionSet;
expect(selections.every(field => field.name.value === 'group')).toBeTruthy();
expect(selections.every((field) => field.name.value === 'group')).toBeTruthy();
expect(selections).toHaveLength(2);
expect(selections.map(sel => sel.arguments[0].value.value)).toStrictEqual([
expect(selections.map((sel) => sel.arguments[0].value.value)).toStrictEqual([
`${TARGET_NAMESPACE}/${STARTED_GROUP_1.import_target.new_name}`,
`${TARGET_NAMESPACE}/${STARTED_GROUP_2.import_target.new_name}`,
]);
@ -167,7 +169,7 @@ describe('Bulk import status poller', () => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map(group =>
bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) =>
generateFakeEntry(group),
),
},
@ -189,7 +191,7 @@ describe('Bulk import status poller', () => {
clientMock.cache.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map(group =>
bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) =>
generateFakeEntry(group),
),
},

View File

@ -22,13 +22,14 @@ const issuable2 = {
const pathIdSeparator = PathIdSeparator.Issue;
const findFormInput = wrapper => wrapper.find('.js-add-issuable-form-input').element;
const findFormInput = (wrapper) => wrapper.find('.js-add-issuable-form-input').element;
const findRadioInput = (inputs, value) => inputs.filter(input => input.element.value === value)[0];
const findRadioInput = (inputs, value) =>
inputs.filter((input) => input.element.value === value)[0];
const findRadioInputs = wrapper => wrapper.findAll('[name="linked-issue-type-radio"]');
const findRadioInputs = (wrapper) => wrapper.findAll('[name="linked-issue-type-radio"]');
const constructWrapper = props => {
const constructWrapper = (props) => {
return shallowMount(AddIssuableForm, {
propsData: {
inputValue: '',
@ -192,7 +193,7 @@ describe('AddIssuableForm', () => {
});
describe('when the form is submitted', () => {
it('emits an event with a "relates_to" link type when the "relates to" radio input selected', done => {
it('emits an event with a "relates_to" link type when the "relates to" radio input selected', (done) => {
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO;
@ -207,7 +208,7 @@ describe('AddIssuableForm', () => {
});
});
it('emits an event with a "blocks" link type when the "blocks" radio input selected', done => {
it('emits an event with a "blocks" link type when the "blocks" radio input selected', (done) => {
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS;
@ -222,7 +223,7 @@ describe('AddIssuableForm', () => {
});
});
it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', done => {
it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', (done) => {
jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY;
@ -237,7 +238,7 @@ describe('AddIssuableForm', () => {
});
});
it('shows error message when error is present', done => {
it('shows error message when error is present', (done) => {
const itemAddFailureMessage = 'Something went wrong while submitting.';
wrapper.setProps({
hasError: true,

View File

@ -25,7 +25,8 @@ describe('DiscussionFilter component', () => {
const filterDiscussion = jest.fn();
const findFilter = filterType => wrapper.find(`.dropdown-item[data-filter-type="${filterType}"]`);
const findFilter = (filterType) =>
wrapper.find(`.dropdown-item[data-filter-type="${filterType}"]`);
const mountComponent = () => {
const discussions = [
@ -145,7 +146,7 @@ describe('DiscussionFilter component', () => {
window.mrTabs = undefined;
});
it('only renders when discussion tab is active', done => {
it('only renders when discussion tab is active', (done) => {
eventHub.$emit('MergeRequestTabChange', 'commit');
wrapper.vm.$nextTick(() => {
@ -160,7 +161,7 @@ describe('DiscussionFilter component', () => {
window.location.hash = '';
});
it('updates the filter when the URL links to a note', done => {
it('updates the filter when the URL links to a note', (done) => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
wrapper.vm.currentValue = discussionFiltersMock[2].value;
wrapper.vm.handleLocationHash();
@ -171,7 +172,7 @@ describe('DiscussionFilter component', () => {
});
});
it('does not update the filter when the current filter is "Show all activity"', done => {
it('does not update the filter when the current filter is "Show all activity"', (done) => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
wrapper.vm.handleLocationHash();
@ -181,7 +182,7 @@ describe('DiscussionFilter component', () => {
});
});
it('only updates filter when the URL links to a note', done => {
it('only updates filter when the URL links to a note', (done) => {
window.location.hash = `testing123`;
wrapper.vm.handleLocationHash();
@ -191,7 +192,7 @@ describe('DiscussionFilter component', () => {
});
});
it('fetches discussions when there is a hash', done => {
it('fetches discussions when there is a hash', (done) => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
wrapper.vm.currentValue = discussionFiltersMock[2].value;
jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
@ -203,7 +204,7 @@ describe('DiscussionFilter component', () => {
});
});
it('does not fetch discussions when there is no hash', done => {
it('does not fetch discussions when there is no hash', (done) => {
window.location.hash = '';
jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
wrapper.vm.handleLocationHash();

View File

@ -25,8 +25,9 @@ describe('CI Lint Results', () => {
};
const findTable = () => wrapper.find(GlTable);
const findByTestId = selector => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`);
const findAllByTestId = selector => () => wrapper.findAll(`[data-testid="ci-lint-${selector}"]`);
const findByTestId = (selector) => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`);
const findAllByTestId = (selector) => () =>
wrapper.findAll(`[data-testid="ci-lint-${selector}"]`);
const findLinkToDoc = () => wrapper.find(GlLink);
const findErrors = findByTestId('errors');
const findWarnings = findByTestId('warnings');
@ -37,7 +38,7 @@ describe('CI Lint Results', () => {
const findBeforeScripts = findAllByTestId('before-script');
const findScripts = findAllByTestId('script');
const findAfterScripts = findAllByTestId('after-script');
const filterEmptyScripts = property => mockJobs.filter(job => job[property].length !== 0);
const filterEmptyScripts = (property) => mockJobs.filter((job) => job[property].length !== 0);
afterEach(() => {
wrapper.destroy();

View File

@ -185,7 +185,9 @@ describe('ServiceDeskSetting', () => {
const expectedTemplates = [''].concat(templates);
const dropdown = findTemplateDropdown();
const dropdownList = Array.from(dropdown.element.children).map(option => option.innerText);
const dropdownList = Array.from(dropdown.element.children).map(
(option) => option.innerText,
);
expect(dropdown.element.children).toHaveLength(expectedTemplates.length);
expect(dropdownList.includes('Bug')).toEqual(true);

View File

@ -41,9 +41,12 @@ describe('registry_header', () => {
describe('header', () => {
it('has a title', () => {
mountComponent();
mountComponent({ metadataLoading: true });
expect(findTitleArea().props('title')).toBe(CONTAINER_REGISTRY_TITLE);
expect(findTitleArea().props()).toMatchObject({
title: CONTAINER_REGISTRY_TITLE,
metadataLoading: true,
});
});
it('has a commands slot', () => {

View File

@ -116,6 +116,7 @@ describe('List Page', () => {
expect(findRegistryHeader().exists()).toBe(true);
expect(findRegistryHeader().props()).toMatchObject({
imagesCount: 2,
metadataLoading: false,
});
});
@ -170,6 +171,12 @@ describe('List Page', () => {
expect(findCliCommands().exists()).toBe(false);
});
it('title has the metadataLoading props set to true', () => {
mountComponent();
expect(findRegistryHeader().props('metadataLoading')).toBe(true);
});
});
describe('list is empty', () => {

View File

@ -88,18 +88,6 @@ Object {
},
],
"milestones": Array [
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
Object {
"description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123",
@ -112,6 +100,18 @@ Object {
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/1",
},
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
],
"name": "The first release",
"releasedAt": "2018-12-10T00:00:00Z",
@ -216,18 +216,6 @@ Object {
},
],
"milestones": Array [
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
Object {
"description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123",
@ -240,6 +228,18 @@ Object {
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/1",
},
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
],
"name": "The first release",
"releasedAt": "2018-12-10T00:00:00Z",

View File

@ -54,17 +54,7 @@ describe('Release block milestone info', () => {
});
it('renders a list of links to all associated milestones', () => {
// The API currently returns the milestones in a non-deterministic order,
// which causes the frontend fixture used by this test to return the
// milestones in one order locally and a different order in the CI pipeline.
// This is a bug and is tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/259012
// When this bug is fixed this expectation should be updated to
// assert the expected order.
const containerText = trimText(milestoneListContainer().text());
expect(
containerText.includes('Milestones 12.4 • 12.3') ||
containerText.includes('Milestones 12.3 • 12.4'),
).toBe(true);
expect(milestoneListContainer().text()).toMatchInterpolatedText('Milestones 12.3 • 12.4');
milestones.forEach((m, i) => {
const milestoneLink = milestoneListContainer().findAll(GlLink).at(i);

View File

@ -149,9 +149,11 @@ describe('Snippet Edit app', () => {
const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
const triggerBlobActions = actions => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = paths => {
wrapper.vm.$el.innerHTML = paths.map(path => `<input name="files[]" value="${path}">`).join('');
const triggerBlobActions = (actions) => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = (paths) => {
wrapper.vm.$el.innerHTML = paths
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
};
const getApiData = ({
id,
@ -189,7 +191,7 @@ describe('Snippet Edit app', () => {
it.each([[{}], [{ snippetGid: '' }]])(
'should render all required components with %s',
props => {
(props) => {
createComponent(props);
expect(wrapper.find(TitleField).exists()).toBe(true);
@ -257,7 +259,7 @@ describe('Snippet Edit app', () => {
describe('default visibility', () => {
it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
'marks %s visibility by default',
async visibility => {
async (visibility) => {
createComponent({
props: { snippetGid: '' },
selectedLevel: visibility,

View File

@ -57,7 +57,7 @@ describe('User Lists Show Mutations', () => {
});
it('adds the new IDs to the state unless empty', () => {
newIds.filter(id => id).forEach(id => expect(mockState.userIds).toContain(id));
newIds.filter((id) => id).forEach((id) => expect(mockState.userIds).toContain(id));
});
it('does not add duplicate IDs to the state', () => {
@ -80,7 +80,9 @@ describe('User Lists Show Mutations', () => {
});
it('should leave the rest of the IDs alone', () => {
userIds.filter(id => id !== removedId).forEach(id => expect(mockState.userIds).toContain(id));
userIds
.filter((id) => id !== removedId)
.forEach((id) => expect(mockState.userIds).toContain(id));
});
});
});

View File

@ -1,4 +1,4 @@
import { GlAvatar, GlSprintf, GlLink } from '@gitlab/ui';
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import component from '~/vue_shared/components/registry/title_area.vue';
@ -15,6 +15,7 @@ describe('title area', () => {
const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`);
const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
wrapper = shallowMount(component, {
@ -100,6 +101,29 @@ describe('title area', () => {
expect(findMetadataSlot(name).exists()).toBe(true);
});
});
it('is/are hidden when metadata-loading is true', async () => {
mountComponent({ slots: slotMocks, propsData: { title: 'foo', metadataLoading: true } });
await wrapper.vm.$nextTick();
slotNames.forEach(name => {
expect(findMetadataSlot(name).exists()).toBe(false);
});
});
});
describe('metadata skeleton loader', () => {
it('is hidden when metadata loading is false', () => {
mountComponent();
expect(findSkeletonLoader().exists()).toBe(false);
});
it('is shown when metadata loading is true', () => {
mountComponent({ propsData: { metadataLoading: true } });
expect(findSkeletonLoader().exists()).toBe(true);
});
});
describe('dynamic slots', () => {

View File

@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
const createComponent = config => {
const createComponent = (config) => {
const Component = Vue.extend(stackedProgressBarComponent);
const defaultConfig = {
successLabel: 'Synced',
@ -29,11 +29,12 @@ describe('StackedProgressBarComponent', () => {
vm.$destroy();
});
const findSuccessBarText = wrapper => wrapper.$el.querySelector('.status-green').innerText.trim();
const findNeutralBarText = wrapper =>
const findSuccessBarText = (wrapper) =>
wrapper.$el.querySelector('.status-green').innerText.trim();
const findNeutralBarText = (wrapper) =>
wrapper.$el.querySelector('.status-neutral').innerText.trim();
const findFailureBarText = wrapper => wrapper.$el.querySelector('.status-red').innerText.trim();
const findUnavailableBarText = wrapper =>
const findFailureBarText = (wrapper) => wrapper.$el.querySelector('.status-red').innerText.trim();
const findUnavailableBarText = (wrapper) =>
wrapper.$el.querySelector('.status-unavailable').innerText.trim();
describe('computed', () => {

View File

@ -8,11 +8,11 @@ import {
findByText,
} from '@testing-library/dom';
const isFolderRowOpen = row => row.matches('.folder.is-open');
const isFolderRowOpen = (row) => row.matches('.folder.is-open');
const getLeftSidebar = () => screen.getByTestId('left-sidebar');
export const switchLeftSidebarTab = name => {
export const switchLeftSidebarTab = (name) => {
const sidebar = getLeftSidebar();
const button = getByLabelText(sidebar, name);
@ -23,7 +23,7 @@ export const switchLeftSidebarTab = name => {
export const getStatusBar = () => document.querySelector('.ide-status-bar');
export const waitForMonacoEditor = () =>
new Promise(resolve => window.monaco.editor.onDidCreateEditor(resolve));
new Promise((resolve) => window.monaco.editor.onDidCreateEditor(resolve));
export const findMonacoEditor = () =>
screen.findAllByLabelText(/Editor content;/).then(([x]) => x.closest('.monaco-editor'));
@ -31,7 +31,7 @@ export const findMonacoEditor = () =>
export const findMonacoDiffEditor = () =>
screen.findAllByLabelText(/Editor content;/).then(([x]) => x.closest('.monaco-diff-editor'));
export const findAndSetEditorValue = async value => {
export const findAndSetEditorValue = async (value) => {
const editor = await findMonacoEditor();
const uri = editor.getAttribute('data-uri');
@ -56,10 +56,12 @@ const findFileChild = async (row, name, index = 0) => {
const container = await findFileRowContainer(row);
const children = await findAllByText(container, name, { selector: '.file-row-name' });
return children.map(x => x.closest('.file-row')).find(x => x.dataset.level === index.toString());
return children
.map((x) => x.closest('.file-row'))
.find((x) => x.dataset.level === index.toString());
};
const openFileRow = row => {
const openFileRow = (row) => {
if (!row || isFolderRowOpen(row)) {
return;
}
@ -101,7 +103,7 @@ const fillFileNameModal = async (value, submitText = 'Create file') => {
createButton.click();
};
const findAndClickRootAction = async name => {
const findAndClickRootAction = async (name) => {
const container = await findRootActions();
const button = getByLabelText(container, name);
@ -112,13 +114,13 @@ export const clickPreviewMarkdown = () => {
screen.getByText('Preview Markdown').click();
};
export const openFile = async path => {
export const openFile = async (path) => {
const row = await findAndTraverseToPath(path);
openFileRow(row);
};
export const waitForTabToOpen = fileName =>
export const waitForTabToOpen = (fileName) =>
findByText(document.querySelector('.multi-file-edit-pane'), fileName);
export const createFile = async (path, content) => {
@ -137,10 +139,10 @@ export const createFile = async (path, content) => {
};
export const getFilesList = () => {
return screen.getAllByTestId('file-row-name-container').map(e => e.textContent.trim());
return screen.getAllByTestId('file-row-name-container').map((e) => e.textContent.trim());
};
export const deleteFile = async path => {
export const deleteFile = async (path) => {
const row = await findAndTraverseToPath(path);
clickFileRowAction(row, 'Delete');
};
@ -152,7 +154,7 @@ export const renameFile = async (path, newPath) => {
await fillFileNameModal(newPath, 'Rename file');
};
export const closeFile = async path => {
export const closeFile = async (path) => {
const button = await screen.getByLabelText(`Close ${path}`, {
selector: '.multi-file-tabs button',
});
@ -164,7 +166,7 @@ export const commit = async () => {
switchLeftSidebarTab('Commit');
screen.getByTestId('begin-commit-button').click();
await screen.findByLabelText(/Commit to .+ branch/).then(x => x.click());
await screen.findByLabelText(/Commit to .+ branch/).then((x) => x.click());
screen.getByText('Commit').click();
};

View File

@ -88,12 +88,9 @@ RSpec.describe Mutations::Releases::Create do
it 'creates the release with the correct milestone associations' do
expected_milestone_titles = [milestone_12_3.title, milestone_12_4.title]
actual_milestone_titles = new_release.milestones.map { |m| m.title }
actual_milestone_titles = new_release.milestones.order_by_dates_and_title.map { |m| m.title }
# Right now the milestones are returned in a non-deterministic order.
# `match_array` should be updated to `eq` once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(actual_milestone_titles).to match_array(expected_milestone_titles)
expect(actual_milestone_titles).to eq(expected_milestone_titles)
end
describe 'asset links' do

View File

@ -48,12 +48,7 @@ RSpec.describe Mutations::Releases::Update do
expect(updated_release.name).to eq(name) unless except_for == :name
expect(updated_release.description).to eq(description) unless except_for == :description
expect(updated_release.released_at).to eq(released_at) unless except_for == :released_at
# Right now the milestones are returned in a non-deterministic order.
# Because of this, we need to allow for milestones to be returned in any order.
# Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
# fixed, this can be updated to expect a specific order.
expect(updated_release.milestones).to match_array([milestone_12_3, milestone_12_4]) unless except_for == :milestones
expect(updated_release.milestones.order_by_dates_and_title).to eq([milestone_12_3, milestone_12_4]) unless except_for == :milestones
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::ReleaseMilestonesResolver do
include GraphqlHelpers
let_it_be(:release) { create(:release, :with_milestones, milestones_count: 2) }
let(:resolved) do
resolve(described_class, obj: release)
end
describe '#resolve' do
it "returns an OffsetActiveRecordRelationConnection" do
expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
end
it "includes the release's milestones in the returned OffsetActiveRecordRelationConnection" do
expect(resolved.items).to eq(release.milestones.order_by_dates_and_title)
end
end
end

View File

@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Release do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:release) { create(:release, project: project, author: user) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:release) { create(:release, project: project, author: user) }
it { expect(release).to be_valid }
@ -132,8 +132,10 @@ RSpec.describe Release do
end
describe '#milestone_titles' do
let(:release) { create(:release, :with_milestones) }
let_it_be(:milestone_1) { create(:milestone, project: project, title: 'Milestone 1') }
let_it_be(:milestone_2) { create(:milestone, project: project, title: 'Milestone 2') }
let_it_be(:release) { create(:release, project: project, milestones: [milestone_1, milestone_2]) }
it { expect(release.milestone_titles).to eq(release.milestones.map {|m| m.title }.sort.join(", "))}
it { expect(release.milestone_titles).to eq("#{milestone_1.title}, #{milestone_2.title}")}
end
end

View File

@ -116,11 +116,9 @@ RSpec.describe 'Creation of a new release' do
context 'when all available mutation arguments are provided' do
it_behaves_like 'no errors'
# rubocop: disable CodeReuse/ActiveRecord
it 'returns the new release data' do
create_release
release = mutation_response[:release]
expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}"
expected_attributes = {
@ -139,21 +137,17 @@ RSpec.describe 'Creation of a new release' do
directAssetUrl: expected_direct_asset_url
}]
}
},
milestones: {
nodes: [
{ title: '12.3' },
{ title: '12.4' }
]
}
}
}.with_indifferent_access
expect(release).to include(expected_attributes)
# Right now the milestones are returned in a non-deterministic order.
# This `milestones` test should be moved up into the expect(release)
# above (and `.to include` updated to `.to eq`) once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(release['milestones']['nodes']).to match_array([
{ 'title' => '12.4' },
{ 'title' => '12.3' }
])
expect(mutation_response[:release]).to eq(expected_attributes)
end
# rubocop: enable CodeReuse/ActiveRecord
end
context 'when only the required mutation arguments are provided' do

View File

@ -116,15 +116,7 @@ RSpec.describe 'Updating an existing release' do
it 'updates the correct field and returns the release' do
update_release
expect(mutation_response[:release]).to include(expected_attributes.merge(updates).except(:milestones))
# Right now the milestones are returned in a non-deterministic order.
# Because of this, we need to test milestones separately to allow
# for them to be returned in any order.
# Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
# fixed, this special milestone handling can be removed.
expected_milestones = expected_attributes.merge(updates)[:milestones]
expect(mutation_response[:release][:milestones][:nodes]).to match_array(expected_milestones[:nodes])
expect(mutation_response[:release]).to eq(expected_attributes.merge(updates))
end
end

View File

@ -76,11 +76,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
it 'finds all milestones associated to a release' do
post_query
expected = release.milestones.map do |milestone|
expected = release.milestones.order_by_dates_and_title.map do |milestone|
{ 'id' => global_id_of(milestone), 'title' => milestone.title }
end
expect(data).to match_array(expected)
expect(data).to eq(expected)
end
end
@ -427,4 +427,33 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
end
end
end
describe 'milestone order' do
let(:path) { path_prefix }
let(:current_user) { stranger }
let_it_be(:project) { create(:project, :public) }
let_it_be_with_reload(:release) { create(:release, project: project) }
let(:release_fields) do
query_graphql_field(%{
milestones {
nodes {
title
}
}
})
end
let(:actual_milestone_title_order) do
post_query
data.dig('milestones', 'nodes').map { |m| m['title'] }
end
before do
release.update!(milestones: [milestone_2, milestone_1])
end
it_behaves_like 'correct release milestone order'
end
end

View File

@ -16,9 +16,6 @@ RSpec.describe API::Releases do
project.add_reporter(reporter)
project.add_guest(guest)
project.add_developer(developer)
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
end
describe 'GET /projects/:id/releases' do
@ -294,6 +291,25 @@ RSpec.describe API::Releases do
end
end
context 'when release is associated to mutiple milestones' do
context 'milestones order' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be_with_reload(:release_with_milestones) { create(:release, tag: 'v3.14', project: project) }
let(:actual_milestone_title_order) do
get api("/projects/#{project.id}/releases/#{release_with_milestones.tag}", non_project_member)
json_response['milestones'].map { |m| m['title'] }
end
before do
release_with_milestones.update!(milestones: [milestone_2, milestone_1])
end
it_behaves_like 'correct release milestone order'
end
end
context 'when release has link asset' do
let!(:link) do
create(:release_link,
@ -461,6 +477,10 @@ RSpec.describe API::Releases do
}
end
before do
initialize_tags
end
it 'accepts the request' do
post api("/projects/#{project.id}/releases", maintainer), params: params
@ -858,6 +878,10 @@ RSpec.describe API::Releases do
description: 'Super nice release')
end
before do
initialize_tags
end
it 'accepts the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
@ -1108,4 +1132,9 @@ RSpec.describe API::Releases do
end
end
end
def initialize_tags
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
end
end

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
RSpec.shared_examples 'correct release milestone order' do
let_it_be_with_reload(:milestone_1) { create(:milestone, project: project) }
let_it_be_with_reload(:milestone_2) { create(:milestone, project: project) }
shared_examples 'correct sort order' do
it 'sorts milestonee_1 before milestone_2' do
freeze_time do
expect(actual_milestone_title_order).to eq([milestone_1.title, milestone_2.title])
end
end
end
context 'due_date' do
before do
milestone_1.update!(due_date: Time.zone.now, start_date: 1.day.ago, title: 'z')
milestone_2.update!(due_date: 1.day.from_now, start_date: 2.days.ago, title: 'a')
end
context 'when both milestones have a due_date' do
it_behaves_like 'correct sort order'
end
context 'when one milestone does not have a due_date' do
before do
milestone_2.update!(due_date: nil)
end
it_behaves_like 'correct sort order'
end
end
context 'start_date' do
before do
milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z' )
milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a' )
end
context 'when both milestones have a start_date' do
let(:milestone_2_start_date) { Time.zone.now }
it_behaves_like 'correct sort order'
end
context 'when one milestone does not have a start_date' do
let(:milestone_2_start_date) { nil }
it_behaves_like 'correct sort order'
end
end
context 'title' do
before do
milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a' )
milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z' )
end
it_behaves_like 'correct sort order'
end
end