diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-13 18:10:40 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-13 18:10:40 +0300 |
commit | 9b1b702f0fc3820e13fd3810bf096687d3378dc5 (patch) | |
tree | 8ec6e084f9b0c84ebc0996c8ea64d47389f49e81 | |
parent | 39c1496527de559d5d3a5c3b53d11575f435a4dc (diff) |
Add latest changes from gitlab-org/gitlab@master
81 files changed, 691 insertions, 615 deletions
@@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index a39a504e34b..af8c1d5b50f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index 2532aeea989..a5c8050b772 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -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); } diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index affa20997e9..7e32ede96df 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -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({ diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue index 350d709abfd..8b0265237ba 100644 --- a/app/assets/javascripts/boards/components/board_card_layout.vue +++ b/app/assets/javascripts/boards/components/board_card_layout.vue @@ -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)" diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js index 99351231520..98858f20518 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js @@ -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(); diff --git a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js index 05d938c57ce..eb47c20912e 100644 --- a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js +++ b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js @@ -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], }); } diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 004e38b0a25..42668dec63a 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -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}`); }; diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js index fcac9c519c2..818af4ecb90 100644 --- a/app/assets/javascripts/incidents_settings/constants.js +++ b/app/assets/javascripts/incidents_settings/constants.js @@ -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 */ diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index d569ad573a2..87dc31e6292 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -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" diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue index 71299381aae..f23bb394683 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -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" diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index d48bf1fe7a9..9d1ce01116b 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -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> diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js index 83fd1355f26..a93abbf64df 100644 --- a/app/assets/javascripts/issue_show/issue.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -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, }, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 06bbd406e3a..a50913d3455 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -11,7 +11,7 @@ export default class Store { lockedWarningVisible: false, updateLoading: false, lock_version: 0, - issuableTemplates: [], + issuableTemplates: {}, }; } diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue index 22f734be5aa..b27ab9a39d3 100644 --- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue @@ -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); }, }, }; diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue index 6c7e03e4920..e2529613844 100644 --- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue @@ -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; diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql index 496036f690f..5091d63111f 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql @@ -15,7 +15,7 @@ mutation lintCI($endpoint: String, $content: String, $dry: Boolean) { } afterScript stage - tagList + tags when } } diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js index ec55191c946..81e75c32846 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js +++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js @@ -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, diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index 9217466a0b5..d6884ae121f 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -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" diff --git a/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql b/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql index 683e0ee6a14..f93908aeb04 100644 --- a/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql +++ b/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql @@ -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 diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 22bbd083a5d..bcae79c9679 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -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() { diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss index c7e90ba3451..2ab3bdcc474 100644 --- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss +++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss @@ -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 diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb index f4726638777..71f426a68ab 100644 --- a/app/controllers/projects/templates_controller.rb +++ b/app/controllers/projects/templates_controller.rb @@ -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 diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb index 4d68b963dc3..2419141f260 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -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, diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 254af62f47a..7a4913748e8 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -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 diff --git a/app/helpers/issuables_description_templates_helper.rb b/app/helpers/issuables_description_templates_helper.rb new file mode 100644 index 00000000000..ffb20db65d0 --- /dev/null +++ b/app/helpers/issuables_description_templates_helper.rb @@ -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 diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index da142cbed0e..de5cae2e9e2 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -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", diff --git a/app/models/concerns/can_housekeep_repository.rb b/app/models/concerns/can_housekeep_repository.rb new file mode 100644 index 00000000000..6c7d6ac37ef --- /dev/null +++ b/app/models/concerns/can_housekeep_repository.rb @@ -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 diff --git a/app/models/license_template.rb b/app/models/license_template.rb index bd24259984b..f6961da6d33 100644 --- a/app/models/license_template.rb +++ b/app/models/license_template.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index ce965140252..b9911fd308b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 407f2ad70f8..f4454db0af8 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -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 diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml index 5fab242ed5c..153235c37d2 100644 --- a/app/views/projects/_service_desk_settings.html.haml +++ b/app/views/projects/_service_desk_settings.html.haml @@ -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 } diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 1dfb0594b27..cde8a5f69dd 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -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 diff --git a/app/views/shared/issuable/form/_template_selector.html.haml b/app/views/shared/issuable/form/_template_selector.html.haml index bf34ea4a1b2..a58bde5aa9e 100644 --- a/app/views/shared/issuable/form/_template_selector.html.haml +++ b/app/views/shared/issuable/form/_template_selector.html.haml @@ -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 diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index 98c9f73fa3a..cb4b712ffbe 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -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>' diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 9ac10aa2d7c..4c4a314a1e6 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -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 diff --git a/app/workers/ci/daily_build_group_report_results_worker.rb b/app/workers/ci/daily_build_group_report_results_worker.rb index a6d3c485e24..687cadc6366 100644 --- a/app/workers/ci/daily_build_group_report_results_worker.rb +++ b/app/workers/ci/daily_build_group_report_results_worker.rb @@ -5,6 +5,8 @@ module Ci include ApplicationWorker include PipelineBackgroundQueue + feature_category :code_testing + idempotent! def perform(pipeline_id) diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb index f8c9994a746..4de56f54f44 100644 --- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb +++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb @@ -6,6 +6,8 @@ module Ci include ApplicationWorker include PipelineBackgroundQueue + feature_category :code_testing + idempotent! def perform(pipeline_id) diff --git a/changelogs/unreleased/21686_persist_squash_commit_sha.yml b/changelogs/unreleased/21686_persist_squash_commit_sha.yml new file mode 100644 index 00000000000..c95028b8492 --- /dev/null +++ b/changelogs/unreleased/21686_persist_squash_commit_sha.yml @@ -0,0 +1,5 @@ +--- +title: Persist 'squash_commit_sha' when squashing +merge_request: 51074 +author: +type: added diff --git a/changelogs/unreleased/292498-webide-switch-before-closing.yml b/changelogs/unreleased/292498-webide-switch-before-closing.yml new file mode 100644 index 00000000000..310dd76275c --- /dev/null +++ b/changelogs/unreleased/292498-webide-switch-before-closing.yml @@ -0,0 +1,5 @@ +--- +title: In WebIDE switch files before closing the active one +merge_request: 51483 +author: +type: fixed diff --git a/changelogs/unreleased/track-ci-template-usage-by-default.yml b/changelogs/unreleased/track-ci-template-usage-by-default.yml new file mode 100644 index 00000000000..64d08d7aa51 --- /dev/null +++ b/changelogs/unreleased/track-ci-template-usage-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Instrument CI template usage across projects +merge_request: 51391 +author: +type: added diff --git a/changelogs/unreleased/yo-master-patch-52337.yml b/changelogs/unreleased/yo-master-patch-52337.yml new file mode 100644 index 00000000000..01578c618e2 --- /dev/null +++ b/changelogs/unreleased/yo-master-patch-52337.yml @@ -0,0 +1,5 @@ +--- +title: Update toggle button in repo general settings +merge_request: 51036 +author: Yogi (@yo) +type: other diff --git a/config/feature_flags/development/persist_squash_commit_sha_for_squashes.yml b/config/feature_flags/development/persist_squash_commit_sha_for_squashes.yml deleted file mode 100644 index 835437278c4..00000000000 --- a/config/feature_flags/development/persist_squash_commit_sha_for_squashes.yml +++ /dev/null @@ -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 diff --git a/config/feature_flags/development/usage_data_track_ci_templates_unique_projects.yml b/config/feature_flags/development/usage_data_track_ci_templates_unique_projects.yml index f3ddfe1f3c1..306e37ac308 100644 --- a/config/feature_flags/development/usage_data_track_ci_templates_unique_projects.yml +++ b/config/feature_flags/development/usage_data_track_ci_templates_unique_projects.yml @@ -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 diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 753fff940e4..424114a3d33 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -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) diff --git a/doc/development/feature_flags/process.md b/doc/development/feature_flags/process.md index 2e3680bb103..7e6299c193c 100644 --- a/doc/development/feature_flags/process.md +++ b/doc/development/feature_flags/process.md @@ -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. diff --git a/doc/development/features_inside_dot_gitlab.md b/doc/development/features_inside_dot_gitlab.md index 08adb7faeb2..36b9064bbc4 100644 --- a/doc/development/features_inside_dot_gitlab.md +++ b/doc/development/features_inside_dot_gitlab.md @@ -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`. diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index 450ee6620a4..78e90d5f1c4 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -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 diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 74406d3e5cf..36410f2e1a7 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -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. diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index 3108bdda7a0..fe1ab292d44 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -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)** + +> [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. -> - 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. +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 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 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) diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 74311eefd83..014e903a012 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -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. diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index 07f122a7828..f7ccbf8a565 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -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. diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index 19244ed697f..95bb35e0dd9 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -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 diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb index b659bff52ad..c5c4277719c 100644 --- a/lib/gitlab/template/base_template.rb +++ b/lib/gitlab/template/base_template.rb @@ -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) diff --git a/lib/gitlab/template/issue_template.rb b/lib/gitlab/template/issue_template.rb index 01b191733d4..244231acb65 100644 --- a/lib/gitlab/template/issue_template.rb +++ b/lib/gitlab/template/issue_template.rb @@ -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 diff --git a/lib/gitlab/template/merge_request_template.rb b/lib/gitlab/template/merge_request_template.rb index 357b31cd82e..5a29d770f53 100644 --- a/lib/gitlab/template/merge_request_template.rb +++ b/lib/gitlab/template/merge_request_template.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4951cd4aa3d..a96f1f00c43 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping index 1c66e19df50..8bf25ea3b5f 100755 --- a/scripts/verify-tff-mapping +++ b/scripts/verify-tff-mapping @@ -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'] }, { diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 01593f4133c..13f329cb0b6 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -160,12 +160,12 @@ RSpec.describe Projects::TemplatesController do end shared_examples 'template names request' do - it 'returns the template names' do + it 'returns the template names', :aggregate_failures do get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(2) - expect(json_response).to match(expected_template_names) + expect(json_response['Project Templates'].size).to eq(2) + expect(json_response['Project Templates'].map { |x| { "name" => x['name'] } }).to match(expected_template_names) end it 'fails for user with no access' do diff --git a/spec/features/issues/issue_state_spec.rb b/spec/features/issues/issue_state_spec.rb index 9145089f5fc..d5a115433aa 100644 --- a/spec/features/issues/issue_state_spec.rb +++ b/spec/features/issues/issue_state_spec.rb @@ -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') } diff --git a/spec/features/projects/releases/user_creates_release_spec.rb b/spec/features/projects/releases/user_creates_release_spec.rb index 0a5f7cc7edd..2acdf983cf2 100644 --- a/spec/features/projects/releases/user_creates_release_spec.rb +++ b/spec/features/projects/releases/user_creates_release_spec.rb @@ -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:' } diff --git a/spec/frontend/editor/editor_ci_schema_ext_spec.js b/spec/frontend/editor/editor_ci_schema_ext_spec.js index 4d65b77cdfc..9dd88aad7e6 100644 --- a/spec/frontend/editor/editor_ci_schema_ext_spec.js +++ b/spec/frontend/editor/editor_ci_schema_ext_spec.js @@ -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'], }); }); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js index 71166234363..9d367714bbe 100644 --- a/spec/frontend/ide/stores/actions/file_spec.js +++ b/spec/frontend/ide/stores/actions/file_spec.js @@ -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); diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap index c3fd4a9bab2..1d34ed3c9ba 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -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 diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js index ec2055ca7d1..74d195e1eb2 100644 --- a/spec/frontend/issue_show/components/app_spec.js +++ b/spec/frontend/issue_show/components/app_spec.js @@ -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(() => { diff --git a/spec/frontend/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js index 9ebab31f1ad..446c03f70e1 100644 --- a/spec/frontend/issue_show/components/fields/description_template_spec.js +++ b/spec/frontend/issue_show/components/fields/description_template_spec.js @@ -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":"/"}]}', ); }); diff --git a/spec/frontend/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js index 4e123f606f6..3a38883d1ec 100644 --- a/spec/frontend/issue_show/components/form_spec.js +++ b/spec/frontend/issue_show/components/form_spec.js @@ -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(); }); diff --git a/spec/frontend/issue_show/mock_data.js b/spec/frontend/issue_show/mock_data.js index 5a31a550088..fd08c95b454 100644 --- a/spec/frontend/issue_show/mock_data.js +++ b/spec/frontend/issue_show/mock_data.js @@ -52,6 +52,7 @@ export const appProps = { markdownDocsPath: '/', projectNamespace: '/', projectPath: '/', + projectId: 1, issuableTemplateNamesPath: '/issuable-templates-path', zoomMeetingUrl, publishedIncidentUrl, diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js index f2d33bd2ad5..5ccf4bbdab4 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js @@ -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( { diff --git a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap b/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap index d7d4d0af90c..8670c44f6f6 100644 --- a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap +++ b/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap @@ -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", diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index b3526b47731..3692e51cf0b 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -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', diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index 00db3553ea5..50434a83ffe 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -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); + }); + + 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.'); + expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.'); + }); }); }); }); diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb new file mode 100644 index 00000000000..199b43c2131 --- /dev/null +++ b/spec/helpers/issuables_description_templates_helper_spec.rb @@ -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 diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 57845904d32..a9ac98fa65b 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -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, diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb index 515f728f515..fe06d1a357c 100644 --- a/spec/models/license_template_spec.rb +++ b/spec/models/license_template_spec.rb @@ -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 diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c0579566d1f..a2b51684d4d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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 diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 4e1d8b3a9a9..dd37d87e3f5 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb new file mode 100644 index 00000000000..dbd4f1927e3 --- /dev/null +++ b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb new file mode 100644 index 00000000000..2f0b95427d2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb @@ -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 diff --git a/spec/tooling/lib/tooling/test_file_finder_spec.rb b/spec/tooling/lib/tooling/test_file_finder_spec.rb deleted file mode 100644 index 683bc647b8a..00000000000 --- a/spec/tooling/lib/tooling/test_file_finder_spec.rb +++ /dev/null @@ -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 diff --git a/tooling/lib/tooling/test_file_finder.rb b/tooling/lib/tooling/test_file_finder.rb deleted file mode 100644 index cf5de190c4a..00000000000 --- a/tooling/lib/tooling/test_file_finder.rb +++ /dev/null @@ -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 |