Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
39c1496527
commit
9b1b702f0f
2
Gemfile
2
Gemfile
|
@ -309,7 +309,7 @@ gem 'pg_query', '~> 1.3.0'
|
||||||
gem 'premailer-rails', '~> 1.10.3'
|
gem 'premailer-rails', '~> 1.10.3'
|
||||||
|
|
||||||
# LabKit: Tracing and Correlation
|
# LabKit: Tracing and Correlation
|
||||||
gem 'gitlab-labkit', '0.13.5'
|
gem 'gitlab-labkit', '0.14.0'
|
||||||
|
|
||||||
# I18n
|
# I18n
|
||||||
gem 'ruby_parser', '~> 3.15', require: false
|
gem 'ruby_parser', '~> 3.15', require: false
|
||||||
|
|
|
@ -432,9 +432,9 @@ GEM
|
||||||
fog-json (~> 1.2.0)
|
fog-json (~> 1.2.0)
|
||||||
mime-types
|
mime-types
|
||||||
ms_rest_azure (~> 0.12.0)
|
ms_rest_azure (~> 0.12.0)
|
||||||
gitlab-labkit (0.13.5)
|
gitlab-labkit (0.14.0)
|
||||||
actionpack (>= 5.0.0, < 6.1.0)
|
actionpack (>= 5.0.0, < 7.0.0)
|
||||||
activesupport (>= 5.0.0, < 6.1.0)
|
activesupport (>= 5.0.0, < 7.0.0)
|
||||||
gitlab-pg_query (~> 1.3)
|
gitlab-pg_query (~> 1.3)
|
||||||
grpc (~> 1.19)
|
grpc (~> 1.19)
|
||||||
jaeger-client (~> 1.1)
|
jaeger-client (~> 1.1)
|
||||||
|
@ -1363,7 +1363,7 @@ DEPENDENCIES
|
||||||
gitlab-chronic (~> 0.10.5)
|
gitlab-chronic (~> 0.10.5)
|
||||||
gitlab-experiment (~> 0.4.4)
|
gitlab-experiment (~> 0.4.4)
|
||||||
gitlab-fog-azure-rm (~> 1.0)
|
gitlab-fog-azure-rm (~> 1.0)
|
||||||
gitlab-labkit (= 0.13.5)
|
gitlab-labkit (= 0.14.0)
|
||||||
gitlab-license (~> 1.0)
|
gitlab-license (~> 1.0)
|
||||||
gitlab-mail_room (~> 0.0.8)
|
gitlab-mail_room (~> 0.0.8)
|
||||||
gitlab-markup (~> 1.7.1)
|
gitlab-markup (~> 1.7.1)
|
||||||
|
|
|
@ -66,6 +66,8 @@ export default class FileTemplateSelector {
|
||||||
reportSelectionName(options) {
|
reportSelectionName(options) {
|
||||||
const opts = options;
|
const opts = options;
|
||||||
opts.query = options.selectedObj.name;
|
opts.query = options.selectedObj.name;
|
||||||
|
opts.data = options.selectedObj;
|
||||||
|
opts.data.source_template_project_id = options.selectedObj.project_id;
|
||||||
|
|
||||||
this.reportSelection(opts);
|
this.reportSelection(opts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
|
||||||
const data = {
|
const data = {
|
||||||
project: this.$dropdown.data('project'),
|
project: this.$dropdown.data('project'),
|
||||||
fullname: this.$dropdown.data('fullname'),
|
fullname: this.$dropdown.data('fullname'),
|
||||||
|
source_template_project_id: query.project_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.reportSelection({
|
this.reportSelection({
|
||||||
|
|
|
@ -4,7 +4,7 @@ import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
|
||||||
import boardsStore from '../stores/boards_store';
|
import boardsStore from '../stores/boards_store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BoardsIssueCard',
|
name: 'BoardCardLayout',
|
||||||
components: {
|
components: {
|
||||||
IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
|
IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
|
||||||
},
|
},
|
||||||
|
@ -81,7 +81,7 @@ export default {
|
||||||
:data-issue-iid="issue.iid"
|
:data-issue-iid="issue.iid"
|
||||||
:data-issue-path="issue.referencePath"
|
:data-issue-path="issue.referencePath"
|
||||||
data-testid="board_card"
|
data-testid="board_card"
|
||||||
class="board-card p-3 rounded"
|
class="board-card gl-p-5 gl-rounded-base"
|
||||||
@mousedown="mouseDown"
|
@mousedown="mouseDown"
|
||||||
@mousemove="mouseMove"
|
@mousemove="mouseMove"
|
||||||
@mouseup="showIssue($event)"
|
@mouseup="showIssue($event)"
|
||||||
|
|
|
@ -437,6 +437,7 @@ export class GitLabDropdown {
|
||||||
groupName = el.data('group');
|
groupName = el.data('group');
|
||||||
if (groupName) {
|
if (groupName) {
|
||||||
selectedIndex = el.data('index');
|
selectedIndex = el.data('index');
|
||||||
|
this.selectedIndex = selectedIndex;
|
||||||
selectedObject = this.renderedData[groupName][selectedIndex];
|
selectedObject = this.renderedData[groupName][selectedIndex];
|
||||||
} else {
|
} else {
|
||||||
selectedIndex = el.closest('li').index();
|
selectedIndex = el.closest('li').index();
|
||||||
|
|
|
@ -17,15 +17,21 @@ export class CiSchemaExtension extends EditorLiteExtension {
|
||||||
* @param {String?} opts.ref - Current ref. Defaults to master
|
* @param {String?} opts.ref - Current ref. Defaults to master
|
||||||
*/
|
*/
|
||||||
registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) {
|
registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) {
|
||||||
const ciSchemaUri = Api.buildUrl(Api.projectFileSchemaPath)
|
const ciSchemaPath = Api.buildUrl(Api.projectFileSchemaPath)
|
||||||
.replace(':namespace_path', projectNamespace)
|
.replace(':namespace_path', projectNamespace)
|
||||||
.replace(':project_path', projectPath)
|
.replace(':project_path', projectPath)
|
||||||
.replace(':ref', ref)
|
.replace(':ref', ref)
|
||||||
.replace(':filename', EXTENSION_CI_SCHEMA_FILE_NAME_MATCH);
|
.replace(':filename', EXTENSION_CI_SCHEMA_FILE_NAME_MATCH);
|
||||||
|
// In order for workers loaded from `data://` as the
|
||||||
|
// ones loaded by monaco editor, we use absolute URLs
|
||||||
|
// to fetch schema files, hence the `gon.gitlab_url`
|
||||||
|
// reference. This prevents error:
|
||||||
|
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
|
||||||
|
const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath;
|
||||||
const modelFileName = this.getModel().uri.path.split('/').pop();
|
const modelFileName = this.getModel().uri.path.split('/').pop();
|
||||||
|
|
||||||
registerSchema({
|
registerSchema({
|
||||||
uri: ciSchemaUri,
|
uri: absoluteSchemaUrl,
|
||||||
fileMatch: [modelFileName],
|
fileMatch: [modelFileName],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,8 @@ export const closeFile = ({ commit, state, dispatch, getters }, file) => {
|
||||||
const indexOfClosedFile = state.openFiles.findIndex((f) => f.key === file.key);
|
const indexOfClosedFile = state.openFiles.findIndex((f) => f.key === file.key);
|
||||||
const fileWasActive = file.active;
|
const fileWasActive = file.active;
|
||||||
|
|
||||||
if (file.pending) {
|
if (state.openFiles.length > 1 && fileWasActive) {
|
||||||
commit(types.REMOVE_PENDING_TAB, file);
|
const nextIndexToOpen = indexOfClosedFile === 0 ? 1 : indexOfClosedFile - 1;
|
||||||
} else {
|
|
||||||
commit(types.TOGGLE_FILE_OPEN, path);
|
|
||||||
commit(types.SET_FILE_ACTIVE, { path, active: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.openFiles.length > 0 && fileWasActive) {
|
|
||||||
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
|
|
||||||
const nextFileToOpen = state.openFiles[nextIndexToOpen];
|
const nextFileToOpen = state.openFiles[nextIndexToOpen];
|
||||||
|
|
||||||
if (nextFileToOpen.pending) {
|
if (nextFileToOpen.pending) {
|
||||||
|
@ -35,14 +28,22 @@ export const closeFile = ({ commit, state, dispatch, getters }, file) => {
|
||||||
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
|
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
dispatch('setFileActive', nextFileToOpen.path);
|
||||||
dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true });
|
dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true });
|
||||||
}
|
}
|
||||||
} else if (!state.openFiles.length) {
|
} else if (state.openFiles.length === 1) {
|
||||||
dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
|
dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
|
||||||
root: true,
|
root: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file.pending) {
|
||||||
|
commit(types.REMOVE_PENDING_TAB, file);
|
||||||
|
} else {
|
||||||
|
commit(types.TOGGLE_FILE_OPEN, path);
|
||||||
|
commit(types.SET_FILE_ACTIVE, { path, active: false });
|
||||||
|
}
|
||||||
|
|
||||||
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
|
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec
|
||||||
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
|
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
|
||||||
'/help/operations/metrics/alerts#trigger-actions-from-alerts';
|
'/help/operations/metrics/alerts#trigger-actions-from-alerts';
|
||||||
export const ISSUE_TEMPLATES_DOCS_LINK =
|
export const ISSUE_TEMPLATES_DOCS_LINK =
|
||||||
'/help/user/project/description_templates#creating-issue-templates';
|
'/help/user/project/description_templates#create-an-issue-template';
|
||||||
|
|
||||||
/* PagerDuty integration settings constants */
|
/* PagerDuty integration settings constants */
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
projectId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
projectNamespace: {
|
projectNamespace: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -303,7 +307,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAndShowForm(templates = []) {
|
updateAndShowForm(templates = {}) {
|
||||||
if (!this.showForm) {
|
if (!this.showForm) {
|
||||||
this.showForm = true;
|
this.showForm = true;
|
||||||
this.store.setFormState({
|
this.store.setFormState({
|
||||||
|
@ -419,6 +423,7 @@ export default {
|
||||||
:markdown-docs-path="markdownDocsPath"
|
:markdown-docs-path="markdownDocsPath"
|
||||||
:markdown-preview-path="markdownPreviewPath"
|
:markdown-preview-path="markdownPreviewPath"
|
||||||
:project-path="projectPath"
|
:project-path="projectPath"
|
||||||
|
:project-id="projectId"
|
||||||
:project-namespace="projectNamespace"
|
:project-namespace="projectNamespace"
|
||||||
:show-delete-button="showDeleteButton"
|
:show-delete-button="showDeleteButton"
|
||||||
:can-attach-file="canAttachFile"
|
:can-attach-file="canAttachFile"
|
||||||
|
|
|
@ -13,14 +13,18 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
issuableTemplates: {
|
issuableTemplates: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => {},
|
||||||
},
|
},
|
||||||
projectPath: {
|
projectPath: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
projectId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
projectNamespace: {
|
projectNamespace: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -48,11 +52,12 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issue">
|
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues">
|
||||||
<button
|
<button
|
||||||
ref="toggle"
|
ref="toggle"
|
||||||
:data-namespace-path="projectNamespace"
|
:data-namespace-path="projectNamespace"
|
||||||
:data-project-path="projectPath"
|
:data-project-path="projectPath"
|
||||||
|
:data-project-id="projectId"
|
||||||
:data-data="issuableTemplatesJson"
|
:data-data="issuableTemplatesJson"
|
||||||
class="dropdown-menu-toggle js-issuable-selector"
|
class="dropdown-menu-toggle js-issuable-selector"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -26,9 +26,9 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
issuableTemplates: {
|
issuableTemplates: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => {},
|
||||||
},
|
},
|
||||||
issuableType: {
|
issuableType: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -46,6 +46,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
projectId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
projectNamespace: {
|
projectNamespace: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -68,7 +72,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasIssuableTemplates() {
|
hasIssuableTemplates() {
|
||||||
return this.issuableTemplates.length;
|
return Object.values(Object(this.issuableTemplates)).length;
|
||||||
},
|
},
|
||||||
showLockedWarning() {
|
showLockedWarning() {
|
||||||
return this.formState.lockedWarningVisible && !this.formState.updateLoading;
|
return this.formState.lockedWarningVisible && !this.formState.updateLoading;
|
||||||
|
@ -127,6 +131,7 @@ export default {
|
||||||
:form-state="formState"
|
:form-state="formState"
|
||||||
:issuable-templates="issuableTemplates"
|
:issuable-templates="issuableTemplates"
|
||||||
:project-path="projectPath"
|
:project-path="projectPath"
|
||||||
|
:project-id="projectId"
|
||||||
:project-namespace="projectNamespace"
|
:project-namespace="projectNamespace"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,6 +54,7 @@ export function initIssueHeaderActions(store) {
|
||||||
issueType: el.dataset.issueType,
|
issueType: el.dataset.issueType,
|
||||||
newIssuePath: el.dataset.newIssuePath,
|
newIssuePath: el.dataset.newIssuePath,
|
||||||
projectPath: el.dataset.projectPath,
|
projectPath: el.dataset.projectPath,
|
||||||
|
projectId: el.dataset.projectId,
|
||||||
reportAbusePath: el.dataset.reportAbusePath,
|
reportAbusePath: el.dataset.reportAbusePath,
|
||||||
submitAsSpamPath: el.dataset.submitAsSpamPath,
|
submitAsSpamPath: el.dataset.submitAsSpamPath,
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default class Store {
|
||||||
lockedWarningVisible: false,
|
lockedWarningVisible: false,
|
||||||
updateLoading: false,
|
updateLoading: false,
|
||||||
lock_version: 0,
|
lock_version: 0,
|
||||||
issuableTemplates: [],
|
issuableTemplates: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { flatten } from 'lodash';
|
||||||
import { CI_CONFIG_STATUS_VALID } from '../../constants';
|
import { CI_CONFIG_STATUS_VALID } from '../../constants';
|
||||||
import CiLintResults from './ci_lint_results.vue';
|
import CiLintResults from './ci_lint_results.vue';
|
||||||
|
|
||||||
|
@ -25,14 +26,18 @@ export default {
|
||||||
return this.ciConfig?.stages || [];
|
return this.ciConfig?.stages || [];
|
||||||
},
|
},
|
||||||
jobs() {
|
jobs() {
|
||||||
return this.stages.reduce((acc, { groups, name: stageName }) => {
|
const groupedJobs = this.stages.reduce((acc, { groups, name: stageName }) => {
|
||||||
return acc.concat(
|
return acc.concat(
|
||||||
groups.map(({ name: groupName }) => ({
|
groups.map(({ jobs }) => {
|
||||||
|
return jobs.map((job) => ({
|
||||||
stage: stageName,
|
stage: stageName,
|
||||||
name: groupName,
|
...job,
|
||||||
})),
|
}));
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
return flatten(groupedJobs);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tagList() {
|
tagList() {
|
||||||
return this.item.tagList?.join(', ');
|
return this.item.tags?.join(', ');
|
||||||
},
|
},
|
||||||
onlyPolicy() {
|
onlyPolicy() {
|
||||||
return this.item.only ? this.item.only.refs.join(', ') : this.item.only;
|
return this.item.only ? this.item.only.refs.join(', ') : this.item.only;
|
||||||
|
|
|
@ -15,7 +15,7 @@ mutation lintCI($endpoint: String, $content: String, $dry: Boolean) {
|
||||||
}
|
}
|
||||||
afterScript
|
afterScript
|
||||||
stage
|
stage
|
||||||
tagList
|
tags
|
||||||
when
|
when
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const resolvers = {
|
||||||
beforeScript: job.before_script,
|
beforeScript: job.before_script,
|
||||||
script: job.script,
|
script: job.script,
|
||||||
afterScript: job.after_script,
|
afterScript: job.after_script,
|
||||||
tagList: job.tag_list,
|
tags: job.tag_list,
|
||||||
environment: job.environment,
|
environment: job.environment,
|
||||||
when: job.when,
|
when: job.when,
|
||||||
allowFailure: job.allow_failure,
|
allowFailure: job.allow_failure,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
|
||||||
import { __, s__, sprintf } from '~/locale';
|
import { __, s__, sprintf } from '~/locale';
|
||||||
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
import httpStatusCodes from '~/lib/utils/http_status';
|
||||||
|
|
||||||
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
||||||
import CiLint from './components/lint/ci_lint.vue';
|
import CiLint from './components/lint/ci_lint.vue';
|
||||||
|
@ -23,7 +24,6 @@ const COMMIT_FAILURE = 'COMMIT_FAILURE';
|
||||||
const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
|
const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
|
||||||
const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
|
const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
|
||||||
const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
|
const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
|
||||||
const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF';
|
|
||||||
const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
|
const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -125,6 +125,9 @@ export default {
|
||||||
isBlobContentLoading() {
|
isBlobContentLoading() {
|
||||||
return this.$apollo.queries.content.loading;
|
return this.$apollo.queries.content.loading;
|
||||||
},
|
},
|
||||||
|
isBlobContentError() {
|
||||||
|
return this.failureType === LOAD_FAILURE_NO_FILE || this.failureType === LOAD_FAILURE_UNKNOWN;
|
||||||
|
},
|
||||||
isCiConfigDataLoading() {
|
isCiConfigDataLoading() {
|
||||||
return this.$apollo.queries.ciConfigData.loading;
|
return this.$apollo.queries.ciConfigData.loading;
|
||||||
},
|
},
|
||||||
|
@ -144,14 +147,11 @@ export default {
|
||||||
},
|
},
|
||||||
failure() {
|
failure() {
|
||||||
switch (this.failureType) {
|
switch (this.failureType) {
|
||||||
case LOAD_FAILURE_NO_REF:
|
|
||||||
return {
|
|
||||||
text: this.$options.alertTexts[LOAD_FAILURE_NO_REF],
|
|
||||||
variant: 'danger',
|
|
||||||
};
|
|
||||||
case LOAD_FAILURE_NO_FILE:
|
case LOAD_FAILURE_NO_FILE:
|
||||||
return {
|
return {
|
||||||
text: this.$options.alertTexts[LOAD_FAILURE_NO_FILE],
|
text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
|
||||||
|
filePath: this.ciConfigPath,
|
||||||
|
}),
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
};
|
};
|
||||||
case LOAD_FAILURE_UNKNOWN:
|
case LOAD_FAILURE_UNKNOWN:
|
||||||
|
@ -182,9 +182,8 @@ export default {
|
||||||
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
|
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
|
||||||
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
|
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
|
||||||
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
|
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
|
||||||
[LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'),
|
[LOAD_FAILURE_NO_FILE]: s__(
|
||||||
[LOAD_FAILURE_NO_REF]: s__(
|
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
|
||||||
'Pipelines|Repository does not have a default branch, please set one.',
|
|
||||||
),
|
),
|
||||||
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
|
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
|
||||||
},
|
},
|
||||||
|
@ -193,12 +192,13 @@ export default {
|
||||||
const { networkError } = error;
|
const { networkError } = error;
|
||||||
|
|
||||||
const { response } = networkError;
|
const { response } = networkError;
|
||||||
if (response?.status === 404) {
|
|
||||||
// 404 for missing CI file
|
// 404 for missing CI file
|
||||||
|
// 400 for blank projects with no repository
|
||||||
|
if (
|
||||||
|
response?.status === httpStatusCodes.NOT_FOUND ||
|
||||||
|
response?.status === httpStatusCodes.BAD_REQUEST
|
||||||
|
) {
|
||||||
this.reportFailure(LOAD_FAILURE_NO_FILE);
|
this.reportFailure(LOAD_FAILURE_NO_FILE);
|
||||||
} else if (response?.status === 400) {
|
|
||||||
// 400 for a missing ref when no default branch is set
|
|
||||||
this.reportFailure(LOAD_FAILURE_NO_REF);
|
|
||||||
} else {
|
} else {
|
||||||
this.reportFailure(LOAD_FAILURE_UNKNOWN);
|
this.reportFailure(LOAD_FAILURE_UNKNOWN);
|
||||||
}
|
}
|
||||||
|
@ -299,9 +299,9 @@ export default {
|
||||||
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
|
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</gl-alert>
|
</gl-alert>
|
||||||
<div class="gl-mt-4">
|
|
||||||
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
|
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
|
||||||
<div v-else class="file-editor gl-mb-3">
|
<div v-else-if="!isBlobContentError" class="gl-mt-4">
|
||||||
|
<div class="file-editor gl-mb-3">
|
||||||
<div class="info-well gl-display-none gl-display-sm-block">
|
<div class="info-well gl-display-none gl-display-sm-block">
|
||||||
<validation-segment
|
<validation-segment
|
||||||
class="well-segment"
|
class="well-segment"
|
||||||
|
|
|
@ -8,6 +8,19 @@ fragment PipelineStagesConnection on CiConfigStageConnection {
|
||||||
jobs {
|
jobs {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
|
script
|
||||||
|
beforeScript
|
||||||
|
afterScript
|
||||||
|
environment
|
||||||
|
allowFailure
|
||||||
|
tags
|
||||||
|
when
|
||||||
|
only {
|
||||||
|
refs
|
||||||
|
}
|
||||||
|
except {
|
||||||
|
refs
|
||||||
|
}
|
||||||
needs {
|
needs {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
|
|
|
@ -9,6 +9,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
|
this.projectId = this.dropdown.data('projectId');
|
||||||
this.projectPath = this.dropdown.data('projectPath');
|
this.projectPath = this.dropdown.data('projectPath');
|
||||||
this.namespacePath = this.dropdown.data('namespacePath');
|
this.namespacePath = this.dropdown.data('namespacePath');
|
||||||
this.issuableType = this.$dropdownContainer.data('issuableType');
|
this.issuableType = this.$dropdownContainer.data('issuableType');
|
||||||
|
@ -81,21 +82,21 @@ export default class IssuableTemplateSelector extends TemplateSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestFile(query) {
|
requestFile(query) {
|
||||||
this.startLoadingSpinner();
|
const callback = (currentTemplate) => {
|
||||||
|
|
||||||
Api.issueTemplate(
|
|
||||||
this.namespacePath,
|
|
||||||
this.projectPath,
|
|
||||||
query.name,
|
|
||||||
this.issuableType,
|
|
||||||
(err, currentTemplate) => {
|
|
||||||
this.currentTemplate = currentTemplate;
|
this.currentTemplate = currentTemplate;
|
||||||
this.stopLoadingSpinner();
|
this.stopLoadingSpinner();
|
||||||
if (err) return; // Error handled by global AJAX error handler
|
|
||||||
this.setInputValueToTemplateContent();
|
this.setInputValueToTemplateContent();
|
||||||
},
|
};
|
||||||
|
|
||||||
|
this.startLoadingSpinner();
|
||||||
|
|
||||||
|
Api.projectTemplate(
|
||||||
|
this.projectId,
|
||||||
|
this.issuableType,
|
||||||
|
query.name,
|
||||||
|
{ source_template_project_id: query.project_id },
|
||||||
|
callback,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputValueToTemplateContent() {
|
setInputValueToTemplateContent() {
|
||||||
|
|
|
@ -32,13 +32,17 @@
|
||||||
.rotations-modal {
|
.rotations-modal {
|
||||||
.gl-card {
|
.gl-card {
|
||||||
min-width: 75%;
|
min-width: 75%;
|
||||||
width: fit-content;
|
|
||||||
@include gl-bg-gray-10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.gl-modal .modal-md {
|
&.gl-modal .modal-md {
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move to gitlab/ui utilities
|
||||||
|
// https://gitlab.com/gitlab-org/gitlab/-/issues/297502
|
||||||
|
.gl-w-fit-content {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Copied from roadmaps.scss - adapted for on-call schedules
|
//// Copied from roadmaps.scss - adapted for on-call schedules
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Projects::TemplatesController < Projects::ApplicationController
|
class Projects::TemplatesController < Projects::ApplicationController
|
||||||
|
include IssuablesDescriptionTemplatesHelper
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :authorize_can_read_issuable!
|
before_action :authorize_can_read_issuable!
|
||||||
before_action :get_template_class
|
before_action :get_template_class
|
||||||
|
@ -24,10 +26,8 @@ class Projects::TemplatesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def names
|
def names
|
||||||
templates = @template_type.dropdown_names(project)
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json { render json: templates }
|
format.json { render json: issuable_templates(project, params[:template_type]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ class LicenseTemplateFinder
|
||||||
LicenseTemplate.new(
|
LicenseTemplate.new(
|
||||||
key: license.key,
|
key: license.key,
|
||||||
name: license.name,
|
name: license.name,
|
||||||
|
project: project,
|
||||||
nickname: license.nickname,
|
nickname: license.nickname,
|
||||||
category: (license.featured? ? :Popular : :Other),
|
category: (license.featured? ? :Popular : :Other),
|
||||||
content: license.content,
|
content: license.content,
|
||||||
|
|
|
@ -199,7 +199,7 @@ module BlobHelper
|
||||||
|
|
||||||
categories.each_with_object({}) do |category, hash|
|
categories.each_with_object({}) do |category, hash|
|
||||||
hash[category] = grouped[category].map do |item|
|
hash[category] = grouped[category].map do |item|
|
||||||
{ name: item.name, id: item.key }
|
{ name: item.name, id: item.key, project_id: item.project_id }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module IssuablesDescriptionTemplatesHelper
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
include GitlabRoutingHelper
|
||||||
|
|
||||||
|
def template_dropdown_tag(issuable, &block)
|
||||||
|
title = selected_template(issuable) || "Choose a template"
|
||||||
|
options = {
|
||||||
|
toggle_class: 'js-issuable-selector',
|
||||||
|
title: title,
|
||||||
|
filter: true,
|
||||||
|
placeholder: 'Filter',
|
||||||
|
footer_content: true,
|
||||||
|
data: {
|
||||||
|
data: issuable_templates(ref_project, issuable.to_ability_name),
|
||||||
|
field_name: 'issuable_template',
|
||||||
|
selected: selected_template(issuable),
|
||||||
|
project_id: ref_project.id,
|
||||||
|
project_path: ref_project.path,
|
||||||
|
namespace_path: ref_project.namespace.full_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown_tag(title, options: options) do
|
||||||
|
capture(&block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def issuable_templates(project, issuable_type)
|
||||||
|
strong_memoize(:issuable_templates) do
|
||||||
|
supported_issuable_types = %w[issue merge_request]
|
||||||
|
|
||||||
|
next [] unless supported_issuable_types.include?(issuable_type)
|
||||||
|
|
||||||
|
template_dropdown_names(TemplateFinder.build(issuable_type.pluralize.to_sym, project).execute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def issuable_templates_names(issuable)
|
||||||
|
issuable_templates(ref_project, issuable.to_ability_name).map { |template| template[:name] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def selected_template(issuable)
|
||||||
|
params[:issuable_template] if issuable_templates(ref_project, issuable.to_ability_name).values.flatten.any? { |template| template[:name] == params[:issuable_template] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_names_path(parent, issuable)
|
||||||
|
return '' unless parent.is_a?(Project)
|
||||||
|
|
||||||
|
project_template_names_path(parent, template_type: issuable.to_ability_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_dropdown_names(items)
|
||||||
|
grouped = items.group_by(&:category)
|
||||||
|
categories = grouped.keys
|
||||||
|
|
||||||
|
categories.each_with_object({}) do |category, hash|
|
||||||
|
hash[category] = grouped[category].map do |item|
|
||||||
|
{ name: item.name, id: item.key, project_id: item.try(:project_id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
module IssuablesHelper
|
module IssuablesHelper
|
||||||
include GitlabRoutingHelper
|
include GitlabRoutingHelper
|
||||||
|
include IssuablesDescriptionTemplatesHelper
|
||||||
|
|
||||||
def sidebar_gutter_toggle_icon
|
def sidebar_gutter_toggle_icon
|
||||||
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
|
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
|
||||||
|
@ -75,28 +76,6 @@ module IssuablesHelper
|
||||||
.to_json
|
.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_dropdown_tag(issuable, &block)
|
|
||||||
title = selected_template(issuable) || "Choose a template"
|
|
||||||
options = {
|
|
||||||
toggle_class: 'js-issuable-selector',
|
|
||||||
title: title,
|
|
||||||
filter: true,
|
|
||||||
placeholder: 'Filter',
|
|
||||||
footer_content: true,
|
|
||||||
data: {
|
|
||||||
data: issuable_templates(issuable),
|
|
||||||
field_name: 'issuable_template',
|
|
||||||
selected: selected_template(issuable),
|
|
||||||
project_path: ref_project.path,
|
|
||||||
namespace_path: ref_project.namespace.full_path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdown_tag(title, options: options) do
|
|
||||||
capture(&block)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def users_dropdown_label(selected_users)
|
def users_dropdown_label(selected_users)
|
||||||
case selected_users.length
|
case selected_users.length
|
||||||
when 0
|
when 0
|
||||||
|
@ -294,6 +273,7 @@ module IssuablesHelper
|
||||||
|
|
||||||
{
|
{
|
||||||
projectPath: ref_project.path,
|
projectPath: ref_project.path,
|
||||||
|
projectId: ref_project.id,
|
||||||
projectNamespace: ref_project.namespace.full_path
|
projectNamespace: ref_project.namespace.full_path
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -369,24 +349,6 @@ module IssuablesHelper
|
||||||
cookies[:collapsed_gutter] == 'true'
|
cookies[:collapsed_gutter] == 'true'
|
||||||
end
|
end
|
||||||
|
|
||||||
def issuable_templates(issuable)
|
|
||||||
@issuable_templates ||=
|
|
||||||
case issuable
|
|
||||||
when Issue
|
|
||||||
ref_project.repository.issue_template_names
|
|
||||||
when MergeRequest
|
|
||||||
ref_project.repository.merge_request_template_names
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def issuable_templates_names(issuable)
|
|
||||||
issuable_templates(issuable).map { |template| template[:name] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def selected_template(issuable)
|
|
||||||
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def issuable_todo_button_data(issuable, is_collapsed)
|
def issuable_todo_button_data(issuable, is_collapsed)
|
||||||
{
|
{
|
||||||
todo_text: _('Add a to do'),
|
todo_text: _('Add a to do'),
|
||||||
|
@ -424,12 +386,6 @@ module IssuablesHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_names_path(parent, issuable)
|
|
||||||
return '' unless parent.is_a?(Project)
|
|
||||||
|
|
||||||
project_template_names_path(parent, template_type: issuable.class.name.underscore)
|
|
||||||
end
|
|
||||||
|
|
||||||
def issuable_sidebar_options(issuable)
|
def issuable_sidebar_options(issuable)
|
||||||
{
|
{
|
||||||
endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
|
endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module CanHousekeepRepository
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def pushes_since_gc
|
||||||
|
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
|
||||||
|
end
|
||||||
|
|
||||||
|
def increment_pushes_since_gc
|
||||||
|
Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_pushes_since_gc
|
||||||
|
Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pushes_since_gc_redis_shared_state_key
|
||||||
|
"#{self.class.name.underscore.pluralize}/#{id}/pushes_since_gc"
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,11 +12,12 @@ class LicenseTemplate
|
||||||
(fullname|name\sof\s(author|copyright\sowner))
|
(fullname|name\sof\s(author|copyright\sowner))
|
||||||
[\>\}\]]}xi.freeze
|
[\>\}\]]}xi.freeze
|
||||||
|
|
||||||
attr_reader :key, :name, :category, :nickname, :url, :meta
|
attr_reader :key, :name, :project, :category, :nickname, :url, :meta
|
||||||
|
|
||||||
def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {})
|
def initialize(key:, name:, project:, category:, content:, nickname: nil, url: nil, meta: {})
|
||||||
@key = key
|
@key = key
|
||||||
@name = name
|
@name = name
|
||||||
|
@project = project
|
||||||
@category = category
|
@category = category
|
||||||
@content = content
|
@content = content
|
||||||
@nickname = nickname
|
@nickname = nickname
|
||||||
|
@ -24,6 +25,22 @@ class LicenseTemplate
|
||||||
@meta = meta
|
@meta = meta
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_id
|
||||||
|
project&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_path
|
||||||
|
project&.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def namespace_id
|
||||||
|
project&.namespace&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def namespace_path
|
||||||
|
project&.namespace&.full_path
|
||||||
|
end
|
||||||
|
|
||||||
def popular?
|
def popular?
|
||||||
category == :Popular
|
category == :Popular
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Project < ApplicationRecord
|
||||||
include FromUnion
|
include FromUnion
|
||||||
include IgnorableColumns
|
include IgnorableColumns
|
||||||
include Integration
|
include Integration
|
||||||
|
include CanHousekeepRepository
|
||||||
include EachBatch
|
include EachBatch
|
||||||
extend Gitlab::Cache::RequestCache
|
extend Gitlab::Cache::RequestCache
|
||||||
extend Gitlab::Utils::Override
|
extend Gitlab::Utils::Override
|
||||||
|
@ -2122,18 +2123,6 @@ class Project < ApplicationRecord
|
||||||
(auto_devops || build_auto_devops)&.predefined_variables
|
(auto_devops || build_auto_devops)&.predefined_variables
|
||||||
end
|
end
|
||||||
|
|
||||||
def pushes_since_gc
|
|
||||||
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
|
|
||||||
end
|
|
||||||
|
|
||||||
def increment_pushes_since_gc
|
|
||||||
Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_pushes_since_gc
|
|
||||||
Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def route_map_for(commit_sha)
|
def route_map_for(commit_sha)
|
||||||
@route_maps_by_commit ||= Hash.new do |h, sha|
|
@route_maps_by_commit ||= Hash.new do |h, sha|
|
||||||
h[sha] = begin
|
h[sha] = begin
|
||||||
|
@ -2634,10 +2623,6 @@ class Project < ApplicationRecord
|
||||||
from && self != from
|
from && self != from
|
||||||
end
|
end
|
||||||
|
|
||||||
def pushes_since_gc_redis_shared_state_key
|
|
||||||
"projects/#{id}/pushes_since_gc"
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_project_statistics
|
def update_project_statistics
|
||||||
stats = statistics || build_statistics
|
stats = statistics || build_statistics
|
||||||
stats.update(namespace_id: namespace_id)
|
stats.update(namespace_id: namespace_id)
|
||||||
|
|
|
@ -88,13 +88,9 @@ module MergeRequests
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_merge
|
def try_merge
|
||||||
merge = repository.merge(current_user, source, merge_request, commit_message)
|
repository.merge(current_user, source, merge_request, commit_message).tap do
|
||||||
|
merge_request.update_column(:squash_commit_sha, source) if merge_request.squash_on_merge?
|
||||||
if merge_request.squash_on_merge? && Feature.enabled?(:persist_squash_commit_sha_for_squashes, project)
|
|
||||||
merge_request.update_column(:squash_commit_sha, source)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
merge
|
|
||||||
rescue Gitlab::Git::PreReceiveError => e
|
rescue Gitlab::Git::PreReceiveError => e
|
||||||
raise MergeError,
|
raise MergeError,
|
||||||
"Something went wrong during merge pre-receive hook. #{e.message}".strip
|
"Something went wrong during merge pre-receive hook. #{e.message}".strip
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) }
|
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) }
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
|
||||||
%button.btn.js-settings-toggle
|
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||||
= expanded ? _('Collapse') : _('Expand')
|
= expanded ? _('Collapse') : _('Expand')
|
||||||
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
|
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
|
||||||
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
%section.settings.general-settings.no-animate.expanded#js-general-settings
|
%section.settings.general-settings.no-animate.expanded#js-general-settings
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
|
||||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse')
|
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse')
|
||||||
%p= _('Update your project name, topics, description, and avatar.')
|
%p= _('Update your project name, topics, description, and avatar.')
|
||||||
.settings-content= render 'projects/settings/general'
|
.settings-content= render 'projects/settings/general'
|
||||||
|
|
||||||
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { qa_selector: 'visibility_features_permissions_content' } }
|
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { qa_selector: 'visibility_features_permissions_content' } }
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
|
||||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
||||||
%p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.')
|
%p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.')
|
||||||
|
|
||||||
.settings-content
|
.settings-content
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
%section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
|
%section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
|
||||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
||||||
= render_if_exists 'projects/merge_request_settings_description_text'
|
= render_if_exists 'projects/merge_request_settings_description_text'
|
||||||
|
|
||||||
.settings-content
|
.settings-content
|
||||||
|
@ -48,8 +48,7 @@
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||||
= s_('ProjectSettings|Badges')
|
= s_('ProjectSettings|Badges')
|
||||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
||||||
= expanded ? _('Collapse') : _('Expand')
|
|
||||||
%p
|
%p
|
||||||
= s_('ProjectSettings|Customize this project\'s badges.')
|
= s_('ProjectSettings|Customize this project\'s badges.')
|
||||||
= link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
|
= link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
|
||||||
|
@ -63,7 +62,7 @@
|
||||||
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
|
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
|
||||||
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
|
||||||
%p= _('Housekeeping, export, path, transfer, remove, archive.')
|
%p= _('Housekeeping, export, path, transfer, remove, archive.')
|
||||||
|
|
||||||
.settings-content
|
.settings-content
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
- issuable = local_assigns.fetch(:issuable, nil)
|
- issuable = local_assigns.fetch(:issuable, nil)
|
||||||
|
|
||||||
- return unless issuable && issuable_templates(issuable).any?
|
- return unless issuable && issuable_templates(ref_project, issuable.class.name.underscore).any?
|
||||||
|
|
||||||
.issuable-form-select-holder.selectbox.form-group
|
.issuable-form-select-holder.selectbox.form-group
|
||||||
.js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } }
|
.js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name.pluralize } }
|
||||||
= template_dropdown_tag(issuable) do
|
= template_dropdown_tag(issuable) do
|
||||||
%ul.dropdown-footer-list
|
%ul.dropdown-footer-list
|
||||||
%li
|
%li
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- issuable = local_assigns.fetch(:issuable)
|
- issuable = local_assigns.fetch(:issuable)
|
||||||
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
|
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
|
||||||
- form = local_assigns.fetch(:form)
|
- form = local_assigns.fetch(:form)
|
||||||
- no_issuable_templates = issuable_templates(issuable).empty?
|
- no_issuable_templates = issuable_templates(ref_project, issuable.class.name.underscore).empty?
|
||||||
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
|
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
|
||||||
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
|
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
|
||||||
- toggle_wip_link_end = '</a>'
|
- toggle_wip_link_end = '</a>'
|
||||||
|
|
|
@ -1094,7 +1094,7 @@
|
||||||
:idempotent: true
|
:idempotent: true
|
||||||
:tags: []
|
:tags: []
|
||||||
- :name: pipeline_background:ci_daily_build_group_report_results
|
- :name: pipeline_background:ci_daily_build_group_report_results
|
||||||
:feature_category: :continuous_integration
|
:feature_category: :code_testing
|
||||||
:has_external_dependencies:
|
:has_external_dependencies:
|
||||||
:urgency: :low
|
:urgency: :low
|
||||||
:resource_boundary: :unknown
|
:resource_boundary: :unknown
|
||||||
|
@ -1102,7 +1102,7 @@
|
||||||
:idempotent: true
|
:idempotent: true
|
||||||
:tags: []
|
:tags: []
|
||||||
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
|
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
|
||||||
:feature_category: :continuous_integration
|
:feature_category: :code_testing
|
||||||
:has_external_dependencies:
|
:has_external_dependencies:
|
||||||
:urgency: :low
|
:urgency: :low
|
||||||
:resource_boundary: :unknown
|
:resource_boundary: :unknown
|
||||||
|
|
|
@ -5,6 +5,8 @@ module Ci
|
||||||
include ApplicationWorker
|
include ApplicationWorker
|
||||||
include PipelineBackgroundQueue
|
include PipelineBackgroundQueue
|
||||||
|
|
||||||
|
feature_category :code_testing
|
||||||
|
|
||||||
idempotent!
|
idempotent!
|
||||||
|
|
||||||
def perform(pipeline_id)
|
def perform(pipeline_id)
|
||||||
|
|
|
@ -6,6 +6,8 @@ module Ci
|
||||||
include ApplicationWorker
|
include ApplicationWorker
|
||||||
include PipelineBackgroundQueue
|
include PipelineBackgroundQueue
|
||||||
|
|
||||||
|
feature_category :code_testing
|
||||||
|
|
||||||
idempotent!
|
idempotent!
|
||||||
|
|
||||||
def perform(pipeline_id)
|
def perform(pipeline_id)
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Persist 'squash_commit_sha' when squashing
|
||||||
|
merge_request: 51074
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: In WebIDE switch files before closing the active one
|
||||||
|
merge_request: 51483
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Instrument CI template usage across projects
|
||||||
|
merge_request: 51391
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Update toggle button in repo general settings
|
||||||
|
merge_request: 51036
|
||||||
|
author: Yogi (@yo)
|
||||||
|
type: other
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: persist_squash_commit_sha_for_squashes
|
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50178
|
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294243
|
|
||||||
milestone: '13.8'
|
|
||||||
type: development
|
|
||||||
group: group::source code
|
|
||||||
default_enabled: false
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296880
|
||||||
milestone: '13.8'
|
milestone: '13.8'
|
||||||
type: development
|
type: development
|
||||||
group: group::configure
|
group: group::configure
|
||||||
default_enabled: false
|
default_enabled: true
|
||||||
|
|
|
@ -15,7 +15,8 @@ CATEGORY_TABLE_HEADER = <<MARKDOWN
|
||||||
To spread load more evenly across eligible reviewers, Danger has picked a candidate for each
|
To spread load more evenly across eligible reviewers, Danger has picked a candidate for each
|
||||||
review slot, based on their timezone. Feel free to
|
review slot, based on their timezone. Feel free to
|
||||||
[override these selections](https://about.gitlab.com/handbook/engineering/projects/#gitlab)
|
[override these selections](https://about.gitlab.com/handbook/engineering/projects/#gitlab)
|
||||||
if you think someone else would be better-suited, or the chosen person is unavailable.
|
if you think someone else would be better-suited
|
||||||
|
or use the [GitLab Review Workload Dashboard](https://gitlab-org.gitlab.io/gitlab-roulette/) to find other available reviewers.
|
||||||
|
|
||||||
To read more on how to use the reviewer roulette, please take a look at the
|
To read more on how to use the reviewer roulette, please take a look at the
|
||||||
[Engineering workflow](https://about.gitlab.com/handbook/engineering/workflow/#basics)
|
[Engineering workflow](https://about.gitlab.com/handbook/engineering/workflow/#basics)
|
||||||
|
|
|
@ -148,3 +148,30 @@ they speed up the process as managing incidents now becomes _much_ easier. Once
|
||||||
continuous deployments are easier to perform, the time to iterate on a feature
|
continuous deployments are easier to perform, the time to iterate on a feature
|
||||||
is reduced even further, as you no longer need to wait weeks before your changes
|
is reduced even further, as you no longer need to wait weeks before your changes
|
||||||
are available on GitLab.com.
|
are available on GitLab.com.
|
||||||
|
|
||||||
|
### The benefits of feature flags
|
||||||
|
|
||||||
|
It may seem like feature flags are configuration, which goes against our [convention-over-configuration](https://about.gitlab.com/handbook/product/product-principles/#convention-over-configuration)
|
||||||
|
principle. However, configuration is by definition something that is user-manageable.
|
||||||
|
Feature flags are not intended to be user-editable. Instead, they are intended as a tool for Engineers
|
||||||
|
and Site Reliability Engineers to use to de-risk their changes. Feature flags are the shim that gets us
|
||||||
|
to Continuous Delivery with our mono repo and without having to deploy the entire codebase on every change.
|
||||||
|
Feature flags are created to ensure that we can safely rollout our work on our terms.
|
||||||
|
If we use Feature Flags as a configuration, we are doing it wrong and are indeed in violation of our
|
||||||
|
principles. If something needs to be configured, we should intentionally make it configuration from the
|
||||||
|
first moment.
|
||||||
|
|
||||||
|
Some of the benefits of using development-type feature flags are:
|
||||||
|
|
||||||
|
1. It enables Continuous Delivery for GitLab.com.
|
||||||
|
1. It significantly reduces Mean-Time-To-Recovery.
|
||||||
|
1. It helps engineers to monitor and reduce the impact of their changes gradually, at any scale,
|
||||||
|
allowing us to be more metrics-driven and execute good DevOps practices, [shifting some responsibility "left"](https://devops.com/why-its-time-for-site-reliability-engineering-to-shift-left/).
|
||||||
|
1. Controlled feature rollout timing: without feature flags, we would need to wait until a specific
|
||||||
|
deployment was complete (which at GitLab could be at any time).
|
||||||
|
1. Increased psychological safety: when a feature flag is used, an engineer has the confidence that if anything goes wrong they can quickly disable the code and minimize the impact of a change that might be risky.
|
||||||
|
1. Improved throughput: when a change is less risky because a flag exists, theoretical tests about
|
||||||
|
scalability can potentially become unnecessary or less important. This allows an engineer to
|
||||||
|
potentially test a feature on a small project, monitor the impact, and proceed. The alternative might
|
||||||
|
be to build complex benchmarks locally, or on staging, or on another GitLab deployment, which has an
|
||||||
|
outsized impact on the time it can take to build and release a feature.
|
||||||
|
|
|
@ -10,8 +10,8 @@ We have implemented standard features that depend on configuration files in the
|
||||||
When implementing new features, please refer to these existing features to avoid conflicts:
|
When implementing new features, please refer to these existing features to avoid conflicts:
|
||||||
|
|
||||||
- [Custom Dashboards](../operations/metrics/dashboards/index.md#add-a-new-dashboard-to-your-project): `.gitlab/dashboards/`.
|
- [Custom Dashboards](../operations/metrics/dashboards/index.md#add-a-new-dashboard-to-your-project): `.gitlab/dashboards/`.
|
||||||
- [Issue Templates](../user/project/description_templates.md#creating-issue-templates): `.gitlab/issue_templates/`.
|
- [Issue Templates](../user/project/description_templates.md#create-an-issue-template): `.gitlab/issue_templates/`.
|
||||||
- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_templates/`.
|
- [Merge Request Templates](../user/project/description_templates.md#create-a-merge-request-template): `.gitlab/merge_request_templates/`.
|
||||||
- [GitLab Kubernetes Agents](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
|
- [GitLab Kubernetes Agents](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
|
||||||
- [CODEOWNERS](../user/project/code_owners.md#how-to-set-up-code-owners): `.gitlab/CODEOWNERS`.
|
- [CODEOWNERS](../user/project/code_owners.md#how-to-set-up-code-owners): `.gitlab/CODEOWNERS`.
|
||||||
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
|
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
|
||||||
|
|
|
@ -52,7 +52,7 @@ With Maintainer or higher [permissions](../../user/permissions.md), you can enab
|
||||||
1. Navigate to **Settings > Operations > Incidents** and expand **Incidents**.
|
1. Navigate to **Settings > Operations > Incidents** and expand **Incidents**.
|
||||||
1. Check the **Create an incident** checkbox.
|
1. Check the **Create an incident** checkbox.
|
||||||
1. To customize the incident, select an
|
1. To customize the incident, select an
|
||||||
[issue template](../../user/project/description_templates.md#creating-issue-templates).
|
[issue template](../../user/project/description_templates.md#create-an-issue-template).
|
||||||
1. To send [an email notification](alert_notifications.md#email-notifications) to users
|
1. To send [an email notification](alert_notifications.md#email-notifications) to users
|
||||||
with [Developer permissions](../../user/permissions.md), select
|
with [Developer permissions](../../user/permissions.md), select
|
||||||
**Send a separate email notification to Developers**. Email notifications are
|
**Send a separate email notification to Developers**. Email notifications are
|
||||||
|
|
|
@ -726,6 +726,9 @@ To enable this feature, navigate to the group settings page, expand the
|
||||||
|
|
||||||
![Group file template settings](img/group_file_template_settings.png)
|
![Group file template settings](img/group_file_template_settings.png)
|
||||||
|
|
||||||
|
To learn how to create templates for issues and merge requests, visit
|
||||||
|
[Description templates](../project/description_templates.md).
|
||||||
|
|
||||||
#### Group-level project templates **(PREMIUM)**
|
#### Group-level project templates **(PREMIUM)**
|
||||||
|
|
||||||
Define project templates at a group level by setting a group as the template source.
|
Define project templates at a group level by setting a group as the template source.
|
||||||
|
|
|
@ -6,16 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Description templates
|
# Description templates
|
||||||
|
|
||||||
>[Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
|
||||||
|
|
||||||
We all know that a properly submitted issue is more likely to be addressed in
|
We all know that a properly submitted issue is more likely to be addressed in
|
||||||
a timely manner by the developers of a project.
|
a timely manner by the developers of a project.
|
||||||
|
|
||||||
Description templates allow you to define context-specific templates for issue
|
With description templates, you can define context-specific templates for issue and merge request
|
||||||
and merge request description fields for your project, as well as help filter
|
description fields for your project, and filter out a lot of unnecessary noise from issues.
|
||||||
out a lot of unnecessary noise from issues.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
By using the description templates, users that create a new issue or merge
|
By using the description templates, users that create a new issue or merge
|
||||||
request can select a description template to help them communicate with other
|
request can select a description template to help them communicate with other
|
||||||
|
@ -28,7 +25,10 @@ Description templates must be written in [Markdown](../markdown.md) and stored
|
||||||
in your project's repository under a directory named `.gitlab`. Only the
|
in your project's repository under a directory named `.gitlab`. Only the
|
||||||
templates of the default branch are taken into account.
|
templates of the default branch are taken into account.
|
||||||
|
|
||||||
## Use-cases
|
To learn how to create templates for various file types in groups, visit
|
||||||
|
[Group file templates](../group/index.md#group-file-templates).
|
||||||
|
|
||||||
|
## Use cases
|
||||||
|
|
||||||
- Add a template to be used in every issue for a specific project,
|
- Add a template to be used in every issue for a specific project,
|
||||||
giving instructions and guidelines, requiring for information specific to that subject.
|
giving instructions and guidelines, requiring for information specific to that subject.
|
||||||
|
@ -40,7 +40,7 @@ templates of the default branch are taken into account.
|
||||||
- You can also create issues and merge request templates for different
|
- You can also create issues and merge request templates for different
|
||||||
stages of your workflow, for example, feature proposal, feature improvement, or a bug report.
|
stages of your workflow, for example, feature proposal, feature improvement, or a bug report.
|
||||||
|
|
||||||
## Creating issue templates
|
## Create an issue template
|
||||||
|
|
||||||
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
|
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
|
||||||
directory in your repository. Commit and push to your default branch.
|
directory in your repository. Commit and push to your default branch.
|
||||||
|
@ -65,13 +65,13 @@ To create the `.gitlab/issue_templates` directory:
|
||||||
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)
|
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)
|
||||||
and see if you can choose a description template.
|
and see if you can choose a description template.
|
||||||
|
|
||||||
## Creating merge request templates
|
## Create a merge request template
|
||||||
|
|
||||||
Similarly to issue templates, create a new Markdown (`.md`) file inside the
|
Similarly to issue templates, create a new Markdown (`.md`) file inside the
|
||||||
`.gitlab/merge_request_templates/` directory in your repository. Commit and
|
`.gitlab/merge_request_templates/` directory in your repository. Commit and
|
||||||
push to your default branch.
|
push to your default branch.
|
||||||
|
|
||||||
## Using the templates
|
## Use the templates
|
||||||
|
|
||||||
Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
|
Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
|
||||||
This enables the `Bug` dropdown option when creating or editing issues. When
|
This enables the `Bug` dropdown option when creating or editing issues. When
|
||||||
|
@ -80,15 +80,46 @@ to the issue description field. The **Reset template** button discards any
|
||||||
changes you made after picking the template and returns it to its initial status.
|
changes you made after picking the template and returns it to its initial status.
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
You can create short-cut links to create an issue using a designated template. For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
|
You can create shortcut links to create an issue using a designated template.
|
||||||
|
For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
|
||||||
|
|
||||||
![Description templates](img/description_templates.png)
|
![Description templates](img/description_templates.png)
|
||||||
|
|
||||||
## Setting a default template for merge requests and issues **(STARTER)**
|
### Set an issue and merge request description template at group level **(PREMIUM)**
|
||||||
|
|
||||||
> - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46222) in GitLab 13.8.
|
||||||
> - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
|
|
||||||
> - Templates for merge requests were [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9.
|
Templates are most useful, because you can create a template once and use it multiple times.
|
||||||
|
To re-use templates [you've created](../project/description_templates.md#create-an-issue-template):
|
||||||
|
|
||||||
|
1. Go to your project's `Settings > General > Templates`.
|
||||||
|
1. From the dropdown, select your template project as the template repository at group level.
|
||||||
|
|
||||||
|
![Group template settings](../group/img/group_file_template_settings.png)
|
||||||
|
|
||||||
|
### Set an issue and merge request description template at instance level **(PREMIUM ONLY)**
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46222) in GitLab 13.8.
|
||||||
|
|
||||||
|
Similar to group templates, issue and merge request templates can also be set up at the instance level.
|
||||||
|
This results in those templates being available in all projects within the instance.
|
||||||
|
Only instance administrators can set instance-level templates.
|
||||||
|
|
||||||
|
To set the instance-level description template repository:
|
||||||
|
|
||||||
|
1. Select the **Admin Area** icon (**{admin}**).
|
||||||
|
1. Select **Templates**.
|
||||||
|
1. From the dropdown, select your template project as the template repository at instance level.
|
||||||
|
|
||||||
|
Learn more about [instance template repository](../admin_area/settings/instance_template_repository.md).
|
||||||
|
|
||||||
|
![Setting templates in the Admin Area](../admin_area/settings/img/file_template_admin_area.png)
|
||||||
|
|
||||||
|
### Set a default template for merge requests and issues **(STARTER)**
|
||||||
|
|
||||||
|
> - This feature was introduced before [description templates](#description-templates) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
|
||||||
|
> - Templates for issues [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
|
||||||
|
> - Templates for merge requests [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9.
|
||||||
|
|
||||||
The visibility of issues and/or merge requests should be set to either "Everyone
|
The visibility of issues and/or merge requests should be set to either "Everyone
|
||||||
with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
|
with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
|
||||||
|
@ -113,52 +144,47 @@ pre-filled with the text you entered in the template(s).
|
||||||
|
|
||||||
## Description template example
|
## Description template example
|
||||||
|
|
||||||
We make use of Description Templates for Issues and Merge Requests within the GitLab Community
|
We make use of description templates for issues and merge requests in the GitLab project.
|
||||||
Edition project. Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
|
Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
|
||||||
for some examples.
|
for some examples.
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
It's possible to use [quick actions](quick_actions.md) within description templates to quickly add
|
It's possible to use [quick actions](quick_actions.md) in description templates to quickly add
|
||||||
labels, assignees, and milestones. The quick actions are only executed if the user submitting
|
labels, assignees, and milestones. The quick actions are only executed if the user submitting
|
||||||
the issue or merge request has the permissions to perform the relevant actions.
|
the issue or merge request has the permissions to perform the relevant actions.
|
||||||
|
|
||||||
Here is an example of a Bug report template:
|
Here is an example of a Bug report template:
|
||||||
|
|
||||||
```plaintext
|
```markdown
|
||||||
Summary
|
## Summary
|
||||||
|
|
||||||
(Summarize the bug encountered concisely)
|
(Summarize the bug encountered concisely)
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
Steps to reproduce
|
|
||||||
|
|
||||||
(How one can reproduce the issue - this is very important)
|
(How one can reproduce the issue - this is very important)
|
||||||
|
|
||||||
|
## Example Project
|
||||||
|
|
||||||
Example Project
|
(If possible, please create an example project here on GitLab.com that exhibits the problematic
|
||||||
|
behaviour, and link to it here in the bug report.
|
||||||
|
If you are using an older version of GitLab, this will also determine whether the bug has been fixed
|
||||||
|
in a more recent version)
|
||||||
|
|
||||||
(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
|
## What is the current bug behavior?
|
||||||
|
|
||||||
(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
|
|
||||||
|
|
||||||
|
|
||||||
What is the current bug behavior?
|
|
||||||
|
|
||||||
(What actually happens)
|
(What actually happens)
|
||||||
|
|
||||||
|
## What is the expected correct behavior?
|
||||||
What is the expected correct behavior?
|
|
||||||
|
|
||||||
(What you should see instead)
|
(What you should see instead)
|
||||||
|
|
||||||
|
## Relevant logs and/or screenshots
|
||||||
|
|
||||||
Relevant logs and/or screenshots
|
(Paste any relevant logs - please use code blocks (```) to format console output, logs, and code, as
|
||||||
|
it's very hard to read otherwise.)
|
||||||
|
|
||||||
(Paste any relevant logs - please use code blocks (```) to format console output,
|
## Possible fixes
|
||||||
logs, and code as it's very hard to read otherwise.)
|
|
||||||
|
|
||||||
|
|
||||||
Possible fixes
|
|
||||||
|
|
||||||
(If you can, link to the line of code that might be responsible for the problem)
|
(If you can, link to the line of code that might be responsible for the problem)
|
||||||
|
|
||||||
|
|
|
@ -217,7 +217,7 @@ You can then see issue statuses in the [issue list](#issues-list) and the
|
||||||
|
|
||||||
## Other Issue actions
|
## Other Issue actions
|
||||||
|
|
||||||
- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
|
- [Create an issue from a template](../../project/description_templates.md#use-the-templates)
|
||||||
- [Set a due date](due_dates.md)
|
- [Set a due date](due_dates.md)
|
||||||
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
|
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
|
||||||
in order to change their status, assignee, milestone, or labels in bulk.
|
in order to change their status, assignee, milestone, or labels in bulk.
|
||||||
|
|
|
@ -102,7 +102,7 @@ To edit a file:
|
||||||
in the bottom-right corner.
|
in the bottom-right corner.
|
||||||
1. When you're done, click **Submit changes...**.
|
1. When you're done, click **Submit changes...**.
|
||||||
1. (Optional) Adjust the default title and description of the merge request that will be submitted
|
1. (Optional) Adjust the default title and description of the merge request that will be submitted
|
||||||
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#creating-merge-request-templates)
|
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#create-a-merge-request-template)
|
||||||
from the dropdown menu and edit it accordingly.
|
from the dropdown menu and edit it accordingly.
|
||||||
1. Click **Submit changes**.
|
1. Click **Submit changes**.
|
||||||
1. A new merge request is automatically created and you can assign a colleague for review.
|
1. A new merge request is automatically created and you can assign a colleague for review.
|
||||||
|
|
|
@ -45,9 +45,10 @@ module API
|
||||||
|
|
||||||
get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do
|
get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do
|
||||||
begin
|
begin
|
||||||
template = TemplateFinder
|
template = TemplateFinder.build(
|
||||||
.build(params[:type], user_project, name: params[:name])
|
params[:type], user_project, name: params[:name],
|
||||||
.execute
|
source_template_project_id: params[:source_template_project_id]
|
||||||
|
).execute
|
||||||
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
||||||
not_found!('Template')
|
not_found!('Template')
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ module Gitlab
|
||||||
def initialize(path, project = nil, category: nil)
|
def initialize(path, project = nil, category: nil)
|
||||||
@path = path
|
@path = path
|
||||||
@category = category
|
@category = category
|
||||||
|
@project = project
|
||||||
@finder = self.class.finder(project)
|
@finder = self.class.finder(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,6 +32,22 @@ module Gitlab
|
||||||
# override with a comment to be placed at the top of the blob.
|
# override with a comment to be placed at the top of the blob.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_id
|
||||||
|
@project&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_path
|
||||||
|
@project&.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def namespace_id
|
||||||
|
@project&.namespace&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def namespace_path
|
||||||
|
@project&.namespace&.full_path
|
||||||
|
end
|
||||||
|
|
||||||
# Present for compatibility with license templates, which can replace text
|
# Present for compatibility with license templates, which can replace text
|
||||||
# like `[fullname]` with a user-specified string. This is a no-op for
|
# like `[fullname]` with a user-specified string. This is a no-op for
|
||||||
# other templates
|
# other templates
|
||||||
|
@ -82,11 +99,11 @@ module Gitlab
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
def by_category(category, project = nil)
|
def by_category(category, project = nil, empty_category_title: nil)
|
||||||
directory = category_directory(category)
|
directory = category_directory(category)
|
||||||
files = finder(project).list_files_for(directory)
|
files = finder(project).list_files_for(directory)
|
||||||
|
|
||||||
files.map { |f| new(f, project, category: category) }.sort
|
files.map { |f| new(f, project, category: category.presence || empty_category_title) }.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
def category_directory(category)
|
def category_directory(category)
|
||||||
|
|
|
@ -15,6 +15,10 @@ module Gitlab
|
||||||
def finder(project)
|
def finder(project)
|
||||||
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def by_category(category, project = nil, empty_category_title: nil)
|
||||||
|
super(category, project, empty_category_title: _('Project Templates'))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,10 @@ module Gitlab
|
||||||
def finder(project)
|
def finder(project)
|
||||||
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def by_category(category, project = nil, empty_category_title: nil)
|
||||||
|
super(category, project, empty_category_title: _('Project Templates'))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20790,9 +20790,6 @@ msgstr ""
|
||||||
msgid "Pipelines|More Information"
|
msgid "Pipelines|More Information"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|No CI file found in this repository, please add one."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
|
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20805,9 +20802,6 @@ msgstr ""
|
||||||
msgid "Pipelines|Project cache successfully reset."
|
msgid "Pipelines|Project cache successfully reset."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|Repository does not have a default branch, please set one."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Pipelines|Revoke"
|
msgid "Pipelines|Revoke"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20829,6 +20823,9 @@ msgstr ""
|
||||||
msgid "Pipelines|There are currently no pipelines."
|
msgid "Pipelines|There are currently no pipelines."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
|
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -21882,6 +21879,9 @@ msgstr ""
|
||||||
msgid "Project ID"
|
msgid "Project ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Project Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Project URL"
|
msgid "Project URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -41,8 +41,8 @@ tests = [
|
||||||
|
|
||||||
{
|
{
|
||||||
explanation: 'Tooling should map to respective spec',
|
explanation: 'Tooling should map to respective spec',
|
||||||
source: 'tooling/lib/tooling/test_file_finder.rb',
|
source: 'tooling/lib/tooling/helm3_client.rb',
|
||||||
expected: ['spec/tooling/lib/tooling/test_file_finder_spec.rb']
|
expected: ['spec/tooling/lib/tooling/helm3_client_spec.rb']
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -160,12 +160,12 @@ RSpec.describe Projects::TemplatesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'template names request' do
|
shared_examples 'template names request' do
|
||||||
it 'returns the template names' do
|
it 'returns the template names', :aggregate_failures do
|
||||||
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
|
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response.size).to eq(2)
|
expect(json_response['Project Templates'].size).to eq(2)
|
||||||
expect(json_response).to match(expected_template_names)
|
expect(json_response['Project Templates'].map { |x| { "name" => x['name'] } }).to match(expected_template_names)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'fails for user with no access' do
|
it 'fails for user with no access' do
|
||||||
|
|
|
@ -41,7 +41,7 @@ RSpec.describe 'issue state', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'when open' do
|
describe 'when open', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297348' do
|
||||||
context 'when clicking the top `Close issue` button', :aggregate_failures do
|
context 'when clicking the top `Close issue` button', :aggregate_failures do
|
||||||
let(:open_issue) { create(:issue, project: project) }
|
let(:open_issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ RSpec.describe 'issue state', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'when closed' do
|
describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297201' do
|
||||||
context 'when clicking the top `Reopen issue` button', :aggregate_failures do
|
context 'when clicking the top `Reopen issue` button', :aggregate_failures do
|
||||||
let(:closed_issue) { create(:issue, project: project, state: 'closed') }
|
let(:closed_issue) { create(:issue, project: project, state: 'closed') }
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ RSpec.describe 'User creates release', :js do
|
||||||
expect(page.find('.ref-selector button')).to have_content(project.default_branch)
|
expect(page.find('.ref-selector button')).to have_content(project.default_branch)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the "Save release" button is clicked' do
|
context 'when the "Save release" button is clicked', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297507' do
|
||||||
let(:tag_name) { 'v1.0' }
|
let(:tag_name) { 'v1.0' }
|
||||||
let(:release_title) { 'A most magnificent release' }
|
let(:release_title) { 'A most magnificent release' }
|
||||||
let(:release_notes) { 'Best. Release. **Ever.** :rocket:' }
|
let(:release_notes) { 'Best. Release. **Ever.** :rocket:' }
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { languages } from 'monaco-editor';
|
import { languages } from 'monaco-editor';
|
||||||
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
import EditorLite from '~/editor/editor_lite';
|
import EditorLite from '~/editor/editor_lite';
|
||||||
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
|
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
|
||||||
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
|
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
|
||||||
|
@ -9,6 +10,7 @@ describe('~/editor/editor_ci_config_ext', () => {
|
||||||
let editor;
|
let editor;
|
||||||
let instance;
|
let instance;
|
||||||
let editorEl;
|
let editorEl;
|
||||||
|
let originalGitlabUrl;
|
||||||
|
|
||||||
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
|
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
|
||||||
setFixtures('<div id="editor"></div>');
|
setFixtures('<div id="editor"></div>');
|
||||||
|
@ -22,6 +24,15 @@ describe('~/editor/editor_ci_config_ext', () => {
|
||||||
instance.use(new CiSchemaExtension());
|
instance.use(new CiSchemaExtension());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
originalGitlabUrl = gon.gitlab_url;
|
||||||
|
gon.gitlab_url = TEST_HOST;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
gon.gitlab_url = originalGitlabUrl;
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createMockEditor();
|
createMockEditor();
|
||||||
});
|
});
|
||||||
|
@ -73,7 +84,7 @@ describe('~/editor/editor_ci_config_ext', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getConfiguredYmlSchema()).toEqual({
|
expect(getConfiguredYmlSchema()).toEqual({
|
||||||
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
|
uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
|
||||||
fileMatch: [defaultBlobPath],
|
fileMatch: [defaultBlobPath],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -87,7 +98,7 @@ describe('~/editor/editor_ci_config_ext', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getConfiguredYmlSchema()).toEqual({
|
expect(getConfiguredYmlSchema()).toEqual({
|
||||||
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
|
uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
|
||||||
fileMatch: ['another-ci-filename.yml'],
|
fileMatch: ['another-ci-filename.yml'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -75,7 +75,7 @@ describe('IDE store file actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes file & opens next available file', () => {
|
it('switches to the next available file before closing the current one ', () => {
|
||||||
const f = file('newOpenFile');
|
const f = file('newOpenFile');
|
||||||
|
|
||||||
store.state.openFiles.push(f);
|
store.state.openFiles.push(f);
|
||||||
|
@ -90,10 +90,12 @@ describe('IDE store file actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes file if it pending', () => {
|
it('removes file if it pending', () => {
|
||||||
store.state.openFiles.push({
|
store.state.openFiles = [
|
||||||
|
{
|
||||||
...localFile,
|
...localFile,
|
||||||
pending: true,
|
pending: true,
|
||||||
});
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return store.dispatch('closeFile', localFile).then(() => {
|
return store.dispatch('closeFile', localFile).then(() => {
|
||||||
expect(store.state.openFiles.length).toBe(0);
|
expect(store.state.openFiles.length).toBe(0);
|
||||||
|
|
|
@ -35,7 +35,7 @@ exports[`Alert integration settings form default state should match the default
|
||||||
Incident template (optional)
|
Incident template (optional)
|
||||||
|
|
||||||
<gl-link-stub
|
<gl-link-stub
|
||||||
href="/help/user/project/description_templates#creating-issue-templates"
|
href="/help/user/project/description_templates#create-an-issue-template"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<gl-icon-stub
|
<gl-icon-stub
|
||||||
|
|
|
@ -423,7 +423,9 @@ describe('Issuable output', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the form if template names request is successful', () => {
|
it('shows the form if template names request is successful', () => {
|
||||||
const mockData = [{ name: 'Bug' }];
|
const mockData = {
|
||||||
|
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
|
||||||
|
};
|
||||||
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
|
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
|
||||||
|
|
||||||
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
|
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
|
||||||
|
|
|
@ -14,7 +14,10 @@ describe('Issue description template component', () => {
|
||||||
vm = new Component({
|
vm = new Component({
|
||||||
propsData: {
|
propsData: {
|
||||||
formState,
|
formState,
|
||||||
issuableTemplates: [{ name: 'test' }],
|
issuableTemplates: {
|
||||||
|
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
|
||||||
|
},
|
||||||
|
projectId: 1,
|
||||||
projectPath: '/',
|
projectPath: '/',
|
||||||
projectNamespace: '/',
|
projectNamespace: '/',
|
||||||
},
|
},
|
||||||
|
@ -23,7 +26,7 @@ describe('Issue description template component', () => {
|
||||||
|
|
||||||
it('renders templates as JSON array in data attribute', () => {
|
it('renders templates as JSON array in data attribute', () => {
|
||||||
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
|
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
|
||||||
'[{"name":"test"}]',
|
'{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ describe('Inline edit form component', () => {
|
||||||
markdownPreviewPath: '/',
|
markdownPreviewPath: '/',
|
||||||
markdownDocsPath: '/',
|
markdownDocsPath: '/',
|
||||||
projectPath: '/',
|
projectPath: '/',
|
||||||
|
projectId: 1,
|
||||||
projectNamespace: '/',
|
projectNamespace: '/',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,7 +43,11 @@ describe('Inline edit form component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders template selector when templates exists', () => {
|
it('renders template selector when templates exists', () => {
|
||||||
createComponent({ issuableTemplates: ['test'] });
|
createComponent({
|
||||||
|
issuableTemplates: {
|
||||||
|
test: [{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
|
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const appProps = {
|
||||||
markdownDocsPath: '/',
|
markdownDocsPath: '/',
|
||||||
projectNamespace: '/',
|
projectNamespace: '/',
|
||||||
projectPath: '/',
|
projectPath: '/',
|
||||||
|
projectId: 1,
|
||||||
issuableTemplateNamesPath: '/issuable-templates-path',
|
issuableTemplateNamesPath: '/issuable-templates-path',
|
||||||
zoomMeetingUrl,
|
zoomMeetingUrl,
|
||||||
publishedIncidentUrl,
|
publishedIncidentUrl,
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
|
||||||
const findAlert = () => wrapper.find(GlAlert);
|
const findAlert = () => wrapper.find(GlAlert);
|
||||||
const findLintParameters = () => findAllByTestId('ci-lint-parameter');
|
const findLintParameters = () => findAllByTestId('ci-lint-parameter');
|
||||||
const findLintParameterAt = (i) => findLintParameters().at(i);
|
const findLintParameterAt = (i) => findLintParameters().at(i);
|
||||||
|
const findLintValueAt = (i) => findAllByTestId('ci-lint-value').at(i);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
@ -50,6 +51,20 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
|
||||||
expect(findLintParameterAt(2).text()).toBe('Build Job - job_build');
|
expect(findLintParameterAt(2).text()).toBe('Build Job - job_build');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('displays jobs details', () => {
|
||||||
|
expect(findLintParameters()).toHaveLength(3);
|
||||||
|
|
||||||
|
expect(findLintValueAt(0).text()).toMatchInterpolatedText(
|
||||||
|
'echo "test 1" Only policy: branches, tags When: on_success',
|
||||||
|
);
|
||||||
|
expect(findLintValueAt(1).text()).toMatchInterpolatedText(
|
||||||
|
'echo "test 2" Only policy: branches, tags When: on_success',
|
||||||
|
);
|
||||||
|
expect(findLintValueAt(2).text()).toMatchInterpolatedText(
|
||||||
|
'echo "build" Only policy: branches, tags When: on_success',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('displays invalid results', () => {
|
it('displays invalid results', () => {
|
||||||
createComponent(
|
createComponent(
|
||||||
{
|
{
|
||||||
|
|
|
@ -27,7 +27,7 @@ Object {
|
||||||
"echo 'script 1'",
|
"echo 'script 1'",
|
||||||
],
|
],
|
||||||
"stage": "test",
|
"stage": "test",
|
||||||
"tagList": Array [
|
"tags": Array [
|
||||||
"tag 1",
|
"tag 1",
|
||||||
],
|
],
|
||||||
"when": "on_success",
|
"when": "on_success",
|
||||||
|
@ -61,7 +61,7 @@ Object {
|
||||||
"echo 'script 2'",
|
"echo 'script 2'",
|
||||||
],
|
],
|
||||||
"stage": "test",
|
"stage": "test",
|
||||||
"tagList": Array [
|
"tags": Array [
|
||||||
"tag 2",
|
"tag 2",
|
||||||
],
|
],
|
||||||
"when": "on_success",
|
"when": "on_success",
|
||||||
|
|
|
@ -35,6 +35,21 @@ job_build:
|
||||||
needs: ["job_test_2"]
|
needs: ["job_test_2"]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const mockJobFields = {
|
||||||
|
beforeScript: [],
|
||||||
|
afterScript: [],
|
||||||
|
environment: null,
|
||||||
|
allowFailure: false,
|
||||||
|
tags: [],
|
||||||
|
when: 'on_success',
|
||||||
|
only: { refs: ['branches', 'tags'], __typename: 'CiJobLimitType' },
|
||||||
|
except: null,
|
||||||
|
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
|
||||||
|
__typename: 'CiConfigJob',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock result of the graphql query at:
|
||||||
|
// app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
|
||||||
export const mockCiConfigQueryResponse = {
|
export const mockCiConfigQueryResponse = {
|
||||||
data: {
|
data: {
|
||||||
ciConfig: {
|
ciConfig: {
|
||||||
|
@ -54,8 +69,8 @@ export const mockCiConfigQueryResponse = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
name: 'job_test_1',
|
name: 'job_test_1',
|
||||||
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
|
script: ['echo "test 1"'],
|
||||||
__typename: 'CiConfigJob',
|
...mockJobFields,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
__typename: 'CiConfigJobConnection',
|
__typename: 'CiConfigJobConnection',
|
||||||
|
@ -69,9 +84,8 @@ export const mockCiConfigQueryResponse = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
name: 'job_test_2',
|
name: 'job_test_2',
|
||||||
|
script: ['echo "test 2"'],
|
||||||
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
|
...mockJobFields,
|
||||||
__typename: 'CiConfigJob',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
__typename: 'CiConfigJobConnection',
|
__typename: 'CiConfigJobConnection',
|
||||||
|
@ -94,11 +108,8 @@ export const mockCiConfigQueryResponse = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
name: 'job_build',
|
name: 'job_build',
|
||||||
needs: {
|
script: ['echo "build"'],
|
||||||
nodes: [{ name: 'job_test_2', __typename: 'CiConfigNeed' }],
|
...mockJobFields,
|
||||||
__typename: 'CiConfigNeedConnection',
|
|
||||||
},
|
|
||||||
__typename: 'CiConfigJob',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
__typename: 'CiConfigJobConnection',
|
__typename: 'CiConfigJobConnection',
|
||||||
|
|
|
@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||||
|
|
||||||
|
import httpStatusCodes from '~/lib/utils/http_status';
|
||||||
import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
|
import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||||
import {
|
import {
|
||||||
mockCiConfigPath,
|
mockCiConfigPath,
|
||||||
|
@ -414,11 +415,19 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
|
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('no error is shown when data is set', async () => {
|
describe('when file exists', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
createComponentWithApollo();
|
createComponentWithApollo();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows editor and commit form', () => {
|
||||||
|
expect(findEditorLite().exists()).toBe(true);
|
||||||
|
expect(findTextEditor().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no error is shown when data is set', async () => {
|
||||||
expect(findAlert().exists()).toBe(false);
|
expect(findAlert().exists()).toBe(false);
|
||||||
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
|
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
|
||||||
});
|
});
|
||||||
|
@ -433,31 +442,45 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
projectPath: mockProjectFullPath,
|
projectPath: mockProjectFullPath,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when no file exists', () => {
|
||||||
|
const expectedAlertMsg =
|
||||||
|
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
|
||||||
|
|
||||||
|
it('does not show editor or commit form', async () => {
|
||||||
|
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
|
||||||
|
createComponentWithApollo();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findEditorLite().exists()).toBe(false);
|
||||||
|
expect(findTextEditor().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows a 404 error message', async () => {
|
it('shows a 404 error message', async () => {
|
||||||
mockBlobContentData.mockRejectedValueOnce({
|
mockBlobContentData.mockRejectedValueOnce({
|
||||||
response: {
|
response: {
|
||||||
status: 404,
|
status: httpStatusCodes.NOT_FOUND,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
createComponentWithApollo();
|
createComponentWithApollo();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(findAlert().text()).toBe('No CI file found in this repository, please add one.');
|
expect(findAlert().text()).toBe(expectedAlertMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a 400 error message', async () => {
|
it('shows a 400 error message', async () => {
|
||||||
mockBlobContentData.mockRejectedValueOnce({
|
mockBlobContentData.mockRejectedValueOnce({
|
||||||
response: {
|
response: {
|
||||||
status: 400,
|
status: httpStatusCodes.BAD_REQUEST,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
createComponentWithApollo();
|
createComponentWithApollo();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(findAlert().text()).toBe('Repository does not have a default branch, please set one.');
|
expect(findAlert().text()).toBe(expectedAlertMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a unkown error message', async () => {
|
it('shows a unkown error message', async () => {
|
||||||
|
@ -468,4 +491,5 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
|
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe IssuablesDescriptionTemplatesHelper do
|
||||||
|
include_context 'project issuable templates context'
|
||||||
|
|
||||||
|
describe '#issuable_templates' do
|
||||||
|
let_it_be(:inherited_from) { nil }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:parent_group) { create(:group) }
|
||||||
|
let_it_be(:project) { create(:project, :custom_repo, files: issuable_template_files) }
|
||||||
|
let_it_be(:group_member) { create(:group_member, :developer, group: parent_group, user: user) }
|
||||||
|
let_it_be(:project_member) { create(:project_member, :developer, user: user, project: project) }
|
||||||
|
|
||||||
|
context 'when project has no parent group' do
|
||||||
|
it_behaves_like 'project issuable templates'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project has parent group' do
|
||||||
|
before do
|
||||||
|
project.update!(group: parent_group)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project parent group does not have a file template project' do
|
||||||
|
it_behaves_like 'project issuable templates'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project parent group has a file template project' do
|
||||||
|
let_it_be(:file_template_project) { create(:project, :custom_repo, group: parent_group, files: issuable_template_files) }
|
||||||
|
let_it_be(:group) { create(:group, parent: parent_group) }
|
||||||
|
let_it_be(:project) { create(:project, :custom_repo, group: group, files: issuable_template_files) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.update!(group: group)
|
||||||
|
parent_group.update_columns(file_template_project_id: file_template_project.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project issuable templates'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -199,6 +199,7 @@ RSpec.describe IssuablesHelper do
|
||||||
markdownDocsPath: '/help/user/markdown',
|
markdownDocsPath: '/help/user/markdown',
|
||||||
lockVersion: issue.lock_version,
|
lockVersion: issue.lock_version,
|
||||||
projectPath: @project.path,
|
projectPath: @project.path,
|
||||||
|
projectId: @project.id,
|
||||||
projectNamespace: @project.namespace.path,
|
projectNamespace: @project.namespace.path,
|
||||||
initialTitleHtml: issue.title,
|
initialTitleHtml: issue.title,
|
||||||
initialTitleText: issue.title,
|
initialTitleText: issue.title,
|
||||||
|
|
|
@ -57,6 +57,6 @@ RSpec.describe LicenseTemplate do
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_template(content)
|
def build_template(content)
|
||||||
described_class.new(key: 'foo', name: 'foo', category: :Other, content: content)
|
described_class.new(key: 'foo', name: 'foo', project: nil, category: :Other, content: content)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2977,56 +2977,9 @@ RSpec.describe Project, factory_default: :keep do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#pushes_since_gc' do
|
it_behaves_like 'can housekeep repository' do
|
||||||
let(:project) { build_stubbed(:project) }
|
let(:resource) { build_stubbed(:project) }
|
||||||
|
let(:resource_key) { 'projects' }
|
||||||
after do
|
|
||||||
project.reset_pushes_since_gc
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'without any pushes' do
|
|
||||||
it 'returns 0' do
|
|
||||||
expect(project.pushes_since_gc).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a number of pushes' do
|
|
||||||
it 'returns the number of pushes' do
|
|
||||||
3.times { project.increment_pushes_since_gc }
|
|
||||||
|
|
||||||
expect(project.pushes_since_gc).to eq(3)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#increment_pushes_since_gc' do
|
|
||||||
let(:project) { build_stubbed(:project) }
|
|
||||||
|
|
||||||
after do
|
|
||||||
project.reset_pushes_since_gc
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'increments the number of pushes since the last GC' do
|
|
||||||
3.times { project.increment_pushes_since_gc }
|
|
||||||
|
|
||||||
expect(project.pushes_since_gc).to eq(3)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#reset_pushes_since_gc' do
|
|
||||||
let(:project) { build_stubbed(:project) }
|
|
||||||
|
|
||||||
after do
|
|
||||||
project.reset_pushes_since_gc
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resets the number of pushes since the last GC' do
|
|
||||||
3.times { project.increment_pushes_since_gc }
|
|
||||||
|
|
||||||
project.reset_pushes_since_gc
|
|
||||||
|
|
||||||
expect(project.pushes_since_gc).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#deployment_variables' do
|
describe '#deployment_variables' do
|
||||||
|
|
|
@ -19,12 +19,8 @@ RSpec.describe MergeRequests::MergeService do
|
||||||
{ commit_message: 'Awesome message', sha: merge_request.diff_head_sha }
|
{ commit_message: 'Awesome message', sha: merge_request.diff_head_sha }
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:feature_flag_persist_squash) { true }
|
|
||||||
|
|
||||||
context 'valid params' do
|
context 'valid params' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(persist_squash_commit_sha_for_squashes: feature_flag_persist_squash)
|
|
||||||
|
|
||||||
allow(service).to receive(:execute_hooks)
|
allow(service).to receive(:execute_hooks)
|
||||||
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
|
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
|
||||||
|
|
||||||
|
@ -90,14 +86,6 @@ RSpec.describe MergeRequests::MergeService do
|
||||||
|
|
||||||
expect(merge_request.squash_commit_sha).to eq(squash_commit.id)
|
expect(merge_request.squash_commit_sha).to eq(squash_commit.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when feature flag is disabled' do
|
|
||||||
let(:feature_flag_persist_squash) { false }
|
|
||||||
|
|
||||||
it 'does not populate squash_commit_sha' do
|
|
||||||
expect(merge_request.squash_commit_sha).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_context 'project issuable templates context' do
|
||||||
|
let_it_be(:issuable_template_files) do
|
||||||
|
{
|
||||||
|
'.gitlab/issue_templates/issue-bar.md' => 'Issue Template Bar',
|
||||||
|
'.gitlab/issue_templates/issue-foo.md' => 'Issue Template Foo',
|
||||||
|
'.gitlab/issue_templates/issue-bad.txt' => 'Issue Template Bad',
|
||||||
|
'.gitlab/issue_templates/issue-baz.xyz' => 'Issue Template Baz',
|
||||||
|
|
||||||
|
'.gitlab/merge_request_templates/merge_request-bar.md' => 'Merge Request Template Bar',
|
||||||
|
'.gitlab/merge_request_templates/merge_request-foo.md' => 'Merge Request Template Foo',
|
||||||
|
'.gitlab/merge_request_templates/merge_request-bad.txt' => 'Merge Request Template Bad',
|
||||||
|
'.gitlab/merge_request_templates/merge_request-baz.xyz' => 'Merge Request Template Baz'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'project issuable templates' do
|
||||||
|
context 'issuable templates' do
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:current_user).and_return(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only md files as issue templates' do
|
||||||
|
expect(helper.issuable_templates(project, 'issue')).to eq(expected_templates('issue'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only md files as merge_request templates' do
|
||||||
|
expect(helper.issuable_templates(project, 'merge_request')).to eq(expected_templates('merge_request'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expected_templates(issuable_type)
|
||||||
|
expectation = {}
|
||||||
|
|
||||||
|
expectation["Project Templates"] = templates(issuable_type, project)
|
||||||
|
expectation["Group #{inherited_from.namespace.full_name}"] = templates(issuable_type, inherited_from) if inherited_from.present?
|
||||||
|
|
||||||
|
expectation
|
||||||
|
end
|
||||||
|
|
||||||
|
def templates(issuable_type, inherited_from)
|
||||||
|
[
|
||||||
|
{ id: "#{issuable_type}-bar", name: "#{issuable_type}-bar", project_id: inherited_from.id },
|
||||||
|
{ id: "#{issuable_type}-foo", name: "#{issuable_type}-foo", project_id: inherited_from.id }
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,45 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_examples 'can housekeep repository' do
|
||||||
|
context 'with a clean redis state', :clean_gitlab_redis_shared_state do
|
||||||
|
describe '#pushes_since_gc' do
|
||||||
|
context 'without any pushes' do
|
||||||
|
it 'returns 0' do
|
||||||
|
expect(resource.pushes_since_gc).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a number of pushes' do
|
||||||
|
it 'returns the number of pushes' do
|
||||||
|
3.times { resource.increment_pushes_since_gc }
|
||||||
|
|
||||||
|
expect(resource.pushes_since_gc).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#increment_pushes_since_gc' do
|
||||||
|
it 'increments the number of pushes since the last GC' do
|
||||||
|
3.times { resource.increment_pushes_since_gc }
|
||||||
|
|
||||||
|
expect(resource.pushes_since_gc).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#reset_pushes_since_gc' do
|
||||||
|
it 'resets the number of pushes since the last GC' do
|
||||||
|
3.times { resource.increment_pushes_since_gc }
|
||||||
|
|
||||||
|
resource.reset_pushes_since_gc
|
||||||
|
|
||||||
|
expect(resource.pushes_since_gc).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#pushes_since_gc_redis_shared_state_key' do
|
||||||
|
it 'returns the proper redis key format' do
|
||||||
|
expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,175 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative '../../../../tooling/lib/tooling/test_file_finder'
|
|
||||||
|
|
||||||
RSpec.describe Tooling::TestFileFinder do
|
|
||||||
subject { described_class.new(file) }
|
|
||||||
|
|
||||||
describe '#test_files' do
|
|
||||||
context 'when given non .rb files' do
|
|
||||||
let(:file) { 'app/assets/images/emoji.png' }
|
|
||||||
|
|
||||||
it 'does not return a test file' do
|
|
||||||
expect(subject.test_files).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given file in app/' do
|
|
||||||
let(:file) { 'app/finders/admin/projects_finder.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching app spec file' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/finders/admin/projects_finder_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given file in lib/' do
|
|
||||||
let(:file) { 'lib/banzai/color_parser.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching app spec file' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a file in tooling/' do
|
|
||||||
let(:file) { 'tooling/lib/tooling/test_file_finder.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching tooling test' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/tooling/lib/tooling/test_file_finder_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a test file' do
|
|
||||||
let(:file) { 'spec/lib/banzai/color_parser_spec.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching test file itself' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given an app file in ee/' do
|
|
||||||
let(:file) { 'ee/app/models/analytics/cycle_analytics/group_level.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching ee/ test file' do
|
|
||||||
expect(subject.test_files).to contain_exactly('ee/spec/models/analytics/cycle_analytics/group_level_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given an ee extension module file' do
|
|
||||||
let(:file) { 'ee/app/models/ee/user.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching ee/ class test file, ee extension module test file and the foss class test file' do
|
|
||||||
test_files = ['ee/spec/models/user_spec.rb', 'ee/spec/models/ee/user_spec.rb', 'spec/app/models/user_spec.rb']
|
|
||||||
expect(subject.test_files).to contain_exactly(*test_files)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a test file in ee/' do
|
|
||||||
let(:file) { 'ee/spec/models/container_registry/event_spec.rb' }
|
|
||||||
|
|
||||||
it 'returns the test file itself' do
|
|
||||||
expect(subject.test_files).to contain_exactly('ee/spec/models/container_registry/event_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a module test file in ee/' do
|
|
||||||
let(:file) { 'ee/spec/models/ee/appearance_spec.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching module test file itself and the corresponding spec model test file' do
|
|
||||||
test_files = ['ee/spec/models/ee/appearance_spec.rb', 'spec/models/appearance_spec.rb']
|
|
||||||
expect(subject.test_files).to contain_exactly(*test_files)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a factory file' do
|
|
||||||
let(:file) { 'spec/factories/users.rb' }
|
|
||||||
|
|
||||||
it 'returns spec/factories_spec.rb file' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/factories_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given an ee factory file' do
|
|
||||||
let(:file) { 'ee/spec/factories/users.rb' }
|
|
||||||
|
|
||||||
it 'returns spec/factories_spec.rb file' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/factories_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given db/structure.sql' do
|
|
||||||
let(:file) { 'db/structure.sql' }
|
|
||||||
|
|
||||||
it 'returns spec/db/schema_spec.rb' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/db/schema_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given an initializer' do
|
|
||||||
let(:file) { 'config/initializers/action_mailer_hooks.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching initializer spec' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/initializers/action_mailer_hooks_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a haml view' do
|
|
||||||
let(:file) { 'app/views/admin/users/_user.html.haml' }
|
|
||||||
|
|
||||||
it 'returns the matching view spec' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/views/admin/users/_user.html.haml_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a haml view in ee/' do
|
|
||||||
let(:file) { 'ee/app/views/admin/users/_user.html.haml' }
|
|
||||||
|
|
||||||
it 'returns the matching view spec' do
|
|
||||||
expect(subject.test_files).to contain_exactly('ee/spec/views/admin/users/_user.html.haml_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a migration file' do
|
|
||||||
let(:file) { 'db/migrate/20191023152913_add_default_and_free_plans.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching migration spec' do
|
|
||||||
test_files = %w[
|
|
||||||
spec/migrations/add_default_and_free_plans_spec.rb
|
|
||||||
spec/migrations/20191023152913_add_default_and_free_plans_spec.rb
|
|
||||||
]
|
|
||||||
expect(subject.test_files).to contain_exactly(*test_files)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given a post-migration file' do
|
|
||||||
let(:file) { 'db/post_migrate/20200608072931_backfill_imported_snippet_repositories.rb' }
|
|
||||||
|
|
||||||
it 'returns the matching migration spec' do
|
|
||||||
test_files = %w[
|
|
||||||
spec/migrations/backfill_imported_snippet_repositories_spec.rb
|
|
||||||
spec/migrations/20200608072931_backfill_imported_snippet_repositories_spec.rb
|
|
||||||
]
|
|
||||||
expect(subject.test_files).to contain_exactly(*test_files)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with foss_test_only: true' do
|
|
||||||
subject { Tooling::TestFileFinder.new(file, foss_test_only: true) }
|
|
||||||
|
|
||||||
context 'when given a module file in ee/' do
|
|
||||||
let(:file) { 'ee/app/models/ee/user.rb' }
|
|
||||||
|
|
||||||
it 'returns only the corresponding spec model test file in foss' do
|
|
||||||
expect(subject.test_files).to contain_exactly('spec/app/models/user_spec.rb')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when given an app file in ee/' do
|
|
||||||
let(:file) { 'ee/app/models/approval.rb' }
|
|
||||||
|
|
||||||
it 'returns no test file in foss' do
|
|
||||||
expect(subject.test_files).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,94 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'set'
|
|
||||||
|
|
||||||
module Tooling
|
|
||||||
class TestFileFinder
|
|
||||||
EE_PREFIX = 'ee/'
|
|
||||||
|
|
||||||
def initialize(file, foss_test_only: false)
|
|
||||||
@file = file
|
|
||||||
@foss_test_only = foss_test_only
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_files
|
|
||||||
impacted_tests = ee_impact | non_ee_impact | either_impact
|
|
||||||
impacted_tests.impact(@file)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
attr_reader :file, :foss_test_only, :result
|
|
||||||
|
|
||||||
class ImpactedTestFile
|
|
||||||
attr_reader :pattern_matchers
|
|
||||||
|
|
||||||
def initialize(prefix: nil)
|
|
||||||
@pattern_matchers = {}
|
|
||||||
@prefix = prefix
|
|
||||||
|
|
||||||
yield self if block_given?
|
|
||||||
end
|
|
||||||
|
|
||||||
def associate(pattern, &block)
|
|
||||||
@pattern_matchers[%r{^#{@prefix}#{pattern}}] = block
|
|
||||||
end
|
|
||||||
|
|
||||||
def impact(file)
|
|
||||||
@pattern_matchers.each_with_object(Set.new) do |(pattern, block), result|
|
|
||||||
if (match = pattern.match(file))
|
|
||||||
test_files = block.call(match)
|
|
||||||
result.merge(Array(test_files))
|
|
||||||
end
|
|
||||||
end.to_a
|
|
||||||
end
|
|
||||||
|
|
||||||
def |(other)
|
|
||||||
self.class.new do |combined_matcher|
|
|
||||||
self.pattern_matchers.each do |pattern, block|
|
|
||||||
combined_matcher.associate(pattern, &block)
|
|
||||||
end
|
|
||||||
other.pattern_matchers.each do |pattern, block|
|
|
||||||
combined_matcher.associate(pattern, &block)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def ee_impact
|
|
||||||
ImpactedTestFile.new(prefix: EE_PREFIX) do |impact|
|
|
||||||
unless foss_test_only
|
|
||||||
impact.associate(%r{app/(.+)\.rb$}) { |match| "#{EE_PREFIX}spec/#{match[1]}_spec.rb" }
|
|
||||||
impact.associate(%r{app/(.*/)ee/(.+)\.rb$}) { |match| "#{EE_PREFIX}spec/#{match[1]}#{match[2]}_spec.rb" }
|
|
||||||
impact.associate(%r{lib/(.+)\.rb$}) { |match| "#{EE_PREFIX}spec/lib/#{match[1]}_spec.rb" }
|
|
||||||
end
|
|
||||||
|
|
||||||
impact.associate(%r{(?!spec)(.*/)ee/(.+)\.rb$}) { |match| "spec/#{match[1]}#{match[2]}_spec.rb" }
|
|
||||||
impact.associate(%r{spec/(.*/)ee/(.+)\.rb$}) { |match| "spec/#{match[1]}#{match[2]}.rb" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def non_ee_impact
|
|
||||||
ImpactedTestFile.new do |impact|
|
|
||||||
impact.associate(%r{app/(.+)\.rb$}) { |match| "spec/#{match[1]}_spec.rb" }
|
|
||||||
impact.associate(%r{(tooling/)?lib/(.+)\.rb$}) { |match| "spec/#{match[1]}lib/#{match[2]}_spec.rb" }
|
|
||||||
impact.associate(%r{config/initializers/(.+)\.rb$}) { |match| "spec/initializers/#{match[1]}_spec.rb" }
|
|
||||||
impact.associate('db/structure.sql') { 'spec/db/schema_spec.rb' }
|
|
||||||
impact.associate(%r{db/(?:post_)?migrate/([0-9]+)_(.+)\.rb$}) do |match|
|
|
||||||
[
|
|
||||||
"spec/migrations/#{match[2]}_spec.rb",
|
|
||||||
"spec/migrations/#{match[1]}_#{match[2]}_spec.rb"
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def either_impact
|
|
||||||
ImpactedTestFile.new(prefix: %r{^(?<prefix>#{EE_PREFIX})?}) do |impact|
|
|
||||||
impact.associate(%r{app/views/(?<view>.+)\.haml$}) { |match| "#{match[:prefix]}spec/views/#{match[:view]}.haml_spec.rb" }
|
|
||||||
impact.associate(%r{spec/(.+)_spec\.rb$}) { |match| match[0] }
|
|
||||||
impact.associate(%r{spec/factories/.+\.rb$}) { 'spec/factories_spec.rb' }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue