Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-13 15:10:40 +00:00
parent 39c1496527
commit 9b1b702f0f
81 changed files with 691 additions and 615 deletions

View File

@ -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

View File

@ -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)

View File

@ -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);
} }

View File

@ -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({

View File

@ -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)"

View File

@ -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();

View File

@ -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],
}); });
} }

View File

@ -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}`);
}; };

View File

@ -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 */

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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,
}, },

View File

@ -11,7 +11,7 @@ export default class Store {
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
lock_version: 0, lock_version: 0,
issuableTemplates: [], issuableTemplates: {},
}; };
} }

View File

@ -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 }) => {
stage: stageName, return jobs.map((job) => ({
name: groupName, stage: stageName,
})), ...job,
}));
}),
); );
}, []); }, []);
return flatten(groupedJobs);
}, },
}, },
}; };

View File

@ -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;

View File

@ -15,7 +15,7 @@ mutation lintCI($endpoint: String, $content: String, $dry: Boolean) {
} }
afterScript afterScript
stage stage
tagList tags
when when
} }
} }

View File

@ -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,

View File

@ -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-if="!isBlobContentError" class="gl-mt-4">
<div v-else class="file-editor gl-mb-3"> <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"

View File

@ -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

View File

@ -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) {
const callback = (currentTemplate) => {
this.currentTemplate = currentTemplate;
this.stopLoadingSpinner();
this.setInputValueToTemplateContent();
};
this.startLoadingSpinner(); this.startLoadingSpinner();
Api.issueTemplate( Api.projectTemplate(
this.namespacePath, this.projectId,
this.projectPath,
query.name,
this.issuableType, this.issuableType,
(err, currentTemplate) => { query.name,
this.currentTemplate = currentTemplate; { source_template_project_id: query.project_id },
this.stopLoadingSpinner(); callback,
if (err) return; // Error handled by global AJAX error handler
this.setInputValueToTemplateContent();
},
); );
return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent() {

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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>'

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,5 @@
---
title: Persist 'squash_commit_sha' when squashing
merge_request: 51074
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: In WebIDE switch files before closing the active one
merge_request: 51483
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Instrument CI template usage across projects
merge_request: 51391
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update toggle button in repo general settings
merge_request: 51036
author: Yogi (@yo)
type: other

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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`.

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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']
}, },
{ {

View File

@ -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

View File

@ -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') }

View File

@ -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:' }

View File

@ -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'],
}); });
}); });

View File

@ -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, {
pending: true, ...localFile,
}); 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);

View File

@ -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

View File

@ -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(() => {

View File

@ -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":"/"}]}',
); );
}); });

View File

@ -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();
}); });

View File

@ -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,

View File

@ -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(
{ {

View File

@ -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",

View File

@ -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',

View File

@ -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,58 +415,81 @@ 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', () => {
createComponentWithApollo(); beforeEach(async () => {
createComponentWithApollo();
await waitForPromises(); await waitForPromises();
});
expect(findAlert().exists()).toBe(false); it('shows editor and commit form', () => {
expect(findEditorLite().attributes('value')).toBe(mockCiYml); expect(findEditorLite().exists()).toBe(true);
}); expect(findTextEditor().exists()).toBe(true);
});
it('ci config query is called with correct variables', async () => { it('no error is shown when data is set', async () => {
createComponentWithApollo(); 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({ await waitForPromises();
content: mockCiYml,
projectPath: mockProjectFullPath, expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
});
}); });
}); });
it('shows a 404 error message', async () => { describe('when no file exists', () => {
mockBlobContentData.mockRejectedValueOnce({ const expectedAlertMsg =
response: { 'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
status: 404,
}, 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 () => { expect(findAlert().text()).toBe(expectedAlertMsg);
mockBlobContentData.mockRejectedValueOnce({
response: {
status: 400,
},
}); });
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 () => { expect(findAlert().text()).toBe(expectedAlertMsg);
mockBlobContentData.mockRejectedValueOnce(new Error('My error!')); });
createComponentWithApollo();
await waitForPromises();
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.');
});
}); });
}); });
}); });

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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