Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-16 18:14:09 +00:00
parent 6aaec2fc6c
commit cab5a484fe
65 changed files with 718 additions and 500 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
0ab93a0bfd52d6c13203a0b183b2fcb9d6770334e5b1bd00a28fb623b65c428d

View File

@ -0,0 +1 @@
870100261e3704522d390885b8ff13ebbcb093aa508d79b90f9738f6a0fffd10

View File

@ -0,0 +1 @@
0cc2f19a8e31d9418ffd4fa1307f5210f0f2d781b957d417f06e19aca0b53985

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
![move issue - button](img/sidebar_move_issue.png)
- 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**:
![close issue - button](img/button_close_issue_v13_6.png)
- 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.
![close issue from the issue board](img/close_issue_from_board.gif)
### 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.
![merge request closing issue when merged](img/merge_request_closes_issue_v13_11.png)
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).
![disable issue auto close - settings](img/disable_issue_auto_close.png)
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.
![Change the issue type](img/issue_type_change_v13_12.png)
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.
![Similar issues](img/similar_issues.png)
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`).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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} ${*}"

12
scripts/db_tasks Executable file
View File

@ -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} ${*}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -198,6 +198,7 @@ merge_requests:
- system_note_metadata
- note_authors
- cleanup_schedule
- compliance_violations
external_pull_requests:
- project
merge_request_diff:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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