Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml20
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue75
-rw-r--r--app/assets/javascripts/blob/components/constants.js6
-rw-r--r--app/assets/javascripts/boards/models/list.js45
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js47
-rw-r--r--app/assets/javascripts/commons/polyfills.js2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js1
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js3
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/getters.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutations.js9
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/snippets/fragments/author.fragment.graphql8
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.query.graphql6
-rw-r--r--app/finders/keys_finder.rb15
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--changelogs/unreleased/21901-support-for-toc-in-readme-files.yml5
-rw-r--r--changelogs/unreleased/Remove-addMultipleIssues-function-logic-from-list-js.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.86.0.yml5
-rw-r--r--doc/administration/audit_events.md1
-rw-r--r--doc/user/group/saml_sso/index.md14
-rw-r--r--lib/api/keys.rb2
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb36
-rw-r--r--lib/banzai/filter/table_of_contents_tag_filter.rb45
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/feature/gitaly.rb1
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb12
-rw-r--r--spec/finders/keys_finder_spec.rb200
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js109
-rw-r--r--spec/frontend/boards/components/issue_card_inner_scoped_label_spec.js (renamed from spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js)2
-rw-r--r--spec/frontend/boards/components/issue_due_date_spec.js (renamed from spec/javascripts/boards/components/issue_due_date_spec.js)0
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js29
-rw-r--r--spec/frontend/error_tracking/store/list/mutation_spec.js25
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb23
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb31
-rw-r--r--spec/requests/api/internal/base_spec.rb6
-rw-r--r--spec/services/users/destroy_service_spec.rb9
-rw-r--r--spec/support/helpers/filter_spec_helper.rb4
43 files changed, 604 insertions, 249 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 3d890dfea7e..3b88dcfe45d 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -75,16 +75,18 @@
changes: *code-backstage-qa-patterns
when: on_success
-.rails:rules:master-refs:
+.rails:rules:master-refs-code-backstage-qa:
rules:
- <<: *if-master-refs
+ changes: *code-backstage-qa-patterns
when: on_success
-.rails:rules:master-refs-ee-only:
+.rails:rules:master-refs-code-backstage-qa-ee-only:
rules:
- <<: *if-not-ee
when: never
- <<: *if-master-refs
+ changes: *code-backstage-qa-patterns
when: on_success
.rails:rules:ee-only:
@@ -330,12 +332,12 @@ coverage:
rspec quarantine pg9:
extends:
- .rspec-base-quarantine
- - .rails:rules:master-refs
+ - .rails:rules:master-refs-code-backstage-qa
.rspec-base-pg10:
extends:
- .rspec-base
- - .rails:rules:master-refs
+ - .rails:rules:master-refs-code-backstage-qa
- .use-pg10
rspec unit pg10:
@@ -357,7 +359,7 @@ rspec system pg10:
rspec-ee quarantine pg9:
extends:
- .rspec-base-quarantine
- - .rails:rules:master-refs-ee-only
+ - .rails:rules:master-refs-code-backstage-qa-ee-only
variables:
RSPEC_OPTS: "--tag quarantine -- ee/spec/"
@@ -365,25 +367,25 @@ rspec-ee migration pg10:
extends:
- .rspec-ee-base-pg10
- .rspec-base-migration
- - .rails:rules:master-refs
+ - .rails:rules:master-refs-code-backstage-qa
parallel: 2
rspec-ee unit pg10:
extends:
- .rspec-ee-base-pg10
- - .rails:rules:master-refs
+ - .rails:rules:master-refs-code-backstage-qa
parallel: 10
rspec-ee integration pg10:
extends:
- .rspec-ee-base-pg10
- - .rails:rules:master-refs
+ - .rails:rules:master-refs-code-backstage-qa
parallel: 3
rspec-ee system pg10:
extends:
- .rspec-ee-base-pg10
- - .rails:rules:master-refs
+ - .rails:rules:master-refs-code-backstage-qa
parallel: 5
# ee + master-only jobs #
#########################
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index f288d11142d..b7844a6ffdc 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.85.0
+1.86.0
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
new file mode 100644
index 00000000000..7acdd574359
--- /dev/null
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ RICH_BLOB_VIEWER,
+ RICH_BLOB_VIEWER_TITLE,
+ SIMPLE_BLOB_VIEWER,
+ SIMPLE_BLOB_VIEWER_TITLE,
+} from './constants';
+
+export default {
+ components: {
+ GlIcon,
+ GlButtonGroup,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ viewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
+ };
+ },
+ computed: {
+ isSimpleViewer() {
+ return this.viewer === SIMPLE_BLOB_VIEWER;
+ },
+ isRichViewer() {
+ return this.viewer === RICH_BLOB_VIEWER;
+ },
+ },
+ methods: {
+ switchToViewer(viewer) {
+ if (viewer !== this.viewer) {
+ this.viewer = viewer;
+ this.$emit('switch-viewer', viewer);
+ }
+ },
+ },
+ SIMPLE_BLOB_VIEWER,
+ RICH_BLOB_VIEWER,
+ SIMPLE_BLOB_VIEWER_TITLE,
+ RICH_BLOB_VIEWER_TITLE,
+};
+</script>
+<template>
+ <gl-button-group class="js-blob-viewer-switcher ml-2">
+ <gl-button
+ v-gl-tooltip.hover
+ :aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
+ :title="$options.SIMPLE_BLOB_VIEWER_TITLE"
+ :selected="isSimpleViewer"
+ :class="{ active: isSimpleViewer }"
+ @click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
+ >
+ <gl-icon name="code" :size="14" />
+ </gl-button>
+ <gl-button
+ v-gl-tooltip.hover
+ :aria-label="$options.RICH_BLOB_VIEWER_TITLE"
+ :title="$options.RICH_BLOB_VIEWER_TITLE"
+ :selected="isRichViewer"
+ :class="{ active: isRichViewer }"
+ @click="switchToViewer($options.RICH_BLOB_VIEWER)"
+ >
+ <gl-icon name="document" :size="14" />
+ </gl-button>
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js
index fe2ac7b7c51..d3fed9e51e9 100644
--- a/app/assets/javascripts/blob/components/constants.js
+++ b/app/assets/javascripts/blob/components/constants.js
@@ -3,3 +3,9 @@ import { __ } from '~/locale';
export const BTN_COPY_CONTENTS_TITLE = __('Copy file contents');
export const BTN_RAW_TITLE = __('Open raw');
export const BTN_DOWNLOAD_TITLE = __('Download');
+
+export const SIMPLE_BLOB_VIEWER = 'simple';
+export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source');
+
+export const RICH_BLOB_VIEWER = 'rich';
+export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file');
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 299864c279b..ff50b8ed7d1 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -161,50 +161,7 @@ class List {
}
addMultipleIssues(issues, listFrom, newIndex) {
- let moveBeforeId = null;
- let moveAfterId = null;
-
- const listHasIssues = issues.every(issue => this.findIssue(issue.id));
-
- if (!listHasIssues) {
- if (newIndex !== undefined) {
- if (this.issues[newIndex - 1]) {
- moveBeforeId = this.issues[newIndex - 1].id;
- }
-
- if (this.issues[newIndex]) {
- moveAfterId = this.issues[newIndex].id;
- }
-
- this.issues.splice(newIndex, 0, ...issues);
- } else {
- this.issues.push(...issues);
- }
-
- if (this.label) {
- issues.forEach(issue => issue.addLabel(this.label));
- }
-
- if (this.assignee) {
- if (listFrom && listFrom.type === 'assignee') {
- issues.forEach(issue => issue.removeAssignee(listFrom.assignee));
- }
- issues.forEach(issue => issue.addAssignee(this.assignee));
- }
-
- if (IS_EE && this.milestone) {
- if (listFrom && listFrom.type === 'milestone') {
- issues.forEach(issue => issue.removeMilestone(listFrom.milestone));
- }
- issues.forEach(issue => issue.addMilestone(this.milestone));
- }
-
- if (listFrom) {
- this.issuesSize += issues.length;
-
- this.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
- }
- }
+ boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex);
}
addIssue(issue, listFrom, newIndex) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index df8b7a2df6c..e5ce8b70a4f 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -131,6 +131,53 @@ const boardsStore = {
listFrom.update();
},
+ addMultipleListIssues(list, issues, listFrom, newIndex) {
+ let moveBeforeId = null;
+ let moveAfterId = null;
+
+ const listHasIssues = issues.every(issue => list.findIssue(issue.id));
+
+ if (!listHasIssues) {
+ if (newIndex !== undefined) {
+ if (list.issues[newIndex - 1]) {
+ moveBeforeId = list.issues[newIndex - 1].id;
+ }
+
+ if (list.issues[newIndex]) {
+ moveAfterId = list.issues[newIndex].id;
+ }
+
+ list.issues.splice(newIndex, 0, ...issues);
+ } else {
+ list.issues.push(...issues);
+ }
+
+ if (list.label) {
+ issues.forEach(issue => issue.addLabel(list.label));
+ }
+
+ if (list.assignee) {
+ if (listFrom && listFrom.type === 'assignee') {
+ issues.forEach(issue => issue.removeAssignee(listFrom.assignee));
+ }
+ issues.forEach(issue => issue.addAssignee(list.assignee));
+ }
+
+ if (IS_EE && list.milestone) {
+ if (listFrom && listFrom.type === 'milestone') {
+ issues.forEach(issue => issue.removeMilestone(listFrom.milestone));
+ }
+ issues.forEach(issue => issue.addMilestone(list.milestone));
+ }
+
+ if (listFrom) {
+ list.issuesSize += issues.length;
+
+ list.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
+ }
+ }
+ },
+
startMoving(list, issue) {
Object.assign(this.moving, { list, issue });
},
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index dec3c637c22..5e04b0573d2 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,5 +1,3 @@
-import 'core-js/stable';
-
// Browser polyfills
import 'formdata-polyfill';
import './polyfills/custom_event';
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 1c996cbc13b..a90f446159d 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -168,6 +168,7 @@ export default {
'setIndexPath',
'fetchPaginatedResults',
'updateStatus',
+ 'removeIgnoredResolvedErrors',
]),
setSearchText(text) {
this.errorSearchQuery = text;
@@ -196,9 +197,9 @@ export default {
updateIssueStatus(errorId, status) {
this.updateStatus({
endpoint: this.getIssueUpdatePath(errorId),
- redirectUrl: this.listPath,
status,
});
+ this.removeIgnoredResolvedErrors(errorId);
},
},
};
@@ -235,7 +236,6 @@ export default {
</gl-dropdown>
<div class="filtered-search-input-container flex-fill">
<gl-form-input
- v-model="errorSearchQuery"
class="pl-2 filtered-search"
:disabled="loading"
:placeholder="__('Search or filter results…')"
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index d96ac7f524e..6f8573c0f4d 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -100,4 +100,8 @@ export const fetchPaginatedResults = ({ commit, dispatch }, cursor) => {
dispatch('startPolling');
};
+export const removeIgnoredResolvedErrors = ({ commit }, error) => {
+ commit(types.REMOVE_IGNORED_RESOLVED_ERRORS, error);
+};
+
export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/list/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
index c3468b7eabd..23495cbf01d 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutation_types.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
@@ -9,3 +9,4 @@ export const SET_ENDPOINT = 'SET_ENDPOINT';
export const SET_SORT_FIELD = 'SET_SORT_FIELD';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_CURSOR = 'SET_CURSOR';
+export const REMOVE_IGNORED_RESOLVED_ERRORS = 'REMOVE_IGNORED_RESOLVED_ERRORS';
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index dd5cde0576a..38d156263fb 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -59,4 +59,7 @@ export default {
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
+ [types.REMOVE_IGNORED_RESOLVED_ERRORS](state, error) {
+ state.errors = state.errors.filter(err => err.id !== error);
+ },
};
diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js
index d77e5f15469..e27fe9c079e 100644
--- a/app/assets/javascripts/error_tracking_settings/store/getters.js
+++ b/app/assets/javascripts/error_tracking_settings/store/getters.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { isMatch } from 'lodash';
import { __, s__, sprintf } from '~/locale';
import { getDisplayName } from '../utils';
@@ -7,7 +7,7 @@ export const hasProjects = state => Boolean(state.projects) && state.projects.le
export const isProjectInvalid = (state, getters) =>
Boolean(state.selectedProject) &&
getters.hasProjects &&
- !state.projects.some(project => _.isMatch(state.selectedProject, project));
+ !state.projects.some(project => isMatch(state.selectedProject, project));
export const dropdownLabel = (state, getters) => {
if (state.selectedProject !== null) {
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js
index 133f25264b9..e1986eb694b 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutations.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { pick } from 'lodash';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
import { projectKeys } from '../utils';
@@ -12,7 +12,7 @@ export default {
.map(convertObjectPropsToCamelCase)
// The `pick` strips out extra properties returned from Sentry.
// Such properties could be problematic later, e.g. when checking whether `projects` contains `selectedProject`
- .map(project => _.pick(project, projectKeys));
+ .map(project => pick(project, projectKeys));
},
[types.RESET_CONNECT](state) {
state.connectSuccessful = false;
@@ -29,10 +29,7 @@ export default {
state.operationsSettingsEndpoint = operationsSettingsEndpoint;
if (project) {
- state.selectedProject = _.pick(
- convertObjectPropsToCamelCase(JSON.parse(project)),
- projectKeys,
- );
+ state.selectedProject = pick(convertObjectPropsToCamelCase(JSON.parse(project)), projectKeys);
}
},
[types.UPDATE_API_HOST](state, apiHost) {
diff --git a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
new file mode 100644
index 00000000000..9a2ff1c1648
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql
@@ -0,0 +1,6 @@
+fragment Author on User {
+ avatarUrl
+ name
+ username
+ webUrl
+}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 4ae3e813c36..71bdf279861 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -10,6 +10,7 @@ import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import TestReports from './components/test_reports/test_reports.vue';
import testReportsStore from './stores/test_reports';
+import axios from '~/lib/utils/axios_utils';
Vue.use(Translate);
@@ -111,5 +112,12 @@ export default () => {
return createElement('test-reports');
},
});
+
+ axios
+ .get(dataset.testReportEndpoint)
+ .then(({ data }) => {
+ document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
+ })
+ .catch(() => {});
}
};
diff --git a/app/assets/javascripts/snippets/fragments/author.fragment.graphql b/app/assets/javascripts/snippets/fragments/author.fragment.graphql
deleted file mode 100644
index 2684bd0fa37..00000000000
--- a/app/assets/javascripts/snippets/fragments/author.fragment.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-fragment Author on Snippet {
- author {
- name,
- avatarUrl,
- username,
- webUrl
- }
-} \ No newline at end of file
diff --git a/app/assets/javascripts/snippets/queries/snippet.query.graphql b/app/assets/javascripts/snippets/queries/snippet.query.graphql
index 1cb2c86c4d8..c58a5168ba3 100644
--- a/app/assets/javascripts/snippets/queries/snippet.query.graphql
+++ b/app/assets/javascripts/snippets/queries/snippet.query.graphql
@@ -1,6 +1,6 @@
#import '../fragments/snippetBase.fragment.graphql'
#import '../fragments/project.fragment.graphql'
-#import '../fragments/author.fragment.graphql'
+#import "~/graphql_shared/fragments/author.fragment.graphql"
query GetSnippetQuery($ids: [ID!]) {
snippets(ids: $ids) {
@@ -8,7 +8,9 @@ query GetSnippetQuery($ids: [ID!]) {
node {
...SnippetBase
...Project
- ...Author
+ author {
+ ...Author
+ }
}
}
}
diff --git a/app/finders/keys_finder.rb b/app/finders/keys_finder.rb
index 6fd914c88cd..0263d809246 100644
--- a/app/finders/keys_finder.rb
+++ b/app/finders/keys_finder.rb
@@ -8,16 +8,13 @@ class KeysFinder
'md5' => 'fingerprint'
}.freeze
- def initialize(current_user, params)
- @current_user = current_user
+ def initialize(params)
@params = params
end
def execute
- raise GitLabAccessDeniedError unless current_user.admin?
-
keys = by_key_type
- keys = by_user(keys)
+ keys = by_users(keys)
keys = sort(keys)
by_fingerprint(keys)
@@ -25,7 +22,7 @@ class KeysFinder
private
- attr_reader :current_user, :params
+ attr_reader :params
def by_key_type
if params[:key_type] == 'ssh'
@@ -39,10 +36,10 @@ class KeysFinder
keys.order_last_used_at_desc
end
- def by_user(keys)
- return keys unless params[:user]
+ def by_users(keys)
+ return keys unless params[:users]
- keys.for_user(params[:user])
+ keys.for_user(params[:users])
end
def by_fingerprint(keys)
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 4d8cba5168d..cdd75d43a78 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -18,7 +18,7 @@
%li.js-tests-tab-link
= link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
= s_('TestReports|Tests')
- %span.badge.badge-pill= pipeline.test_reports.total_count
+ %span.badge.badge-pill.js-test-report-badge-counter
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content
diff --git a/changelogs/unreleased/21901-support-for-toc-in-readme-files.yml b/changelogs/unreleased/21901-support-for-toc-in-readme-files.yml
new file mode 100644
index 00000000000..d063172ee45
--- /dev/null
+++ b/changelogs/unreleased/21901-support-for-toc-in-readme-files.yml
@@ -0,0 +1,5 @@
+---
+title: Support for table of contents tag in GitLab Flavored Markdown
+merge_request: 24196
+author:
+type: added
diff --git a/changelogs/unreleased/Remove-addMultipleIssues-function-logic-from-list-js.yml b/changelogs/unreleased/Remove-addMultipleIssues-function-logic-from-list-js.yml
new file mode 100644
index 00000000000..19c690a23b0
--- /dev/null
+++ b/changelogs/unreleased/Remove-addMultipleIssues-function-logic-from-list-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes store logic from issue board models
+merge_request: 21404
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/gitaly-version-v1.86.0.yml b/changelogs/unreleased/gitaly-version-v1.86.0.yml
new file mode 100644
index 00000000000..066912ebe9e
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.86.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.86.0
+merge_request: 24610
+author:
+type: changed
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index fa217c31eb2..f1c9fdcb876 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -106,6 +106,7 @@ recorded:
- Grant OAuth access
- Started/stopped user impersonation
- Changed username
+- User was deleted
It is possible to filter particular actions by choosing an audit data type from
the filter dropdown box. You can further filter by specific group, project or user
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 99749ccf2ae..6fa7a0397f4 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -81,6 +81,20 @@ Since use of the group-managed account requires the use of SSO, users of group-m
- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
- Contributions in the group (e.g. issues, merge requests) will remain intact.
+##### Credentials inventory for Group-managed accounts **(ULTIMATE)**
+
+> [Introduced in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/38133)
+
+Owners who manage user accounts in a group can view the following details of personal access tokens and SSH keys:
+
+- Owners
+- Scopes
+- Usage patterns
+
+To access the Credentials inventory of a group, navigate to **{shield}** **Security & Compliance > Credentials** in your group's sidebar.
+
+This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md).
+
#### Assertions
When using group-managed accounts, the following user details need to be passed to GitLab as SAML
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index bec3dc9bd97..b730e027063 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -26,7 +26,7 @@ module API
get do
authenticated_with_can_read_all_resources!
- key = KeysFinder.new(current_user, params).execute
+ key = KeysFinder.new(params).execute
not_found!('Key') unless key
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 0c1bbd2d250..dec4ec871f1 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -25,12 +25,10 @@ module Banzai
# * [[http://example.com/images/logo.png]]
# * [[http://example.com/images/logo.png|alt=Logo]]
#
- # - Insert a Table of Contents list:
- #
- # * [[_TOC_]]
- #
# Based on Gollum::Filter::Tags
#
+ # Note: the table of contents tag is now handled by TableOfContentsTagFilter
+ #
# Context options:
# :project_wiki (required) - Current project wiki.
#
@@ -64,23 +62,11 @@ module Banzai
def call
doc.search(".//text()").each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+ next unless node.content =~ TAGS_PATTERN
- # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
- # before this one, it will be converted into `[[<em>TOC</em>]]`, so it
- # needs special-case handling
- if toc_tag?(node)
- process_toc_tag(node)
- else
- content = node.content
-
- next unless content =~ TAGS_PATTERN
-
- html = process_tag($1)
+ html = process_tag($1)
- if html && html != node.content
- node.replace(html)
- end
- end
+ node.replace(html) if html && html != node.content
end
doc
@@ -88,12 +74,6 @@ module Banzai
private
- # Replace an entire `[[<em>TOC</em>]]` node with the result generated by
- # TableOfContentsFilter
- def process_toc_tag(node)
- node.parent.parent.replace(result[:toc].presence || '')
- end
-
# Process a single tag into its final HTML form.
#
# tag - The String tag contents (the stuff inside the double brackets).
@@ -129,12 +109,6 @@ module Banzai
end
end
- def toc_tag?(node)
- node.content == 'TOC' &&
- node.parent.name == 'em' &&
- node.parent.parent.text == '[[TOC]]'
- end
-
def image?(path)
path =~ ALLOWED_IMAGE_EXTENSIONS
end
diff --git a/lib/banzai/filter/table_of_contents_tag_filter.rb b/lib/banzai/filter/table_of_contents_tag_filter.rb
new file mode 100644
index 00000000000..13d0a6a4cc7
--- /dev/null
+++ b/lib/banzai/filter/table_of_contents_tag_filter.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # Using `[[_TOC_]]`, inserts a Table of Contents list.
+ # This syntax is based on the Gollum syntax. This way we have
+ # some consistency between with wiki and normal markdown.
+ # If there ever emerges a markdown standard, we can implement
+ # that here.
+ #
+ # The support for this has been removed from GollumTagsFilter
+ #
+ # Based on Banzai::Filter::GollumTagsFilter
+ class TableOfContentsTagFilter < HTML::Pipeline::Filter
+ TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(., 'TOC')])
+
+ def call
+ return doc if context[:no_header_anchors]
+
+ doc.xpath(TEXT_QUERY).each do |node|
+ # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
+ # before this one, it will be converted into `[[<em>TOC</em>]]`, so it
+ # needs special-case handling
+ process_toc_tag(node) if toc_tag?(node)
+ end
+
+ doc
+ end
+
+ private
+
+ # Replace an entire `[[<em>TOC</em>]]` node with the result generated by
+ # TableOfContentsFilter
+ def process_toc_tag(node)
+ node.parent.parent.replace(result[:toc].presence || '')
+ end
+
+ def toc_tag?(node)
+ node.content == 'TOC' &&
+ node.parent.name == 'em' &&
+ node.parent.parent.text == '[[TOC]]'
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index f6c12cdb53b..dad0d95685e 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -32,6 +32,7 @@ module Banzai
Filter::InlineMetricsFilter,
Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
+ Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
Filter::SuggestionFilter,
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 6e27b500802..7e3b9378d10 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -11,6 +11,7 @@ class Feature
inforef_uploadpack_cache
commit_without_batch_check
use_core_delta_islands
+ use_git_protocol_v2
].freeze
DEFAULT_ON_FLAGS = Set.new([]).freeze
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e290839dc69..8ffcc85219e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6732,6 +6732,12 @@ msgstr ""
msgid "Display name"
msgstr ""
+msgid "Display rendered file"
+msgstr ""
+
+msgid "Display source"
+msgstr ""
+
msgid "Displays dependencies and known vulnerabilities, based on the %{linkStart}latest pipeline%{linkEnd} scan"
msgstr ""
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 5fe99c0ad1d..8d9a397de9a 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -356,6 +356,18 @@ describe 'Pipeline', :js do
end
end
+ context 'test tabs' do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ it 'shows badge counter in Tests tab' do
+ visit_pipeline
+ wait_for_requests
+
+ expect(pipeline.test_reports.total_count).to eq(4)
+ expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s)
+ end
+ end
+
context 'retrying jobs' do
before do
visit_pipeline
diff --git a/spec/finders/keys_finder_spec.rb b/spec/finders/keys_finder_spec.rb
index 7605d066ddf..bae4a542484 100644
--- a/spec/finders/keys_finder_spec.rb
+++ b/spec/finders/keys_finder_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe KeysFinder do
- subject { described_class.new(user, params).execute }
+ subject { described_class.new(params).execute }
let(:user) { create(:user) }
let(:params) { {} }
@@ -20,159 +20,149 @@ describe KeysFinder do
let!(:key_2) { create(:personal_key, last_used_at: nil, user: user) }
let!(:key_3) { create(:personal_key, last_used_at: 2.days.ago) }
- context 'with a regular user' do
- it 'raises GitLabAccessDeniedError' do
- expect { subject }.to raise_error(KeysFinder::GitLabAccessDeniedError)
- end
- end
+ context 'key_type' do
+ let!(:deploy_key) { create(:deploy_key) }
- context 'with an admin user' do
- let(:user) {create(:admin)}
+ context 'when `key_type` is `ssh`' do
+ before do
+ params[:key_type] = 'ssh'
+ end
+
+ it 'returns only SSH keys' do
+ expect(subject).to contain_exactly(key_1, key_2, key_3)
+ end
+ end
- context 'key_type' do
- let!(:deploy_key) { create(:deploy_key) }
+ context 'when `key_type` is not specified' do
+ it 'returns all types of keys' do
+ expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
+ end
+ end
+ end
- context 'when `key_type` is `ssh`' do
+ context 'fingerprint' do
+ context 'with invalid fingerprint' do
+ context 'with invalid MD5 fingerprint' do
before do
- params[:key_type] = 'ssh'
+ params[:fingerprint] = '11:11:11:11'
end
- it 'returns only SSH keys' do
- expect(subject).to contain_exactly(key_1, key_2, key_3)
+ it 'raises InvalidFingerprint' do
+ expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
end
end
- context 'when `key_type` is not specified' do
- it 'returns all types of keys' do
- expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
+ context 'with invalid SHA fingerprint' do
+ before do
+ params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
+ end
+
+ it 'raises InvalidFingerprint' do
+ expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
end
end
end
- context 'fingerprint' do
- context 'with invalid fingerprint' do
- context 'with invalid MD5 fingerprint' do
+ context 'with valid fingerprints' do
+ let!(:deploy_key) do
+ create(:deploy_key,
+ user: user,
+ key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1017k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
+ fingerprint: '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4',
+ fingerprint_sha256: '4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk')
+ end
+
+ context 'personal key with valid MD5 params' do
+ context 'with an existent fingerprint' do
before do
- params[:fingerprint] = '11:11:11:11'
+ params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
end
- it 'raises InvalidFingerprint' do
- expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
+ it 'returns the key' do
+ expect(subject).to eq(key_1)
+ expect(subject.user).to eq(user)
end
end
- context 'with invalid SHA fingerprint' do
+ context 'deploy key with an existent fingerprint' do
before do
- params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
+ params[:fingerprint] = '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4'
end
- it 'raises InvalidFingerprint' do
- expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
+ it 'returns the key' do
+ expect(subject).to eq(deploy_key)
+ expect(subject.user).to eq(user)
end
end
- end
-
- context 'with valid fingerprints' do
- let!(:deploy_key) do
- create(:deploy_key,
- user: user,
- key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1017k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
- fingerprint: '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4',
- fingerprint_sha256: '4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk')
- end
-
- context 'personal key with valid MD5 params' do
- context 'with an existent fingerprint' do
- before do
- params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
- end
- it 'returns the key' do
- expect(subject).to eq(key_1)
- expect(subject.user).to eq(user)
- end
+ context 'with a non-existent fingerprint' do
+ before do
+ params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
end
- context 'deploy key with an existent fingerprint' do
- before do
- params[:fingerprint] = '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4'
- end
-
- it 'returns the key' do
- expect(subject).to eq(deploy_key)
- expect(subject.user).to eq(user)
- end
+ it 'returns nil' do
+ expect(subject).to be_nil
end
+ end
+ end
- context 'with a non-existent fingerprint' do
- before do
- params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
- end
+ context 'personal key with valid SHA256 params' do
+ context 'with an existent fingerprint' do
+ before do
+ params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
+ end
- it 'returns nil' do
- expect(subject).to be_nil
- end
+ it 'returns key' do
+ expect(subject).to eq(key_1)
+ expect(subject.user).to eq(user)
end
end
- context 'personal key with valid SHA256 params' do
- context 'with an existent fingerprint' do
- before do
- params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
- end
-
- it 'returns key' do
- expect(subject).to eq(key_1)
- expect(subject.user).to eq(user)
- end
+ context 'deploy key with an existent fingerprint' do
+ before do
+ params[:fingerprint] = 'SHA256:4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk'
end
- context 'deploy key with an existent fingerprint' do
- before do
- params[:fingerprint] = 'SHA256:4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk'
- end
-
- it 'returns key' do
- expect(subject).to eq(deploy_key)
- expect(subject.user).to eq(user)
- end
+ it 'returns key' do
+ expect(subject).to eq(deploy_key)
+ expect(subject.user).to eq(user)
end
+ end
- context 'with a non-existent fingerprint' do
- before do
- params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
- end
+ context 'with a non-existent fingerprint' do
+ before do
+ params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
+ end
- it 'returns nil' do
- expect(subject).to be_nil
- end
+ it 'returns nil' do
+ expect(subject).to be_nil
end
end
end
end
+ end
- context 'user' do
- context 'without user' do
- it 'contains ssh_keys of all users in the system' do
- expect(subject).to contain_exactly(key_1, key_2, key_3)
- end
+ context 'user' do
+ context 'without user' do
+ it 'contains ssh_keys of all users in the system' do
+ expect(subject).to contain_exactly(key_1, key_2, key_3)
end
+ end
- context 'with user' do
- before do
- params[:user] = user
- end
+ context 'with user' do
+ before do
+ params[:users] = user
+ end
- it 'contains ssh_keys of only the specified users' do
- expect(subject).to contain_exactly(key_1, key_2)
- end
+ it 'contains ssh_keys of only the specified users' do
+ expect(subject).to contain_exactly(key_1, key_2)
end
end
+ end
- context 'sort order' do
- it 'sorts in last_used_at_desc order' do
- expect(subject).to eq([key_3, key_1, key_2])
- end
+ context 'sort order' do
+ it 'sorts in last_used_at_desc order' do
+ expect(subject).to eq([key_3, key_1, key_2])
end
end
end
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
new file mode 100644
index 00000000000..ff0b005f441
--- /dev/null
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -0,0 +1,109 @@
+import { mount } from '@vue/test-utils';
+import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
+import {
+ RICH_BLOB_VIEWER,
+ RICH_BLOB_VIEWER_TITLE,
+ SIMPLE_BLOB_VIEWER,
+ SIMPLE_BLOB_VIEWER_TITLE,
+} from '~/blob/components/constants';
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
+import { Blob } from './mock_data';
+
+describe('Blob Header Viewer Switcher', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ wrapper = mount(BlobHeaderViewerSwitcher, {
+ propsData: {
+ blob: Object.assign({}, Blob, props),
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('intiialization', () => {
+ it('is initialized with rich viewer as preselected when richViewer exists', () => {
+ createComponent();
+ expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
+ });
+
+ it('is initialized with simple viewer as preselected when richViewer does not exists', () => {
+ createComponent({ richViewer: null });
+ expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER);
+ });
+ });
+
+ describe('rendering', () => {
+ let btnGroup;
+ let buttons;
+
+ beforeEach(() => {
+ createComponent();
+ btnGroup = wrapper.find(GlButtonGroup);
+ buttons = wrapper.findAll(GlButton);
+ });
+
+ it('renders gl-button-group component', () => {
+ expect(btnGroup.exists()).toBe(true);
+ });
+
+ it('renders exactly 2 buttons with predefined actions', () => {
+ expect(buttons.length).toBe(2);
+ [SIMPLE_BLOB_VIEWER_TITLE, RICH_BLOB_VIEWER_TITLE].forEach((title, i) => {
+ expect(buttons.at(i).attributes('title')).toBe(title);
+ });
+ });
+ });
+
+ describe('viewer changes', () => {
+ let buttons;
+ let simpleBtn;
+ let richBtn;
+
+ beforeEach(() => {
+ createComponent();
+ buttons = wrapper.findAll(GlButton);
+ simpleBtn = buttons.at(0);
+ richBtn = buttons.at(1);
+ });
+
+ it('does not switch the viewer if the selected one is already active', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
+ richBtn.vm.$emit('click');
+ expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
+ expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ });
+
+ it('emits an event when a Simple Viewer button is clicked', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ simpleBtn.vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER);
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', SIMPLE_BLOB_VIEWER);
+ });
+ });
+
+ it('emits an event when a Rich Viewer button is clicked', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.setData({ viewer: SIMPLE_BLOB_VIEWER });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ richBtn.vm.$emit('click');
+ })
+ .then(() => {
+ expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', RICH_BLOB_VIEWER);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js b/spec/frontend/boards/components/issue_card_inner_scoped_label_spec.js
index 6ac51ebdb2d..7389cb14ecb 100644
--- a/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js
+++ b/spec/frontend/boards/components/issue_card_inner_scoped_label_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue';
describe('IssueCardInnerScopedLabel Component', () => {
diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js
index 68e26b68f04..68e26b68f04 100644
--- a/spec/javascripts/boards/components/issue_due_date_spec.js
+++ b/spec/frontend/boards/components/issue_due_date_spec.js
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 9cf73d54d9b..b632b461eb9 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -62,6 +62,7 @@ describe('ErrorTrackingList', () => {
sortByField: jest.fn(),
fetchPaginatedResults: jest.fn(),
updateStatus: jest.fn(),
+ removeIgnoredResolvedErrors: jest.fn(),
};
const state = {
@@ -221,6 +222,8 @@ describe('ErrorTrackingList', () => {
});
describe('When the ignore button on an error is clicked', () => {
+ const ignoreErrorButton = () => wrapper.find({ ref: 'ignoreError' });
+
beforeEach(() => {
store.state.list.loading = false;
store.state.list.errors = errorsList;
@@ -235,20 +238,30 @@ describe('ErrorTrackingList', () => {
});
it('sends the "ignored" status and error ID', () => {
- wrapper.find({ ref: 'ignoreError' }).trigger('click');
+ ignoreErrorButton().trigger('click');
expect(actions.updateStatus).toHaveBeenCalledWith(
expect.anything(),
{
endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
- redirectUrl: '/error_tracking',
status: 'ignored',
},
undefined,
);
});
+
+ it('calls an action to remove the item from the list', () => {
+ ignoreErrorButton().trigger('click');
+ expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
+ expect.anything(),
+ '1',
+ undefined,
+ );
+ });
});
describe('When the resolve button on an error is clicked', () => {
+ const resolveErrorButton = () => wrapper.find({ ref: 'resolveError' });
+
beforeEach(() => {
store.state.list.loading = false;
store.state.list.errors = errorsList;
@@ -263,17 +276,25 @@ describe('ErrorTrackingList', () => {
});
it('sends "resolved" status and error ID', () => {
- wrapper.find({ ref: 'resolveError' }).trigger('click');
+ resolveErrorButton().trigger('click');
expect(actions.updateStatus).toHaveBeenCalledWith(
expect.anything(),
{
endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
- redirectUrl: '/error_tracking',
status: 'resolved',
},
undefined,
);
});
+
+ it('calls an action to remove the item from the list', () => {
+ resolveErrorButton().trigger('click');
+ expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
+ expect.anything(),
+ '1',
+ undefined,
+ );
+ });
});
describe('When error tracking is disabled and user is not allowed to enable it', () => {
diff --git a/spec/frontend/error_tracking/store/list/mutation_spec.js b/spec/frontend/error_tracking/store/list/mutation_spec.js
index 44a75b6aa1f..65f11aeeda1 100644
--- a/spec/frontend/error_tracking/store/list/mutation_spec.js
+++ b/spec/frontend/error_tracking/store/list/mutation_spec.js
@@ -5,6 +5,7 @@ import * as types from '~/error_tracking/store/list/mutation_types';
const ADD_RECENT_SEARCH = mutations[types.ADD_RECENT_SEARCH];
const CLEAR_RECENT_SEARCHES = mutations[types.CLEAR_RECENT_SEARCHES];
const LOAD_RECENT_SEARCHES = mutations[types.LOAD_RECENT_SEARCHES];
+const REMOVE_IGNORED_RESOLVED_ERRORS = mutations[types.REMOVE_IGNORED_RESOLVED_ERRORS];
describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => {
@@ -114,5 +115,29 @@ describe('Error tracking mutations', () => {
expect(localStorage.getItem).toHaveBeenCalledWith('recent-searches/project/errors.json');
});
});
+
+ describe('REMOVE_IGNORED_RESOLVED_ERRORS', () => {
+ it('removes ignored or resolved errors from list', () => {
+ state.errors = [
+ {
+ id: 1,
+ status: 'unresolved',
+ },
+ {
+ id: 2,
+ status: 'ignored',
+ },
+ {
+ id: 3,
+ status: 'unresolved',
+ },
+ ];
+ const ignoredError = state.errors[2].id;
+
+ REMOVE_IGNORED_RESOLVED_ERRORS(state, ignoredError);
+
+ expect(state.errors).not.toContain(ignoredError);
+ });
+ });
});
});
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index 9d179ef2a49..1580177eaad 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -100,19 +100,4 @@ describe Banzai::Filter::GollumTagsFilter do
expect(doc.at_css('code').text).to eq '[[link-in-backticks]]'
end
end
-
- context 'table of contents' do
- it 'replaces [[<em>TOC</em>]] with ToC result' do
- doc = described_class.call("<p>[[<em>TOC</em>]]</p>", { project_wiki: project_wiki }, { toc: "FOO" })
-
- expect(doc.to_html).to eq("FOO")
- end
-
- it 'handles an empty ToC result' do
- input = "<p>[[<em>TOC</em>]]</p>"
- doc = described_class.call(input, project_wiki: project_wiki)
-
- expect(doc.to_html).to eq ''
- end
- end
end
diff --git a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
new file mode 100644
index 00000000000..20f32d7347d
--- /dev/null
+++ b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::TableOfContentsTagFilter do
+ include FilterSpecHelper
+
+ context 'table of contents' do
+ let(:html) { '<p>[[<em>TOC</em>]]</p>' }
+
+ it 'replaces [[<em>TOC</em>]] with ToC result' do
+ doc = filter(html, {}, { toc: "FOO" })
+
+ expect(doc.to_html).to eq("FOO")
+ end
+
+ it 'handles an empty ToC result' do
+ doc = filter(html)
+
+ expect(doc.to_html).to eq ''
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index f63b86d1451..4fa39da3eb4 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -99,4 +99,35 @@ describe Banzai::Pipeline::FullPipeline do
end
end
end
+
+ describe 'table of contents' do
+ let(:project) { create(:project, :public) }
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ [[_TOC_]]
+
+ # Header
+ MARKDOWN
+ end
+ let(:invalid_markdown) do
+ <<-MARKDOWN.strip_heredoc
+ test [[_TOC_]]
+
+ # Header
+ MARKDOWN
+ end
+
+ it 'inserts a table of contents' do
+ output = described_class.to_html(markdown, project: project)
+
+ expect(output).to include("<ul class=\"section-nav\">")
+ expect(output).to include("<li><a href=\"#header\">Header</a></li>")
+ end
+
+ it 'does not insert a table of contents' do
+ output = described_class.to_html(invalid_markdown, project: project)
+
+ expect(output).to include("test [[<em>TOC</em>]]")
+ end
+ end
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index cb10bebfd98..677ea071012 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -326,7 +326,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
- expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true')
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today)
end
end
@@ -346,7 +346,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
- expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true')
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true')
expect(user.reload.last_activity_on).to be_nil
end
end
@@ -594,7 +594,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
- expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true')
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true')
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index d9335cef5cc..f972fb4c5b9 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -121,10 +121,17 @@ describe Users::DestroyService do
before do
solo_owned.group_members = [member]
- service.execute(user)
+ end
+
+ it 'returns the user with attached errors' do
+ expect(service.execute(user)).to be(user)
+ expect(user.errors.full_messages).to eq([
+ 'You must transfer ownership or delete groups before you can remove user'
+ ])
end
it 'does not delete the user' do
+ service.execute(user)
expect(User.find(user.id)).to eq user
end
end
diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index e27b0de2497..c165128040f 100644
--- a/spec/support/helpers/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -15,7 +15,7 @@ module FilterSpecHelper
# context - Hash context for the filter. (default: {project: project})
#
# Returns a Nokogiri::XML::DocumentFragment
- def filter(html, context = {})
+ def filter(html, context = {}, result = nil)
if defined?(project)
context.reverse_merge!(project: project)
end
@@ -25,7 +25,7 @@ module FilterSpecHelper
context = context.merge(render_context: render_context)
- described_class.call(html, context)
+ described_class.call(html, context, result)
end
# Get an instance of the Filter class