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'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '0.13.5'
gem 'gitlab-labkit', '0.14.0'
# I18n
gem 'ruby_parser', '~> 3.15', require: false

View File

@ -432,9 +432,9 @@ GEM
fog-json (~> 1.2.0)
mime-types
ms_rest_azure (~> 0.12.0)
gitlab-labkit (0.13.5)
actionpack (>= 5.0.0, < 6.1.0)
activesupport (>= 5.0.0, < 6.1.0)
gitlab-labkit (0.14.0)
actionpack (>= 5.0.0, < 7.0.0)
activesupport (>= 5.0.0, < 7.0.0)
gitlab-pg_query (~> 1.3)
grpc (~> 1.19)
jaeger-client (~> 1.1)
@ -1363,7 +1363,7 @@ DEPENDENCIES
gitlab-chronic (~> 0.10.5)
gitlab-experiment (~> 0.4.4)
gitlab-fog-azure-rm (~> 1.0)
gitlab-labkit (= 0.13.5)
gitlab-labkit (= 0.14.0)
gitlab-license (~> 1.0)
gitlab-mail_room (~> 0.0.8)
gitlab-markup (~> 1.7.1)

View File

@ -66,6 +66,8 @@ export default class FileTemplateSelector {
reportSelectionName(options) {
const opts = options;
opts.query = options.selectedObj.name;
opts.data = options.selectedObj;
opts.data.source_template_project_id = options.selectedObj.project_id;
this.reportSelection(opts);
}

View File

@ -30,6 +30,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
const data = {
project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'),
source_template_project_id: query.project_id,
};
this.reportSelection({

View File

@ -4,7 +4,7 @@ import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
name: 'BoardCardLayout',
components: {
IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
},
@ -81,7 +81,7 @@ export default {
:data-issue-iid="issue.iid"
:data-issue-path="issue.referencePath"
data-testid="board_card"
class="board-card p-3 rounded"
class="board-card gl-p-5 gl-rounded-base"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"

View File

@ -437,6 +437,7 @@ export class GitLabDropdown {
groupName = el.data('group');
if (groupName) {
selectedIndex = el.data('index');
this.selectedIndex = selectedIndex;
selectedObject = this.renderedData[groupName][selectedIndex];
} else {
selectedIndex = el.closest('li').index();

View File

@ -17,15 +17,21 @@ export class CiSchemaExtension extends EditorLiteExtension {
* @param {String?} opts.ref - Current ref. Defaults to master
*/
registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) {
const ciSchemaUri = Api.buildUrl(Api.projectFileSchemaPath)
const ciSchemaPath = Api.buildUrl(Api.projectFileSchemaPath)
.replace(':namespace_path', projectNamespace)
.replace(':project_path', projectPath)
.replace(':ref', ref)
.replace(':filename', EXTENSION_CI_SCHEMA_FILE_NAME_MATCH);
// In order for workers loaded from `data://` as the
// ones loaded by monaco editor, we use absolute URLs
// to fetch schema files, hence the `gon.gitlab_url`
// reference. This prevents error:
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath;
const modelFileName = this.getModel().uri.path.split('/').pop();
registerSchema({
uri: ciSchemaUri,
uri: absoluteSchemaUrl,
fileMatch: [modelFileName],
});
}

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 fileWasActive = file.active;
if (file.pending) {
commit(types.REMOVE_PENDING_TAB, file);
} else {
commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false });
}
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
if (state.openFiles.length > 1 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 1 : indexOfClosedFile - 1;
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
@ -35,14 +28,22 @@ export const closeFile = ({ commit, state, dispatch, getters }, file) => {
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
dispatch('setFileActive', nextFileToOpen.path);
dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true });
}
} else if (!state.openFiles.length) {
} else if (state.openFiles.length === 1) {
dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, {
root: true,
});
}
if (file.pending) {
commit(types.REMOVE_PENDING_TAB, file);
} else {
commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false });
}
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
};

View File

@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
'/help/operations/metrics/alerts#trigger-actions-from-alerts';
export const ISSUE_TEMPLATES_DOCS_LINK =
'/help/user/project/description_templates#creating-issue-templates';
'/help/user/project/description_templates#create-an-issue-template';
/* PagerDuty integration settings constants */

View File

@ -132,6 +132,10 @@ export default {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
projectNamespace: {
type: String,
required: true,
@ -303,7 +307,7 @@ export default {
});
},
updateAndShowForm(templates = []) {
updateAndShowForm(templates = {}) {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
@ -419,6 +423,7 @@ export default {
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-id="projectId"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"

View File

@ -13,14 +13,18 @@ export default {
required: true,
},
issuableTemplates: {
type: Array,
type: Object,
required: false,
default: () => [],
default: () => {},
},
projectPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
projectNamespace: {
type: String,
required: true,
@ -48,11 +52,12 @@ export default {
</script>
<template>
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issue">
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues">
<button
ref="toggle"
:data-namespace-path="projectNamespace"
:data-project-path="projectPath"
:data-project-id="projectId"
:data-data="issuableTemplatesJson"
class="dropdown-menu-toggle js-issuable-selector"
type="button"

View File

@ -26,9 +26,9 @@ export default {
required: true,
},
issuableTemplates: {
type: Array,
type: Object,
required: false,
default: () => [],
default: () => {},
},
issuableType: {
type: String,
@ -46,6 +46,10 @@ export default {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
projectNamespace: {
type: String,
required: true,
@ -68,7 +72,7 @@ export default {
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
return Object.values(Object(this.issuableTemplates)).length;
},
showLockedWarning() {
return this.formState.lockedWarningVisible && !this.formState.updateLoading;
@ -127,6 +131,7 @@ export default {
:form-state="formState"
:issuable-templates="issuableTemplates"
:project-path="projectPath"
:project-id="projectId"
:project-namespace="projectNamespace"
/>
</div>

View File

@ -54,6 +54,7 @@ export function initIssueHeaderActions(store) {
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},

View File

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

View File

@ -1,4 +1,5 @@
<script>
import { flatten } from 'lodash';
import { CI_CONFIG_STATUS_VALID } from '../../constants';
import CiLintResults from './ci_lint_results.vue';
@ -25,14 +26,18 @@ export default {
return this.ciConfig?.stages || [];
},
jobs() {
return this.stages.reduce((acc, { groups, name: stageName }) => {
const groupedJobs = this.stages.reduce((acc, { groups, name: stageName }) => {
return acc.concat(
groups.map(({ name: groupName }) => ({
stage: stageName,
name: groupName,
})),
groups.map(({ jobs }) => {
return jobs.map((job) => ({
stage: stageName,
...job,
}));
}),
);
}, []);
return flatten(groupedJobs);
},
},
};

View File

@ -14,7 +14,7 @@ export default {
},
computed: {
tagList() {
return this.item.tagList?.join(', ');
return this.item.tags?.join(', ');
},
onlyPolicy() {
return this.item.only ? this.item.only.refs.join(', ') : this.item.only;

View File

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

View File

@ -27,7 +27,7 @@ export const resolvers = {
beforeScript: job.before_script,
script: job.script,
afterScript: job.after_script,
tagList: job.tag_list,
tags: job.tag_list,
environment: job.environment,
when: job.when,
allowFailure: job.allow_failure,

View File

@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import httpStatusCodes from '~/lib/utils/http_status';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import CiLint from './components/lint/ci_lint.vue';
@ -23,7 +24,6 @@ const COMMIT_FAILURE = 'COMMIT_FAILURE';
const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF';
const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export default {
@ -125,6 +125,9 @@ export default {
isBlobContentLoading() {
return this.$apollo.queries.content.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE || this.failureType === LOAD_FAILURE_UNKNOWN;
},
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
@ -144,14 +147,11 @@ export default {
},
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_REF:
return {
text: this.$options.alertTexts[LOAD_FAILURE_NO_REF],
variant: 'danger',
};
case LOAD_FAILURE_NO_FILE:
return {
text: this.$options.alertTexts[LOAD_FAILURE_NO_FILE],
text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
@ -182,9 +182,8 @@ export default {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'),
[LOAD_FAILURE_NO_REF]: s__(
'Pipelines|Repository does not have a default branch, please set one.',
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
@ -193,12 +192,13 @@ export default {
const { networkError } = error;
const { response } = networkError;
if (response?.status === 404) {
// 404 for missing CI file
// 404 for missing CI file
// 400 for blank projects with no repository
if (
response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST
) {
this.reportFailure(LOAD_FAILURE_NO_FILE);
} else if (response?.status === 400) {
// 400 for a missing ref when no default branch is set
this.reportFailure(LOAD_FAILURE_NO_REF);
} else {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
@ -299,9 +299,9 @@ export default {
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
</ul>
</gl-alert>
<div class="gl-mt-4">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else class="file-editor gl-mb-3">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
<div class="file-editor gl-mb-3">
<div class="info-well gl-display-none gl-display-sm-block">
<validation-segment
class="well-segment"

View File

@ -8,6 +8,19 @@ fragment PipelineStagesConnection on CiConfigStageConnection {
jobs {
nodes {
name
script
beforeScript
afterScript
environment
allowFailure
tags
when
only {
refs
}
except {
refs
}
needs {
nodes {
name

View File

@ -9,6 +9,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
super(...args);
this.projectId = this.dropdown.data('projectId');
this.projectPath = this.dropdown.data('projectPath');
this.namespacePath = this.dropdown.data('namespacePath');
this.issuableType = this.$dropdownContainer.data('issuableType');
@ -81,21 +82,21 @@ export default class IssuableTemplateSelector extends TemplateSelector {
}
requestFile(query) {
const callback = (currentTemplate) => {
this.currentTemplate = currentTemplate;
this.stopLoadingSpinner();
this.setInputValueToTemplateContent();
};
this.startLoadingSpinner();
Api.issueTemplate(
this.namespacePath,
this.projectPath,
query.name,
Api.projectTemplate(
this.projectId,
this.issuableType,
(err, currentTemplate) => {
this.currentTemplate = currentTemplate;
this.stopLoadingSpinner();
if (err) return; // Error handled by global AJAX error handler
this.setInputValueToTemplateContent();
},
query.name,
{ source_template_project_id: query.project_id },
callback,
);
return;
}
setInputValueToTemplateContent() {

View File

@ -32,13 +32,17 @@
.rotations-modal {
.gl-card {
min-width: 75%;
width: fit-content;
@include gl-bg-gray-10;
}
&.gl-modal .modal-md {
max-width: 640px;
}
// TODO: move to gitlab/ui utilities
// https://gitlab.com/gitlab-org/gitlab/-/issues/297502
.gl-w-fit-content {
width: fit-content;
}
}
//// Copied from roadmaps.scss - adapted for on-call schedules

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Projects::TemplatesController < Projects::ApplicationController
include IssuablesDescriptionTemplatesHelper
before_action :authenticate_user!
before_action :authorize_can_read_issuable!
before_action :get_template_class
@ -24,10 +26,8 @@ class Projects::TemplatesController < Projects::ApplicationController
end
def names
templates = @template_type.dropdown_names(project)
respond_to do |format|
format.json { render json: templates }
format.json { render json: issuable_templates(project, params[:template_type]) }
end
end

View File

@ -36,6 +36,7 @@ class LicenseTemplateFinder
LicenseTemplate.new(
key: license.key,
name: license.name,
project: project,
nickname: license.nickname,
category: (license.featured? ? :Popular : :Other),
content: license.content,

View File

@ -199,7 +199,7 @@ module BlobHelper
categories.each_with_object({}) do |category, hash|
hash[category] = grouped[category].map do |item|
{ name: item.name, id: item.key }
{ name: item.name, id: item.key, project_id: item.project_id }
end
end
end

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
include GitlabRoutingHelper
include IssuablesDescriptionTemplatesHelper
def sidebar_gutter_toggle_icon
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
@ -75,28 +76,6 @@ module IssuablesHelper
.to_json
end
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
toggle_class: 'js-issuable-selector',
title: title,
filter: true,
placeholder: 'Filter',
footer_content: true,
data: {
data: issuable_templates(issuable),
field_name: 'issuable_template',
selected: selected_template(issuable),
project_path: ref_project.path,
namespace_path: ref_project.namespace.full_path
}
}
dropdown_tag(title, options: options) do
capture(&block)
end
end
def users_dropdown_label(selected_users)
case selected_users.length
when 0
@ -294,6 +273,7 @@ module IssuablesHelper
{
projectPath: ref_project.path,
projectId: ref_project.id,
projectNamespace: ref_project.namespace.full_path
}
end
@ -369,24 +349,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
end
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
when Issue
ref_project.repository.issue_template_names
when MergeRequest
ref_project.repository.merge_request_template_names
end
end
def issuable_templates_names(issuable)
issuable_templates(issuable).map { |template| template[:name] }
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
def issuable_todo_button_data(issuable, is_collapsed)
{
todo_text: _('Add a to do'),
@ -424,12 +386,6 @@ module IssuablesHelper
end
end
def template_names_path(parent, issuable)
return '' unless parent.is_a?(Project)
project_template_names_path(parent, template_type: issuable.class.name.underscore)
end
def issuable_sidebar_options(issuable)
{
endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",

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))
[\>\}\]]}xi.freeze
attr_reader :key, :name, :category, :nickname, :url, :meta
attr_reader :key, :name, :project, :category, :nickname, :url, :meta
def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {})
def initialize(key:, name:, project:, category:, content:, nickname: nil, url: nil, meta: {})
@key = key
@name = name
@project = project
@category = category
@content = content
@nickname = nickname
@ -24,6 +25,22 @@ class LicenseTemplate
@meta = meta
end
def project_id
project&.id
end
def project_path
project&.path
end
def namespace_id
project&.namespace&.id
end
def namespace_path
project&.namespace&.full_path
end
def popular?
category == :Popular
end

View File

@ -34,6 +34,7 @@ class Project < ApplicationRecord
include FromUnion
include IgnorableColumns
include Integration
include CanHousekeepRepository
include EachBatch
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
@ -2122,18 +2123,6 @@ class Project < ApplicationRecord
(auto_devops || build_auto_devops)&.predefined_variables
end
def pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
end
def increment_pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
end
def reset_pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
end
def route_map_for(commit_sha)
@route_maps_by_commit ||= Hash.new do |h, sha|
h[sha] = begin
@ -2634,10 +2623,6 @@ class Project < ApplicationRecord
from && self != from
end
def pushes_since_gc_redis_shared_state_key
"projects/#{id}/pushes_since_gc"
end
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)

View File

@ -88,13 +88,9 @@ module MergeRequests
end
def try_merge
merge = repository.merge(current_user, source, merge_request, commit_message)
if merge_request.squash_on_merge? && Feature.enabled?(:persist_squash_commit_sha_for_squashes, project)
merge_request.update_column(:squash_commit_sha, source)
repository.merge(current_user, source, merge_request, commit_message).tap do
merge_request.update_column(:squash_commit_sha, source) if merge_request.squash_on_merge?
end
merge
rescue Gitlab::Git::PreReceiveError => e
raise MergeError,
"Something went wrong during merge pre-receive hook. #{e.message}".strip

View File

@ -2,7 +2,7 @@
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
%button.btn.js-settings-toggle
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }

View File

@ -8,14 +8,14 @@
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse')
%p= _('Update your project name, topics, description, and avatar.')
.settings-content= render 'projects/settings/general'
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { qa_selector: 'visibility_features_permissions_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
%p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.')
.settings-content
@ -30,7 +30,7 @@
%section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
= render_if_exists 'projects/merge_request_settings_description_text'
.settings-content
@ -48,8 +48,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('ProjectSettings|Badges')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
%p
= s_('ProjectSettings|Customize this project\'s badges.')
= link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges')
@ -63,7 +62,7 @@
%section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
%p= _('Housekeeping, export, path, transfer, remove, archive.')
.settings-content

View File

@ -1,9 +1,9 @@
- issuable = local_assigns.fetch(:issuable, nil)
- return unless issuable && issuable_templates(issuable).any?
- return unless issuable && issuable_templates(ref_project, issuable.class.name.underscore).any?
.issuable-form-select-holder.selectbox.form-group
.js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } }
.js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name.pluralize } }
= template_dropdown_tag(issuable) do
%ul.dropdown-footer-list
%li

View File

@ -1,7 +1,7 @@
- issuable = local_assigns.fetch(:issuable)
- has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty?
- no_issuable_templates = issuable_templates(ref_project, issuable.class.name.underscore).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
- toggle_wip_link_end = '</a>'

View File

@ -1094,7 +1094,7 @@
:idempotent: true
:tags: []
- :name: pipeline_background:ci_daily_build_group_report_results
:feature_category: :continuous_integration
:feature_category: :code_testing
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@ -1102,7 +1102,7 @@
:idempotent: true
:tags: []
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
:feature_category: :continuous_integration
:feature_category: :code_testing
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown

View File

@ -5,6 +5,8 @@ module Ci
include ApplicationWorker
include PipelineBackgroundQueue
feature_category :code_testing
idempotent!
def perform(pipeline_id)

View File

@ -6,6 +6,8 @@ module Ci
include ApplicationWorker
include PipelineBackgroundQueue
feature_category :code_testing
idempotent!
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'
type: development
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
review slot, based on their timezone. Feel free to
[override these selections](https://about.gitlab.com/handbook/engineering/projects/#gitlab)
if you think someone else would be better-suited, or the chosen person is unavailable.
if you think someone else would be better-suited
or use the [GitLab Review Workload Dashboard](https://gitlab-org.gitlab.io/gitlab-roulette/) to find other available reviewers.
To read more on how to use the reviewer roulette, please take a look at the
[Engineering workflow](https://about.gitlab.com/handbook/engineering/workflow/#basics)

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
is reduced even further, as you no longer need to wait weeks before your changes
are available on GitLab.com.
### The benefits of feature flags
It may seem like feature flags are configuration, which goes against our [convention-over-configuration](https://about.gitlab.com/handbook/product/product-principles/#convention-over-configuration)
principle. However, configuration is by definition something that is user-manageable.
Feature flags are not intended to be user-editable. Instead, they are intended as a tool for Engineers
and Site Reliability Engineers to use to de-risk their changes. Feature flags are the shim that gets us
to Continuous Delivery with our mono repo and without having to deploy the entire codebase on every change.
Feature flags are created to ensure that we can safely rollout our work on our terms.
If we use Feature Flags as a configuration, we are doing it wrong and are indeed in violation of our
principles. If something needs to be configured, we should intentionally make it configuration from the
first moment.
Some of the benefits of using development-type feature flags are:
1. It enables Continuous Delivery for GitLab.com.
1. It significantly reduces Mean-Time-To-Recovery.
1. It helps engineers to monitor and reduce the impact of their changes gradually, at any scale,
allowing us to be more metrics-driven and execute good DevOps practices, [shifting some responsibility "left"](https://devops.com/why-its-time-for-site-reliability-engineering-to-shift-left/).
1. Controlled feature rollout timing: without feature flags, we would need to wait until a specific
deployment was complete (which at GitLab could be at any time).
1. Increased psychological safety: when a feature flag is used, an engineer has the confidence that if anything goes wrong they can quickly disable the code and minimize the impact of a change that might be risky.
1. Improved throughput: when a change is less risky because a flag exists, theoretical tests about
scalability can potentially become unnecessary or less important. This allows an engineer to
potentially test a feature on a small project, monitor the impact, and proceed. The alternative might
be to build complex benchmarks locally, or on staging, or on another GitLab deployment, which has an
outsized impact on the time it can take to build and release a feature.

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:
- [Custom Dashboards](../operations/metrics/dashboards/index.md#add-a-new-dashboard-to-your-project): `.gitlab/dashboards/`.
- [Issue Templates](../user/project/description_templates.md#creating-issue-templates): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_templates/`.
- [Issue Templates](../user/project/description_templates.md#create-an-issue-template): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#create-a-merge-request-template): `.gitlab/merge_request_templates/`.
- [GitLab Kubernetes Agents](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
- [CODEOWNERS](../user/project/code_owners.md#how-to-set-up-code-owners): `.gitlab/CODEOWNERS`.
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.

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. Check the **Create an incident** checkbox.
1. To customize the incident, select an
[issue template](../../user/project/description_templates.md#creating-issue-templates).
[issue template](../../user/project/description_templates.md#create-an-issue-template).
1. To send [an email notification](alert_notifications.md#email-notifications) to users
with [Developer permissions](../../user/permissions.md), select
**Send a separate email notification to Developers**. Email notifications are

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)
To learn how to create templates for issues and merge requests, visit
[Description templates](../project/description_templates.md).
#### Group-level project templates **(PREMIUM)**
Define project templates at a group level by setting a group as the template source.

View File

@ -6,16 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Description templates
>[Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
We all know that a properly submitted issue is more likely to be addressed in
a timely manner by the developers of a project.
Description templates allow you to define context-specific templates for issue
and merge request description fields for your project, as well as help filter
out a lot of unnecessary noise from issues.
## Overview
With description templates, you can define context-specific templates for issue and merge request
description fields for your project, and filter out a lot of unnecessary noise from issues.
By using the description templates, users that create a new issue or merge
request can select a description template to help them communicate with other
@ -28,7 +25,10 @@ Description templates must be written in [Markdown](../markdown.md) and stored
in your project's repository under a directory named `.gitlab`. Only the
templates of the default branch are taken into account.
## Use-cases
To learn how to create templates for various file types in groups, visit
[Group file templates](../group/index.md#group-file-templates).
## Use cases
- Add a template to be used in every issue for a specific project,
giving instructions and guidelines, requiring for information specific to that subject.
@ -40,7 +40,7 @@ templates of the default branch are taken into account.
- You can also create issues and merge request templates for different
stages of your workflow, for example, feature proposal, feature improvement, or a bug report.
## Creating issue templates
## Create an issue template
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
directory in your repository. Commit and push to your default branch.
@ -65,13 +65,13 @@ To create the `.gitlab/issue_templates` directory:
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)
and see if you can choose a description template.
## Creating merge request templates
## Create a merge request template
Similarly to issue templates, create a new Markdown (`.md`) file inside the
`.gitlab/merge_request_templates/` directory in your repository. Commit and
push to your default branch.
## Using the templates
## Use the templates
Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
This enables the `Bug` dropdown option when creating or editing issues. When
@ -80,15 +80,46 @@ to the issue description field. The **Reset template** button discards any
changes you made after picking the template and returns it to its initial status.
NOTE:
You can create short-cut links to create an issue using a designated template. For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
You can create shortcut links to create an issue using a designated template.
For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(STARTER)**
### Set an issue and merge request description template at group level **(PREMIUM)**
> - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
> - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
> - Templates for merge requests were [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46222) in GitLab 13.8.
Templates are most useful, because you can create a template once and use it multiple times.
To re-use templates [you've created](../project/description_templates.md#create-an-issue-template):
1. Go to your project's `Settings > General > Templates`.
1. From the dropdown, select your template project as the template repository at group level.
![Group template settings](../group/img/group_file_template_settings.png)
### Set an issue and merge request description template at instance level **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46222) in GitLab 13.8.
Similar to group templates, issue and merge request templates can also be set up at the instance level.
This results in those templates being available in all projects within the instance.
Only instance administrators can set instance-level templates.
To set the instance-level description template repository:
1. Select the **Admin Area** icon (**{admin}**).
1. Select **Templates**.
1. From the dropdown, select your template project as the template repository at instance level.
Learn more about [instance template repository](../admin_area/settings/instance_template_repository.md).
![Setting templates in the Admin Area](../admin_area/settings/img/file_template_admin_area.png)
### Set a default template for merge requests and issues **(STARTER)**
> - This feature was introduced before [description templates](#description-templates) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
> - Templates for issues [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
> - Templates for merge requests [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9.
The visibility of issues and/or merge requests should be set to either "Everyone
with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
@ -113,52 +144,47 @@ pre-filled with the text you entered in the template(s).
## Description template example
We make use of Description Templates for Issues and Merge Requests within the GitLab Community
Edition project. Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
We make use of description templates for issues and merge requests in the GitLab project.
Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
for some examples.
NOTE:
It's possible to use [quick actions](quick_actions.md) within description templates to quickly add
It's possible to use [quick actions](quick_actions.md) in description templates to quickly add
labels, assignees, and milestones. The quick actions are only executed if the user submitting
the issue or merge request has the permissions to perform the relevant actions.
Here is an example of a Bug report template:
```plaintext
Summary
```markdown
## Summary
(Summarize the bug encountered concisely)
Steps to reproduce
## Steps to reproduce
(How one can reproduce the issue - this is very important)
## Example Project
Example Project
(If possible, please create an example project here on GitLab.com that exhibits the problematic
behaviour, and link to it here in the bug report.
If you are using an older version of GitLab, this will also determine whether the bug has been fixed
in a more recent version)
(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
What is the current bug behavior?
## What is the current bug behavior?
(What actually happens)
What is the expected correct behavior?
## What is the expected correct behavior?
(What you should see instead)
## Relevant logs and/or screenshots
Relevant logs and/or screenshots
(Paste any relevant logs - please use code blocks (```) to format console output, logs, and code, as
it's very hard to read otherwise.)
(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)
Possible fixes
## Possible fixes
(If you can, link to the line of code that might be responsible for the problem)

View File

@ -217,7 +217,7 @@ You can then see issue statuses in the [issue list](#issues-list) and the
## Other Issue actions
- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
- [Create an issue from a template](../../project/description_templates.md#use-the-templates)
- [Set a due date](due_dates.md)
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
in order to change their status, assignee, milestone, or labels in bulk.

View File

@ -102,7 +102,7 @@ To edit a file:
in the bottom-right corner.
1. When you're done, click **Submit changes...**.
1. (Optional) Adjust the default title and description of the merge request that will be submitted
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#creating-merge-request-templates)
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#create-a-merge-request-template)
from the dropdown menu and edit it accordingly.
1. Click **Submit changes**.
1. A new merge request is automatically created and you can assign a colleague for review.

View File

@ -45,9 +45,10 @@ module API
get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do
begin
template = TemplateFinder
.build(params[:type], user_project, name: params[:name])
.execute
template = TemplateFinder.build(
params[:type], user_project, name: params[:name],
source_template_project_id: params[:source_template_project_id]
).execute
rescue ::Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
not_found!('Template')
end

View File

@ -8,6 +8,7 @@ module Gitlab
def initialize(path, project = nil, category: nil)
@path = path
@category = category
@project = project
@finder = self.class.finder(project)
end
@ -31,6 +32,22 @@ module Gitlab
# override with a comment to be placed at the top of the blob.
end
def project_id
@project&.id
end
def project_path
@project&.path
end
def namespace_id
@project&.namespace&.id
end
def namespace_path
@project&.namespace&.full_path
end
# Present for compatibility with license templates, which can replace text
# like `[fullname]` with a user-specified string. This is a no-op for
# other templates
@ -82,11 +99,11 @@ module Gitlab
raise NotImplementedError
end
def by_category(category, project = nil)
def by_category(category, project = nil, empty_category_title: nil)
directory = category_directory(category)
files = finder(project).list_files_for(directory)
files.map { |f| new(f, project, category: category) }.sort
files.map { |f| new(f, project, category: category.presence || empty_category_title) }.sort
end
def category_directory(category)

View File

@ -15,6 +15,10 @@ module Gitlab
def finder(project)
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end
end
end

View File

@ -15,6 +15,10 @@ module Gitlab
def finder(project)
Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end
end
end

View File

@ -20790,9 +20790,6 @@ msgstr ""
msgid "Pipelines|More Information"
msgstr ""
msgid "Pipelines|No CI file found in this repository, please add one."
msgstr ""
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr ""
@ -20805,9 +20802,6 @@ msgstr ""
msgid "Pipelines|Project cache successfully reset."
msgstr ""
msgid "Pipelines|Repository does not have a default branch, please set one."
msgstr ""
msgid "Pipelines|Revoke"
msgstr ""
@ -20829,6 +20823,9 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again."
msgstr ""
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr ""
@ -21882,6 +21879,9 @@ msgstr ""
msgid "Project ID"
msgstr ""
msgid "Project Templates"
msgstr ""
msgid "Project URL"
msgstr ""

View File

@ -41,8 +41,8 @@ tests = [
{
explanation: 'Tooling should map to respective spec',
source: 'tooling/lib/tooling/test_file_finder.rb',
expected: ['spec/tooling/lib/tooling/test_file_finder_spec.rb']
source: 'tooling/lib/tooling/helm3_client.rb',
expected: ['spec/tooling/lib/tooling/helm3_client_spec.rb']
},
{

View File

@ -160,12 +160,12 @@ RSpec.describe Projects::TemplatesController do
end
shared_examples 'template names request' do
it 'returns the template names' do
it 'returns the template names', :aggregate_failures do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
expect(json_response).to match(expected_template_names)
expect(json_response['Project Templates'].size).to eq(2)
expect(json_response['Project Templates'].map { |x| { "name" => x['name'] } }).to match(expected_template_names)
end
it 'fails for user with no access' do

View File

@ -41,7 +41,7 @@ RSpec.describe 'issue state', :js do
end
end
describe 'when open' do
describe 'when open', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297348' do
context 'when clicking the top `Close issue` button', :aggregate_failures do
let(:open_issue) { create(:issue, project: project) }
@ -63,7 +63,7 @@ RSpec.describe 'issue state', :js do
end
end
describe 'when closed' do
describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297201' do
context 'when clicking the top `Reopen issue` button', :aggregate_failures do
let(:closed_issue) { create(:issue, project: project, state: 'closed') }

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)
end
context 'when the "Save release" button is clicked' do
context 'when the "Save release" button is clicked', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297507' do
let(:tag_name) { 'v1.0' }
let(:release_title) { 'A most magnificent release' }
let(:release_notes) { 'Best. Release. **Ever.** :rocket:' }

View File

@ -1,4 +1,5 @@
import { languages } from 'monaco-editor';
import { TEST_HOST } from 'helpers/test_constants';
import EditorLite from '~/editor/editor_lite';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
@ -9,6 +10,7 @@ describe('~/editor/editor_ci_config_ext', () => {
let editor;
let instance;
let editorEl;
let originalGitlabUrl;
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
setFixtures('<div id="editor"></div>');
@ -22,6 +24,15 @@ describe('~/editor/editor_ci_config_ext', () => {
instance.use(new CiSchemaExtension());
};
beforeAll(() => {
originalGitlabUrl = gon.gitlab_url;
gon.gitlab_url = TEST_HOST;
});
afterAll(() => {
gon.gitlab_url = originalGitlabUrl;
});
beforeEach(() => {
createMockEditor();
});
@ -73,7 +84,7 @@ describe('~/editor/editor_ci_config_ext', () => {
});
expect(getConfiguredYmlSchema()).toEqual({
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
fileMatch: [defaultBlobPath],
});
});
@ -87,7 +98,7 @@ describe('~/editor/editor_ci_config_ext', () => {
});
expect(getConfiguredYmlSchema()).toEqual({
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
fileMatch: ['another-ci-filename.yml'],
});
});

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');
store.state.openFiles.push(f);
@ -90,10 +90,12 @@ describe('IDE store file actions', () => {
});
it('removes file if it pending', () => {
store.state.openFiles.push({
...localFile,
pending: true,
});
store.state.openFiles = [
{
...localFile,
pending: true,
},
];
return store.dispatch('closeFile', localFile).then(() => {
expect(store.state.openFiles.length).toBe(0);

View File

@ -35,7 +35,7 @@ exports[`Alert integration settings form default state should match the default
Incident template (optional)
<gl-link-stub
href="/help/user/project/description_templates#creating-issue-templates"
href="/help/user/project/description_templates#create-an-issue-template"
target="_blank"
>
<gl-icon-stub

View File

@ -423,7 +423,9 @@ describe('Issuable output', () => {
});
it('shows the form if template names request is successful', () => {
const mockData = [{ name: 'Bug' }];
const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {

View File

@ -14,7 +14,10 @@ describe('Issue description template component', () => {
vm = new Component({
propsData: {
formState,
issuableTemplates: [{ name: 'test' }],
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
},
projectId: 1,
projectPath: '/',
projectNamespace: '/',
},
@ -23,7 +26,7 @@ describe('Issue description template component', () => {
it('renders templates as JSON array in data attribute', () => {
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
'[{"name":"test"}]',
'{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}',
);
});

View File

@ -19,6 +19,7 @@ describe('Inline edit form component', () => {
markdownPreviewPath: '/',
markdownDocsPath: '/',
projectPath: '/',
projectId: 1,
projectNamespace: '/',
};
@ -42,7 +43,11 @@ describe('Inline edit form component', () => {
});
it('renders template selector when templates exists', () => {
createComponent({ issuableTemplates: ['test'] });
createComponent({
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' }],
},
});
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
});

View File

@ -52,6 +52,7 @@ export const appProps = {
markdownDocsPath: '/',
projectNamespace: '/',
projectPath: '/',
projectId: 1,
issuableTemplateNamesPath: '/issuable-templates-path',
zoomMeetingUrl,
publishedIncidentUrl,

View File

@ -23,6 +23,7 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
const findAlert = () => wrapper.find(GlAlert);
const findLintParameters = () => findAllByTestId('ci-lint-parameter');
const findLintParameterAt = (i) => findLintParameters().at(i);
const findLintValueAt = (i) => findAllByTestId('ci-lint-value').at(i);
afterEach(() => {
wrapper.destroy();
@ -50,6 +51,20 @@ describe('~/pipeline_editor/components/lint/ci_lint.vue', () => {
expect(findLintParameterAt(2).text()).toBe('Build Job - job_build');
});
it('displays jobs details', () => {
expect(findLintParameters()).toHaveLength(3);
expect(findLintValueAt(0).text()).toMatchInterpolatedText(
'echo "test 1" Only policy: branches, tags When: on_success',
);
expect(findLintValueAt(1).text()).toMatchInterpolatedText(
'echo "test 2" Only policy: branches, tags When: on_success',
);
expect(findLintValueAt(2).text()).toMatchInterpolatedText(
'echo "build" Only policy: branches, tags When: on_success',
);
});
it('displays invalid results', () => {
createComponent(
{

View File

@ -27,7 +27,7 @@ Object {
"echo 'script 1'",
],
"stage": "test",
"tagList": Array [
"tags": Array [
"tag 1",
],
"when": "on_success",
@ -61,7 +61,7 @@ Object {
"echo 'script 2'",
],
"stage": "test",
"tagList": Array [
"tags": Array [
"tag 2",
],
"when": "on_success",

View File

@ -35,6 +35,21 @@ job_build:
needs: ["job_test_2"]
`;
const mockJobFields = {
beforeScript: [],
afterScript: [],
environment: null,
allowFailure: false,
tags: [],
when: 'on_success',
only: { refs: ['branches', 'tags'], __typename: 'CiJobLimitType' },
except: null,
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
__typename: 'CiConfigJob',
};
// Mock result of the graphql query at:
// app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
export const mockCiConfigQueryResponse = {
data: {
ciConfig: {
@ -54,8 +69,8 @@ export const mockCiConfigQueryResponse = {
nodes: [
{
name: 'job_test_1',
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
__typename: 'CiConfigJob',
script: ['echo "test 1"'],
...mockJobFields,
},
],
__typename: 'CiConfigJobConnection',
@ -69,9 +84,8 @@ export const mockCiConfigQueryResponse = {
nodes: [
{
name: 'job_test_2',
needs: { nodes: [], __typename: 'CiConfigNeedConnection' },
__typename: 'CiConfigJob',
script: ['echo "test 2"'],
...mockJobFields,
},
],
__typename: 'CiConfigJobConnection',
@ -94,11 +108,8 @@ export const mockCiConfigQueryResponse = {
nodes: [
{
name: 'job_build',
needs: {
nodes: [{ name: 'job_test_2', __typename: 'CiConfigNeed' }],
__typename: 'CiConfigNeedConnection',
},
__typename: 'CiConfigJob',
script: ['echo "build"'],
...mockJobFields,
},
],
__typename: 'CiConfigJobConnection',

View File

@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
import {
mockCiConfigPath,
@ -414,58 +415,81 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
});
it('no error is shown when data is set', async () => {
createComponentWithApollo();
describe('when file exists', () => {
beforeEach(async () => {
createComponentWithApollo();
await waitForPromises();
await waitForPromises();
});
expect(findAlert().exists()).toBe(false);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
it('shows editor and commit form', () => {
expect(findEditorLite().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(true);
});
it('ci config query is called with correct variables', async () => {
createComponentWithApollo();
it('no error is shown when data is set', async () => {
expect(findAlert().exists()).toBe(false);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
await waitForPromises();
it('ci config query is called with correct variables', async () => {
createComponentWithApollo();
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
await waitForPromises();
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
});
});
});
it('shows a 404 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: 404,
},
describe('when no file exists', () => {
const expectedAlertMsg =
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
it('does not show editor or commit form', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findEditorLite().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(false);
});
createComponentWithApollo();
await waitForPromises();
it('shows a 404 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
createComponentWithApollo();
expect(findAlert().text()).toBe('No CI file found in this repository, please add one.');
});
await waitForPromises();
it('shows a 400 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: 400,
},
expect(findAlert().text()).toBe(expectedAlertMsg);
});
createComponentWithApollo();
await waitForPromises();
it('shows a 400 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
},
});
createComponentWithApollo();
expect(findAlert().text()).toBe('Repository does not have a default branch, please set one.');
});
await waitForPromises();
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe(expectedAlertMsg);
});
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
});
});
});
});

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',
lockVersion: issue.lock_version,
projectPath: @project.path,
projectId: @project.id,
projectNamespace: @project.namespace.path,
initialTitleHtml: issue.title,
initialTitleText: issue.title,

View File

@ -57,6 +57,6 @@ RSpec.describe LicenseTemplate do
end
def build_template(content)
described_class.new(key: 'foo', name: 'foo', category: :Other, content: content)
described_class.new(key: 'foo', name: 'foo', project: nil, category: :Other, content: content)
end
end

View File

@ -2977,56 +2977,9 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '#pushes_since_gc' do
let(:project) { build_stubbed(:project) }
after do
project.reset_pushes_since_gc
end
context 'without any pushes' do
it 'returns 0' do
expect(project.pushes_since_gc).to eq(0)
end
end
context 'with a number of pushes' do
it 'returns the number of pushes' do
3.times { project.increment_pushes_since_gc }
expect(project.pushes_since_gc).to eq(3)
end
end
end
describe '#increment_pushes_since_gc' do
let(:project) { build_stubbed(:project) }
after do
project.reset_pushes_since_gc
end
it 'increments the number of pushes since the last GC' do
3.times { project.increment_pushes_since_gc }
expect(project.pushes_since_gc).to eq(3)
end
end
describe '#reset_pushes_since_gc' do
let(:project) { build_stubbed(:project) }
after do
project.reset_pushes_since_gc
end
it 'resets the number of pushes since the last GC' do
3.times { project.increment_pushes_since_gc }
project.reset_pushes_since_gc
expect(project.pushes_since_gc).to eq(0)
end
it_behaves_like 'can housekeep repository' do
let(:resource) { build_stubbed(:project) }
let(:resource_key) { 'projects' }
end
describe '#deployment_variables' do

View File

@ -19,12 +19,8 @@ RSpec.describe MergeRequests::MergeService do
{ commit_message: 'Awesome message', sha: merge_request.diff_head_sha }
end
let(:feature_flag_persist_squash) { true }
context 'valid params' do
before do
stub_feature_flags(persist_squash_commit_sha_for_squashes: feature_flag_persist_squash)
allow(service).to receive(:execute_hooks)
expect(merge_request).to receive(:update_and_mark_in_progress_merge_commit_sha).twice.and_call_original
@ -90,14 +86,6 @@ RSpec.describe MergeRequests::MergeService do
expect(merge_request.squash_commit_sha).to eq(squash_commit.id)
end
context 'when feature flag is disabled' do
let(:feature_flag_persist_squash) { false }
it 'does not populate squash_commit_sha' do
expect(merge_request.squash_commit_sha).to be_nil
end
end
end
end

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