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'
|
||||
|
||||
# LabKit: Tracing and Correlation
|
||||
gem 'gitlab-labkit', '0.13.5'
|
||||
gem 'gitlab-labkit', '0.14.0'
|
||||
|
||||
# I18n
|
||||
gem 'ruby_parser', '~> 3.15', require: false
|
||||
|
|
|
@ -432,9 +432,9 @@ GEM
|
|||
fog-json (~> 1.2.0)
|
||||
mime-types
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
gitlab-labkit (0.13.5)
|
||||
actionpack (>= 5.0.0, < 6.1.0)
|
||||
activesupport (>= 5.0.0, < 6.1.0)
|
||||
gitlab-labkit (0.14.0)
|
||||
actionpack (>= 5.0.0, < 7.0.0)
|
||||
activesupport (>= 5.0.0, < 7.0.0)
|
||||
gitlab-pg_query (~> 1.3)
|
||||
grpc (~> 1.19)
|
||||
jaeger-client (~> 1.1)
|
||||
|
@ -1363,7 +1363,7 @@ DEPENDENCIES
|
|||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-experiment (~> 0.4.4)
|
||||
gitlab-fog-azure-rm (~> 1.0)
|
||||
gitlab-labkit (= 0.13.5)
|
||||
gitlab-labkit (= 0.14.0)
|
||||
gitlab-license (~> 1.0)
|
||||
gitlab-mail_room (~> 0.0.8)
|
||||
gitlab-markup (~> 1.7.1)
|
||||
|
|
|
@ -66,6 +66,8 @@ export default class FileTemplateSelector {
|
|||
reportSelectionName(options) {
|
||||
const opts = options;
|
||||
opts.query = options.selectedObj.name;
|
||||
opts.data = options.selectedObj;
|
||||
opts.data.source_template_project_id = options.selectedObj.project_id;
|
||||
|
||||
this.reportSelection(opts);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
|
|||
const data = {
|
||||
project: this.$dropdown.data('project'),
|
||||
fullname: this.$dropdown.data('fullname'),
|
||||
source_template_project_id: query.project_id,
|
||||
};
|
||||
|
||||
this.reportSelection({
|
||||
|
|
|
@ -4,7 +4,7 @@ import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
|
|||
import boardsStore from '../stores/boards_store';
|
||||
|
||||
export default {
|
||||
name: 'BoardsIssueCard',
|
||||
name: 'BoardCardLayout',
|
||||
components: {
|
||||
IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ export default {
|
|||
:data-issue-iid="issue.iid"
|
||||
:data-issue-path="issue.referencePath"
|
||||
data-testid="board_card"
|
||||
class="board-card p-3 rounded"
|
||||
class="board-card gl-p-5 gl-rounded-base"
|
||||
@mousedown="mouseDown"
|
||||
@mousemove="mouseMove"
|
||||
@mouseup="showIssue($event)"
|
||||
|
|
|
@ -437,6 +437,7 @@ export class GitLabDropdown {
|
|||
groupName = el.data('group');
|
||||
if (groupName) {
|
||||
selectedIndex = el.data('index');
|
||||
this.selectedIndex = selectedIndex;
|
||||
selectedObject = this.renderedData[groupName][selectedIndex];
|
||||
} else {
|
||||
selectedIndex = el.closest('li').index();
|
||||
|
|
|
@ -17,15 +17,21 @@ export class CiSchemaExtension extends EditorLiteExtension {
|
|||
* @param {String?} opts.ref - Current ref. Defaults to master
|
||||
*/
|
||||
registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) {
|
||||
const ciSchemaUri = Api.buildUrl(Api.projectFileSchemaPath)
|
||||
const ciSchemaPath = Api.buildUrl(Api.projectFileSchemaPath)
|
||||
.replace(':namespace_path', projectNamespace)
|
||||
.replace(':project_path', projectPath)
|
||||
.replace(':ref', ref)
|
||||
.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();
|
||||
|
||||
registerSchema({
|
||||
uri: ciSchemaUri,
|
||||
uri: absoluteSchemaUrl,
|
||||
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 fileWasActive = file.active;
|
||||
|
||||
if (file.pending) {
|
||||
commit(types.REMOVE_PENDING_TAB, file);
|
||||
} 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;
|
||||
if (state.openFiles.length > 1 && fileWasActive) {
|
||||
const nextIndexToOpen = indexOfClosedFile === 0 ? 1 : indexOfClosedFile - 1;
|
||||
const nextFileToOpen = state.openFiles[nextIndexToOpen];
|
||||
|
||||
if (nextFileToOpen.pending) {
|
||||
|
@ -35,14 +28,22 @@ export const closeFile = ({ commit, state, dispatch, getters }, file) => {
|
|||
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
|
||||
});
|
||||
} else {
|
||||
dispatch('setFileActive', nextFileToOpen.path);
|
||||
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}/`, {
|
||||
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}`);
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec
|
|||
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
|
||||
'/help/operations/metrics/alerts#trigger-actions-from-alerts';
|
||||
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 */
|
||||
|
||||
|
|
|
@ -132,6 +132,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
projectNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -303,7 +307,7 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
updateAndShowForm(templates = []) {
|
||||
updateAndShowForm(templates = {}) {
|
||||
if (!this.showForm) {
|
||||
this.showForm = true;
|
||||
this.store.setFormState({
|
||||
|
@ -419,6 +423,7 @@ export default {
|
|||
:markdown-docs-path="markdownDocsPath"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:project-path="projectPath"
|
||||
:project-id="projectId"
|
||||
:project-namespace="projectNamespace"
|
||||
:show-delete-button="showDeleteButton"
|
||||
:can-attach-file="canAttachFile"
|
||||
|
|
|
@ -13,14 +13,18 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
issuableTemplates: {
|
||||
type: Array,
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => [],
|
||||
default: () => {},
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
projectNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -48,11 +52,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issue">
|
||||
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues">
|
||||
<button
|
||||
ref="toggle"
|
||||
:data-namespace-path="projectNamespace"
|
||||
:data-project-path="projectPath"
|
||||
:data-project-id="projectId"
|
||||
:data-data="issuableTemplatesJson"
|
||||
class="dropdown-menu-toggle js-issuable-selector"
|
||||
type="button"
|
||||
|
|
|
@ -26,9 +26,9 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
issuableTemplates: {
|
||||
type: Array,
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => [],
|
||||
default: () => {},
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
|
@ -46,6 +46,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
projectNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -68,7 +72,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
hasIssuableTemplates() {
|
||||
return this.issuableTemplates.length;
|
||||
return Object.values(Object(this.issuableTemplates)).length;
|
||||
},
|
||||
showLockedWarning() {
|
||||
return this.formState.lockedWarningVisible && !this.formState.updateLoading;
|
||||
|
@ -127,6 +131,7 @@ export default {
|
|||
:form-state="formState"
|
||||
:issuable-templates="issuableTemplates"
|
||||
:project-path="projectPath"
|
||||
:project-id="projectId"
|
||||
:project-namespace="projectNamespace"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,7 @@ export function initIssueHeaderActions(store) {
|
|||
issueType: el.dataset.issueType,
|
||||
newIssuePath: el.dataset.newIssuePath,
|
||||
projectPath: el.dataset.projectPath,
|
||||
projectId: el.dataset.projectId,
|
||||
reportAbusePath: el.dataset.reportAbusePath,
|
||||
submitAsSpamPath: el.dataset.submitAsSpamPath,
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@ export default class Store {
|
|||
lockedWarningVisible: false,
|
||||
updateLoading: false,
|
||||
lock_version: 0,
|
||||
issuableTemplates: [],
|
||||
issuableTemplates: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { flatten } from 'lodash';
|
||||
import { CI_CONFIG_STATUS_VALID } from '../../constants';
|
||||
import CiLintResults from './ci_lint_results.vue';
|
||||
|
||||
|
@ -25,14 +26,18 @@ export default {
|
|||
return this.ciConfig?.stages || [];
|
||||
},
|
||||
jobs() {
|
||||
return this.stages.reduce((acc, { groups, name: stageName }) => {
|
||||
const groupedJobs = this.stages.reduce((acc, { groups, name: stageName }) => {
|
||||
return acc.concat(
|
||||
groups.map(({ name: groupName }) => ({
|
||||
stage: stageName,
|
||||
name: groupName,
|
||||
})),
|
||||
groups.map(({ jobs }) => {
|
||||
return jobs.map((job) => ({
|
||||
stage: stageName,
|
||||
...job,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return flatten(groupedJobs);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
tagList() {
|
||||
return this.item.tagList?.join(', ');
|
||||
return this.item.tags?.join(', ');
|
||||
},
|
||||
onlyPolicy() {
|
||||
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
|
||||
stage
|
||||
tagList
|
||||
tags
|
||||
when
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export const resolvers = {
|
|||
beforeScript: job.before_script,
|
||||
script: job.script,
|
||||
afterScript: job.after_script,
|
||||
tagList: job.tag_list,
|
||||
tags: job.tag_list,
|
||||
environment: job.environment,
|
||||
when: job.when,
|
||||
allowFailure: job.allow_failure,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
|
|||
import { __, s__, sprintf } from '~/locale';
|
||||
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
||||
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 CiLint from './components/lint/ci_lint.vue';
|
||||
|
@ -23,7 +24,6 @@ const COMMIT_FAILURE = 'COMMIT_FAILURE';
|
|||
const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
|
||||
const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
|
||||
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';
|
||||
|
||||
export default {
|
||||
|
@ -125,6 +125,9 @@ export default {
|
|||
isBlobContentLoading() {
|
||||
return this.$apollo.queries.content.loading;
|
||||
},
|
||||
isBlobContentError() {
|
||||
return this.failureType === LOAD_FAILURE_NO_FILE || this.failureType === LOAD_FAILURE_UNKNOWN;
|
||||
},
|
||||
isCiConfigDataLoading() {
|
||||
return this.$apollo.queries.ciConfigData.loading;
|
||||
},
|
||||
|
@ -144,14 +147,11 @@ export default {
|
|||
},
|
||||
failure() {
|
||||
switch (this.failureType) {
|
||||
case LOAD_FAILURE_NO_REF:
|
||||
return {
|
||||
text: this.$options.alertTexts[LOAD_FAILURE_NO_REF],
|
||||
variant: 'danger',
|
||||
};
|
||||
case LOAD_FAILURE_NO_FILE:
|
||||
return {
|
||||
text: this.$options.alertTexts[LOAD_FAILURE_NO_FILE],
|
||||
text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
|
||||
filePath: this.ciConfigPath,
|
||||
}),
|
||||
variant: 'danger',
|
||||
};
|
||||
case LOAD_FAILURE_UNKNOWN:
|
||||
|
@ -182,9 +182,8 @@ export default {
|
|||
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
|
||||
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
|
||||
[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_REF]: s__(
|
||||
'Pipelines|Repository does not have a default branch, please set one.',
|
||||
[LOAD_FAILURE_NO_FILE]: s__(
|
||||
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor 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 { 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);
|
||||
} else if (response?.status === 400) {
|
||||
// 400 for a missing ref when no default branch is set
|
||||
this.reportFailure(LOAD_FAILURE_NO_REF);
|
||||
} else {
|
||||
this.reportFailure(LOAD_FAILURE_UNKNOWN);
|
||||
}
|
||||
|
@ -299,9 +299,9 @@ export default {
|
|||
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
|
||||
</ul>
|
||||
</gl-alert>
|
||||
<div class="gl-mt-4">
|
||||
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
|
||||
<div v-else class="file-editor gl-mb-3">
|
||||
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-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">
|
||||
<validation-segment
|
||||
class="well-segment"
|
||||
|
|
|
@ -8,6 +8,19 @@ fragment PipelineStagesConnection on CiConfigStageConnection {
|
|||
jobs {
|
||||
nodes {
|
||||
name
|
||||
script
|
||||
beforeScript
|
||||
afterScript
|
||||
environment
|
||||
allowFailure
|
||||
tags
|
||||
when
|
||||
only {
|
||||
refs
|
||||
}
|
||||
except {
|
||||
refs
|
||||
}
|
||||
needs {
|
||||
nodes {
|
||||
name
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
|
|||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.projectId = this.dropdown.data('projectId');
|
||||
this.projectPath = this.dropdown.data('projectPath');
|
||||
this.namespacePath = this.dropdown.data('namespacePath');
|
||||
this.issuableType = this.$dropdownContainer.data('issuableType');
|
||||
|
@ -81,21 +82,21 @@ export default class IssuableTemplateSelector extends TemplateSelector {
|
|||
}
|
||||
|
||||
requestFile(query) {
|
||||
const callback = (currentTemplate) => {
|
||||
this.currentTemplate = currentTemplate;
|
||||
this.stopLoadingSpinner();
|
||||
this.setInputValueToTemplateContent();
|
||||
};
|
||||
|
||||
this.startLoadingSpinner();
|
||||
|
||||
Api.issueTemplate(
|
||||
this.namespacePath,
|
||||
this.projectPath,
|
||||
query.name,
|
||||
Api.projectTemplate(
|
||||
this.projectId,
|
||||
this.issuableType,
|
||||
(err, currentTemplate) => {
|
||||
this.currentTemplate = currentTemplate;
|
||||
this.stopLoadingSpinner();
|
||||
if (err) return; // Error handled by global AJAX error handler
|
||||
this.setInputValueToTemplateContent();
|
||||
},
|
||||
query.name,
|
||||
{ source_template_project_id: query.project_id },
|
||||
callback,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValueToTemplateContent() {
|
||||
|
|
|
@ -32,13 +32,17 @@
|
|||
.rotations-modal {
|
||||
.gl-card {
|
||||
min-width: 75%;
|
||||
width: fit-content;
|
||||
@include gl-bg-gray-10;
|
||||
}
|
||||
|
||||
&.gl-modal .modal-md {
|
||||
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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Projects::TemplatesController < Projects::ApplicationController
|
||||
include IssuablesDescriptionTemplatesHelper
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :authorize_can_read_issuable!
|
||||
before_action :get_template_class
|
||||
|
@ -24,10 +26,8 @@ class Projects::TemplatesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def names
|
||||
templates = @template_type.dropdown_names(project)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: templates }
|
||||
format.json { render json: issuable_templates(project, params[:template_type]) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ class LicenseTemplateFinder
|
|||
LicenseTemplate.new(
|
||||
key: license.key,
|
||||
name: license.name,
|
||||
project: project,
|
||||
nickname: license.nickname,
|
||||
category: (license.featured? ? :Popular : :Other),
|
||||
content: license.content,
|
||||
|
|
|
@ -199,7 +199,7 @@ module BlobHelper
|
|||
|
||||
categories.each_with_object({}) do |category, hash|
|
||||
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
|
||||
|
|
|
@ -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
|
||||
include GitlabRoutingHelper
|
||||
include IssuablesDescriptionTemplatesHelper
|
||||
|
||||
def sidebar_gutter_toggle_icon
|
||||
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
|
||||
|
@ -75,28 +76,6 @@ module IssuablesHelper
|
|||
.to_json
|
||||
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)
|
||||
case selected_users.length
|
||||
when 0
|
||||
|
@ -294,6 +273,7 @@ module IssuablesHelper
|
|||
|
||||
{
|
||||
projectPath: ref_project.path,
|
||||
projectId: ref_project.id,
|
||||
projectNamespace: ref_project.namespace.full_path
|
||||
}
|
||||
end
|
||||
|
@ -369,24 +349,6 @@ module IssuablesHelper
|
|||
cookies[:collapsed_gutter] == 'true'
|
||||
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)
|
||||
{
|
||||
todo_text: _('Add a to do'),
|
||||
|
@ -424,12 +386,6 @@ module IssuablesHelper
|
|||
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)
|
||||
{
|
||||
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))
|
||||
[\>\}\]]}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
|
||||
@name = name
|
||||
@project = project
|
||||
@category = category
|
||||
@content = content
|
||||
@nickname = nickname
|
||||
|
@ -24,6 +25,22 @@ class LicenseTemplate
|
|||
@meta = meta
|
||||
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?
|
||||
category == :Popular
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ class Project < ApplicationRecord
|
|||
include FromUnion
|
||||
include IgnorableColumns
|
||||
include Integration
|
||||
include CanHousekeepRepository
|
||||
include EachBatch
|
||||
extend Gitlab::Cache::RequestCache
|
||||
extend Gitlab::Utils::Override
|
||||
|
@ -2122,18 +2123,6 @@ class Project < ApplicationRecord
|
|||
(auto_devops || build_auto_devops)&.predefined_variables
|
||||
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)
|
||||
@route_maps_by_commit ||= Hash.new do |h, sha|
|
||||
h[sha] = begin
|
||||
|
@ -2634,10 +2623,6 @@ class Project < ApplicationRecord
|
|||
from && self != from
|
||||
end
|
||||
|
||||
def pushes_since_gc_redis_shared_state_key
|
||||
"projects/#{id}/pushes_since_gc"
|
||||
end
|
||||
|
||||
def update_project_statistics
|
||||
stats = statistics || build_statistics
|
||||
stats.update(namespace_id: namespace_id)
|
||||
|
|
|
@ -88,13 +88,9 @@ module MergeRequests
|
|||
end
|
||||
|
||||
def try_merge
|
||||
merge = repository.merge(current_user, source, merge_request, commit_message)
|
||||
|
||||
if merge_request.squash_on_merge? && Feature.enabled?(:persist_squash_commit_sha_for_squashes, project)
|
||||
merge_request.update_column(:squash_commit_sha, source)
|
||||
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?
|
||||
end
|
||||
|
||||
merge
|
||||
rescue Gitlab::Git::PreReceiveError => e
|
||||
raise MergeError,
|
||||
"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) }
|
||||
.settings-header
|
||||
%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')
|
||||
- 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 }
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
%section.settings.general-settings.no-animate.expanded#js-general-settings
|
||||
.settings-header
|
||||
%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.')
|
||||
.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' } }
|
||||
.settings-header
|
||||
%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.')
|
||||
|
||||
.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)] }
|
||||
.settings-header
|
||||
%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'
|
||||
|
||||
.settings-content
|
||||
|
@ -48,8 +48,7 @@
|
|||
.settings-header
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= s_('ProjectSettings|Badges')
|
||||
%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
|
||||
= s_('ProjectSettings|Customize this project\'s 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) }
|
||||
.settings-header
|
||||
%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.')
|
||||
|
||||
.settings-content
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
- 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
|
||||
.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
|
||||
%ul.dropdown-footer-list
|
||||
%li
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- issuable = local_assigns.fetch(:issuable)
|
||||
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
|
||||
- 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'
|
||||
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
|
||||
- toggle_wip_link_end = '</a>'
|
||||
|
|
|
@ -1094,7 +1094,7 @@
|
|||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: pipeline_background:ci_daily_build_group_report_results
|
||||
:feature_category: :continuous_integration
|
||||
:feature_category: :code_testing
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
|
@ -1102,7 +1102,7 @@
|
|||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
|
||||
:feature_category: :continuous_integration
|
||||
:feature_category: :code_testing
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
|
|
|
@ -5,6 +5,8 @@ module Ci
|
|||
include ApplicationWorker
|
||||
include PipelineBackgroundQueue
|
||||
|
||||
feature_category :code_testing
|
||||
|
||||
idempotent!
|
||||
|
||||
def perform(pipeline_id)
|
||||
|
|
|
@ -6,6 +6,8 @@ module Ci
|
|||
include ApplicationWorker
|
||||
include PipelineBackgroundQueue
|
||||
|
||||
feature_category :code_testing
|
||||
|
||||
idempotent!
|
||||
|
||||
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'
|
||||
type: development
|
||||
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
|
||||
review slot, based on their timezone. Feel free to
|
||||
[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
|
||||
[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
|
||||
is reduced even further, as you no longer need to wait weeks before your changes
|
||||
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:
|
||||
|
||||
- [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/`.
|
||||
- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_templates/`.
|
||||
- [Issue Templates](../user/project/description_templates.md#create-an-issue-template): `.gitlab/issue_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/`.
|
||||
- [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`.
|
||||
|
|
|
@ -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. Check the **Create an incident** checkbox.
|
||||
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
|
||||
with [Developer permissions](../../user/permissions.md), select
|
||||
**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)
|
||||
|
||||
To learn how to create templates for issues and merge requests, visit
|
||||
[Description templates](../project/description_templates.md).
|
||||
|
||||
#### Group-level project templates **(PREMIUM)**
|
||||
|
||||
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
|
||||
|
||||
>[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
|
||||
a timely manner by the developers of a project.
|
||||
|
||||
Description templates allow you to define context-specific templates for issue
|
||||
and merge request description fields for your project, as well as help filter
|
||||
out a lot of unnecessary noise from issues.
|
||||
|
||||
## Overview
|
||||
With description templates, you can define context-specific templates for issue and merge request
|
||||
description fields for your project, and filter out a lot of unnecessary noise from issues.
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
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,
|
||||
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
|
||||
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/`
|
||||
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)
|
||||
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
|
||||
`.gitlab/merge_request_templates/` directory in your repository. Commit and
|
||||
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`.
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
## 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.
|
||||
> - 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.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46222) in GitLab 13.8.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
We make use of Description Templates for Issues and Merge Requests within the GitLab Community
|
||||
Edition project. Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
|
||||
We make use of description templates for issues and merge requests in the GitLab project.
|
||||
Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
|
||||
for some examples.
|
||||
|
||||
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
|
||||
the issue or merge request has the permissions to perform the relevant actions.
|
||||
|
||||
Here is an example of a Bug report template:
|
||||
|
||||
```plaintext
|
||||
Summary
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
|
||||
Steps to reproduce
|
||||
## Steps to reproduce
|
||||
|
||||
(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)
|
||||
|
||||
(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 is the current bug behavior?
|
||||
|
||||
(What actually happens)
|
||||
|
||||
|
||||
What is the expected correct behavior?
|
||||
## What is the expected correct behavior?
|
||||
|
||||
(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,
|
||||
logs, and code as it's very hard to read otherwise.)
|
||||
|
||||
|
||||
Possible fixes
|
||||
## Possible fixes
|
||||
|
||||
(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
|
||||
|
||||
- [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)
|
||||
- [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.
|
||||
|
|
|
@ -102,7 +102,7 @@ To edit a file:
|
|||
in the bottom-right corner.
|
||||
1. When you're done, click **Submit changes...**.
|
||||
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.
|
||||
1. Click **Submit changes**.
|
||||
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
|
||||
begin
|
||||
template = TemplateFinder
|
||||
.build(params[:type], user_project, name: params[:name])
|
||||
.execute
|
||||
template = TemplateFinder.build(
|
||||
params[:type], user_project, name: params[:name],
|
||||
source_template_project_id: params[:source_template_project_id]
|
||||
).execute
|
||||
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
||||
not_found!('Template')
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ module Gitlab
|
|||
def initialize(path, project = nil, category: nil)
|
||||
@path = path
|
||||
@category = category
|
||||
@project = project
|
||||
@finder = self.class.finder(project)
|
||||
end
|
||||
|
||||
|
@ -31,6 +32,22 @@ module Gitlab
|
|||
# override with a comment to be placed at the top of the blob.
|
||||
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
|
||||
# like `[fullname]` with a user-specified string. This is a no-op for
|
||||
# other templates
|
||||
|
@ -82,11 +99,11 @@ module Gitlab
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def by_category(category, project = nil)
|
||||
def by_category(category, project = nil, empty_category_title: nil)
|
||||
directory = category_directory(category)
|
||||
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
|
||||
|
||||
def category_directory(category)
|
||||
|
|
|
@ -15,6 +15,10 @@ module Gitlab
|
|||
def finder(project)
|
||||
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
||||
end
|
||||
|
||||
def by_category(category, project = nil, empty_category_title: nil)
|
||||
super(category, project, empty_category_title: _('Project Templates'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,10 @@ module Gitlab
|
|||
def finder(project)
|
||||
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
|
||||
end
|
||||
|
||||
def by_category(category, project = nil, empty_category_title: nil)
|
||||
super(category, project, empty_category_title: _('Project Templates'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20790,9 +20790,6 @@ msgstr ""
|
|||
msgid "Pipelines|More Information"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -20805,9 +20802,6 @@ msgstr ""
|
|||
msgid "Pipelines|Project cache successfully reset."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Repository does not have a default branch, please set one."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Revoke"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20829,6 +20823,9 @@ msgstr ""
|
|||
msgid "Pipelines|There are currently no pipelines."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21882,6 +21879,9 @@ msgstr ""
|
|||
msgid "Project ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project Templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project URL"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@ tests = [
|
|||
|
||||
{
|
||||
explanation: 'Tooling should map to respective spec',
|
||||
source: 'tooling/lib/tooling/test_file_finder.rb',
|
||||
expected: ['spec/tooling/lib/tooling/test_file_finder_spec.rb']
|
||||
source: 'tooling/lib/tooling/helm3_client.rb',
|
||||
expected: ['spec/tooling/lib/tooling/helm3_client_spec.rb']
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
@ -160,12 +160,12 @@ RSpec.describe Projects::TemplatesController do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.size).to eq(2)
|
||||
expect(json_response).to match(expected_template_names)
|
||||
expect(json_response['Project Templates'].size).to eq(2)
|
||||
expect(json_response['Project Templates'].map { |x| { "name" => x['name'] } }).to match(expected_template_names)
|
||||
end
|
||||
|
||||
it 'fails for user with no access' do
|
||||
|
|
|
@ -41,7 +41,7 @@ RSpec.describe 'issue state', :js do
|
|||
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
|
||||
let(:open_issue) { create(:issue, project: project) }
|
||||
|
||||
|
@ -63,7 +63,7 @@ RSpec.describe 'issue state', :js do
|
|||
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
|
||||
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)
|
||||
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(:release_title) { 'A most magnificent release' }
|
||||
let(:release_notes) { 'Best. Release. **Ever.** :rocket:' }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { languages } from 'monaco-editor';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import EditorLite from '~/editor/editor_lite';
|
||||
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
|
||||
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
|
||||
|
@ -9,6 +10,7 @@ describe('~/editor/editor_ci_config_ext', () => {
|
|||
let editor;
|
||||
let instance;
|
||||
let editorEl;
|
||||
let originalGitlabUrl;
|
||||
|
||||
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
|
||||
setFixtures('<div id="editor"></div>');
|
||||
|
@ -22,6 +24,15 @@ describe('~/editor/editor_ci_config_ext', () => {
|
|||
instance.use(new CiSchemaExtension());
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
originalGitlabUrl = gon.gitlab_url;
|
||||
gon.gitlab_url = TEST_HOST;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
gon.gitlab_url = originalGitlabUrl;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
createMockEditor();
|
||||
});
|
||||
|
@ -73,7 +84,7 @@ describe('~/editor/editor_ci_config_ext', () => {
|
|||
});
|
||||
|
||||
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],
|
||||
});
|
||||
});
|
||||
|
@ -87,7 +98,7 @@ describe('~/editor/editor_ci_config_ext', () => {
|
|||
});
|
||||
|
||||
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'],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
||||
store.state.openFiles.push(f);
|
||||
|
@ -90,10 +90,12 @@ describe('IDE store file actions', () => {
|
|||
});
|
||||
|
||||
it('removes file if it pending', () => {
|
||||
store.state.openFiles.push({
|
||||
...localFile,
|
||||
pending: true,
|
||||
});
|
||||
store.state.openFiles = [
|
||||
{
|
||||
...localFile,
|
||||
pending: true,
|
||||
},
|
||||
];
|
||||
|
||||
return store.dispatch('closeFile', localFile).then(() => {
|
||||
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)
|
||||
|
||||
<gl-link-stub
|
||||
href="/help/user/project/description_templates#creating-issue-templates"
|
||||
href="/help/user/project/description_templates#create-an-issue-template"
|
||||
target="_blank"
|
||||
>
|
||||
<gl-icon-stub
|
||||
|
|
|
@ -423,7 +423,9 @@ describe('Issuable output', () => {
|
|||
});
|
||||
|
||||
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]));
|
||||
|
||||
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
|
||||
|
|
|
@ -14,7 +14,10 @@ describe('Issue description template component', () => {
|
|||
vm = new Component({
|
||||
propsData: {
|
||||
formState,
|
||||
issuableTemplates: [{ name: 'test' }],
|
||||
issuableTemplates: {
|
||||
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
|
||||
},
|
||||
projectId: 1,
|
||||
projectPath: '/',
|
||||
projectNamespace: '/',
|
||||
},
|
||||
|
@ -23,7 +26,7 @@ describe('Issue description template component', () => {
|
|||
|
||||
it('renders templates as JSON array in data attribute', () => {
|
||||
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: '/',
|
||||
markdownDocsPath: '/',
|
||||
projectPath: '/',
|
||||
projectId: 1,
|
||||
projectNamespace: '/',
|
||||
};
|
||||
|
||||
|
@ -42,7 +43,11 @@ describe('Inline edit form component', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -52,6 +52,7 @@ export const appProps = {
|
|||
markdownDocsPath: '/',
|
||||
projectNamespace: '/',
|
||||
projectPath: '/',
|
||||
projectId: 1,
|
||||
issuableTemplateNamesPath: '/issuable-templates-path',
|
||||
zoomMeetingUrl,
|
||||
publishedIncidentUrl,
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
|
|||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findLintParameters = () => findAllByTestId('ci-lint-parameter');
|
||||
const findLintParameterAt = (i) => findLintParameters().at(i);
|
||||
const findLintValueAt = (i) => findAllByTestId('ci-lint-value').at(i);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -50,6 +51,20 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
|
|||
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', () => {
|
||||
createComponent(
|
||||
{
|
||||
|
|
|
@ -27,7 +27,7 @@ Object {
|
|||
"echo 'script 1'",
|
||||
],
|
||||
"stage": "test",
|
||||
"tagList": Array [
|
||||
"tags": Array [
|
||||
"tag 1",
|
||||
],
|
||||
"when": "on_success",
|
||||
|
@ -61,7 +61,7 @@ Object {
|
|||
"echo 'script 2'",
|
||||
],
|
||||
"stage": "test",
|
||||
"tagList": Array [
|
||||
"tags": Array [
|
||||
"tag 2",
|
||||
],
|
||||
"when": "on_success",
|
||||
|
|
|
@ -35,6 +35,21 @@ job_build:
|
|||
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 = {
|
||||
data: {
|
||||
ciConfig: {
|
||||
|
@ -54,8 +69,8 @@ export const mockCiConfigQueryResponse = {
|
|||
nodes: [
|
||||
{
|
||||
name: 'job_test_1',
|
||||
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
|
||||
__typename: 'CiConfigJob',
|
||||
script: ['echo "test 1"'],
|
||||
...mockJobFields,
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigJobConnection',
|
||||
|
@ -69,9 +84,8 @@ export const mockCiConfigQueryResponse = {
|
|||
nodes: [
|
||||
{
|
||||
name: 'job_test_2',
|
||||
|
||||
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
|
||||
__typename: 'CiConfigJob',
|
||||
script: ['echo "test 2"'],
|
||||
...mockJobFields,
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigJobConnection',
|
||||
|
@ -94,11 +108,8 @@ export const mockCiConfigQueryResponse = {
|
|||
nodes: [
|
||||
{
|
||||
name: 'job_build',
|
||||
needs: {
|
||||
nodes: [{ name: 'job_test_2', __typename: 'CiConfigNeed' }],
|
||||
__typename: 'CiConfigNeedConnection',
|
||||
},
|
||||
__typename: 'CiConfigJob',
|
||||
script: ['echo "build"'],
|
||||
...mockJobFields,
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigJobConnection',
|
||||
|
|
|
@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import VueApollo from 'vue-apollo';
|
||||
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 {
|
||||
mockCiConfigPath,
|
||||
|
@ -414,58 +415,81 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
|||
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
|
||||
});
|
||||
|
||||
it('no error is shown when data is set', async () => {
|
||||
createComponentWithApollo();
|
||||
describe('when file exists', () => {
|
||||
beforeEach(async () => {
|
||||
createComponentWithApollo();
|
||||
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
|
||||
});
|
||||
it('shows editor and commit form', () => {
|
||||
expect(findEditorLite().exists()).toBe(true);
|
||||
expect(findTextEditor().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ci config query is called with correct variables', async () => {
|
||||
createComponentWithApollo();
|
||||
it('no error is shown when data is set', async () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
it('ci config query is called with correct variables', async () => {
|
||||
createComponentWithApollo();
|
||||
|
||||
expect(mockCiConfigData).toHaveBeenCalledWith({
|
||||
content: mockCiYml,
|
||||
projectPath: mockProjectFullPath,
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockCiConfigData).toHaveBeenCalledWith({
|
||||
content: mockCiYml,
|
||||
projectPath: mockProjectFullPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a 404 error message', async () => {
|
||||
mockBlobContentData.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 404,
|
||||
},
|
||||
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);
|
||||
});
|
||||
createComponentWithApollo();
|
||||
|
||||
await waitForPromises();
|
||||
it('shows a 404 error message', async () => {
|
||||
mockBlobContentData.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: httpStatusCodes.NOT_FOUND,
|
||||
},
|
||||
});
|
||||
createComponentWithApollo();
|
||||
|
||||
expect(findAlert().text()).toBe('No CI file found in this repository, please add one.');
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
it('shows a 400 error message', async () => {
|
||||
mockBlobContentData.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 400,
|
||||
},
|
||||
expect(findAlert().text()).toBe(expectedAlertMsg);
|
||||
});
|
||||
createComponentWithApollo();
|
||||
|
||||
await waitForPromises();
|
||||
it('shows a 400 error message', async () => {
|
||||
mockBlobContentData.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: httpStatusCodes.BAD_REQUEST,
|
||||
},
|
||||
});
|
||||
createComponentWithApollo();
|
||||
|
||||
expect(findAlert().text()).toBe('Repository does not have a default branch, please set one.');
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
it('shows a unkown error message', async () => {
|
||||
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
|
||||
createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
expect(findAlert().text()).toBe(expectedAlertMsg);
|
||||
});
|
||||
|
||||
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
|
||||
it('shows a unkown error message', async () => {
|
||||
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
|
||||
createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
|
||||
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',
|
||||
lockVersion: issue.lock_version,
|
||||
projectPath: @project.path,
|
||||
projectId: @project.id,
|
||||
projectNamespace: @project.namespace.path,
|
||||
initialTitleHtml: issue.title,
|
||||
initialTitleText: issue.title,
|
||||
|
|
|
@ -57,6 +57,6 @@ RSpec.describe LicenseTemplate do
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -2977,56 +2977,9 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#pushes_since_gc' do
|
||||
let(:project) { build_stubbed(:project) }
|
||||
|
||||
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
|
||||
it_behaves_like 'can housekeep repository' do
|
||||
let(:resource) { build_stubbed(:project) }
|
||||
let(:resource_key) { 'projects' }
|
||||
end
|
||||
|
||||
describe '#deployment_variables' do
|
||||
|
|
|
@ -19,12 +19,8 @@ RSpec.describe MergeRequests::MergeService do
|
|||
{ commit_message: 'Awesome message', sha: merge_request.diff_head_sha }
|
||||
end
|
||||
|
||||
let(:feature_flag_persist_squash) { true }
|
||||
|
||||
context 'valid params' do
|
||||
before do
|
||||
stub_feature_flags(persist_squash_commit_sha_for_squashes: feature_flag_persist_squash)
|
||||
|
||||
allow(service).to receive(:execute_hooks)
|
||||
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)
|
||||
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
|
||||
|
||||
|
|
|
@ -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