Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6aaec2fc6c
commit
cab5a484fe
|
|
@ -346,8 +346,8 @@ rspec fast_spec_helper minimal:
|
|||
db:rollback:
|
||||
extends: .db-job-base
|
||||
script:
|
||||
- scripts/db_migrate VERSION=20181228175414
|
||||
- scripts/db_migrate SKIP_SCHEMA_VERSION_CHECK=true
|
||||
- scripts/db_tasks db:migrate VERSION=20181228175414
|
||||
- scripts/db_tasks db:migrate SKIP_SCHEMA_VERSION_CHECK=true
|
||||
|
||||
db:migrate:reset:
|
||||
extends: .db-job-base
|
||||
|
|
@ -372,15 +372,13 @@ db:migrate-from-previous-major-version:
|
|||
- git checkout -f $CI_COMMIT_SHA
|
||||
- SETUP_DB=false USE_BUNDLE_INSTALL=true bash scripts/prepare_build.sh
|
||||
script:
|
||||
- run_timed_command "scripts/db_migrate"
|
||||
- run_timed_command "scripts/db_tasks db:migrate"
|
||||
|
||||
db:migrate-from-previous-major-version-decomposed:
|
||||
extends:
|
||||
- db:migrate-from-previous-major-version
|
||||
- .decomposed-database
|
||||
- .rails:rules:decomposed-databases
|
||||
variables:
|
||||
GITLAB_MIGRATE_MAIN_ONLY: "true"
|
||||
|
||||
.db:check-schema-base:
|
||||
extends:
|
||||
|
|
@ -388,7 +386,7 @@ db:migrate-from-previous-major-version-decomposed:
|
|||
variables:
|
||||
TAG_TO_CHECKOUT: "v14.4.0"
|
||||
script:
|
||||
- run_timed_command "scripts/db_migrate"
|
||||
- run_timed_command "scripts/db_tasks db:migrate"
|
||||
- scripts/schema_changed.sh
|
||||
- scripts/validate_migration_timestamps
|
||||
|
||||
|
|
@ -411,6 +409,12 @@ db:check-migrations:
|
|||
- scripts/validate_migration_schema
|
||||
allow_failure: true
|
||||
|
||||
db:check-migrations-decomposed:
|
||||
extends:
|
||||
- db:check-migrations
|
||||
- .decomposed-database
|
||||
- .rails:rules:decomposed-databases
|
||||
|
||||
db:gitlabcom-database-testing:
|
||||
extends: .rails:rules:db:gitlabcom-database-testing
|
||||
stage: test
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
.allure-report-base:
|
||||
image:
|
||||
name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.4.1
|
||||
name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.4.2
|
||||
entrypoint: [""]
|
||||
stage: post-qa
|
||||
variables:
|
||||
|
|
|
|||
|
|
@ -53,10 +53,6 @@ Rails/SaveBang:
|
|||
- ee/spec/services/status_page/trigger_publish_service_spec.rb
|
||||
- ee/spec/services/todo_service_spec.rb
|
||||
- ee/spec/services/vulnerability_feedback/create_service_spec.rb
|
||||
- qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
|
||||
- qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
|
||||
- qa/qa/specs/features/ee/browser_ui/3_create/repository/pull_mirroring_over_http_spec.rb
|
||||
- qa/qa/specs/features/ee/browser_ui/3_create/repository/pull_mirroring_over_ssh_with_key_spec.rb
|
||||
- spec/lib/backup/manager_spec.rb
|
||||
- spec/lib/gitlab/alerting/alert_spec.rb
|
||||
- spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
GlSprintf,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
||||
import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
|
||||
|
|
@ -138,6 +139,9 @@ export default {
|
|||
initialEmail: {
|
||||
default: '',
|
||||
},
|
||||
isAnonymousSearchDisabled: {
|
||||
default: false,
|
||||
},
|
||||
isIssueRepositioningDisabled: {
|
||||
default: false,
|
||||
},
|
||||
|
|
@ -183,12 +187,22 @@ export default {
|
|||
sortKey = defaultSortKey;
|
||||
}
|
||||
|
||||
const isSearchDisabled =
|
||||
this.isAnonymousSearchDisabled &&
|
||||
!this.isSignedIn &&
|
||||
window.location.search.includes('search=');
|
||||
|
||||
if (isSearchDisabled) {
|
||||
this.showAnonymousSearchingMessage();
|
||||
}
|
||||
|
||||
return {
|
||||
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
||||
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
||||
filterTokens: getFilterTokens(window.location.search),
|
||||
filterTokens: isSearchDisabled ? [] : getFilterTokens(window.location.search),
|
||||
issues: [],
|
||||
issuesCounts: {},
|
||||
issuesError: null,
|
||||
pageInfo: {},
|
||||
pageParams: getInitialPageParams(sortKey),
|
||||
showBulkEditSidebar: false,
|
||||
|
|
@ -210,7 +224,8 @@ export default {
|
|||
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
|
||||
},
|
||||
error(error) {
|
||||
createFlash({ message: this.$options.i18n.errorFetchingIssues, captureError: true, error });
|
||||
this.issuesError = this.$options.i18n.errorFetchingIssues;
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
skip() {
|
||||
return !this.hasAnyIssues;
|
||||
|
|
@ -226,7 +241,8 @@ export default {
|
|||
return data[this.namespace] ?? {};
|
||||
},
|
||||
error(error) {
|
||||
createFlash({ message: this.$options.i18n.errorFetchingCounts, captureError: true, error });
|
||||
this.issuesError = this.$options.i18n.errorFetchingCounts;
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
skip() {
|
||||
return !this.hasAnyIssues;
|
||||
|
|
@ -387,6 +403,8 @@ export default {
|
|||
tokens.push(...this.eeSearchTokens);
|
||||
}
|
||||
|
||||
tokens.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return tokens;
|
||||
},
|
||||
showPaginationControls() {
|
||||
|
|
@ -518,7 +536,14 @@ export default {
|
|||
}
|
||||
this.state = state;
|
||||
},
|
||||
handleDismissAlert() {
|
||||
this.issuesError = null;
|
||||
},
|
||||
handleFilter(filter) {
|
||||
if (this.isAnonymousSearchDisabled && !this.isSignedIn) {
|
||||
this.showAnonymousSearchingMessage();
|
||||
return;
|
||||
}
|
||||
this.pageParams = getInitialPageParams(this.sortKey);
|
||||
this.filterTokens = filter;
|
||||
},
|
||||
|
|
@ -569,7 +594,8 @@ export default {
|
|||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
createFlash({ message: this.$options.i18n.reorderError, captureError: true, error });
|
||||
this.issuesError = this.$options.i18n.reorderError;
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
},
|
||||
handleSort(sortKey) {
|
||||
|
|
@ -583,6 +609,12 @@ export default {
|
|||
}
|
||||
this.sortKey = sortKey;
|
||||
},
|
||||
showAnonymousSearchingMessage() {
|
||||
createFlash({
|
||||
message: this.$options.i18n.anonymousSearchingMessage,
|
||||
type: FLASH_TYPES.NOTICE,
|
||||
});
|
||||
},
|
||||
showIssueRepositioningMessage() {
|
||||
createFlash({
|
||||
message: this.$options.i18n.issueRepositioningMessage,
|
||||
|
|
@ -607,6 +639,7 @@ export default {
|
|||
:sort-options="sortOptions"
|
||||
:initial-sort-by="sortKey"
|
||||
:issuables="issues"
|
||||
:error="issuesError"
|
||||
label-filter-param="label_name"
|
||||
:tabs="$options.IssuableListTabs"
|
||||
:current-tab="state"
|
||||
|
|
@ -620,6 +653,7 @@ export default {
|
|||
:has-previous-page="pageInfo.hasPreviousPage"
|
||||
:url-params="urlParams"
|
||||
@click-tab="handleClickTab"
|
||||
@dismiss-alert="handleDismissAlert"
|
||||
@filter="handleFilter"
|
||||
@next-page="handleNextPage"
|
||||
@previous-page="handlePreviousPage"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const availableSortOptionsJira = [
|
|||
];
|
||||
|
||||
export const i18n = {
|
||||
anonymousSearchingMessage: __('You must sign in to search for specific terms.'),
|
||||
calendarLabel: __('Subscribe to calendar'),
|
||||
closed: __('CLOSED'),
|
||||
closedMoved: __('CLOSED (MOVED)'),
|
||||
|
|
@ -136,6 +137,7 @@ export const DUE_DATE_VALUES = [
|
|||
DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS,
|
||||
];
|
||||
|
||||
export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
|
||||
export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
|
||||
export const CREATED_ASC = 'CREATED_ASC';
|
||||
export const CREATED_DESC = 'CREATED_DESC';
|
||||
|
|
@ -157,42 +159,28 @@ export const UPDATED_DESC = 'UPDATED_DESC';
|
|||
export const WEIGHT_ASC = 'WEIGHT_ASC';
|
||||
export const WEIGHT_DESC = 'WEIGHT_DESC';
|
||||
|
||||
const PRIORITY_ASC_SORT = 'priority_asc';
|
||||
const CREATED_DATE_SORT = 'created_date';
|
||||
const CREATED_ASC_SORT = 'created_asc';
|
||||
const UPDATED_DESC_SORT = 'updated_desc';
|
||||
const UPDATED_ASC_SORT = 'updated_asc';
|
||||
const MILESTONE_SORT = 'milestone';
|
||||
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
|
||||
const DUE_DATE_DESC_SORT = 'due_date_desc';
|
||||
const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
|
||||
const POPULARITY_ASC_SORT = 'popularity_asc';
|
||||
const WEIGHT_DESC_SORT = 'weight_desc';
|
||||
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
|
||||
const TITLE_ASC_SORT = 'title_asc';
|
||||
const TITLE_DESC_SORT = 'title_desc';
|
||||
|
||||
export const urlSortParams = {
|
||||
[PRIORITY_ASC]: PRIORITY_ASC_SORT,
|
||||
[PRIORITY_DESC]: PRIORITY,
|
||||
[CREATED_ASC]: CREATED_ASC_SORT,
|
||||
[CREATED_DESC]: CREATED_DATE_SORT,
|
||||
[UPDATED_ASC]: UPDATED_ASC_SORT,
|
||||
[UPDATED_DESC]: UPDATED_DESC_SORT,
|
||||
[MILESTONE_DUE_ASC]: MILESTONE_SORT,
|
||||
[MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
|
||||
[DUE_DATE_ASC]: DUE_DATE,
|
||||
[DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
|
||||
[POPULARITY_ASC]: POPULARITY_ASC_SORT,
|
||||
[POPULARITY_DESC]: POPULARITY,
|
||||
[LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
|
||||
[LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
|
||||
[PRIORITY_ASC]: 'priority',
|
||||
[PRIORITY_DESC]: 'priority_desc',
|
||||
[CREATED_ASC]: 'created_asc',
|
||||
[CREATED_DESC]: 'created_date',
|
||||
[UPDATED_ASC]: 'updated_asc',
|
||||
[UPDATED_DESC]: 'updated_desc',
|
||||
[MILESTONE_DUE_ASC]: 'milestone',
|
||||
[MILESTONE_DUE_DESC]: 'milestone_due_desc',
|
||||
[DUE_DATE_ASC]: 'due_date',
|
||||
[DUE_DATE_DESC]: 'due_date_desc',
|
||||
[POPULARITY_ASC]: 'popularity_asc',
|
||||
[POPULARITY_DESC]: 'popularity',
|
||||
[LABEL_PRIORITY_ASC]: 'label_priority',
|
||||
[LABEL_PRIORITY_DESC]: 'label_priority_desc',
|
||||
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
|
||||
[WEIGHT_ASC]: WEIGHT,
|
||||
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
|
||||
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
|
||||
[TITLE_ASC]: TITLE_ASC_SORT,
|
||||
[TITLE_DESC]: TITLE_DESC_SORT,
|
||||
[WEIGHT_ASC]: 'weight',
|
||||
[WEIGHT_DESC]: 'weight_desc',
|
||||
[BLOCKING_ISSUES_ASC]: 'blocking_issues_asc',
|
||||
[BLOCKING_ISSUES_DESC]: 'blocking_issues_desc',
|
||||
[TITLE_ASC]: 'title_asc',
|
||||
[TITLE_DESC]: 'title_desc',
|
||||
};
|
||||
|
||||
export const MAX_LIST_SIZE = 10;
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export function mountIssuesListApp() {
|
|||
hasMultipleIssueAssigneesFeature,
|
||||
importCsvIssuesPath,
|
||||
initialEmail,
|
||||
isAnonymousSearchDisabled,
|
||||
isIssueRepositioningDisabled,
|
||||
isProject,
|
||||
isSignedIn,
|
||||
|
|
@ -162,6 +163,7 @@ export function mountIssuesListApp() {
|
|||
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
|
||||
hasIterationsFeature: parseBoolean(hasIterationsFeature),
|
||||
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
|
||||
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
|
||||
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
|
||||
isProject: parseBoolean(isProject),
|
||||
isSignedIn: parseBoolean(isSignedIn),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
API_PARAM,
|
||||
BLOCKING_ISSUES_ASC,
|
||||
BLOCKING_ISSUES_DESC,
|
||||
CREATED_ASC,
|
||||
CREATED_DESC,
|
||||
|
|
@ -143,7 +144,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
|
|||
id: sortOptions.length + 1,
|
||||
title: __('Blocking'),
|
||||
sortDirection: {
|
||||
ascending: BLOCKING_ISSUES_DESC,
|
||||
ascending: BLOCKING_ISSUES_ASC,
|
||||
descending: BLOCKING_ISSUES_DESC,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||
|
|
@ -19,6 +19,7 @@ export default {
|
|||
tag: 'ul',
|
||||
},
|
||||
components: {
|
||||
GlAlert,
|
||||
GlKeysetPagination,
|
||||
GlSkeletonLoading,
|
||||
IssuableTabs,
|
||||
|
|
@ -156,6 +157,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -277,6 +283,7 @@ export default {
|
|||
@onFilter="$emit('filter', $event)"
|
||||
@onSort="$emit('sort', $event)"
|
||||
/>
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="$emit('dismiss-alert')">{{ error }}</gl-alert>
|
||||
<issuable-bulk-edit-sidebar :expanded="showBulkEditSidebar">
|
||||
<template #bulk-edit-actions>
|
||||
<slot name="bulk-edit-actions" :checked-issuables="bulkEditIssuables"></slot>
|
||||
|
|
|
|||
|
|
@ -172,6 +172,14 @@ module Repositories
|
|||
return unless lfs_object
|
||||
|
||||
LfsObjectsProject.link_to_project!(lfs_object, project)
|
||||
|
||||
Gitlab::AppJsonLogger.info(message: "LFS object auto-linked to forked project",
|
||||
lfs_object_oid: lfs_object.oid,
|
||||
lfs_object_size: lfs_object.size,
|
||||
source_project_id: project.fork_source.id,
|
||||
source_project_path: project.fork_source.full_path,
|
||||
target_project_id: project.project_id,
|
||||
target_project_path: project.full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AvatarsHelper
|
||||
DEFAULT_AVATAR_PATH = 'no_avatar.png'
|
||||
|
||||
def project_icon(project, options = {})
|
||||
source_icon(project, options)
|
||||
end
|
||||
|
|
@ -36,11 +34,11 @@ module AvatarsHelper
|
|||
end
|
||||
|
||||
def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
|
||||
return gravatar_icon(nil, size, scale) unless user
|
||||
return default_avatar if blocked_or_unconfirmed?(user) && !can_admin?(current_user)
|
||||
|
||||
user_avatar = user.avatar_url(size: size, only_path: only_path)
|
||||
user_avatar || default_avatar
|
||||
if user
|
||||
user.avatar_url(size: size, only_path: only_path) || default_avatar
|
||||
else
|
||||
gravatar_icon(nil, size, scale)
|
||||
end
|
||||
end
|
||||
|
||||
def gravatar_icon(user_email = '', size = nil, scale = 2)
|
||||
|
|
@ -49,7 +47,7 @@ module AvatarsHelper
|
|||
end
|
||||
|
||||
def default_avatar
|
||||
ActionController::Base.helpers.image_path(DEFAULT_AVATAR_PATH)
|
||||
ActionController::Base.helpers.image_path('no_avatar.png')
|
||||
end
|
||||
|
||||
def author_avatar(commit_or_event, options = {})
|
||||
|
|
@ -159,14 +157,4 @@ module AvatarsHelper
|
|||
source.name[0, 1].upcase
|
||||
end
|
||||
end
|
||||
|
||||
def blocked_or_unconfirmed?(user)
|
||||
user.blocked? || !user.confirmed?
|
||||
end
|
||||
|
||||
def can_admin?(user)
|
||||
return false unless user
|
||||
|
||||
user.can_admin_all_resources?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ module IssuesHelper
|
|||
calendar_path: url_for(safe_params.merge(calendar_url_options)),
|
||||
empty_state_svg_path: image_path('illustrations/issues.svg'),
|
||||
full_path: namespace.full_path,
|
||||
is_anonymous_search_disabled: Feature.enabled?(:disable_anonymous_search, type: :ops).to_s,
|
||||
is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
|
||||
is_signed_in: current_user.present?.to_s,
|
||||
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@ module Projects
|
|||
# Git data (e.g. a list of branch names).
|
||||
flush_caches(project)
|
||||
|
||||
if Feature.enabled?(:abort_deleted_project_pipelines, default_enabled: :yaml)
|
||||
::Ci::AbortPipelinesService.new.execute(project.all_pipelines, :project_deleted)
|
||||
end
|
||||
::Ci::AbortPipelinesService.new.execute(project.all_pipelines, :project_deleted)
|
||||
|
||||
Projects::UnlinkForkService.new(project, current_user).execute
|
||||
|
||||
|
|
@ -133,9 +131,7 @@ module Projects
|
|||
destroy_web_hooks!
|
||||
destroy_project_bots!
|
||||
|
||||
if ::Feature.enabled?(:ci_optimize_project_records_destruction, project, default_enabled: :yaml) &&
|
||||
Feature.enabled?(:abort_deleted_project_pipelines, default_enabled: :yaml)
|
||||
|
||||
if ::Feature.enabled?(:ci_optimize_project_records_destruction, project, default_enabled: :yaml)
|
||||
destroy_ci_records!
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,6 @@
|
|||
%strong= _("Auto-close referenced issues on default branch")
|
||||
.form-text.text-muted
|
||||
= _("When merge requests and commits in the default branch close, any issues they reference also close.")
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues.md', anchor: 'disabling-automatic-issue-closing'), target: '_blank'
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues.md', anchor: 'closing-issues-automatically'), target: '_blank'
|
||||
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
- count_badge_classes = 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex'
|
||||
- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
|
||||
|
||||
= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'jobs-tabs' } } ) do
|
||||
= gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do
|
||||
= _('All')
|
||||
%span{ class: count_badge_classes }
|
||||
= limited_counter_with_delimiter(all_builds)
|
||||
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds), { class: count_badge_classes })
|
||||
= gl_tab_link_to build_path_proc.call('pending'), { item_active: scope == 'pending' } do
|
||||
= _('Pending')
|
||||
%span{ class: count_badge_classes }
|
||||
= limited_counter_with_delimiter(all_builds.pending)
|
||||
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds.pending), { class: count_badge_classes })
|
||||
= gl_tab_link_to build_path_proc.call('running'), { item_active: scope == 'running' } do
|
||||
= _('Running')
|
||||
%span{ class: count_badge_classes }
|
||||
= limited_counter_with_delimiter(all_builds.running)
|
||||
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds.running), { class: count_badge_classes })
|
||||
= gl_tab_link_to build_path_proc.call('finished'), { item_active: scope == 'finished' } do
|
||||
= _('Finished')
|
||||
%span{ class: count_badge_classes }
|
||||
= limited_counter_with_delimiter(all_builds.finished)
|
||||
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds.finished), { class: count_badge_classes })
|
||||
|
|
|
|||
|
|
@ -12,14 +12,21 @@ class PurgeDependencyProxyCacheWorker
|
|||
queue_namespace :dependency_proxy
|
||||
feature_category :dependency_proxy
|
||||
|
||||
UPDATE_BATCH_SIZE = 100
|
||||
|
||||
def perform(current_user_id, group_id)
|
||||
@current_user = User.find_by_id(current_user_id)
|
||||
@group = Group.find_by_id(group_id)
|
||||
|
||||
return unless valid?
|
||||
|
||||
@group.dependency_proxy_blobs.destroy_all # rubocop:disable Cop/DestroyAll
|
||||
@group.dependency_proxy_manifests.destroy_all # rubocop:disable Cop/DestroyAll
|
||||
@group.dependency_proxy_blobs.each_batch(of: UPDATE_BATCH_SIZE) do |batch|
|
||||
batch.update_all(status: :expired)
|
||||
end
|
||||
|
||||
@group.dependency_proxy_manifests.each_batch(of: UPDATE_BATCH_SIZE) do |batch|
|
||||
batch.update_all(status: :expired)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: abort_deleted_project_pipelines
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1220
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/301106
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: true
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: lfs_auto_link_fork_source
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75972
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348243
|
||||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::source code
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateMergeRequestsComplianceViolations < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
create_table :merge_requests_compliance_violations do |t|
|
||||
t.bigint :violating_user_id, null: false
|
||||
t.bigint :merge_request_id, null: false
|
||||
t.integer :reason, limit: 2, null: false
|
||||
t.index :violating_user_id
|
||||
t.index [:merge_request_id, :violating_user_id, :reason], unique: true, name: 'index_merge_requests_compliance_violations_unique_columns'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddFkComplianceViolationsMergeRequest < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :merge_requests_compliance_violations,
|
||||
:merge_requests,
|
||||
column: :merge_request_id,
|
||||
on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :merge_requests_compliance_violations, column: :merge_request_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddFkComplianceViolationsViolatingUser < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :merge_requests_compliance_violations,
|
||||
:users,
|
||||
column: :violating_user_id,
|
||||
on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :merge_requests_compliance_violations, column: :violating_user_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
0ab93a0bfd52d6c13203a0b183b2fcb9d6770334e5b1bd00a28fb623b65c428d
|
||||
|
|
@ -0,0 +1 @@
|
|||
870100261e3704522d390885b8ff13ebbcb093aa508d79b90f9738f6a0fffd10
|
||||
|
|
@ -0,0 +1 @@
|
|||
0cc2f19a8e31d9418ffd4fa1307f5210f0f2d781b957d417f06e19aca0b53985
|
||||
|
|
@ -16272,6 +16272,22 @@ CREATE SEQUENCE merge_requests_closing_issues_id_seq
|
|||
|
||||
ALTER SEQUENCE merge_requests_closing_issues_id_seq OWNED BY merge_requests_closing_issues.id;
|
||||
|
||||
CREATE TABLE merge_requests_compliance_violations (
|
||||
id bigint NOT NULL,
|
||||
violating_user_id bigint NOT NULL,
|
||||
merge_request_id bigint NOT NULL,
|
||||
reason smallint NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE merge_requests_compliance_violations_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE merge_requests_compliance_violations_id_seq OWNED BY merge_requests_compliance_violations.id;
|
||||
|
||||
CREATE SEQUENCE merge_requests_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
|
|
@ -21883,6 +21899,8 @@ ALTER TABLE ONLY merge_requests ALTER COLUMN id SET DEFAULT nextval('merge_reque
|
|||
|
||||
ALTER TABLE ONLY merge_requests_closing_issues ALTER COLUMN id SET DEFAULT nextval('merge_requests_closing_issues_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY merge_requests_compliance_violations ALTER COLUMN id SET DEFAULT nextval('merge_requests_compliance_violations_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY merge_trains ALTER COLUMN id SET DEFAULT nextval('merge_trains_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY metrics_dashboard_annotations ALTER COLUMN id SET DEFAULT nextval('metrics_dashboard_annotations_id_seq'::regclass);
|
||||
|
|
@ -23632,6 +23650,9 @@ ALTER TABLE ONLY merge_request_user_mentions
|
|||
ALTER TABLE ONLY merge_requests_closing_issues
|
||||
ADD CONSTRAINT merge_requests_closing_issues_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY merge_requests_compliance_violations
|
||||
ADD CONSTRAINT merge_requests_compliance_violations_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY merge_requests
|
||||
ADD CONSTRAINT merge_requests_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -26708,6 +26729,10 @@ CREATE INDEX index_merge_requests_closing_issues_on_issue_id ON merge_requests_c
|
|||
|
||||
CREATE INDEX index_merge_requests_closing_issues_on_merge_request_id ON merge_requests_closing_issues USING btree (merge_request_id);
|
||||
|
||||
CREATE INDEX index_merge_requests_compliance_violations_on_violating_user_id ON merge_requests_compliance_violations USING btree (violating_user_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_merge_requests_compliance_violations_unique_columns ON merge_requests_compliance_violations USING btree (merge_request_id, violating_user_id, reason);
|
||||
|
||||
CREATE INDEX index_merge_requests_on_assignee_id ON merge_requests USING btree (assignee_id);
|
||||
|
||||
CREATE INDEX index_merge_requests_on_author_id ON merge_requests USING btree (author_id);
|
||||
|
|
@ -29250,6 +29275,9 @@ ALTER TABLE ONLY geo_event_log
|
|||
ALTER TABLE ONLY deployments
|
||||
ADD CONSTRAINT fk_289bba3222 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY merge_requests_compliance_violations
|
||||
ADD CONSTRAINT fk_290ec1ab02 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY coverage_fuzzing_corpuses
|
||||
ADD CONSTRAINT fk_29f6f15f82 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -29925,6 +29953,9 @@ ALTER TABLE ONLY pages_domains
|
|||
ALTER TABLE ONLY application_settings
|
||||
ADD CONSTRAINT fk_ec757bd087 FOREIGN KEY (file_template_project_id) REFERENCES projects(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY merge_requests_compliance_violations
|
||||
ADD CONSTRAINT fk_ec881c1c6f FOREIGN KEY (violating_user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY events
|
||||
ADD CONSTRAINT fk_edfd187b6f FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ Only issues from projects that are in groups can be promoted. When you attempt t
|
|||
issue, a warning is displayed. Promoting a confidential issue to an epic makes all information
|
||||
related to the issue public as epics are public to group members.
|
||||
|
||||
When the quick action is executed:
|
||||
When an issue is promoted to an epic:
|
||||
|
||||
- An epic is created in the same group as the project of the issue.
|
||||
- Subscribers of the issue are notified that the epic was created.
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ The following table lists project permissions available for each role:
|
|||
| [Issues](project/issues/index.md):<br>View related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Issues](project/issues/index.md):<br>Set weight | ✓ (*16*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Issues](project/issues/index.md):<br>View [confidential issues](project/issues/confidential_issues.md) | (*2*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Issues](project/issues/index.md):<br>Close / reopen | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Issues](project/issues/index.md):<br>Lock threads | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Issues](project/issues/index.md):<br>Manage related issues | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Issues](project/issues/index.md):<br>Manage tracker | | ✓ | ✓ | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Issues can be imported to a project by uploading a CSV file with the columns
|
||||
`title` and `description`. Other columns are **not** imported. If you want to
|
||||
retain columns such as labels and milestones, consider the [Move Issue feature](managing_issues.md#moving-issues).
|
||||
retain columns such as labels and milestones, consider the [Move Issue feature](managing_issues.md#move-an-issue).
|
||||
|
||||
The user uploading the CSV file is set as the author of the imported issues.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -32,8 +32,8 @@ To learn how the GitLab Strategic Marketing department uses GitLab issues with [
|
|||
- [Create issues](managing_issues.md#create-an-issue)
|
||||
- [Create an issue from a template](../../project/description_templates.md#use-the-templates)
|
||||
- [Edit issues](managing_issues.md#edit-an-issue)
|
||||
- [Move issues](managing_issues.md#moving-issues)
|
||||
- [Close issues](managing_issues.md#closing-issues)
|
||||
- [Move issues](managing_issues.md#move-an-issue)
|
||||
- [Close issues](managing_issues.md#close-an-issue)
|
||||
- [Delete issues](managing_issues.md#delete-an-issue)
|
||||
- [Promote issues](managing_issues.md#promote-an-issue-to-an-epic)
|
||||
- [Set a due date](due_dates.md)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ to the projects in the group.
|
|||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Guest role](../../permissions.md) for a project in the group.
|
||||
- You must have at least the [Guest role](../../permissions.md) for the project in the group.
|
||||
|
||||
To create an issue from a group:
|
||||
|
||||
|
|
@ -224,170 +224,191 @@ You can edit an issue's title and description.
|
|||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for a project.
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
To edit an issue, select **Edit title and description** (**{pencil}**).
|
||||
To edit an issue:
|
||||
|
||||
### Bulk edit issues at the project level
|
||||
1. To the right of the title, select **Edit title and description** (**{pencil}**).
|
||||
1. Edit the available fields.
|
||||
1. Select **Save changes**.
|
||||
|
||||
> - Assigning epic ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2.
|
||||
### Bulk edit issues from a project
|
||||
|
||||
> - Assigning epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2.
|
||||
> - Editing health status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218395) in GitLab 13.2.
|
||||
> - Editing iteration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/196806) in GitLab 13.9.
|
||||
|
||||
Users with permission level of [Reporter or higher](../../permissions.md) can manage issues.
|
||||
You can edit multiple issues at a time when you're in a project.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
To edit multiple issues at the same time:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Issues**.
|
||||
1. Select **Edit issues**. A sidebar on the right of your screen appears.
|
||||
1. Select the checkboxes next to each issue you want to edit.
|
||||
1. From the sidebar, edit the available fields.
|
||||
1. Select **Update all**.
|
||||
|
||||
When bulk editing issues in a project, you can edit the following attributes:
|
||||
|
||||
- Status (open/closed)
|
||||
- Assignee
|
||||
- Status (open or closed)
|
||||
- [Assignees](#assignee)
|
||||
- [Epic](../../group/epics/index.md)
|
||||
- [Milestone](../milestones/index.md)
|
||||
- [Labels](../labels.md)
|
||||
- [Health status](#health-status)
|
||||
- Notification subscription
|
||||
- [Notification](../../profile/notifications.md) subscription
|
||||
- [Iteration](../../group/iterations/index.md)
|
||||
|
||||
To update multiple project issues at the same time:
|
||||
|
||||
1. In a project, go to **Issues > List**.
|
||||
1. Click **Edit issues**. A sidebar on the right-hand side of your screen appears with editable fields.
|
||||
1. Select the checkboxes next to each issue you want to edit.
|
||||
1. Select the appropriate fields and their values from the sidebar.
|
||||
1. Click **Update all**.
|
||||
|
||||
### Bulk edit issues at the group level
|
||||
### Bulk edit issues from a group
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7249) in GitLab 12.1.
|
||||
> - Assigning epic ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2.
|
||||
> - Assigning epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2.
|
||||
> - Editing health status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218395) in GitLab 13.2.
|
||||
> - Editing iteration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/196806) in GitLab 13.9.
|
||||
|
||||
Users with permission level of [Reporter or higher](../../permissions.md) can manage issues.
|
||||
You can edit multiple issues across multiple projects when you're in a group.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for a group.
|
||||
|
||||
To edit multiple issues at the same time:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues**.
|
||||
1. Select **Edit issues**. A sidebar on the right of your screen appears.
|
||||
1. Select the checkboxes next to each issue you want to edit.
|
||||
1. From the sidebar, edit the available fields.
|
||||
1. Select **Update all**.
|
||||
|
||||
When bulk editing issues in a group, you can edit the following attributes:
|
||||
|
||||
- [Epic](../../group/epics/index.md)
|
||||
- [Milestone](../milestones/index.md)
|
||||
- [Iteration](../../group/iterations/index.md)
|
||||
- [Labels](../labels.md)
|
||||
- [Health status](#health-status)
|
||||
- [Iteration](../../group/iterations/index.md)
|
||||
|
||||
To update multiple project issues at the same time:
|
||||
## Move an issue
|
||||
|
||||
1. In a group, go to **Issues > List**.
|
||||
1. Click **Edit issues**. A sidebar on the right-hand side of your screen appears with editable fields.
|
||||
1. Select the checkboxes next to each issue you want to edit.
|
||||
1. Select the appropriate fields and their values from the sidebar.
|
||||
1. Click **Update all**.
|
||||
|
||||
## Moving issues
|
||||
|
||||
Moving an issue copies it to the target project, and closes it in the originating project.
|
||||
When you move an issue, it's closed and copied to the target project.
|
||||
The original issue is not deleted. A system note, which indicates
|
||||
where it came from and went to, is added to both issues.
|
||||
|
||||
The "Move issue" button is at the bottom of the right-sidebar when viewing the issue.
|
||||
Prerequisites:
|
||||
|
||||

|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
### Moving issues in bulk **(FREE SELF)**
|
||||
To move an issue:
|
||||
|
||||
If you have advanced technical skills you can also bulk move all the issues from
|
||||
one project to another in the rails console. The below script moves all issues
|
||||
that are not in status **closed** from one project to another.
|
||||
1. Go to the issue.
|
||||
1. On the right sidebar, select **Move issue**.
|
||||
1. Search for a project to move the issue to.
|
||||
1. Select **Move**.
|
||||
|
||||
To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below
|
||||
script. Please be sure to change `project`, `admin_user`, and `target_project` to your values.
|
||||
We do also recommend [creating a backup](../../../raketasks/backup_restore.md) before
|
||||
attempting any changes in the console.
|
||||
### Bulk move issues **(FREE SELF)**
|
||||
|
||||
```ruby
|
||||
project = Project.find_by_full_path('full path of the project where issues are moved from')
|
||||
issues = project.issues
|
||||
admin_user = User.find_by_username('username of admin user') # make sure user has permissions to move the issues
|
||||
target_project = Project.find_by_full_path('full path of target project where issues moved to')
|
||||
You can move all open issues from one project to another.
|
||||
|
||||
issues.each do |issue|
|
||||
if issue.state != "closed" && issue.moved_to.nil?
|
||||
Issues::MoveService.new(project, admin_user).execute(issue, target_project)
|
||||
else
|
||||
puts "issue with id: #{issue.id} and title: #{issue.title} was not moved"
|
||||
end
|
||||
end; nil
|
||||
```
|
||||
Prerequisites:
|
||||
|
||||
## Closing issues
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
When you decide that an issue is resolved, or no longer needed, you can close the issue.
|
||||
To do it:
|
||||
|
||||
1. Optional (but recommended). [Create a backup](../../../raketasks/backup_restore.md) before
|
||||
attempting any changes in the console.
|
||||
1. Open the [Rails console](../../../administration/operations/rails_console.md).
|
||||
1. Run the following script. Make sure to change `project`, `admin_user`, and `target_project` to
|
||||
your values.
|
||||
|
||||
```ruby
|
||||
project = Project.find_by_full_path('full path of the project where issues are moved from')
|
||||
issues = project.issues
|
||||
admin_user = User.find_by_username('username of admin user') # make sure user has permissions to move the issues
|
||||
target_project = Project.find_by_full_path('full path of target project where issues moved to')
|
||||
|
||||
issues.each do |issue|
|
||||
if issue.state != "closed" && issue.moved_to.nil?
|
||||
Issues::MoveService.new(project, admin_user).execute(issue, target_project)
|
||||
else
|
||||
puts "issue with id: #{issue.id} and title: #{issue.title} was not moved"
|
||||
end
|
||||
end; nil
|
||||
```
|
||||
|
||||
1. To exit the Rails console, enter `quit`.
|
||||
|
||||
## Close an issue
|
||||
|
||||
When you decide that an issue is resolved or no longer needed, you can close it.
|
||||
The issue is marked as closed but is not deleted.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
To close an issue, you can do the following:
|
||||
|
||||
- Select **Close issue**:
|
||||
|
||||

|
||||
|
||||
- At the top of the issue, select **Close issue**.
|
||||
- In an [issue board](../issue_board.md), drag an issue card from its list into the **Closed** list.
|
||||
|
||||

|
||||
|
||||
### Reopen a closed issue
|
||||
|
||||
To reopen a closed issue, select **Reopen issue**.
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
To reopen a closed issue, at the top of the issue, select **Reopen issue**.
|
||||
A reopened issue is no different from any other open issue.
|
||||
|
||||
### Closing issues automatically
|
||||
|
||||
When a commit or merge request resolves issues, the issues
|
||||
can be closed automatically when the commit reaches the project's default branch.
|
||||
You can close issues automatically by using certain words in the commit message or MR description.
|
||||
|
||||
If a commit message or merge request description contains text matching a [defined pattern](#default-closing-pattern),
|
||||
all issues referenced in the matched text are closed. This happens when the commit
|
||||
is pushed to a project's [**default** branch](../repository/branches/default.md),
|
||||
or when a commit or merge request is merged into it.
|
||||
If a commit message or merge request description contains text matching the [defined pattern](#default-closing-pattern),
|
||||
all issues referenced in the matched text are closed when either:
|
||||
|
||||
For example, if `Closes #4, #6, Related to #5` is included in a Merge Request
|
||||
description, issues `#4` and `#6` are closed automatically when the MR is merged, but not `#5`.
|
||||
Using `Related to` flags `#5` as a [related issue](related_issues.md),
|
||||
but is not closed automatically.
|
||||
- The commit is pushed to a project's [**default** branch](../repository/branches/default.md).
|
||||
- The commit or merge request is merged into the default branch.
|
||||
|
||||

|
||||
For example, if you include `Closes #4, #6, Related to #5` in a merge request
|
||||
description:
|
||||
|
||||
If the issue is in a different repository than the MR, add the full URL for the issue(s):
|
||||
|
||||
```markdown
|
||||
Closes #4, #6, and https://gitlab.com/<username>/<projectname>/issues/<xxx>
|
||||
```
|
||||
|
||||
For performance reasons, automatic issue closing is disabled for the very first
|
||||
push from an existing repository.
|
||||
- Issues `#4` and `#6` are closed automatically when the MR is merged.
|
||||
- Issue `#5` is marked as a [related issue](related_issues.md), but it's not closed automatically.
|
||||
|
||||
Alternatively, when you [create a merge request from an issue](../merge_requests/getting_started.md#merge-requests-to-close-issues),
|
||||
it inherits the issue's milestone and labels.
|
||||
|
||||
For performance reasons, automatic issue closing is disabled for the very first
|
||||
push from an existing repository.
|
||||
|
||||
#### Default closing pattern
|
||||
|
||||
When not specified, this default issue closing pattern is used:
|
||||
To automatically close an issue, use the following keywords followed by the issue reference.
|
||||
|
||||
```shell
|
||||
\b((?:[Cc]los(?:e[sd]?|ing)|\b[Ff]ix(?:e[sd]|ing)?|\b[Rr]esolv(?:e[sd]?|ing)|\b[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?: *,? +and +| *,? *)?)|([A-Z][A-Z0-9_]+-\d+))+)
|
||||
```
|
||||
|
||||
This translates to the following keywords:
|
||||
Available keywords:
|
||||
|
||||
- Close, Closes, Closed, Closing, close, closes, closed, closing
|
||||
- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
|
||||
- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
|
||||
- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing
|
||||
|
||||
Note that `%{issue_ref}` is a complex regular expression defined inside the GitLab
|
||||
source code that can match references to:
|
||||
Available issue reference formats:
|
||||
|
||||
- A local issue (`#123`).
|
||||
- A cross-project issue (`group/project#123`).
|
||||
- A link to an issue (`https://gitlab.example.com/group/project/issues/123`).
|
||||
- The full URL of an issue (`https://gitlab.example.com/group/project/issues/123`).
|
||||
|
||||
For example the following commit message:
|
||||
For example:
|
||||
|
||||
```plaintext
|
||||
Awesome commit message
|
||||
|
|
@ -397,47 +418,75 @@ This commit is also related to #17 and fixes #18, #19
|
|||
and https://gitlab.example.com/group/otherproject/issues/23.
|
||||
```
|
||||
|
||||
closes `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to,
|
||||
The previous commit message closes `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to,
|
||||
as well as `#22` and `#23` in `group/otherproject`. `#17` is not closed as it does
|
||||
not match the pattern. It works with multi-line commit messages as well as one-liners
|
||||
when used from the command line with `git commit -m`.
|
||||
not match the pattern.
|
||||
|
||||
#### Disabling automatic issue closing
|
||||
You can use the closing patterns in multi-line commit messages or one-liners
|
||||
done from the command line with `git commit -m`.
|
||||
|
||||
The default issue closing pattern regex:
|
||||
|
||||
```shell
|
||||
\b((?:[Cc]los(?:e[sd]?|ing)|\b[Ff]ix(?:e[sd]|ing)?|\b[Rr]esolv(?:e[sd]?|ing)|\b[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?: *,? +and +| *,? *)?)|([A-Z][A-Z0-9_]+-\d+))+)
|
||||
```
|
||||
|
||||
#### Disable automatic issue closing
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/19754) in GitLab 12.7.
|
||||
|
||||
The automatic issue closing feature can be disabled on a per-project basis
|
||||
in the [project's repository settings](../settings/index.md). Referenced
|
||||
issues are still displayed, but are not closed automatically.
|
||||
You can disable the automatic issue closing feature on a per-project basis
|
||||
in the [project's settings](../settings/index.md).
|
||||
|
||||

|
||||
Prerequisites:
|
||||
|
||||
The automatic issue closing is also disabled in a project if the project has the issue tracker
|
||||
- You must have at least the [Maintainer role](../../permissions.md) for the project.
|
||||
|
||||
To disable automatic issue closing:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > Repository**.
|
||||
1. Expand **Default branch**.
|
||||
1. Select **Auto-close referenced issues on default branch**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
Referenced issues are still displayed, but are not closed automatically.
|
||||
|
||||
The automatic issue closing is disabled by default in a project if the project has the issue tracker
|
||||
disabled. If you want to enable automatic issue closing, make sure to
|
||||
[enable GitLab Issues](../settings/index.md#sharing-and-permissions).
|
||||
|
||||
This only applies to issues affected by new merge requests or commits. Already
|
||||
closed issues remain as-is.
|
||||
Changing this setting applies only to new merge requests or commits. Already
|
||||
closed issues remain as they are.
|
||||
If issue tracking is enabled, disabling automatic issue closing only applies to merge requests
|
||||
attempting to automatically close issues within the same project.
|
||||
attempting to automatically close issues in the same project.
|
||||
Merge requests in other projects can still close another project's issues.
|
||||
|
||||
#### Customizing the issue closing pattern **(FREE SELF)**
|
||||
#### Customize the issue closing pattern **(FREE SELF)**
|
||||
|
||||
In order to change the default issue closing pattern, GitLab administrators must edit the
|
||||
Prerequisites:
|
||||
|
||||
- You must have the [administrator access level](../../../administration/index.md) for your GitLab instance.
|
||||
|
||||
To change the default issue closing pattern, edit the
|
||||
[`gitlab.rb` or `gitlab.yml` file](../../../administration/issue_closing_pattern.md)
|
||||
of your installation.
|
||||
|
||||
## Change the issue type
|
||||
|
||||
Users with the [Developer role](../../permissions.md)
|
||||
can change an issue's type. To do this, edit the issue and select an issue type from the
|
||||
**Issue type** selector menu:
|
||||
Prerequisites:
|
||||
|
||||
- [Issue](index.md)
|
||||
- [Incident](../../../operations/incident_management/index.md)
|
||||
- You must be the issue author or have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||

|
||||
To change issue type:
|
||||
|
||||
1. To the right of the title, select **Edit title and description** (**{pencil}**).
|
||||
1. Edit the issue and select an issue type from the **Issue type** dropdown list:
|
||||
|
||||
- Issue
|
||||
- [Incident](../../../operations/incident_management/index.md)
|
||||
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Delete an issue
|
||||
|
||||
|
|
@ -463,16 +512,16 @@ Alternatively:
|
|||
> - Moved to GitLab Premium in 12.8.
|
||||
> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in GitLab Premium 13.6.
|
||||
|
||||
You can promote an issue to an epic in the immediate parent group.
|
||||
You can promote an issue to an [epic](../../group/epics/index.md) in the immediate parent group.
|
||||
|
||||
To promote an issue to an epic:
|
||||
|
||||
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**) button.
|
||||
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
|
||||
1. Select **Promote to epic**.
|
||||
|
||||
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
|
||||
|
||||
Read more about promoting an issue to an epic on the [Manage epics page](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic).
|
||||
Read more about [promoting an issues to epics](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic).
|
||||
|
||||
## Add an issue to an iteration **(PREMIUM)**
|
||||
|
||||
|
|
@ -481,14 +530,12 @@ Read more about promoting an issue to an epic on the [Manage epics page](../../g
|
|||
|
||||
To add an issue to an [iteration](../../group/iterations/index.md):
|
||||
|
||||
1. Go to your issue.
|
||||
1. Go to the issue.
|
||||
1. On the right sidebar, in the **Iteration** section, select **Edit**.
|
||||
1. From the dropdown list, select the iteration to associate this issue with.
|
||||
1. Select any area outside the dropdown list.
|
||||
|
||||
You can also use the `/iteration`
|
||||
[quick action](../quick_actions.md#issues-merge-requests-and-epics)
|
||||
in a comment or description field.
|
||||
Alternatively, you can use the `/iteration` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
|
||||
|
||||
## Copy issue reference
|
||||
|
||||
|
|
@ -509,16 +556,16 @@ Read more about issue references in [GitLab-Flavored Markdown](../../markdown.md
|
|||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18816) in GitLab 13.8.
|
||||
|
||||
You can create a comment in an issue by sending an email.
|
||||
Sending an email to this address creates a comment that contains the email body.
|
||||
|
||||
To learn more about creating comments by sending an email and the necessary configuration, see
|
||||
[Reply to a comment by sending email](../../discussions/index.md#reply-to-a-comment-by-sending-email).
|
||||
|
||||
To copy the issue's email address:
|
||||
|
||||
1. Go to the issue.
|
||||
1. On the right sidebar, next to **Issue email**, select **Copy Reference** (**{copy-to-clipboard}**).
|
||||
|
||||
Sending an email to this address creates a comment containing the email body.
|
||||
To learn more about creating comments by sending an email and the necessary configuration, see
|
||||
[Reply to a comment by sending email](../../discussions/index.md#reply-to-a-comment-by-sending-email).
|
||||
|
||||
## Real-time sidebar
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 13.3. Disabled by default.
|
||||
|
|
@ -552,37 +599,46 @@ To change the assignee on an issue:
|
|||
|
||||
## Similar issues
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22866) in GitLab 11.6.
|
||||
To prevent duplication of issues on the same topic, GitLab searches for similar issues
|
||||
when you create a new issue.
|
||||
|
||||
To prevent duplication of issues for the same topic, GitLab searches for similar issues
|
||||
when new issues are being created.
|
||||
Prerequisites:
|
||||
|
||||
As you type in the title field of the **New Issue** page, GitLab searches titles and descriptions
|
||||
across all issues to in the current project. Only issues you have access to are returned.
|
||||
Up to five similar issues, sorted by most recently updated, are displayed below the title box.
|
||||
[GraphQL](../../../api/graphql/index.md) must be enabled to use this feature.
|
||||
- [GraphQL](../../../api/graphql/index.md) must be enabled.
|
||||
|
||||

|
||||
As you type in the title text box of the **New issue** page, GitLab searches titles and descriptions
|
||||
across all issues in the current project. Only issues you have access to are returned.
|
||||
Up to five similar issues, sorted by most recently updated, are displayed below the title text box.
|
||||
|
||||
## Health status **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in GitLab Ultimate 12.10.
|
||||
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in GitLab Ultimate 13.4 and later.
|
||||
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in GitLab Ultimate 13.6.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in GitLab 12.10.
|
||||
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in GitLab 13.4 and later.
|
||||
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in GitLab 13.6.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7.
|
||||
|
||||
To help you track issue statuses, you can assign a status to each issue.
|
||||
This marks issues as progressing as planned or needs attention to keep on schedule:
|
||||
This status marks issues as progressing as planned or needing attention to keep on schedule.
|
||||
|
||||
- On track (green)
|
||||
- Needs attention (amber)
|
||||
- At risk (red)
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the [Reporter role](../../permissions.md) for the project.
|
||||
|
||||
To edit health status of an issue:
|
||||
|
||||
1. Go to the issue.
|
||||
1. On the right sidebar, in the **Health status** section, select **Edit**.
|
||||
1. From the dropdown list, select the status to add to this issue:
|
||||
|
||||
- On track (green)
|
||||
- Needs attention (amber)
|
||||
- At risk (red)
|
||||
|
||||
You can then see the issue's status in the issues list and the epic tree.
|
||||
|
||||
After an issue is closed, its health status can't be edited and the **Edit** button becomes disabled
|
||||
until the issue is reopened.
|
||||
|
||||
You can then see issue statuses in the issues list and the epic tree.
|
||||
|
||||
## Publish an issue **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906) in GitLab 13.1.
|
||||
|
|
@ -591,3 +647,16 @@ If a status page application is associated with the project, you can use the `/p
|
|||
[quick action](../quick_actions.md) to publish the issue.
|
||||
|
||||
For more information, see [GitLab Status Page](../../../operations/incident_management/status_page.md).
|
||||
|
||||
## Issue-related quick actions
|
||||
|
||||
You can also use [quick actions](../quick_actions.md#issues-merge-requests-and-epics) to manage issues.
|
||||
|
||||
Some actions don't have corresponding UI buttons yet.
|
||||
You can do the following **only by using quick actions**:
|
||||
|
||||
- [Add or remove a Zoom meeting](associate_zoom_meeting.md) (`/zoom` and `/remove_zoom`).
|
||||
- [Publish an issue](#publish-an-issue) (`/publish`).
|
||||
- Clone an issue to the same or another project (`/clone`).
|
||||
- Close an issue and mark as a duplicate of another issue (`/duplicate`).
|
||||
- Copy labels and milestone from another merge request in the project (`/copy_metadata`).
|
||||
|
|
|
|||
|
|
@ -36,12 +36,15 @@ for the most popular hosting services:
|
|||
- [Bluehost](https://www.bluehost.com/help/article/dns-management-add-edit-or-delete-dns-entries)
|
||||
- [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website)
|
||||
- [cPanel](https://documentation.cpanel.net/display/84Docs/Edit+DNS+Zone)
|
||||
- [DigitalOcean](https://docs.digitalocean.com/products/networking/dns/how-to/manage-records/)
|
||||
- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-)
|
||||
- [Gandi](https://docs.gandi.net/en/domain_names/faq/dns_records.html)
|
||||
- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238)
|
||||
- [Hostgator](https://www.hostgator.com/help/article/changing-dns-records)
|
||||
- [Inmotion hosting](https://www.bluehost.com/help/article/dns-management-add-edit-or-delete-dns-entries)
|
||||
- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain)
|
||||
- [Microsoft](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb727018(v=technet.10))
|
||||
- [Namecheap](https://www.namecheap.com/support/knowledgebase/subcategory/2237/host-records-setup/)
|
||||
|
||||
<!-- vale gitlab.Spelling = YES -->
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,6 @@ module API
|
|||
|
||||
feature_category :dependency_proxy
|
||||
|
||||
helpers do
|
||||
def obtain_new_purge_cache_lease
|
||||
Gitlab::ExclusiveLease
|
||||
.new("dependency_proxy:delete_group_blobs:#{user_group.id}",
|
||||
timeout: 1.hour)
|
||||
.try_obtain
|
||||
end
|
||||
end
|
||||
|
||||
after_validation do
|
||||
authorize! :admin_group, user_group
|
||||
end
|
||||
|
|
@ -29,9 +20,6 @@ module API
|
|||
delete ':id/dependency_proxy/cache' do
|
||||
not_found! unless user_group.dependency_proxy_feature_available?
|
||||
|
||||
message = 'This request has already been made. It may take some time to purge the cache. You can run this at most once an hour for a given group'
|
||||
render_api_error!(message, 409) unless obtain_new_purge_cache_lease
|
||||
|
||||
# rubocop:disable CodeReuse/Worker
|
||||
PurgeDependencyProxyCacheWorker.perform_async(current_user.id, user_group.id)
|
||||
# rubocop:enable CodeReuse/Worker
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ members: :gitlab_main
|
|||
merge_request_assignees: :gitlab_main
|
||||
merge_request_blocks: :gitlab_main
|
||||
merge_request_cleanup_schedules: :gitlab_main
|
||||
merge_requests_compliance_violations: :gitlab_main
|
||||
merge_request_context_commit_diff_files: :gitlab_main
|
||||
merge_request_context_commits: :gitlab_main
|
||||
merge_request_diff_commits: :gitlab_main
|
||||
|
|
|
|||
|
|
@ -140,19 +140,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def inject_context_for_exception(event, ex)
|
||||
case ex
|
||||
when ActiveRecord::StatementInvalid
|
||||
# StatementInvalid may be caused by a statement timeout or a bad query
|
||||
event.extra[:sql] = normalize_query(ex.sql.to_s)
|
||||
else
|
||||
inject_context_for_exception(event, ex.cause) if ex.cause.present?
|
||||
end
|
||||
end
|
||||
sql = Gitlab::ExceptionLogFormatter.find_sql(ex)
|
||||
|
||||
def normalize_query(sql)
|
||||
PgQuery.normalize(sql)
|
||||
rescue PgQuery::ParseError
|
||||
sql
|
||||
event.extra[:sql] = sql if sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,18 +2,41 @@
|
|||
|
||||
module Gitlab
|
||||
module ExceptionLogFormatter
|
||||
def self.format!(exception, payload)
|
||||
return unless exception
|
||||
class << self
|
||||
def format!(exception, payload)
|
||||
return unless exception
|
||||
|
||||
# Elasticsearch/Fluentd don't handle nested structures well.
|
||||
# Use periods to flatten the fields.
|
||||
payload.merge!(
|
||||
'exception.class' => exception.class.name,
|
||||
'exception.message' => exception.message
|
||||
)
|
||||
# Elasticsearch/Fluentd don't handle nested structures well.
|
||||
# Use periods to flatten the fields.
|
||||
payload.merge!(
|
||||
'exception.class' => exception.class.name,
|
||||
'exception.message' => exception.message
|
||||
)
|
||||
|
||||
if exception.backtrace
|
||||
payload['exception.backtrace'] = Rails.backtrace_cleaner.clean(exception.backtrace)
|
||||
if exception.backtrace
|
||||
payload['exception.backtrace'] = Rails.backtrace_cleaner.clean(exception.backtrace)
|
||||
end
|
||||
|
||||
if sql = find_sql(exception)
|
||||
payload['exception.sql'] = sql
|
||||
end
|
||||
end
|
||||
|
||||
def find_sql(exception)
|
||||
if exception.is_a?(ActiveRecord::StatementInvalid)
|
||||
# StatementInvalid may be caused by a statement timeout or a bad query
|
||||
normalize_query(exception.sql.to_s)
|
||||
elsif exception.cause.present?
|
||||
find_sql(exception.cause)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_query(sql)
|
||||
PgQuery.normalize(sql)
|
||||
rescue PgQuery::ParseError
|
||||
sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -190,6 +190,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def checkout_version(version, target_dir)
|
||||
# Explicitly setting the git protocol version to v2 allows older Git binaries
|
||||
# to do have a shallow clone obtain objects by object ID.
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} config protocol.version 2])
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --quiet origin #{version}])
|
||||
run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout -f --quiet FETCH_HEAD --])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ namespace :gitlab do
|
|||
raise
|
||||
end
|
||||
|
||||
desc 'GitLab | Gitaly | Install or upgrade gitaly'
|
||||
task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
|
||||
desc 'GitLab | Gitaly | Clone and checkout gitaly'
|
||||
task :clone, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
|
||||
warn_user_is_not_gitlab
|
||||
|
||||
unless args.dir.present? && args.storage_path.present?
|
||||
|
|
@ -51,6 +51,11 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]")
|
|||
version = Gitlab::GitalyClient.expected_server_version
|
||||
|
||||
checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1])
|
||||
end
|
||||
|
||||
desc 'GitLab | Gitaly | Install or upgrade gitaly'
|
||||
task :install, [:dir, :storage_path, :repo] => [:gitlab_environment, 'gitlab:gitaly:clone'] do |t, args|
|
||||
warn_user_is_not_gitlab
|
||||
|
||||
storage_paths = { 'default' => args.storage_path }
|
||||
Gitlab::SetupHelper::Gitaly.create_configuration(args.dir, storage_paths)
|
||||
|
|
|
|||
|
|
@ -41531,6 +41531,9 @@ msgstr ""
|
|||
msgid "committed"
|
||||
msgstr ""
|
||||
|
||||
msgid "compliance violation has already been recorded"
|
||||
msgstr ""
|
||||
|
||||
msgid "container_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ module QA
|
|||
mirror_settings.authentication_method = 'Password'
|
||||
mirror_settings.password = Runtime::User.password
|
||||
mirror_settings.mirror_repository
|
||||
mirror_settings.update target_project_uri
|
||||
mirror_settings.update target_project_uri # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ module QA
|
|||
mirror_settings.authentication_method = 'Password'
|
||||
mirror_settings.password = Runtime::User.password
|
||||
mirror_settings.mirror_repository
|
||||
mirror_settings.update target_project_uri
|
||||
mirror_settings.update target_project_uri # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
root_path="$(cd "$(dirname "$0")/.." || exit ; pwd -P)"
|
||||
|
||||
if [[ -d "${root_path}/ee/" || "${GITLAB_MIGRATE_MAIN_ONLY}" == "true" ]]; then
|
||||
task="db:migrate:main"
|
||||
else
|
||||
task="db:migrate"
|
||||
fi
|
||||
|
||||
eval "bundle exec rake ${task} ${*}"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
root_path="$(cd "$(dirname "$0")/.." || exit ; pwd -P)"
|
||||
task=$1
|
||||
|
||||
shift
|
||||
|
||||
if [[ -d "${root_path}/ee/" || "${DECOMPOSED_DB}" == "true" ]]; then
|
||||
task="${task}:main"
|
||||
fi
|
||||
|
||||
eval "bundle exec rake ${task} ${*}"
|
||||
|
|
@ -30,7 +30,8 @@ class MigrationSchemaValidator
|
|||
committed_migrations.reverse_each do |filename|
|
||||
version = find_migration_version(filename)
|
||||
|
||||
run("bin/rails db:migrate:down VERSION=#{version}")
|
||||
run("scripts/db_tasks db:migrate:down VERSION=#{version}")
|
||||
run("scripts/db_tasks db:schema:dump")
|
||||
end
|
||||
|
||||
git_command = "git diff #{diff_target} -- #{FILENAME}"
|
||||
|
|
@ -40,7 +41,8 @@ class MigrationSchemaValidator
|
|||
end
|
||||
|
||||
def validate_schema_on_migrate!
|
||||
run('bin/rails db:migrate')
|
||||
run("scripts/db_tasks db:migrate")
|
||||
run("scripts/db_tasks db:schema:dump")
|
||||
|
||||
git_command = "git diff -- #{FILENAME}"
|
||||
base_message = "the committed #{FILENAME} does not match the one generated by running added migrations"
|
||||
|
|
|
|||
|
|
@ -243,10 +243,6 @@ RSpec.describe 'User page' do
|
|||
expect(page).to have_content("@#{user.username}")
|
||||
end
|
||||
|
||||
it 'shows default avatar' do
|
||||
expect(page).to have_css('//img[data-src^="/assets/no_avatar"]')
|
||||
end
|
||||
|
||||
it_behaves_like 'default brand title page meta description'
|
||||
end
|
||||
|
||||
|
|
@ -290,10 +286,6 @@ RSpec.describe 'User page' do
|
|||
expect(page).to have_content("This user has a private profile")
|
||||
end
|
||||
|
||||
it 'shows default avatar' do
|
||||
expect(page).to have_css('//img[data-src^="/assets/no_avatar"]')
|
||||
end
|
||||
|
||||
it_behaves_like 'default brand title page meta description'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
|
@ -47,6 +48,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import { scrollUp } from '~/lib/utils/scroll_utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
||||
jest.mock('@sentry/browser');
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/scroll_utils', () => ({
|
||||
scrollUp: jest.fn().mockName('scrollUpMock'),
|
||||
|
|
@ -357,6 +359,27 @@ describe('CE IssuesListApp component', () => {
|
|||
|
||||
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
|
||||
});
|
||||
|
||||
describe('when anonymous searching is performed', () => {
|
||||
beforeEach(() => {
|
||||
setWindowLocation(locationSearch);
|
||||
|
||||
wrapper = mountComponent({
|
||||
provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('is not set from url params', () => {
|
||||
expect(findIssuableList().props('initialFilterValue')).toEqual([]);
|
||||
});
|
||||
|
||||
it('shows an alert to tell the user they must be signed in to search', () => {
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: IssuesListApp.i18n.anonymousSearchingMessage,
|
||||
type: FLASH_TYPES.NOTICE,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -505,11 +528,7 @@ describe('CE IssuesListApp component', () => {
|
|||
|
||||
describe('when user is signed out', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
provide: {
|
||||
isSignedIn: false,
|
||||
},
|
||||
});
|
||||
wrapper = mountComponent({ provide: { isSignedIn: false } });
|
||||
});
|
||||
|
||||
it('does not render My-Reaction or Confidential tokens', () => {
|
||||
|
|
@ -541,20 +560,20 @@ describe('CE IssuesListApp component', () => {
|
|||
window.gon = originalGon;
|
||||
});
|
||||
|
||||
it('renders all tokens', () => {
|
||||
it('renders all tokens alphabetically', () => {
|
||||
const preloadedAuthors = [
|
||||
{ ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
|
||||
];
|
||||
|
||||
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_TYPE },
|
||||
{ type: TOKEN_TYPE_RELEASE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
{ type: TOKEN_TYPE_RELEASE },
|
||||
{ type: TOKEN_TYPE_TYPE },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -574,13 +593,18 @@ describe('CE IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
it('shows an error message', () => {
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
captureError: true,
|
||||
error: new Error('Network error: ERROR'),
|
||||
message,
|
||||
});
|
||||
expect(findIssuableList().props('error')).toBe(message);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Network error: ERROR'));
|
||||
});
|
||||
});
|
||||
|
||||
it('clears error message when "dismiss-alert" event is emitted from IssuableList', () => {
|
||||
wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockRejectedValue(new Error()) });
|
||||
|
||||
findIssuableList().vm.$emit('dismiss-alert');
|
||||
|
||||
expect(findIssuableList().props('error')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
|
|
@ -705,11 +729,10 @@ describe('CE IssuesListApp component', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: IssuesListApp.i18n.reorderError,
|
||||
captureError: true,
|
||||
error: new Error('Request failed with status code 500'),
|
||||
});
|
||||
expect(findIssuableList().props('error')).toBe(IssuesListApp.i18n.reorderError);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(
|
||||
new Error('Request failed with status code 500'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -770,14 +793,36 @@ describe('CE IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
describe('when "filter" event is emitted by IssuableList', () => {
|
||||
beforeEach(() => {
|
||||
it('updates IssuableList with url params', async () => {
|
||||
wrapper = mountComponent();
|
||||
|
||||
findIssuableList().vm.$emit('filter', filteredTokens);
|
||||
await nextTick();
|
||||
|
||||
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
|
||||
});
|
||||
|
||||
it('updates IssuableList with url params', () => {
|
||||
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
|
||||
describe('when anonymous searching is performed', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
|
||||
});
|
||||
|
||||
findIssuableList().vm.$emit('filter', filteredTokens);
|
||||
});
|
||||
|
||||
it('does not update IssuableList with url params ', async () => {
|
||||
const defaultParams = { sort: 'created_date', state: 'opened' };
|
||||
|
||||
expect(findIssuableList().props('urlParams')).toEqual(defaultParams);
|
||||
});
|
||||
|
||||
it('shows an alert to tell the user they must be signed in to search', () => {
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: IssuesListApp.i18n.anonymousSearchingMessage,
|
||||
type: FLASH_TYPES.NOTICE,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
import { GlAlert, GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
|
||||
|
|
@ -36,6 +36,7 @@ const createComponent = ({ props = {}, data = {} } = {}) =>
|
|||
describe('IssuableListRoot', () => {
|
||||
let wrapper;
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
|
||||
const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
|
||||
const findGlPagination = () => wrapper.findComponent(GlPagination);
|
||||
|
|
@ -310,6 +311,30 @@ describe('IssuableListRoot', () => {
|
|||
hasPreviousPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('alert', () => {
|
||||
const error = 'oopsie!';
|
||||
|
||||
it('shows alert when there is an error', () => {
|
||||
wrapper = createComponent({ props: { error } });
|
||||
|
||||
expect(findAlert().text()).toBe(error);
|
||||
});
|
||||
|
||||
it('emits "dismiss-alert" event when dismissed', () => {
|
||||
wrapper = createComponent({ props: { error } });
|
||||
|
||||
findAlert().vm.$emit('dismiss');
|
||||
|
||||
expect(wrapper.emitted('dismiss-alert')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('does not render when there is no error', () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe AvatarsHelper do
|
||||
include UploadHelpers
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
|
@ -146,49 +145,12 @@ RSpec.describe AvatarsHelper do
|
|||
|
||||
describe '#avatar_icon_for_user' do
|
||||
let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
|
||||
let(:helper_args) { [user] }
|
||||
|
||||
shared_examples 'blocked or unconfirmed user with avatar' do
|
||||
it 'returns the default avatar' do
|
||||
expect(helper.avatar_icon_for_user(user).to_s)
|
||||
.to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
|
||||
end
|
||||
|
||||
context 'when the current user is an admin', :enable_admin_mode do
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(current_user)
|
||||
end
|
||||
|
||||
it 'returns the user avatar' do
|
||||
expect(helper.avatar_icon_for_user(user).to_s)
|
||||
.to eq(user.avatar.url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a user object passed' do
|
||||
it 'returns a relative URL for the avatar' do
|
||||
expect(helper.avatar_icon_for_user(user).to_s)
|
||||
.to eq(user.avatar.url)
|
||||
end
|
||||
|
||||
context 'when the user is blocked' do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
|
||||
it_behaves_like 'blocked or unconfirmed user with avatar'
|
||||
end
|
||||
|
||||
context 'when the user is unconfirmed' do
|
||||
before do
|
||||
user.update!(confirmed_at: nil)
|
||||
end
|
||||
|
||||
it_behaves_like 'blocked or unconfirmed user with avatar'
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a user object passed' do
|
||||
|
|
@ -209,7 +171,7 @@ RSpec.describe AvatarsHelper do
|
|||
end
|
||||
|
||||
it 'returns a generic avatar' do
|
||||
expect(helper.gravatar_icon(user_email)).to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
|
||||
expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -219,7 +181,7 @@ RSpec.describe AvatarsHelper do
|
|||
end
|
||||
|
||||
it 'returns a generic avatar when email is blank' do
|
||||
expect(helper.gravatar_icon('')).to match_asset_path(described_class::DEFAULT_AVATAR_PATH)
|
||||
expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
|
||||
end
|
||||
|
||||
it 'returns a valid Gravatar URL' do
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ RSpec.describe IssuesHelper do
|
|||
has_any_issues: project_issues(project).exists?.to_s,
|
||||
import_csv_issues_path: '#',
|
||||
initial_email: project.new_issuable_address(current_user, 'issue'),
|
||||
is_anonymous_search_disabled: 'true',
|
||||
is_issue_repositioning_disabled: 'true',
|
||||
is_project: 'true',
|
||||
is_signed_in: current_user.present?.to_s,
|
||||
|
|
@ -342,6 +343,10 @@ RSpec.describe IssuesHelper do
|
|||
end
|
||||
|
||||
describe '#project_issues_list_data' do
|
||||
before do
|
||||
stub_feature_flags(disable_anonymous_search: true)
|
||||
end
|
||||
|
||||
context 'when user is signed in' do
|
||||
it_behaves_like 'issues list data' do
|
||||
let(:current_user) { double.as_null_object }
|
||||
|
|
|
|||
|
|
@ -157,6 +157,16 @@ RSpec.describe 'lograge', type: :request do
|
|||
expect(log_data['exception.message']).to eq('bad request')
|
||||
expect(log_data['exception.backtrace']).to eq(Gitlab::BacktraceCleaner.clean_backtrace(backtrace))
|
||||
end
|
||||
|
||||
context 'with an ActiveRecord::StatementInvalid' do
|
||||
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') }
|
||||
|
||||
it 'adds the SQL query to the log' do
|
||||
subscriber.process_action(event)
|
||||
|
||||
expect(log_data['exception.sql']).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with etag_route' do
|
||||
|
|
|
|||
|
|
@ -205,26 +205,6 @@ RSpec.describe Gitlab::ErrorTracking do
|
|||
expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do
|
||||
it 'injects the normalized sql query into extra' do
|
||||
allow(exception).to receive(:cause).and_return(ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1'))
|
||||
|
||||
track_exception
|
||||
|
||||
expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the `ActiveRecord::StatementInvalid` is a bad query' do
|
||||
it 'injects the query as-is into extra' do
|
||||
allow(exception).to receive(:cause).and_return(ActiveRecord::StatementInvalid.new(sql: 'SELECT SELECT FROM SELECT'))
|
||||
|
||||
track_exception
|
||||
|
||||
expect(sentry_event.dig('extra', 'sql')).to eq('SELECT SELECT FROM SELECT')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'event processors' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::ExceptionLogFormatter do
|
||||
describe '.format!' do
|
||||
let(:exception) { RuntimeError.new('bad request') }
|
||||
let(:backtrace) { caller }
|
||||
|
||||
let(:payload) { {} }
|
||||
|
||||
before do
|
||||
allow(exception).to receive(:backtrace).and_return(backtrace)
|
||||
end
|
||||
|
||||
it 'adds exception data to log' do
|
||||
described_class.format!(exception, payload)
|
||||
|
||||
expect(payload['exception.class']).to eq('RuntimeError')
|
||||
expect(payload['exception.message']).to eq('bad request')
|
||||
expect(payload['exception.backtrace']).to eq(Gitlab::BacktraceCleaner.clean_backtrace(backtrace))
|
||||
expect(payload['exception.sql']).to be_nil
|
||||
end
|
||||
|
||||
context 'when exception is ActiveRecord::StatementInvalid' do
|
||||
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') }
|
||||
|
||||
it 'adds the normalized SQL query to payload' do
|
||||
described_class.format!(exception, payload)
|
||||
|
||||
expect(payload['exception.sql']).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ActiveRecord::StatementInvalid is wrapped in another exception' do
|
||||
before do
|
||||
allow(exception).to receive(:cause).and_return(ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1'))
|
||||
end
|
||||
|
||||
it 'adds the normalized SQL query to payload' do
|
||||
described_class.format!(exception, payload)
|
||||
|
||||
expect(payload['exception.sql']).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ActiveRecord::StatementInvalid is a bad query' do
|
||||
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT SELECT FROM SELECT') }
|
||||
|
||||
it 'adds the query as-is to payload' do
|
||||
described_class.format!(exception, payload)
|
||||
|
||||
expect(payload['exception.sql']).to eq('SELECT SELECT FROM SELECT')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -198,6 +198,7 @@ merge_requests:
|
|||
- system_note_metadata
|
||||
- note_authors
|
||||
- cleanup_schedule
|
||||
- compliance_violations
|
||||
external_pull_requests:
|
||||
- project
|
||||
merge_request_diff:
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::DependencyProxy, api: true do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:blob) { create(:dependency_proxy_blob )}
|
||||
let_it_be(:group, reload: true) { blob.group }
|
||||
|
|
@ -20,11 +18,8 @@ RSpec.describe API::DependencyProxy, api: true do
|
|||
|
||||
shared_examples 'responding to purge requests' do
|
||||
context 'with feature available and enabled' do
|
||||
let_it_be(:lease_key) { "dependency_proxy:delete_group_blobs:#{group.id}" }
|
||||
|
||||
context 'an admin user' do
|
||||
it 'deletes the blobs and returns no content' do
|
||||
stub_exclusive_lease(lease_key, timeout: 1.hour)
|
||||
expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async)
|
||||
|
||||
subject
|
||||
|
|
@ -32,23 +27,6 @@ RSpec.describe API::DependencyProxy, api: true do
|
|||
expect(response).to have_gitlab_http_status(:accepted)
|
||||
expect(response.body).to eq('202')
|
||||
end
|
||||
|
||||
context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do
|
||||
it 'returns 409 with an error message' do
|
||||
stub_exclusive_lease_taken(lease_key, timeout: 1.hour)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:conflict)
|
||||
expect(response.body).to include('This request has already been made.')
|
||||
end
|
||||
|
||||
it 'executes service only for the first time' do
|
||||
expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async).once
|
||||
|
||||
2.times { subject }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'a non-admin' do
|
||||
|
|
|
|||
|
|
@ -532,6 +532,14 @@ RSpec.describe 'Git LFS API and storage' do
|
|||
end
|
||||
|
||||
it 'links existing LFS objects to other project' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(
|
||||
message: "LFS object auto-linked to forked project",
|
||||
lfs_object_oid: lfs_object.oid,
|
||||
lfs_object_size: lfs_object.size,
|
||||
source_project_id: other_project.id,
|
||||
source_project_path: other_project.full_path,
|
||||
target_project_id: project.id,
|
||||
target_project_path: project.full_path).and_call_original
|
||||
expect(json_response['objects']).to be_kind_of(Array)
|
||||
expect(json_response['objects'].first).to include(sample_object)
|
||||
expect(json_response['objects'].first).not_to have_key('actions')
|
||||
|
|
|
|||
|
|
@ -55,22 +55,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
|
|||
.and change { Ci::Pipeline.count }.by(-1)
|
||||
end
|
||||
|
||||
context 'with abort_deleted_project_pipelines disabled' do
|
||||
stub_feature_flags(abort_deleted_project_pipelines: false)
|
||||
|
||||
it 'avoids N+1 queries' do
|
||||
recorder = ActiveRecord::QueryRecorder.new { destroy_project(project, user, {}) }
|
||||
|
||||
project = create(:project, :repository, namespace: user.namespace)
|
||||
pipeline = create(:ci_pipeline, project: project)
|
||||
builds = create_list(:ci_build, 3, :artifacts, pipeline: pipeline)
|
||||
create(:ci_pipeline_artifact, pipeline: pipeline)
|
||||
create_list(:ci_build_trace_chunk, 3, build: builds[0])
|
||||
|
||||
expect { destroy_project(project, project.owner, {}) }.not_to exceed_query_limit(recorder)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ci_optimize_project_records_destruction disabled' do
|
||||
stub_feature_flags(ci_optimize_project_records_destruction: false)
|
||||
|
||||
|
|
@ -86,7 +70,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with ci_optimize_project_records_destruction and abort_deleted_project_pipelines enabled' do
|
||||
context 'with ci_optimize_project_records_destruction enabled' do
|
||||
it 'avoids N+1 queries' do
|
||||
recorder = ActiveRecord::QueryRecorder.new { destroy_project(project, user, {}) }
|
||||
|
||||
|
|
@ -132,28 +116,26 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
|
|||
destroy_project(project, user, {})
|
||||
end
|
||||
|
||||
context 'with abort_deleted_project_pipelines feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(abort_deleted_project_pipelines: false)
|
||||
end
|
||||
|
||||
it 'does not bulk-fail project ci pipelines' do
|
||||
expect(::Ci::AbortPipelinesService).not_to receive(:new)
|
||||
|
||||
destroy_project(project, user, {})
|
||||
end
|
||||
|
||||
it 'does not destroy CI records via DestroyPipelineService' do
|
||||
expect(::Ci::DestroyPipelineService).not_to receive(:new)
|
||||
|
||||
destroy_project(project, user, {})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with abort_deleted_project_pipelines feature enabled' do
|
||||
context 'with running pipelines to be aborted' do
|
||||
let!(:pipelines) { create_list(:ci_pipeline, 3, :running, project: project) }
|
||||
let(:destroy_pipeline_service) { double('DestroyPipelineService', execute: nil) }
|
||||
|
||||
it 'executes DestroyPipelineService for project ci pipelines' do
|
||||
allow(::Ci::DestroyPipelineService).to receive(:new).and_return(destroy_pipeline_service)
|
||||
|
||||
expect(::Ci::AbortPipelinesService)
|
||||
.to receive_message_chain(:new, :execute)
|
||||
.with(project.all_pipelines, :project_deleted)
|
||||
|
||||
pipelines.each do |pipeline|
|
||||
expect(destroy_pipeline_service)
|
||||
.to receive(:execute)
|
||||
.with(pipeline)
|
||||
end
|
||||
|
||||
destroy_project(project, user, {})
|
||||
end
|
||||
|
||||
context 'with ci_optimize_project_records_destruction disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_optimize_project_records_destruction: false)
|
||||
|
|
@ -173,24 +155,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
|
|||
destroy_project(project, user, {})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ci_optimize_project_records_destruction enabled' do
|
||||
it 'executes DestroyPipelineService for project ci pipelines' do
|
||||
allow(::Ci::DestroyPipelineService).to receive(:new).and_return(destroy_pipeline_service)
|
||||
|
||||
expect(::Ci::AbortPipelinesService)
|
||||
.to receive_message_chain(:new, :execute)
|
||||
.with(project.all_pipelines, :project_deleted)
|
||||
|
||||
pipelines.each do |pipeline|
|
||||
expect(destroy_pipeline_service)
|
||||
.to receive(:execute)
|
||||
.with(pipeline)
|
||||
end
|
||||
|
||||
destroy_project(project, user, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has remote mirrors' do
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@ module GitalySetup
|
|||
Logger.new($stdout, level: level, formatter: ->(_, _, _, msg) { msg })
|
||||
end
|
||||
|
||||
def expand_path(path)
|
||||
File.expand_path(path, File.join(__dir__, '../../..'))
|
||||
end
|
||||
|
||||
def tmp_tests_gitaly_dir
|
||||
File.expand_path('../../../tmp/tests/gitaly', __dir__)
|
||||
expand_path('tmp/tests/gitaly')
|
||||
end
|
||||
|
||||
def tmp_tests_gitaly_bin_dir
|
||||
|
|
@ -27,11 +31,11 @@ module GitalySetup
|
|||
end
|
||||
|
||||
def tmp_tests_gitlab_shell_dir
|
||||
File.expand_path('../../../tmp/tests/gitlab-shell', __dir__)
|
||||
expand_path('tmp/tests/gitlab-shell')
|
||||
end
|
||||
|
||||
def rails_gitlab_shell_secret
|
||||
File.expand_path('../../../.gitlab_shell_secret', __dir__)
|
||||
expand_path('.gitlab_shell_secret')
|
||||
end
|
||||
|
||||
def gemfile
|
||||
|
|
@ -48,7 +52,7 @@ module GitalySetup
|
|||
|
||||
def env
|
||||
{
|
||||
'HOME' => File.expand_path('tmp/tests'),
|
||||
'HOME' => expand_path('tmp/tests'),
|
||||
'GEM_PATH' => Gem.path.join(':'),
|
||||
'BUNDLE_APP_CONFIG' => File.join(gemfile_dir, '.bundle'),
|
||||
'BUNDLE_INSTALL_FLAGS' => nil,
|
||||
|
|
@ -67,7 +71,7 @@ module GitalySetup
|
|||
system('bundle config set --local retry 3', chdir: gemfile_dir)
|
||||
|
||||
if ENV['CI']
|
||||
bundle_path = File.expand_path('../../../vendor/gitaly-ruby', __dir__)
|
||||
bundle_path = expand_path('vendor/gitaly-ruby')
|
||||
system('bundle', 'config', 'set', '--local', 'path', bundle_path, chdir: gemfile_dir)
|
||||
end
|
||||
end
|
||||
|
|
@ -154,7 +158,7 @@ module GitalySetup
|
|||
|
||||
LOGGER.debug "Checking gitaly-ruby bundle...\n"
|
||||
out = ENV['CI'] ? $stdout : '/dev/null'
|
||||
abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: File.dirname(gemfile))
|
||||
abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: gemfile_dir)
|
||||
end
|
||||
|
||||
def read_socket_path(service)
|
||||
|
|
|
|||
|
|
@ -594,6 +594,8 @@ module TestEnv
|
|||
# Not a git SHA, so return early
|
||||
return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID
|
||||
|
||||
return false unless Dir.exist?(component_folder)
|
||||
|
||||
sha, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} rev-parse HEAD), component_folder)
|
||||
return false if exit_status != 0
|
||||
|
||||
|
|
|
|||
|
|
@ -7,26 +7,26 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
|
|||
Rake.application.rake_require 'tasks/gitlab/gitaly'
|
||||
end
|
||||
|
||||
describe 'install' do
|
||||
let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' }
|
||||
let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s }
|
||||
let(:storage_path) { Rails.root.join('tmp/tests/repositories').to_s }
|
||||
let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp }
|
||||
let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' }
|
||||
let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s }
|
||||
let(:storage_path) { Rails.root.join('tmp/tests/repositories').to_s }
|
||||
let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp }
|
||||
|
||||
subject { run_rake_task('gitlab:gitaly:install', clone_path, storage_path) }
|
||||
describe 'clone' do
|
||||
subject { run_rake_task('gitlab:gitaly:clone', clone_path, storage_path) }
|
||||
|
||||
context 'no dir given' do
|
||||
it 'aborts and display a help message' do
|
||||
# avoid writing task output to spec progress
|
||||
allow($stderr).to receive :write
|
||||
expect { run_rake_task('gitlab:gitaly:install') }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/
|
||||
expect { run_rake_task('gitlab:gitaly:clone') }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/
|
||||
end
|
||||
end
|
||||
|
||||
context 'no storage path given' do
|
||||
it 'aborts and display a help message' do
|
||||
allow($stderr).to receive :write
|
||||
expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/
|
||||
expect { run_rake_task('gitlab:gitaly:clone', clone_path) }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -40,11 +40,6 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
|
|||
end
|
||||
|
||||
describe 'checkout or clone' do
|
||||
before do
|
||||
stub_env('CI', false)
|
||||
expect(Dir).to receive(:chdir).with(clone_path)
|
||||
end
|
||||
|
||||
it 'calls checkout_or_clone_version with the right arguments' do
|
||||
expect(main_object)
|
||||
.to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path, clone_opts: %w[--depth 1])
|
||||
|
|
@ -52,6 +47,10 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
|
|||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'install' do
|
||||
subject { run_rake_task('gitlab:gitaly:install', clone_path, storage_path) }
|
||||
|
||||
describe 'gmake/make' do
|
||||
before do
|
||||
|
|
@ -62,10 +61,6 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
|
|||
end
|
||||
|
||||
context 'gmake is available' do
|
||||
before do
|
||||
expect(main_object).to receive(:checkout_or_clone_version)
|
||||
end
|
||||
|
||||
it 'calls gmake in the gitaly directory' do
|
||||
expect(Gitlab::Popen).to receive(:popen)
|
||||
.with(%w[which gmake])
|
||||
|
|
@ -93,7 +88,6 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
|
|||
|
||||
context 'gmake is not available' do
|
||||
before do
|
||||
expect(main_object).to receive(:checkout_or_clone_version)
|
||||
expect(Gitlab::Popen).to receive(:popen)
|
||||
.with(%w[which gmake])
|
||||
.and_return(['', 42])
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ RSpec.describe Gitlab::TaskHelpers do
|
|||
|
||||
describe '#checkout_version' do
|
||||
it 'clones the repo in the target dir' do
|
||||
expect(subject)
|
||||
.to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} config protocol.version 2])
|
||||
expect(subject)
|
||||
.to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet origin #{tag}])
|
||||
expect(subject)
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe PurgeDependencyProxyCacheWorker do
|
||||
let_it_be(:user) { create(:admin) }
|
||||
let_it_be(:blob) { create(:dependency_proxy_blob )}
|
||||
let_it_be(:group, reload: true) { blob.group }
|
||||
let_it_be(:manifest) { create(:dependency_proxy_manifest, group: group )}
|
||||
let_it_be_with_refind(:blob) { create(:dependency_proxy_blob )}
|
||||
let_it_be_with_reload(:group) { blob.group }
|
||||
let_it_be_with_refind(:manifest) { create(:dependency_proxy_manifest, group: group )}
|
||||
let_it_be(:group_id) { group.id }
|
||||
|
||||
subject { described_class.new.perform(user.id, group_id) }
|
||||
|
||||
describe '#perform' do
|
||||
shared_examples 'not removing blobs and manifests' do
|
||||
it 'does not remove blobs and manifests', :aggregate_failures do
|
||||
expect { subject }.not_to change { group.dependency_proxy_blobs.size }
|
||||
expect { subject }.not_to change { group.dependency_proxy_manifests.size }
|
||||
shared_examples 'not expiring blobs and manifests' do
|
||||
it 'does not expire blobs and manifests', :aggregate_failures do
|
||||
expect { subject }.not_to change { blob.status }
|
||||
expect { subject }.not_to change { manifest.status }
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
@ -25,39 +25,36 @@ RSpec.describe PurgeDependencyProxyCacheWorker do
|
|||
include_examples 'an idempotent worker' do
|
||||
let(:job_args) { [user.id, group_id] }
|
||||
|
||||
it 'deletes the blobs and returns ok', :aggregate_failures do
|
||||
expect(group.dependency_proxy_blobs.size).to eq(1)
|
||||
expect(group.dependency_proxy_manifests.size).to eq(1)
|
||||
|
||||
it 'expires the blobs and returns ok', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(group.dependency_proxy_blobs.size).to eq(0)
|
||||
expect(group.dependency_proxy_manifests.size).to eq(0)
|
||||
expect(blob).to be_expired
|
||||
expect(manifest).to be_expired
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it_behaves_like 'not removing blobs and manifests'
|
||||
it_behaves_like 'not expiring blobs and manifests'
|
||||
end
|
||||
end
|
||||
|
||||
context 'a non-admin user' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it_behaves_like 'not removing blobs and manifests'
|
||||
it_behaves_like 'not expiring blobs and manifests'
|
||||
end
|
||||
|
||||
context 'an invalid user id' do
|
||||
let(:user) { double('User', id: 99999 ) }
|
||||
|
||||
it_behaves_like 'not removing blobs and manifests'
|
||||
it_behaves_like 'not expiring blobs and manifests'
|
||||
end
|
||||
|
||||
context 'an invalid group' do
|
||||
let(:group_id) { 99999 }
|
||||
|
||||
it_behaves_like 'not removing blobs and manifests'
|
||||
it_behaves_like 'not expiring blobs and manifests'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue