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--.haml-lint_todo.yml1
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js65
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js189
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js210
-rw-r--r--app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js28
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js145
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js28
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js72
-rw-r--r--app/assets/javascripts/diff_notes/icons/collapse_icon.svg1
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js37
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js99
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js14
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js86
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js56
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue37
-rw-r--r--app/assets/javascripts/diffs/i18n.js14
-rw-r--r--app/assets/javascripts/integrations/edit/components/confirmation_modal.vue60
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue26
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/notes.js45
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue11
-rw-r--r--app/assets/javascripts/single_file_diff.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js6
-rw-r--r--app/assets/stylesheets/pages/builds.scss11
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss42
-rw-r--r--app/controllers/invites_controller.rb21
-rw-r--r--app/mailers/emails/members.rb9
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/clusters/platforms/kubernetes.rb2
-rw-r--r--app/models/member.rb5
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb6
-rw-r--r--app/serializers/pipeline_serializer.rb10
-rw-r--r--app/services/ci/process_pipeline_service.rb4
-rw-r--r--app/views/discussions/_jump_to_next.html.haml9
-rw-r--r--app/views/discussions/_new_issue_for_all_discussions.html.haml8
-rw-r--r--app/views/discussions/_new_issue_for_discussion.html.haml10
-rw-r--r--app/views/discussions/_notes.html.haml18
-rw-r--r--app/views/layouts/_head.html.haml3
-rw-r--r--app/views/notify/autodevops_disabled_email.html.haml2
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml2
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb2
-rw-r--r--app/views/projects/diffs/_line.html.haml3
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml6
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml11
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/shared/icons/_next_discussion.svg1
-rw-r--r--changelogs/unreleased/238628-design-specs-for-confirmation-modal.yml5
-rw-r--r--changelogs/unreleased/257198-align-recognized-markdown-extensions-with-file-icon.yml5
-rw-r--r--changelogs/unreleased/debian_packages.yml5
-rw-r--r--changelogs/unreleased/id-fix-squash-messages.yml5
-rw-r--r--changelogs/unreleased/lm-improve-n-1-pl-serializer.yml5
-rw-r--r--changelogs/unreleased/maintenance-design-collapsed-files.yml5
-rw-r--r--changelogs/unreleased/revert-42465-and-42343.yml5
-rw-r--r--config/feature_flags/development/debian_packages.yml7
-rw-r--r--db/migrate/20200916165232_add_debian_max_file_size_to_plan_limits.rb9
-rw-r--r--db/schema_migrations/202009161652321
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/instance_limits.md3
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json4
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/ci/pipelines/settings.md4
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/debian_group_packages.rb21
-rw-r--r--lib/api/debian_package_endpoints.rb124
-rw-r--r--lib/api/debian_project_packages.rb56
-rw-r--r--locale/gitlab.pot30
-rw-r--r--qa/qa/page/merge_request/show.rb4
-rw-r--r--qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb4
-rw-r--r--qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb4
-rw-r--r--qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb2
-rw-r--r--qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb2
-rw-r--r--qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb2
-rw-r--r--spec/controllers/invites_controller_spec.rb39
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb4
-rw-r--r--spec/fixtures/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb1
-rw-r--r--spec/frontend/diff_comments_store_spec.js136
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js16
-rw-r--r--spec/frontend/integrations/edit/components/confirmation_modal_spec.js51
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js23
-rw-r--r--spec/frontend/integrations/edit/mock_data.js1
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js21
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/mailers/notify_spec.rb36
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/member_spec.rb33
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb39
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb46
-rw-r--r--spec/serializers/merge_request_poll_cached_widget_entity_spec.rb20
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb9
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb4
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb309
93 files changed, 1076 insertions, 1476 deletions
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index 83aba188d2b..55a8f64f5a4 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -216,7 +216,6 @@ linters:
- "app/views/projects/mattermosts/_team_selection.html.haml"
- "app/views/projects/mattermosts/new.html.haml"
- "app/views/projects/merge_requests/_commits.html.haml"
- - "app/views/projects/merge_requests/_discussion.html.haml"
- "app/views/projects/merge_requests/_how_to_merge.html.haml"
- "app/views/projects/merge_requests/_mr_title.html.haml"
- "app/views/projects/merge_requests/conflicts/_commit_stats.html.haml"
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
deleted file mode 100644
index dd60e2c7684..00000000000
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* global CommentsStore */
-
-import $ from 'jquery';
-import Vue from 'vue';
-import { __ } from '~/locale';
-
-const CommentAndResolveBtn = Vue.extend({
- props: {
- discussionId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- textareaIsEmpty: true,
- discussion: {},
- };
- },
- computed: {
- showButton() {
- if (this.discussion) {
- return this.discussion.isResolvable();
- }
- return false;
- },
- isDiscussionResolved() {
- return this.discussion.isResolved();
- },
- buttonText() {
- if (this.textareaIsEmpty) {
- return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
- }
- return this.isDiscussionResolved
- ? __('Comment & unresolve thread')
- : __('Comment & resolve thread');
- },
- },
- created() {
- if (this.discussionId) {
- this.discussion = CommentsStore.state[this.discussionId];
- }
- },
- mounted() {
- if (!this.discussionId) return;
-
- const $textarea = $(
- `.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`,
- );
- this.textareaIsEmpty = $textarea.val() === '';
-
- $textarea.on('input.comment-and-resolve-btn', () => {
- this.textareaIsEmpty = $textarea.val() === '';
- });
- },
- destroyed() {
- if (!this.discussionId) return;
-
- $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off(
- 'input.comment-and-resolve-btn',
- );
- },
-});
-
-Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
deleted file mode 100644
index b5a781cbc92..00000000000
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ /dev/null
@@ -1,189 +0,0 @@
-/* global CommentsStore */
-
-import $ from 'jquery';
-import Vue from 'vue';
-import collapseIcon from '../icons/collapse_icon.svg';
-import Notes from '../../notes';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import { n__ } from '~/locale';
-
-const DiffNoteAvatars = Vue.extend({
- components: {
- userAvatarImage,
- },
- props: {
- discussionId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isVisible: false,
- lineType: '',
- storeState: CommentsStore.state,
- shownAvatars: 3,
- collapseIcon,
- };
- },
- computed: {
- discussionClassName() {
- return `js-diff-avatars-${this.discussionId}`;
- },
- notesSubset() {
- let notes = [];
-
- if (this.discussion) {
- notes = Object.keys(this.discussion.notes)
- .slice(0, this.shownAvatars)
- .map(noteId => this.discussion.notes[noteId]);
- }
-
- return notes;
- },
- extraNotesTitle() {
- if (this.discussion) {
- const extra = this.discussion.notesCount() - this.shownAvatars;
-
- return n__('%d more comment', '%d more comments', extra);
- }
-
- return '';
- },
- discussion() {
- return this.storeState[this.discussionId];
- },
- notesCount() {
- if (this.discussion) {
- return this.discussion.notesCount();
- }
-
- return 0;
- },
- moreText() {
- const plusSign = this.notesCount < 100 ? '+' : '';
-
- return `${plusSign}${this.notesCount - this.shownAvatars}`;
- },
- },
- watch: {
- storeState: {
- handler() {
- this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('_fixTitle');
-
- // We need to add/remove a class to an element that is outside the Vue instance
- this.addNoCommentClass();
- });
- },
- deep: true,
- },
- },
- mounted() {
- this.$nextTick(() => {
- this.addNoCommentClass();
- this.setDiscussionVisible();
-
- this.lineType = $(this.$el)
- .closest('.diff-line-num')
- .hasClass('old_line')
- ? 'old'
- : 'new';
- });
-
- $(document).on('toggle.comments', () => {
- this.$nextTick(() => {
- this.setDiscussionVisible();
- });
- });
- },
- beforeDestroy() {
- this.addNoCommentClass();
- $(document).off('toggle.comments');
- },
- methods: {
- clickedAvatar(e) {
- Notes.instance.onAddDiffNote(e);
-
- // Toggle the active state of the toggle all button
- this.toggleDiscussionsToggleState();
-
- this.$nextTick(() => {
- this.setDiscussionVisible();
-
- $('.has-tooltip', this.$el).tooltip('_fixTitle');
- $('.has-tooltip', this.$el).tooltip('hide');
- });
- },
- addNoCommentClass() {
- const { notesCount } = this;
-
- $(this.$el)
- .closest('.js-avatar-container')
- .toggleClass('no-comment-btn', notesCount > 0)
- .nextUntil('.js-avatar-container')
- .toggleClass('no-comment-btn', notesCount > 0);
- },
- toggleDiscussionsToggleState() {
- const $notesHolders = $(this.$el)
- .closest('.code')
- .find('.notes_holder');
- const $visibleNotesHolders = $notesHolders.filter(':visible');
- const $toggleDiffCommentsBtn = $(this.$el)
- .closest('.diff-file')
- .find('.js-toggle-diff-comments');
-
- $toggleDiffCommentsBtn.toggleClass(
- 'active',
- $notesHolders.length === $visibleNotesHolders.length,
- );
- },
- setDiscussionVisible() {
- this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(
- ':visible',
- );
- },
- getTooltipText(note) {
- return `${note.authorName}: ${note.noteTruncated}`;
- },
- },
- template: `
- <div class="diff-comment-avatar-holders"
- :class="discussionClassName"
- v-show="notesCount !== 0">
- <div v-if="!isVisible">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image
- v-for="note in notesSubset"
- :key="note.id"
- class="diff-comment-avatar js-diff-comment-avatar"
- @click.native="clickedAvatar($event)"
- :img-src="note.authorAvatar"
- :tooltip-text="getTooltipText(note)"
- :data-line-type="lineType"
- :size="19"
- data-html="true"
- />
- <span v-if="notesCount > shownAvatars"
- class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
- data-container="body"
- data-placement="top"
- ref="extraComments"
- role="button"
- :data-line-type="lineType"
- :title="extraNotesTitle"
- @click="clickedAvatar($event)">{{ moreText }}</span>
- </div>
- <button class="diff-notes-collapse js-diff-comment-avatar"
- type="button"
- aria-label="Show comments"
- :data-line-type="lineType"
- @click="clickedAvatar($event)"
- v-if="isVisible"
- v-html="collapseIcon">
- </button>
- </div>
- `,
-});
-
-Vue.component('diff-note-avatars', DiffNoteAvatars);
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
deleted file mode 100644
index 1de00c9f08b..00000000000
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ /dev/null
@@ -1,210 +0,0 @@
-/* eslint-disable func-names, no-continue */
-/* global CommentsStore */
-
-import $ from 'jquery';
-import 'vendor/jquery.scrollTo';
-import Vue from 'vue';
-import { __ } from '~/locale';
-
-import DiscussionMixins from '../mixins/discussion';
-
-const JumpToDiscussion = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- discussionId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- discussions: CommentsStore.state,
- discussion: {},
- };
- },
- computed: {
- buttonText() {
- if (this.discussionId) {
- return __('Jump to next unresolved thread');
- }
- return __('Jump to first unresolved thread');
- },
- allResolved() {
- return this.unresolvedDiscussionCount === 0;
- },
- showButton() {
- if (this.discussionId) {
- if (this.unresolvedDiscussionCount > 1) {
- return true;
- }
- return this.discussionId !== this.lastResolvedId;
- }
- return this.unresolvedDiscussionCount >= 1;
- },
- lastResolvedId() {
- let lastId;
- Object.keys(this.discussions).forEach(discussionId => {
- const discussion = this.discussions[discussionId];
-
- if (!discussion.isResolved()) {
- lastId = discussion.id;
- }
- });
- return lastId;
- },
- },
- created() {
- this.discussion = this.discussions[this.discussionId];
- },
- methods: {
- jumpToNextUnresolvedDiscussion() {
- let discussionsSelector;
- let discussionIdsInScope;
- let firstUnresolvedDiscussionId;
- let nextUnresolvedDiscussionId;
- let activeTab = window.mrTabs.currentAction;
- let hasDiscussionsToJumpTo = true;
- let jumpToFirstDiscussion = !this.discussionId;
-
- const discussionIdsForElements = function(elements) {
- return elements
- .map(function() {
- return $(this).attr('data-discussion-id');
- })
- .toArray();
- };
-
- const { discussions } = this;
-
- if (activeTab === 'diffs') {
- discussionsSelector = '.diffs .notes[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
-
- let unresolvedDiscussionCount = 0;
-
- for (let i = 0; i < discussionIdsInScope.length; i += 1) {
- const discussionId = discussionIdsInScope[i];
- const discussion = discussions[discussionId];
- if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount += 1;
- }
- }
-
- if (this.discussionId && !this.discussion.isResolved()) {
- // If this is the last unresolved discussion on the diffs tab,
- // there are no discussions to jump to.
- if (unresolvedDiscussionCount === 1) {
- hasDiscussionsToJumpTo = false;
- }
- } else if (unresolvedDiscussionCount === 0) {
- // If there are no unresolved discussions on the diffs tab at all,
- // there are no discussions to jump to.
- hasDiscussionsToJumpTo = false;
- }
- } else if (activeTab !== 'show') {
- // If we are on the commits or builds tabs,
- // there are no discussions to jump to.
- hasDiscussionsToJumpTo = false;
- }
-
- if (!hasDiscussionsToJumpTo) {
- // If there are no discussions to jump to on the current page,
- // switch to the notes tab and jump to the first discussion there.
- window.mrTabs.activateTab('show');
- activeTab = 'show';
- jumpToFirstDiscussion = true;
- }
-
- if (activeTab === 'show') {
- discussionsSelector = '.discussion[data-discussion-id]';
- discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
- }
-
- let currentDiscussionFound = false;
- for (let i = 0; i < discussionIdsInScope.length; i += 1) {
- const discussionId = discussionIdsInScope[i];
- const discussion = discussions[discussionId];
-
- if (!discussion) {
- // Discussions for comments on commits in this MR don't have a resolved status.
- continue;
- }
-
- if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
- firstUnresolvedDiscussionId = discussionId;
-
- if (jumpToFirstDiscussion) {
- break;
- }
- }
-
- if (!jumpToFirstDiscussion) {
- if (currentDiscussionFound) {
- if (!discussion.isResolved()) {
- nextUnresolvedDiscussionId = discussionId;
- break;
- } else {
- continue;
- }
- }
-
- if (discussionId === this.discussionId) {
- currentDiscussionFound = true;
- }
- }
- }
-
- nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
-
- if (!nextUnresolvedDiscussionId) {
- return;
- }
-
- let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
-
- if (activeTab === 'show') {
- $target = $target.closest('.note-discussion');
-
- // If the next discussion is closed, toggle it open.
- if ($target.find('.js-toggle-content').is(':hidden')) {
- $target.find('.js-toggle-button i').trigger('click');
- }
- } else if (activeTab === 'diffs') {
- // Resolved discussions are hidden in the diffs tab by default.
- // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
- // When jumping between unresolved discussions on the diffs tab, we show them.
- $target.closest('.content').show();
-
- const $notesHolder = $target.closest('tr.notes_holder');
-
- // Image diff discussions does not use notes_holder
- // so we should keep original $target value in those cases
- if ($notesHolder.length > 0) {
- $target = $notesHolder;
- }
-
- $target.show();
-
- // If we are on the diffs tab, we don't scroll to the discussion itself, but to
- // 4 diff lines above it: the line the discussion was in response to + 3 context
- let prevEl;
- for (let i = 0; i < 4; i += 1) {
- prevEl = $target.prev();
-
- // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
- if (!prevEl.hasClass('line_holder')) {
- break;
- }
-
- $target = prevEl;
- }
- }
-
- $.scrollTo($target, {
- offset: -150,
- });
- },
- },
-});
-
-Vue.component('jump-to-discussion', JumpToDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
deleted file mode 100644
index e0c09aa0eee..00000000000
--- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* global CommentsStore */
-
-import Vue from 'vue';
-
-const NewIssueForDiscussion = Vue.extend({
- props: {
- discussionId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- discussions: CommentsStore.state,
- };
- },
- computed: {
- discussion() {
- return this.discussions[this.discussionId];
- },
- showButton() {
- if (this.discussion) return !this.discussion.isResolved();
- return false;
- },
- },
-});
-
-Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
deleted file mode 100644
index 0943712d0c5..00000000000
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/* global CommentsStore */
-/* global ResolveService */
-
-import $ from 'jquery';
-import Vue from 'vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
-import { sprintf, __ } from '~/locale';
-
-const ResolveBtn = Vue.extend({
- props: {
- noteId: {
- type: Number,
- required: true,
- },
- discussionId: {
- type: String,
- required: true,
- },
- resolved: {
- type: Boolean,
- required: true,
- },
- canResolve: {
- type: Boolean,
- required: true,
- },
- resolvedBy: {
- type: String,
- required: true,
- },
- authorName: {
- type: String,
- required: true,
- },
- authorAvatar: {
- type: String,
- required: true,
- },
- noteTruncated: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- discussions: CommentsStore.state,
- loading: false,
- };
- },
- computed: {
- discussion() {
- return this.discussions[this.discussionId];
- },
- note() {
- return this.discussion ? this.discussion.getNote(this.noteId) : {};
- },
- buttonText() {
- if (this.isResolved) {
- return sprintf(__('Resolved by %{resolvedByName}'), {
- resolvedByName: this.resolvedByName,
- });
- } else if (this.canResolve) {
- return __('Mark as resolved');
- }
-
- return __('Unable to resolve');
- },
- isResolved() {
- if (this.note) {
- return this.note.resolved;
- }
-
- return false;
- },
- resolvedByName() {
- return this.note.resolved_by;
- },
- },
- watch: {
- discussions: {
- handler: 'updateTooltip',
- deep: true,
- },
- },
- mounted() {
- $(this.$refs.button).tooltip({
- container: 'body',
- });
- },
- beforeDestroy() {
- CommentsStore.delete(this.discussionId, this.noteId);
- },
- created() {
- CommentsStore.create({
- discussionId: this.discussionId,
- noteId: this.noteId,
- canResolve: this.canResolve,
- resolved: this.resolved,
- resolvedBy: this.resolvedBy,
- authorName: this.authorName,
- authorAvatar: this.authorAvatar,
- noteTruncated: this.noteTruncated,
- });
- },
- methods: {
- updateTooltip() {
- this.$nextTick(() => {
- $(this.$refs.button)
- .tooltip('hide')
- .tooltip('_fixTitle');
- });
- },
- resolve() {
- if (!this.canResolve) return;
-
- let promise;
- this.loading = true;
-
- if (this.isResolved) {
- promise = ResolveService.unresolve(this.noteId);
- } else {
- promise = ResolveService.resolve(this.noteId);
- }
-
- promise
- .then(resp => resp.json())
- .then(data => {
- this.loading = false;
-
- const resolvedBy = data ? data.resolved_by : null;
-
- CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
- this.discussion.updateHeadline(data);
- gl.mrWidget.checkStatus();
- this.updateTooltip();
- })
- .catch(
- () =>
- new Flash(__('An error occurred when trying to resolve a comment. Please try again.')),
- );
- },
- },
-});
-
-Vue.component('resolve-btn', ResolveBtn);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
deleted file mode 100644
index f960853b25b..00000000000
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* global CommentsStore */
-
-import Vue from 'vue';
-
-import DiscussionMixins from '../mixins/discussion';
-
-window.ResolveCount = Vue.extend({
- mixins: [DiscussionMixins],
- props: {
- loggedOut: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- discussions: CommentsStore.state,
- };
- },
- computed: {
- allResolved() {
- return this.resolvedDiscussionCount === this.discussionCount;
- },
- resolvedCountText() {
- return this.discussionCount === 1 ? 'discussion' : 'discussions';
- },
- },
-});
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
deleted file mode 100644
index 92862d4c933..00000000000
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/* eslint-disable func-names, new-cap */
-
-import $ from 'jquery';
-import Vue from 'vue';
-import './models/discussion';
-import './models/note';
-import './stores/comments';
-import './services/resolve';
-import './mixins/discussion';
-import './components/comment_resolve_btn';
-import './components/jump_to_discussion';
-import './components/resolve_btn';
-import './components/resolve_count';
-import './components/diff_note_avatars';
-import './components/new_issue_for_discussion';
-
-export default () => {
- const projectPathHolder =
- document.querySelector('.merge-request') || document.querySelector('.commit-box');
- const { projectPath } = projectPathHolder.dataset;
- const COMPONENT_SELECTOR =
- 'resolve-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
-
- window.gl = window.gl || {};
- window.gl.diffNoteApps = {};
-
- window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
-
- gl.diffNotesCompileComponents = () => {
- $('diff-note-avatars').each(function() {
- const tmp = Vue.extend({
- template: $(this).get(0).outerHTML,
- });
- const tmpApp = new tmp().$mount();
-
- $(this).replaceWith(tmpApp.$el);
- $(tmpApp.$el).one('remove.vue', () => {
- tmpApp.$destroy();
- tmpApp.$el.remove();
- });
- });
-
- const $components = $(COMPONENT_SELECTOR).filter(function() {
- return $(this).closest('resolve-count').length !== 1;
- });
-
- if ($components) {
- $components.each(function() {
- const $this = $(this);
- const noteId = $this.attr(':note-id');
- const discussionId = $this.attr(':discussion-id');
-
- if ($this.is('comment-and-resolve-btn') && !discussionId) return;
-
- const tmp = Vue.extend({
- template: $this.get(0).outerHTML,
- });
- const tmpApp = new tmp().$mount();
-
- if (noteId) {
- gl.diffNoteApps[`note_${noteId}`] = tmpApp;
- }
-
- $this.replaceWith(tmpApp.$el);
- });
- }
- };
-
- gl.diffNotesCompileComponents();
-
- $(window).trigger('resize.nav');
-};
diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
deleted file mode 100644
index bd4b393cfaa..00000000000
--- a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js
deleted file mode 100644
index ef3001393cf..00000000000
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable guard-for-in, no-restricted-syntax, */
-
-const DiscussionMixins = {
- computed: {
- discussionCount() {
- return Object.keys(this.discussions).length;
- },
- resolvedDiscussionCount() {
- let resolvedCount = 0;
-
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
-
- if (discussion.isResolved()) {
- resolvedCount += 1;
- }
- }
-
- return resolvedCount;
- },
- unresolvedDiscussionCount() {
- let unresolvedCount = 0;
-
- for (const discussionId in this.discussions) {
- const discussion = this.discussions[discussionId];
-
- if (!discussion.isResolved()) {
- unresolvedCount += 1;
- }
- }
-
- return unresolvedCount;
- },
- },
-};
-
-export default DiscussionMixins;
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
deleted file mode 100644
index 97296a40d6e..00000000000
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* eslint-disable guard-for-in, no-restricted-syntax */
-/* global NoteModel */
-
-import $ from 'jquery';
-import Vue from 'vue';
-import { localTimeAgo } from '../../lib/utils/datetime_utility';
-
-class DiscussionModel {
- constructor(discussionId) {
- this.id = discussionId;
- this.notes = {};
- this.loading = false;
- this.canResolve = false;
- }
-
- createNote(noteObj) {
- Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
- }
-
- deleteNote(noteId) {
- Vue.delete(this.notes, noteId);
- }
-
- getNote(noteId) {
- return this.notes[noteId];
- }
-
- notesCount() {
- return Object.keys(this.notes).length;
- }
-
- isResolved() {
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (!note.resolved) {
- return false;
- }
- }
- return true;
- }
-
- resolveAllNotes(resolvedBy) {
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (!note.resolved) {
- note.resolved = true;
- note.resolved_by = resolvedBy;
- }
- }
- }
-
- unResolveAllNotes() {
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (note.resolved) {
- note.resolved = false;
- note.resolved_by = null;
- }
- }
- }
-
- updateHeadline(data) {
- const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`;
- const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`);
-
- if (data.discussion_headline_html) {
- if ($discussionHeadline.length) {
- $discussionHeadline.replaceWith(data.discussion_headline_html);
- } else {
- $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
- }
-
- localTimeAgo($('.js-timeago', `${discussionSelector}`));
- } else {
- $discussionHeadline.remove();
- }
- }
-
- isResolvable() {
- if (!this.canResolve) {
- return false;
- }
-
- for (const noteId in this.notes) {
- const note = this.notes[noteId];
-
- if (note.canResolve) {
- return true;
- }
- }
-
- return false;
- }
-}
-
-window.DiscussionModel = DiscussionModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js
deleted file mode 100644
index 825a69deeec..00000000000
--- a/app/assets/javascripts/diff_notes/models/note.js
+++ /dev/null
@@ -1,14 +0,0 @@
-class NoteModel {
- constructor(discussionId, noteObj) {
- this.discussionId = discussionId;
- this.id = noteObj.noteId;
- this.canResolve = noteObj.canResolve;
- this.resolved = noteObj.resolved;
- this.resolved_by = noteObj.resolvedBy;
- this.authorName = noteObj.authorName;
- this.authorAvatar = noteObj.authorAvatar;
- this.noteTruncated = noteObj.noteTruncated;
- }
-}
-
-window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
deleted file mode 100644
index d6975963977..00000000000
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/* global CommentsStore */
-
-import Vue from 'vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
-import { __ } from '~/locale';
-
-window.gl = window.gl || {};
-
-class ResolveServiceClass {
- constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
- this.discussionResource = Vue.resource(
- `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
- );
- }
-
- resolve(noteId) {
- return this.noteResource.save({ noteId }, {});
- }
-
- unresolve(noteId) {
- return this.noteResource.delete({ noteId }, {});
- }
-
- toggleResolveForDiscussion(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
- const isResolved = discussion.isResolved();
- let promise;
-
- if (isResolved) {
- promise = this.unResolveAll(mergeRequestId, discussionId);
- } else {
- promise = this.resolveAll(mergeRequestId, discussionId);
- }
-
- promise
- .then(resp => resp.json())
- .then(data => {
- discussion.loading = false;
- const resolvedBy = data ? data.resolved_by : null;
-
- if (isResolved) {
- discussion.unResolveAllNotes();
- } else {
- discussion.resolveAllNotes(resolvedBy);
- }
-
- if (gl.mrWidget) gl.mrWidget.checkStatus();
- discussion.updateHeadline(data);
- })
- .catch(
- () =>
- new Flash(__('An error occurred when trying to resolve a discussion. Please try again.')),
- );
- }
-
- resolveAll(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
-
- discussion.loading = true;
-
- return this.discussionResource.save(
- {
- mergeRequestId,
- discussionId,
- },
- {},
- );
- }
-
- unResolveAll(mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId];
-
- discussion.loading = true;
-
- return this.discussionResource.delete(
- {
- mergeRequestId,
- discussionId,
- },
- {},
- );
- }
-}
-
-gl.DiffNotesResolveServiceClass = ResolveServiceClass;
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
deleted file mode 100644
index 9bde18c4edf..00000000000
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/* eslint-disable no-restricted-syntax, guard-for-in */
-/* global DiscussionModel */
-
-import Vue from 'vue';
-
-window.CommentsStore = {
- state: {},
- get(discussionId, noteId) {
- return this.state[discussionId].getNote(noteId);
- },
- createDiscussion(discussionId, canResolve) {
- let discussion = this.state[discussionId];
- if (!this.state[discussionId]) {
- discussion = new DiscussionModel(discussionId);
- Vue.set(this.state, discussionId, discussion);
- }
-
- if (canResolve !== undefined) {
- discussion.canResolve = canResolve;
- }
-
- return discussion;
- },
- create(noteObj) {
- const discussion = this.createDiscussion(noteObj.discussionId);
-
- discussion.createNote(noteObj);
- },
- update(discussionId, noteId, resolved, resolvedBy) {
- const discussion = this.state[discussionId];
- const note = discussion.getNote(noteId);
- note.resolved = resolved;
- note.resolved_by = resolvedBy;
- },
- delete(discussionId, noteId) {
- const discussion = this.state[discussionId];
- discussion.deleteNote(noteId);
-
- if (discussion.notesCount() === 0) {
- Vue.delete(this.state, discussionId);
- }
- },
- unresolvedDiscussionIds() {
- const ids = [];
-
- for (const discussionId in this.state) {
- const discussion = this.state[discussionId];
-
- if (!discussion.isResolved()) {
- ids.push(discussion.id);
- }
- }
-
- return ids;
- },
-};
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 091704c49f7..02396a4ba1b 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,32 +1,26 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
-import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
-import { GENERIC_ERROR, DIFF_FILE } from '../i18n';
export default {
components: {
DiffFileHeader,
DiffContent,
- GlButton,
GlLoadingIcon,
},
directives: {
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
- i18n: {
- genericError: GENERIC_ERROR,
- ...DIFF_FILE,
- },
props: {
file: {
type: Object,
@@ -59,7 +53,7 @@ export default {
...mapGetters('diffs', ['getDiffFileDiscussions']),
viewBlobLink() {
return sprintf(
- this.$options.i18n.blobView,
+ __('You can %{linkStart}view the blob%{linkEnd} instead.'),
{
linkStart: `<a href="${escape(this.file.view_path)}">`,
linkEnd: '</a>',
@@ -81,7 +75,9 @@ export default {
},
forkMessage() {
return sprintf(
- this.$options.i18n.editInFork,
+ __(
+ "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
+ ),
{
tag_start: '<span class="js-file-fork-suggestion-section-action">',
tag_end: '</span>',
@@ -152,7 +148,7 @@ export default {
})
.catch(() => {
this.isLoadingCollapsedDiff = false;
- createFlash(this.$options.i18n.genericError);
+ createFlash(__('Something went wrong on our end. Please try again!'));
});
},
showForkMessage() {
@@ -192,14 +188,14 @@ export default {
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
- >{{ $options.i18n.fork }}</a
+ >{{ __('Fork') }}</a
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
- {{ $options.i18n.cancel }}
+ {{ __('Cancel') }}
</button>
</div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
@@ -209,16 +205,11 @@ export default {
<div v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
- <div
- v-show="isCollapsed"
- class="gl-p-7 gl-bg-gray-10 gl-text-center collapsed-file-warning"
- >
- <p class="gl-mb-8 gl-mt-5">
- {{ $options.i18n.collapsed }}
- </p>
- <gl-button class="gl-mb-5" data-testid="expandButton" @click="handleToggle">
- {{ $options.i18n.expand }}
- </gl-button>
+ <div v-show="isCollapsed" class="nothing-here-block diff-collapsed">
+ {{ __('This diff is collapsed.') }}
+ <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
+ __('Click to expand it.')
+ }}</a>
</div>
<diff-content
v-show="!isCollapsed && !isFileTooLarge"
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
deleted file mode 100644
index 8b91543587c..00000000000
--- a/app/assets/javascripts/diffs/i18n.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { __ } from '~/locale';
-
-export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
-
-export const DIFF_FILE = {
- blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'),
- editInFork: __(
- "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
- ),
- fork: __('Fork'),
- cancel: __('Cancel'),
- collapsed: __('This file is collapsed.'),
- expand: __('Expand file'),
-};
diff --git a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
new file mode 100644
index 00000000000..890381a8f29
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
@@ -0,0 +1,60 @@
+<script>
+import { mapGetters } from 'vuex';
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlModal,
+ },
+ computed: {
+ ...mapGetters(['isSavingOrTesting']),
+ primaryProps() {
+ return {
+ text: __('Save'),
+ attributes: [
+ { variant: 'success' },
+ { category: 'primary' },
+ { disabled: this.isSavingOrTesting },
+ ],
+ };
+ },
+ cancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$emit('submit');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ modal-id="confirmSaveIntegration"
+ size="sm"
+ :title="s__('Integrations|Save settings?')"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ @primary="onSubmit"
+ >
+ <p>
+ {{
+ s__(
+ 'Integrations|Saving will update the default settings for all projects that are not using custom settings.',
+ )
+ }}
+ </p>
+ <p class="gl-mb-0">
+ {{
+ s__(
+ 'Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults.',
+ )
+ }}
+ </p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 0460ed6791e..0fd39c5635d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,8 +1,9 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlModalDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
+import { integrationLevels } from '../constants';
import OverrideDropdown from './override_dropdown.vue';
import ActiveCheckbox from './active_checkbox.vue';
@@ -10,6 +11,7 @@ import JiraTriggerFields from './jira_trigger_fields.vue';
import JiraIssuesFields from './jira_issues_fields.vue';
import TriggerFields from './trigger_fields.vue';
import DynamicField from './dynamic_field.vue';
+import ConfirmationModal from './confirmation_modal.vue';
export default {
name: 'IntegrationForm',
@@ -20,8 +22,12 @@ export default {
JiraIssuesFields,
TriggerFields,
DynamicField,
+ ConfirmationModal,
GlButton,
},
+ directives: {
+ 'gl-modal': GlModalDirective,
+ },
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
@@ -32,6 +38,9 @@ export default {
isJira() {
return this.propsSource.type === 'jira';
},
+ isInstanceLevel() {
+ return this.propsSource.integrationLevel === integrationLevels.INSTANCE;
+ },
showJiraIssuesFields() {
return this.isJira && this.glFeatures.jiraIssuesIntegration;
},
@@ -82,7 +91,21 @@ export default {
v-bind="propsSource.jiraIssuesProps"
/>
<div v-if="isEditable" class="footer-block row-content-block">
+ <template v-if="isInstanceLevel">
+ <gl-button
+ v-gl-modal.confirmSaveIntegration
+ category="primary"
+ variant="success"
+ :loading="isSaving"
+ :disabled="isSavingOrTesting"
+ data-qa-selector="save_changes_button"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <confirmation-modal @submit="onSaveClick" />
+ </template>
<gl-button
+ v-else
category="primary"
variant="success"
type="submit"
@@ -93,6 +116,7 @@ export default {
>
{{ __('Save changes') }}
</gl-button>
+
<gl-button
v-if="propsSource.canTest"
:loading="isTesting"
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index b7cf39db00c..52fa0038fbb 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -396,10 +396,6 @@ export default class MergeRequestTabs {
initChangesDropdown(this.stickyTop);
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
-
localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 340fbe4d887..37bb79defd1 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -479,11 +479,6 @@ export default class Notes {
row = form;
}
- const lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
- const diffAvatarContainer = row
- .prevAll('.line_holder')
- .first()
- .find(`.js-avatar-container.${lineType}_line`);
// is this the first note of discussion?
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
@@ -519,12 +514,6 @@ export default class Notes {
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
- gl.diffNotesCompileComponents();
-
- this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
- }
-
localTimeAgo($('.js-timeago'), false);
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
@@ -538,19 +527,6 @@ export default class Notes {
.get(0);
}
- renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
- let avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
-
- if (!avatarHolder.length) {
- avatarHolder = document.createElement('diff-note-avatars');
- avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
-
- diffAvatarContainer.append(avatarHolder);
-
- gl.diffNotesCompileComponents();
- }
- }
-
/**
* Called in response the main target form has been successfully submitted.
*
@@ -605,10 +581,6 @@ export default class Notes {
form.find('#note_type').val('');
form.find('#note_project_id').remove();
form.find('#in_reply_to_discussion_id').remove();
- form
- .find('.js-comment-resolve-button')
- .closest('comment-and-resolve-btn')
- .remove();
this.parentTimeline = form.parents('.timeline');
if (form.length) {
@@ -714,10 +686,6 @@ export default class Notes {
$note_li.replaceWith($noteEntityEl);
this.setupNewNote($noteEntityEl);
-
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
}
checkContentToAllowEditing($el) {
@@ -844,12 +812,6 @@ export default class Notes {
const $notes = $note.closest('.discussion-notes');
const discussionId = $('.notes', $notes).data('discussionId');
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- if (gl.diffNoteApps[noteElId]) {
- gl.diffNoteApps[noteElId].$destroy();
- }
- }
-
$note.remove();
// check if this is the last note for this line
@@ -979,13 +941,6 @@ export default class Notes {
form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- const $commentBtn = form.find('comment-and-resolve-btn');
- $commentBtn.attr(':discussion-id', `'${discussionID}'`);
-
- gl.diffNotesCompileComponents();
- }
-
form.find('.js-note-text').focus();
form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 7a3923dfefd..a138a3a3425 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,11 +1,8 @@
<script>
-/* eslint-disable vue/no-v-html */
import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlIcon } from '@gitlab/ui';
import Translate from '../../../../../vue_shared/translate';
-// Full path is needed for Jest to be able to correctly mock this file
-import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(Translate);
@@ -20,12 +17,10 @@ export default {
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
+ imageUrl: document.getElementById('pipeline-schedules-callout').dataset.imageUrl,
calloutDismissed: parseBoolean(Cookies.get(cookieKey)),
};
},
- created() {
- this.illustrationSvg = illustrationSvg;
- },
methods: {
dismissCallout() {
this.calloutDismissed = true;
@@ -40,7 +35,9 @@ export default {
<button id="dismiss-callout-btn" class="btn btn-default close" @click="dismissCallout">
<gl-icon name="close" aria-hidden="true" />
</button>
- <div class="svg-container" v-html="illustrationSvg"></div>
+ <div class="svg-container">
+ <img :src="imageUrl" />
+ </div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 586d1e62c2f..5fa6cef7195 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -57,16 +57,10 @@ export default class SingleFileDiff {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
@@ -90,10 +84,6 @@ export default class SingleFileDiff {
}
this.collapsedContent.after(this.content);
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- gl.diffNotesCompileComponents();
- }
-
const $file = $(this.file);
FilesCommentButton.init($file);
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index b70f093e930..91a0ac3aa92 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -9,6 +9,12 @@ const fileExtensionIcons = {
'md.rendered': 'markdown',
markdown: 'markdown',
'markdown.rendered': 'markdown',
+ mdown: 'markdown',
+ 'mdown.rendered': 'markdown',
+ mkd: 'markdown',
+ 'mkd.rendered': 'markdown',
+ mkdn: 'markdown',
+ 'mkdn.rendered': 'markdown',
rst: 'markdown',
blink: 'blink',
css: 'css',
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 04167cbee1b..d7b4db3840e 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -123,20 +123,13 @@
}
.build-header {
- .ci-header-container,
- .header-action-buttons {
- display: flex;
- }
-
- .ci-header-container {
- min-height: 54px;
- }
-
.page-content-header {
padding: 10px 0 9px;
}
.header-action-buttons {
+ display: flex;
+
@include media-breakpoint-down(xs) {
.sidebar-toggle-btn {
margin-top: 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 40530acf1d2..10ab32d1e9a 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1013,31 +1013,35 @@ button.mini-pipeline-graph-dropdown-toggle {
/**
* Terminal
*/
-.terminal-icon {
- margin-left: 3px;
-}
-
-.terminal-container {
- .content-block {
- border-bottom: 0;
- }
+[data-page='projects:jobs:terminal'],
+[data-page='projects:environments:terminal'] {
+ .terminal-container {
+ .content-block {
+ border-bottom: 0;
+ }
- #terminal {
- margin-top: 10px;
- min-height: 450px;
- box-sizing: border-box;
+ #terminal {
+ margin-top: 10px;
- > div {
- min-height: 450px;
+ > div {
+ min-height: 450px;
+ }
}
}
}
-.ci-header-container {
- min-height: 55px;
-
- .text-center {
- padding-top: 12px;
+/**
+ * Pipelines / Jobs header
+ */
+[data-page='projects:pipelines:show'],
+[data-page='projects:jobs:show'] {
+ .ci-header-container {
+ min-height: $gl-spacing-scale-7;
+ display: flex;
+
+ .text-center {
+ padding-top: 12px;
+ }
}
}
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index efbb539c261..af5de684f32 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -13,13 +13,14 @@ class InvitesController < ApplicationController
respond_to :html
def show
- track_experiment('opened')
+ track_new_user_invite_experiment('opened')
accept if skip_invitation_prompt?
end
def accept
if member.accept_invite!(current_user)
- track_experiment('accepted')
+ track_new_user_invite_experiment('accepted')
+ track_invitation_reminders_experiment('accepted')
redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") %
{ member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] }
else
@@ -105,13 +106,25 @@ class InvitesController < ApplicationController
end
end
- def track_experiment(action)
+ def track_new_user_invite_experiment(action)
return unless params[:new_user_invite]
property = params[:new_user_invite] == 'experiment' ? 'experiment_group' : 'control_group'
+ track_experiment(:invite_email, action, property)
+ end
+
+ def track_invitation_reminders_experiment(action)
+ return unless Gitlab::Experimentation.enabled?(:invitation_reminders)
+
+ property = Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group'
+
+ track_experiment(:invitation_reminders, action, property)
+ end
+
+ def track_experiment(experiment_key, action, property)
Gitlab::Tracking.event(
- Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category],
+ Gitlab::Experimentation.experiment(experiment_key).tracking_category,
action,
property: property,
label: Digest::MD5.hexdigest(member.to_global_id.to_s)
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 3a13c5949bd..376b1a723c3 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -77,6 +77,15 @@ module Emails
Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'control_group')
end
end
+
+ if member.invite_to_unknown_user? && Gitlab::Experimentation.enabled?(:invitation_reminders)
+ Gitlab::Tracking.event(
+ Gitlab::Experimentation.experiment(:invitation_reminders).tracking_category,
+ 'sent',
+ property: Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group',
+ label: Digest::MD5.hexdigest(member.to_global_id.to_s)
+ )
+ end
end
def member_invite_accepted_email(member_source_type, member_id)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 7e9e24b2418..f9a55fa9157 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -42,6 +42,7 @@ module Ci
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :latest_statuses, -> { latest }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
@@ -577,11 +578,11 @@ module Ci
end
def retried
- @retried ||= (statuses.order(id: :desc) - statuses.latest)
+ @retried ||= (statuses.order(id: :desc) - latest_statuses)
end
def coverage
- coverage_array = statuses.latest.map(&:coverage).compact
+ coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 1de7c90e50d..b85a902d58b 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -102,7 +102,7 @@ module Clusters
def terminals(environment, data)
pods = filter_by_project_environment(data[:pods], environment.project.full_path_slug, environment.slug)
terminals = pods.flat_map { |pod| terminals_for_pod(api_url, environment.deployment_namespace, pod) }.compact
- terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
+ terminals.each { |terminal| add_terminal_auth(terminal, **terminal_auth) }
end
def kubeclient
diff --git a/app/models/member.rb b/app/models/member.rb
index 0f9950b6e42..7c9586aef76 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -78,7 +78,10 @@ class Member < ApplicationRecord
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
- scope :not_accepted_invitations_by_user, -> (user) { invite.where(invite_accepted_at: nil, created_by: user) }
+ scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
+ scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
+ scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
+ scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
scope :has_access, -> { active.where('access_level > 0') }
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index 002be8be729..f4d81652390 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -15,7 +15,7 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :target_project_id
expose :squash
expose :rebase_in_progress?, as: :rebase_in_progress
- expose :default_squash_commit_message, if: -> (merge_request, _) { merge_request.mergeable? }
+ expose :default_squash_commit_message
expose :commits_count
expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress
@@ -25,10 +25,10 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :source_branch_exists?, as: :source_branch_exists
expose :branch_missing?, as: :branch_missing
- expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity,
- if: -> (merge_request, _) { merge_request.mergeable? } do |merge_request|
+ expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request|
merge_request.recent_commits.without_merge_commits
end
+
expose :diff_head_sha do |merge_request|
merge_request.diff_head_sha.presence
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 45c5a1d3e1c..a45214670fa 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -47,6 +47,7 @@ class PipelineSerializer < BaseSerializer
:retryable_builds,
:scheduled_actions,
:stages,
+ :latest_statuses,
:trigger_requests,
:user,
{
@@ -62,7 +63,14 @@ class PipelineSerializer < BaseSerializer
pending_builds: :project,
project: [:route, { namespace: :route }],
triggered_by_pipeline: [{ project: [:route, { namespace: :route }] }, :user],
- triggered_pipelines: [{ project: [:route, { namespace: :route }] }, :user, :source_job]
+ triggered_pipelines: [
+ {
+ project: [:route, { namespace: :route }]
+ },
+ :source_job,
+ :latest_statuses,
+ :user
+ ]
}
]
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 18bae26613f..e511e26adfe 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -31,14 +31,14 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def update_retried
# find the latest builds for each name
- latest_statuses = pipeline.statuses.latest
+ latest_statuses = pipeline.latest_statuses
.group(:name)
.having('count(*) > 1')
.pluck(Arel.sql('MAX(id)'), 'name')
# mark builds that are retried
if latest_statuses.any?
- pipeline.statuses.latest
+ pipeline.latest_statuses
.where(name: latest_statuses.map(&:second))
.where.not(id: latest_statuses.map(&:first))
.update_all(retried: true)
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
deleted file mode 100644
index 594cdc7e2f2..00000000000
--- a/app/views/discussions/_jump_to_next.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- discussion = local_assigns.fetch(:discussion, nil)
-- if current_user
- %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
- .btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
- %button.btn.gl-button.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
- ":title" => "buttonText",
- ":aria-label" => "buttonText",
- data: { container: "body" } }
- = custom_icon("next_discussion")
diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml
deleted file mode 100644
index 7e3863b691c..00000000000
--- a/app/views/discussions/_new_issue_for_all_discussions.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
- .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
- = link_to custom_icon('icon_mr_issue'),
- new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid),
- title: 'Resolve all discussions in new issue',
- aria: { label: 'Resolve all discussions in new issue' },
- data: { container: 'body' },
- class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip'
diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml
deleted file mode 100644
index e4051ea714a..00000000000
--- a/app/views/discussions/_new_issue_for_discussion.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project)
- %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
- "inline-template" => true }
- .btn-group{ role: "group", "v-if" => "showButton" }
- = link_to custom_icon('icon_mr_issue'),
- new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id),
- title: 'Resolve this thread in a new issue',
- aria: { label: 'Resolve this thread in a new issue' },
- data: { container: 'body' },
- class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip'
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index cdc6c602277..7db318f83b1 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -21,22 +21,8 @@
- if can_create_note?
%a.user-avatar-link.d-none.d-sm-block{ href: user_path(current_user) }
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
- - if discussion.potentially_resolvable?
- - line_type = local_assigns.fetch(:line_type, nil)
-
- .discussion-with-resolve-btn
- .btn-group.discussion-with-resolve-btn{ role: "group" }
- .btn-group{ role: "group" }
- = link_to_reply_discussion(discussion, line_type)
-
- = render "discussions/resolve_all", discussion: discussion
-
- .btn-group.discussion-actions
- = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
- = render "discussions/jump_to_next", discussion: discussion
- - else
- .discussion-with-resolve-btn
- = link_to_reply_discussion(discussion)
+ .discussion-with-resolve-btn
+ = link_to_reply_discussion(discussion)
- elsif !current_user
.disabled-comment.text-center
Please
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index a6c9f4fc0f8..3e85e1f5b90 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -55,12 +55,13 @@
- unless use_startup_css?
= stylesheet_link_tag_defer "themes/#{user_application_theme_css_filename}" if user_application_theme_css_filename
= stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
- = stylesheet_link_tag_defer 'performance_bar' if performance_bar_enabled?
= stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}"
= render 'layouts/startup_css_activation'
+ = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
+
= Gon::Base.render_data(nonce: content_security_policy_nonce)
- if content_for?(:library_javascripts)
diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml
index 65a2f75a3e2..72bcfbdf3af 100644
--- a/app/views/notify/autodevops_disabled_email.html.haml
+++ b/app/views/notify/autodevops_disabled_email.html.haml
@@ -46,4 +46,4 @@
%td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" }
API
-= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
+= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.latest_statuses.failed
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
index f849c017265..c75857e96d7 100644
--- a/app/views/notify/autodevops_disabled_email.text.erb
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -7,7 +7,7 @@ The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
-<% failed = @pipeline.statuses.latest.failed -%>
+<% failed = @pipeline.latest_statuses.failed -%>
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index f01181857ce..575ec8c488e 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -108,4 +108,4 @@
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API
-= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
+= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.latest_statuses.failed
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index b388aad7048..a30e331d892 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -27,7 +27,7 @@ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
-<% failed = @pipeline.statuses.latest.failed -%>
+<% failed = @pipeline.latest_statuses.failed -%>
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index d35443cca1e..4d40071e07c 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -21,9 +21,6 @@
- else
= add_diff_note_button(line_code, diff_file.position(line), type)
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- - discussion = line_discussions.try(:first)
- - if discussion && discussion.resolvable? && !plain
- %diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if plain
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 9587ea4696b..ebe3aad064a 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -20,9 +20,6 @@
%td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- - discussion_left = discussions_left.try(:first)
- - if discussion_left && discussion_left.resolvable?
- %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.rich_text)
- else
%td.old_line.diff-line-num.empty-cell
@@ -41,9 +38,6 @@
%td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- - discussion_right = discussions_right.try(:first)
- - if discussion_right && discussion_right.resolvable?
- %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.rich_text)
- else
%td.old_line.diff-line-num.empty-cell
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
deleted file mode 100644
index ecb51aca847..00000000000
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- content_for :note_actions do
- - if can?(current_user, :update_merge_request, @merge_request)
- - if @merge_request.open?
- = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- - if @merge_request.reopenable?
- = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
- %comment-and-resolve-btn{ "inline-template" => true }
- %button.btn.btn-nr.btn-default.gl-mr-3.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
- {{ buttonText }}
-
-#notes= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 2b2b79d886b..0bcf8ae605a 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -2,7 +2,7 @@
- page_title _("Pipeline Schedules")
-#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules') } }
+#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), image_url: image_path('pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg') } }
.top-area
- schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
diff --git a/app/views/shared/icons/_next_discussion.svg b/app/views/shared/icons/_next_discussion.svg
deleted file mode 100644
index 43559a60cb0..00000000000
--- a/app/views/shared/icons/_next_discussion.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg viewBox="0 0 20 19" ><path d="M15.21 7.783h-3.317c-.268 0-.472.218-.472.486v.953c0 .28.212.486.473.486h3.318v1.575c0 .36.233.452.52.23l3.06-2.37c.274-.213.286-.582 0-.804l-3.06-2.37c-.275-.213-.52-.12-.52.23v1.583zm.57-3.66c-1.558-1.22-3.783-1.98-6.254-1.98C4.816 2.143 1 4.91 1 8.333c0 1.964 1.256 3.715 3.216 4.846-.447 1.615-1.132 2.195-1.732 2.882-.142.174-.304.32-.256.56v.01c.047.213.218.368.41.368h.046c.37-.048.743-.116 1.085-.213 1.645-.425 3.13-1.22 4.377-2.34.447.048.913.077 1.38.077 2.092 0 4.01-.546 5.492-1.454-.416-.208-.798-.475-1.134-.792-1.227.63-2.743 1.008-4.36 1.008-.41 0-.828-.03-1.237-.078l-.543-.058-.41.368c-.78.696-1.655 1.248-2.616 1.654.248-.445.486-.977.667-1.664l.257-.928-.828-.484c-1.646-.948-2.598-2.32-2.598-3.763 0-2.69 3.35-4.952 7.308-4.952 1.893 0 3.647.518 4.962 1.353.393-.266.827-.473 1.29-.61z" /></svg>
diff --git a/changelogs/unreleased/238628-design-specs-for-confirmation-modal.yml b/changelogs/unreleased/238628-design-specs-for-confirmation-modal.yml
new file mode 100644
index 00000000000..03eb155e53c
--- /dev/null
+++ b/changelogs/unreleased/238628-design-specs-for-confirmation-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Add confirmation modal on instance-level integration form
+merge_request: 42840
+author:
+type: changed
diff --git a/changelogs/unreleased/257198-align-recognized-markdown-extensions-with-file-icon.yml b/changelogs/unreleased/257198-align-recognized-markdown-extensions-with-file-icon.yml
new file mode 100644
index 00000000000..5ba79e464aa
--- /dev/null
+++ b/changelogs/unreleased/257198-align-recognized-markdown-extensions-with-file-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Add markdown icon to more file extensions
+merge_request: 43479
+author:
+type: fixed
diff --git a/changelogs/unreleased/debian_packages.yml b/changelogs/unreleased/debian_packages.yml
new file mode 100644
index 00000000000..2504018a6f9
--- /dev/null
+++ b/changelogs/unreleased/debian_packages.yml
@@ -0,0 +1,5 @@
+---
+title: Add Debian API skeleton
+merge_request: 42670
+author: Mathieu Parent
+type: added
diff --git a/changelogs/unreleased/id-fix-squash-messages.yml b/changelogs/unreleased/id-fix-squash-messages.yml
new file mode 100644
index 00000000000..6acbbbee9d7
--- /dev/null
+++ b/changelogs/unreleased/id-fix-squash-messages.yml
@@ -0,0 +1,5 @@
+---
+title: Fix suggested squashed messages for MR
+merge_request: 43508
+author:
+type: fixed
diff --git a/changelogs/unreleased/lm-improve-n-1-pl-serializer.yml b/changelogs/unreleased/lm-improve-n-1-pl-serializer.yml
new file mode 100644
index 00000000000..d919ab72ef4
--- /dev/null
+++ b/changelogs/unreleased/lm-improve-n-1-pl-serializer.yml
@@ -0,0 +1,5 @@
+---
+title: Improve n+1 in pipeline serializer for triggered pipelines
+merge_request: 42421
+author:
+type: performance
diff --git a/changelogs/unreleased/maintenance-design-collapsed-files.yml b/changelogs/unreleased/maintenance-design-collapsed-files.yml
deleted file mode 100644
index fe3c099e8ac..00000000000
--- a/changelogs/unreleased/maintenance-design-collapsed-files.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Color/position tweaks for collapsed diff files
-merge_request: 42465
-author:
-type: other
diff --git a/changelogs/unreleased/revert-42465-and-42343.yml b/changelogs/unreleased/revert-42465-and-42343.yml
new file mode 100644
index 00000000000..4c7342c9d0d
--- /dev/null
+++ b/changelogs/unreleased/revert-42465-and-42343.yml
@@ -0,0 +1,5 @@
+---
+title: 'Revert 42465 and 42343: Expanded collapsed diff files'
+merge_request: 43361
+author:
+type: other
diff --git a/config/feature_flags/development/debian_packages.yml b/config/feature_flags/development/debian_packages.yml
new file mode 100644
index 00000000000..8f706e81b6a
--- /dev/null
+++ b/config/feature_flags/development/debian_packages.yml
@@ -0,0 +1,7 @@
+---
+name: debian_packages
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42670
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/5835
+group: group::package
+type: development
+default_enabled: false
diff --git a/db/migrate/20200916165232_add_debian_max_file_size_to_plan_limits.rb b/db/migrate/20200916165232_add_debian_max_file_size_to_plan_limits.rb
new file mode 100644
index 00000000000..9a91a5d2195
--- /dev/null
+++ b/db/migrate/20200916165232_add_debian_max_file_size_to_plan_limits.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDebianMaxFileSizeToPlanLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :plan_limits, :debian_max_file_size, :bigint, default: 3.gigabytes, null: false
+ end
+end
diff --git a/db/schema_migrations/20200916165232 b/db/schema_migrations/20200916165232
new file mode 100644
index 00000000000..8e97fcf3efa
--- /dev/null
+++ b/db/schema_migrations/20200916165232
@@ -0,0 +1 @@
+18a3981a3becefe6700dd5fea87e8ba9478c0e83ddc80de1b3ee2ed77c221ce6 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 770f072bb94..b63f996487f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14365,7 +14365,8 @@ CREATE TABLE plan_limits (
nuget_max_file_size bigint DEFAULT 524288000 NOT NULL,
pypi_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL,
generic_packages_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL,
- golang_max_file_size bigint DEFAULT 104857600 NOT NULL
+ golang_max_file_size bigint DEFAULT 104857600 NOT NULL,
+ debian_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL
);
CREATE SEQUENCE plan_limits_id_seq
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index abd98002934..e0721ed3c39 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -552,6 +552,9 @@ Plan.default.actual_limits.update!(maven_max_file_size: 100.megabytes)
# For PyPI Packages
Plan.default.actual_limits.update!(pypi_max_file_size: 100.megabytes)
+
+# For Debian Packages
+Plan.default.actual_limits.update!(debian_max_file_size: 100.megabytes)
```
Set the limit to `0` to allow any file size.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index b86dee861be..2b720066911 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -13714,7 +13714,7 @@ type Project {
requestAccessEnabled: Boolean
"""
- Find a single requirement. Available only when feature flag `requirements_management` is enabled.
+ Find a single requirement
"""
requirement(
"""
@@ -13754,7 +13754,7 @@ type Project {
requirementStatesCount: RequirementStatesCount
"""
- Find requirements. Available only when feature flag `requirements_management` is enabled.
+ Find requirements
"""
requirements(
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 16532060e97..d9b5e026506 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -39901,7 +39901,7 @@
},
{
"name": "requirement",
- "description": "Find a single requirement. Available only when feature flag `requirements_management` is enabled.",
+ "description": "Find a single requirement",
"args": [
{
"name": "iid",
@@ -40004,7 +40004,7 @@
},
{
"name": "requirements",
- "description": "Find requirements. Available only when feature flag `requirements_management` is enabled.",
+ "description": "Find requirements",
"args": [
{
"name": "iid",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index dcd8b778996..98fc7c5b239 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1899,7 +1899,7 @@ Autogenerated return type of PipelineRetry.
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
| `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
-| `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. |
+| `requirement` | Requirement | Find a single requirement |
| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state |
| `sastCiConfiguration` | SastCiConfiguration | SAST CI configuration for the project |
| `securityDashboardPath` | String | Path to project's security dashboard |
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index 849eb66d07f..59bca0672d6 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -75,7 +75,7 @@ For information about setting a maximum artifact size for a project, see
> - [Support for external `.gitlab-ci.yml` locations](https://gitlab.com/gitlab-org/gitlab/-/issues/14376) introduced in GitLab 12.6.
By default we look for the `.gitlab-ci.yml` file in the project's root
-directory. If needed, you can specify an alternate path and file name, including locations outside the project.
+directory. If needed, you can specify an alternate path and filename, including locations outside the project.
To customize the path:
@@ -297,7 +297,7 @@ into your `README.md`:
### Badge styles
-Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Currently two styles are available:
+Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Two styles are available:
#### Flat (default)
diff --git a/lib/api/api.rb b/lib/api/api.rb
index df0b773d377..c77ef6616e3 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -196,6 +196,8 @@ module API
mount ::API::ComposerPackages
mount ::API::ConanProjectPackages
mount ::API::ConanInstancePackages
+ mount ::API::DebianGroupPackages
+ mount ::API::DebianProjectPackages
mount ::API::MavenPackages
mount ::API::NpmPackages
mount ::API::GenericPackages
diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb
new file mode 100644
index 00000000000..c56d84ed313
--- /dev/null
+++ b/lib/api/debian_group_packages.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module API
+ class DebianGroupPackages < Grape::API::Instance
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ not_found! unless ::Feature.enabled?(:debian_packages, user_group)
+
+ authorize_read_package!(user_group)
+ end
+
+ namespace ':id/-/packages/debian' do
+ include DebianPackageEndpoints
+ end
+ end
+ end
+end
diff --git a/lib/api/debian_package_endpoints.rb b/lib/api/debian_package_endpoints.rb
new file mode 100644
index 00000000000..79d0b6c2e26
--- /dev/null
+++ b/lib/api/debian_package_endpoints.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+module API
+ module DebianPackageEndpoints
+ extend ActiveSupport::Concern
+
+ DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze
+ COMPONENT_REGEX = %r{[a-z-]+}.freeze
+ ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze
+ LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
+ PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX
+ DISTRIBUTION_REQUIREMENTS = {
+ distribution: DISTRIBUTION_REGEX
+ }.freeze
+ COMPONENT_ARCHITECTURE_REQUIREMENTS = {
+ component: COMPONENT_REGEX,
+ architecture: ARCHITECTURE_REGEX
+ }.freeze
+ COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = {
+ component: COMPONENT_REGEX,
+ letter: LETTER_REGEX,
+ source_package: PACKAGE_REGEX
+ }.freeze
+ FILE_NAME_REQUIREMENTS = {
+ file_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ included do
+ helpers ::API::Helpers::PackagesHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+
+ format :txt
+
+ rescue_from ArgumentError do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ before do
+ require_packages_enabled!
+ end
+
+ params do
+ requires :distribution, type: String, desc: 'The Debian Codename', file_path: true
+ end
+
+ namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
+ # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/Release.gpg
+ desc 'The Release file signature' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get 'Release.gpg' do
+ not_found!
+ end
+
+ # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/Release
+ desc 'The unsigned Release file' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get 'Release' do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
+ 'TODO Release'
+ end
+
+ # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/InRelease
+ desc 'The signed Release file' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get 'InRelease' do
+ not_found!
+ end
+
+ params do
+ requires :component, type: String, desc: 'The Debian Component'
+ requires :architecture, type: String, desc: 'The Debian Architecture'
+ end
+
+ namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
+ # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
+ desc 'The binary files index' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get 'Packages' do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
+ 'TODO Packages'
+ end
+ end
+ end
+
+ params do
+ requires :component, type: String, desc: 'The Debian Component'
+ requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)'
+ requires :source_package, type: String, desc: 'The Debian Source Package Name'
+ end
+
+ namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do
+ # GET {projects|groups}/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name
+ params do
+ requires :file_name, type: String, desc: 'The Debian File Name'
+ end
+ desc 'The package' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ get ':file_name', requirements: FILE_NAME_REQUIREMENTS do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
+ 'TODO File'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb
new file mode 100644
index 00000000000..81f37ddd8da
--- /dev/null
+++ b/lib/api/debian_project_packages.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module API
+ class DebianProjectPackages < Grape::API::Instance
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ not_found! unless ::Feature.enabled?(:debian_packages, user_project)
+
+ authorize_read_package!
+ end
+
+ namespace ':id/-/packages/debian' do
+ include DebianPackageEndpoints
+
+ params do
+ requires :file_name, type: String, desc: 'The file name'
+ end
+
+ namespace 'incoming/:file_name', requirements: FILE_NAME_REQUIREMENTS do
+ # PUT {projects|groups}/:id/-/packages/debian/incoming/:file_name
+ params do
+ requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ put do
+ authorize_upload!(authorized_user_project)
+ bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:debian_max_file_size, params[:file].size)
+
+ package_event('push_package')
+
+ created!
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
+
+ forbidden!
+ end
+
+ # PUT {projects|groups}/:id/-/packages/debian/incoming/:file_name/authorize
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
+ post 'authorize' do
+ authorize_workhorse!(
+ subject: authorized_user_project,
+ has_length: false,
+ maximum_size: authorized_user_project.actual_limits.debian_max_file_size
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 97075cc11a1..f56fadd31e5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2675,12 +2675,6 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred when trying to resolve a comment. Please try again."
-msgstr ""
-
-msgid "An error occurred when trying to resolve a discussion. Please try again."
-msgstr ""
-
msgid "An error occurred when updating the issue weight"
msgstr ""
@@ -10458,9 +10452,6 @@ msgstr ""
msgid "Expand approvers"
msgstr ""
-msgid "Expand file"
-msgstr ""
-
msgid "Expand milestones"
msgstr ""
@@ -13770,6 +13761,15 @@ msgstr ""
msgid "Integrations|Includes commit title and branch"
msgstr ""
+msgid "Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults."
+msgstr ""
+
+msgid "Integrations|Save settings?"
+msgstr ""
+
+msgid "Integrations|Saving will update the default settings for all projects that are not using custom settings."
+msgstr ""
+
msgid "Integrations|Standard"
msgstr ""
@@ -14451,9 +14451,6 @@ msgstr ""
msgid "July"
msgstr ""
-msgid "Jump to first unresolved thread"
-msgstr ""
-
msgid "Jump to next unresolved thread"
msgstr ""
@@ -21760,9 +21757,6 @@ msgstr ""
msgid "Resolved by %{name}"
msgstr ""
-msgid "Resolved by %{resolvedByName}"
-msgstr ""
-
msgid "Resolves IP addresses once and uses them to submit requests"
msgstr ""
@@ -26026,9 +26020,6 @@ msgstr ""
msgid "This field is required."
msgstr ""
-msgid "This file is collapsed."
-msgstr ""
-
msgid "This group"
msgstr ""
@@ -27248,9 +27239,6 @@ msgstr ""
msgid "Unable to load the merge request widget. Try reloading the page."
msgstr ""
-msgid "Unable to resolve"
-msgstr ""
-
msgid "Unable to save iteration. Please try again"
msgstr ""
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 9d386550f4e..08796de7d73 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -164,7 +164,7 @@ module QA
def has_pipeline_status?(text)
# Pipelines can be slow, so we wait a bit longer than the usual 10 seconds
- has_element?(:merge_request_pipeline_info_content, text: text, wait: 30)
+ has_element?(:merge_request_pipeline_info_content, text: text, wait: 60)
end
def has_title?(title)
@@ -198,7 +198,7 @@ module QA
end
def merged?
- has_element?(:merged_status_content, text: 'The changes were merged into', wait: 30)
+ has_element?(:merged_status_content, text: 'The changes were merged into', wait: 60)
end
# Check if the MR is able to be merged
diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb
index 56a1c8b136a..82a06780830 100644
--- a/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb
+++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb
@@ -16,7 +16,7 @@ module QA
project.initialize_with_readme = true
end
- it 'sets labels' do
+ it 'sets labels', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1032' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.commit_message = commit_message
@@ -35,7 +35,7 @@ module QA
end
context 'when labels are set already' do
- it 'removes them' do
+ it 'removes them', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1033' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_content = "Unlabel test #{SecureRandom.hex(8)}"
diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb
index 28051135bab..dde4708874d 100644
--- a/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb
+++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb
@@ -29,7 +29,7 @@ module QA
runner.remove_via_api!
end
- it 'sets merge when pipeline succeeds' do
+ it 'sets merge when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1037' do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
@@ -72,7 +72,7 @@ module QA
expect(merge_request.merge_when_pipeline_succeeds).to be true
end
- it 'merges when pipeline succeeds' do
+ it 'merges when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1036' do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb
index 97099b88cf5..d6bd668fa8a 100644
--- a/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb
+++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb
@@ -17,7 +17,7 @@ module QA
end
end
- it 'removes the source branch' do
+ it 'removes the source branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1035' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = branch
diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb
index 3f2e5c27c51..6072fd8c1a2 100644
--- a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb
+++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb
@@ -16,7 +16,7 @@ module QA
end
end
- it 'sets a target branch' do
+ it 'sets a target branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1034' do
target_branch = "push-options-test-target-#{SecureRandom.hex(8)}"
Resource::Repository::ProjectPush.fabricate! do |push|
diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb
index f10260ad9c7..f49a8a229dc 100644
--- a/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb
+++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb
@@ -14,7 +14,7 @@ module QA
end
end
- it 'sets title and description' do
+ it 'sets title and description', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1038' do
description = "This is a test of MR push options"
title = "MR push options test #{SecureRandom.hex(8)}"
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index 73be4f97e6a..5f13781f893 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -29,6 +29,43 @@ RSpec.describe InvitesController, :snowplow do
end
end
+ shared_examples "tracks the 'accepted' event for the invitation reminders experiment" do
+ before do
+ stub_experiment(invitation_reminders: true)
+ allow(Gitlab::Experimentation).to receive(:enabled_for_attribute?).with(:invitation_reminders, member.invite_email).and_return(experimental_group)
+ end
+
+ context 'when in the control group' do
+ let(:experimental_group) { false }
+
+ it "tracks the 'accepted' event" do
+ request
+
+ expect_snowplow_event(
+ category: 'Growth::Acquisition::Experiment::InvitationReminders',
+ label: md5_member_global_id,
+ property: 'control_group',
+ action: 'accepted'
+ )
+ end
+ end
+
+ context 'when in the experimental group' do
+ let(:experimental_group) { true }
+
+ it "tracks the 'accepted' event" do
+ request
+
+ expect_snowplow_event(
+ category: 'Growth::Acquisition::Experiment::InvitationReminders',
+ label: md5_member_global_id,
+ property: 'experimental_group',
+ action: 'accepted'
+ )
+ end
+ end
+ end
+
describe 'GET #show' do
subject(:request) { get :show, params: params }
@@ -89,6 +126,7 @@ RSpec.describe InvitesController, :snowplow do
end
end
+ it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment"
it_behaves_like 'invalid token'
end
@@ -150,6 +188,7 @@ RSpec.describe InvitesController, :snowplow do
end
end
+ it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment"
it_behaves_like 'invalid token'
end
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
index a857e6b0ddb..0cdc87de761 100644
--- a/spec/features/merge_request/user_expands_diff_spec.rb
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -15,11 +15,11 @@ RSpec.describe 'User expands diff', :js do
it 'allows user to expand diff' do
page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do
- find('[data-testid="expandButton"]').click
+ click_link 'Click to expand it.'
wait_for_requests
- expect(page).not_to have_content('Expand File')
+ expect(page).not_to have_content('Click to expand it.')
expect(page).to have_selector('.code')
end
end
diff --git a/spec/fixtures/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb b/spec/fixtures/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb
new file mode 100644
index 00000000000..c6cac69265a
--- /dev/null
+++ b/spec/fixtures/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb
@@ -0,0 +1 @@
+empty
diff --git a/spec/frontend/diff_comments_store_spec.js b/spec/frontend/diff_comments_store_spec.js
deleted file mode 100644
index 6f25c9dd3bc..00000000000
--- a/spec/frontend/diff_comments_store_spec.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/* global CommentsStore */
-
-import '~/diff_notes/models/discussion';
-import '~/diff_notes/models/note';
-import '~/diff_notes/stores/comments';
-
-function createDiscussion(noteId = 1, resolved = true) {
- CommentsStore.create({
- discussionId: 'a',
- noteId,
- canResolve: true,
- resolved,
- resolvedBy: 'test',
- authorName: 'test',
- authorAvatar: 'test',
- noteTruncated: 'test...',
- });
-}
-
-beforeEach(() => {
- CommentsStore.state = {};
-});
-
-describe('New discussion', () => {
- it('creates new discussion', () => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
-
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- });
-
- it('creates new note in discussion', () => {
- createDiscussion();
- createDiscussion(2);
-
- const discussion = CommentsStore.state.a;
-
- expect(Object.keys(discussion.notes).length).toBe(2);
- });
-});
-
-describe('Get note', () => {
- beforeEach(() => {
- createDiscussion();
- });
-
- it('gets note by ID', () => {
- const note = CommentsStore.get('a', 1);
-
- expect(note).toBeDefined();
- expect(note.id).toBe(1);
- });
-});
-
-describe('Delete discussion', () => {
- beforeEach(() => {
- createDiscussion();
- });
-
- it('deletes discussion by ID', () => {
- CommentsStore.delete('a', 1);
-
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
-
- it('deletes discussion when no more notes', () => {
- createDiscussion();
- createDiscussion(2);
-
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- expect(Object.keys(CommentsStore.state.a.notes).length).toBe(2);
-
- CommentsStore.delete('a', 1);
- CommentsStore.delete('a', 2);
-
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
-});
-
-describe('Update note', () => {
- beforeEach(() => {
- createDiscussion();
- });
-
- it('updates note to be unresolved', () => {
- CommentsStore.update('a', 1, false, 'test');
-
- const note = CommentsStore.get('a', 1);
-
- expect(note.resolved).toBe(false);
- });
-});
-
-describe('Discussion resolved', () => {
- beforeEach(() => {
- createDiscussion();
- });
-
- it('is resolved with single note', () => {
- const discussion = CommentsStore.state.a;
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('is unresolved with 2 notes', () => {
- const discussion = CommentsStore.state.a;
- createDiscussion(2, false);
-
- expect(discussion.isResolved()).toBe(false);
- });
-
- it('is resolved with 2 notes', () => {
- const discussion = CommentsStore.state.a;
- createDiscussion(2);
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('resolve all notes', () => {
- const discussion = CommentsStore.state.a;
- createDiscussion(2, false);
-
- discussion.resolveAllNotes();
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('unresolve all notes', () => {
- const discussion = CommentsStore.state.a;
- createDiscussion(2);
-
- discussion.unResolveAllNotes();
-
- expect(discussion.isResolved()).toBe(false);
- });
-});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 79f0f6bc327..4f1376e2c73 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -90,8 +90,8 @@ describe('DiffFile', () => {
vm.isCollapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.innerText).toContain('This file is collapsed.');
- expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
+ expect(vm.$el.innerText).toContain('This diff is collapsed');
+ expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
done();
});
@@ -102,8 +102,8 @@ describe('DiffFile', () => {
vm.isCollapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.innerText).toContain('This file is collapsed.');
- expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
+ expect(vm.$el.innerText).toContain('This diff is collapsed');
+ expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
done();
});
@@ -121,8 +121,8 @@ describe('DiffFile', () => {
vm.isCollapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.innerText).toContain('This file is collapsed.');
- expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
+ expect(vm.$el.innerText).toContain('This diff is collapsed');
+ expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
done();
});
@@ -135,7 +135,7 @@ describe('DiffFile', () => {
vm.file.viewer.name = diffViewerModes.renamed;
vm.$nextTick(() => {
- expect(vm.$el.innerText).not.toContain('This file is collapsed.');
+ expect(vm.$el.innerText).not.toContain('This diff is collapsed');
done();
});
@@ -148,7 +148,7 @@ describe('DiffFile', () => {
vm.file.viewer.name = diffViewerModes.mode_changed;
vm.$nextTick(() => {
- expect(vm.$el.innerText).not.toContain('This file is collapsed.');
+ expect(vm.$el.innerText).not.toContain('This diff is collapsed');
done();
});
diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
new file mode 100644
index 00000000000..02f311f579f
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
@@ -0,0 +1,51 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { createStore } from '~/integrations/edit/store';
+
+import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
+
+describe('ConfirmationModal', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(ConfirmationModal, {
+ store: createStore(),
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findGlModal = () => wrapper.find(GlModal);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders GlModal with correct copy', () => {
+ expect(findGlModal().exists()).toBe(true);
+ expect(findGlModal().attributes('title')).toBe('Save settings?');
+ expect(findGlModal().text()).toContain(
+ 'Saving will update the default settings for all projects that are not using custom settings.',
+ );
+ expect(findGlModal().text()).toContain(
+ 'Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults.',
+ );
+ });
+
+ it('emits `submit` event when `primary` event is emitted on GlModal', async () => {
+ expect(wrapper.emitted().submit).toBeUndefined();
+
+ findGlModal().vm.$emit('primary');
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().submit).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index eeb5d21d62c..efcc727277a 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -4,6 +4,7 @@ import { createStore } from '~/integrations/edit/store';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
+import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@@ -22,6 +23,7 @@ describe('IntegrationForm', () => {
stubs: {
OverrideDropdown,
ActiveCheckbox,
+ ConfirmationModal,
JiraTriggerFields,
TriggerFields,
},
@@ -40,6 +42,7 @@ describe('IntegrationForm', () => {
const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
const findActiveCheckbox = () => wrapper.find(ActiveCheckbox);
+ const findConfirmationModal = () => wrapper.find(ConfirmationModal);
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
const findTriggerFields = () => wrapper.find(TriggerFields);
@@ -63,6 +66,26 @@ describe('IntegrationForm', () => {
});
});
+ describe('integrationLevel is instance', () => {
+ it('renders ConfirmationModal', () => {
+ createComponent({
+ integrationLevel: 'instance',
+ });
+
+ expect(findConfirmationModal().exists()).toBe(true);
+ });
+ });
+
+ describe('integrationLevel is not instance', () => {
+ it('does not render ConfirmationModal', () => {
+ createComponent({
+ integrationLevel: 'project',
+ });
+
+ expect(findConfirmationModal().exists()).toBe(false);
+ });
+ });
+
describe('type is "slack"', () => {
beforeEach(() => {
createComponent({ type: 'slack' });
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
index 821972b7698..27ba0768331 100644
--- a/spec/frontend/integrations/edit/mock_data.js
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -2,6 +2,7 @@ export const mockIntegrationProps = {
id: 25,
initialActivated: true,
showActive: true,
+ editable: true,
triggerFieldsProps: {
initialTriggerCommit: false,
initialTriggerMergeRequest: false,
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index 5a61f9fca69..5da998d9d2d 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -1,23 +1,18 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
-import '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
-
-jest.mock(
- '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg',
- () => '<svg></svg>',
-);
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
const docsUrl = 'help/ci/scheduled_pipelines';
+const imageUrl = 'pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
describe('Pipeline Schedule Callout', () => {
let calloutComponent;
beforeEach(() => {
setFixtures(`
- <div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
+ <div id='pipeline-schedules-callout' data-docs-url=${docsUrl} data-image-url=${imageUrl}></div>
`);
});
@@ -30,13 +25,13 @@ describe('Pipeline Schedule Callout', () => {
expect(calloutComponent).toBeDefined();
});
- it('correctly sets illustrationSvg', () => {
- expect(calloutComponent.illustrationSvg).toContain('<svg');
- });
-
it('correctly sets docsUrl', () => {
expect(calloutComponent.docsUrl).toContain(docsUrl);
});
+
+ it('correctly sets imageUrl', () => {
+ expect(calloutComponent.imageUrl).toContain(imageUrl);
+ });
});
describe(`when ${cookieKey} cookie is set`, () => {
@@ -68,8 +63,8 @@ describe('Pipeline Schedule Callout', () => {
expect(calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
});
- it('renders the callout svg', () => {
- expect(calloutComponent.$el.outerHTML).toContain('<svg');
+ it('renders the callout img', () => {
+ expect(calloutComponent.$el.outerHTML).toContain('<img');
});
it('renders the callout title', () => {
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3126d87a0d6..57d9c12bad5 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -242,6 +242,7 @@ ci_pipelines:
- latest_builds_report_results
- messages
- pipeline_artifacts
+- latest_statuses
ci_refs:
- project
- ci_pipelines
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index b9f95a9eb00..fa03c374633 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1508,12 +1508,44 @@ RSpec.describe Notify do
)
end
- describe 'group invitation' do
+ describe 'invitations' do
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) { invite_to_group(group, inviter: inviter) }
let(:inviter) { owner }
- subject { described_class.member_invited_email('group', group_member.id, group_member.invite_token) }
+ subject { described_class.member_invited_email('Group', group_member.id, group_member.invite_token) }
+
+ shared_examples "tracks the 'sent' event for the invitation reminders experiment" do
+ before do
+ stub_experiment(invitation_reminders: true)
+ allow(Gitlab::Experimentation).to receive(:enabled_for_attribute?).with(:invitation_reminders, group_member.invite_email).and_return(experimental_group)
+ end
+
+ it "tracks the 'sent' event", :snowplow do
+ subject.deliver_now
+
+ expect_snowplow_event(
+ category: 'Growth::Acquisition::Experiment::InvitationReminders',
+ label: Digest::MD5.hexdigest(group_member.to_global_id.to_s),
+ property: experimental_group ? 'experimental_group' : 'control_group',
+ action: 'sent'
+ )
+ end
+ end
+
+ describe 'tracking for the invitation reminders experiment' do
+ context 'when invite email is in the experimental group' do
+ let(:experimental_group) { true }
+
+ it_behaves_like "tracks the 'sent' event for the invitation reminders experiment"
+ end
+
+ context 'when invite email is in the control group' do
+ let(:experimental_group) { false }
+
+ it_behaves_like "tracks the 'sent' event for the invitation reminders experiment"
+ end
+ end
context 'when invite_email_experiment is disabled' do
before do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index a07ef705357..d33ccf0e6f2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2436,7 +2436,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
describe '#retry_failed' do
- let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
+ let(:latest_status) { pipeline.latest_statuses.pluck(:status) }
before do
stub_not_protect_default_branch
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index fb732b81c54..7fe3717e251 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -205,6 +205,16 @@ RSpec.describe Member do
it { expect(described_class.non_request).to include @accepted_request_member }
end
+ describe '.not_accepted_invitations' do
+ let_it_be(:not_accepted_invitation) { create(:project_member, :invited) }
+ let_it_be(:accepted_invitation) { create(:project_member, :invited, invite_accepted_at: Date.today) }
+
+ subject { described_class.not_accepted_invitations }
+
+ it { is_expected.to include(not_accepted_invitation) }
+ it { is_expected.not_to include(accepted_invitation) }
+ end
+
describe '.not_accepted_invitations_by_user' do
let(:invited_by_user) { create(:project_member, :invited, project: project, created_by: @owner_user) }
@@ -218,6 +228,29 @@ RSpec.describe Member do
it { is_expected.to contain_exactly(invited_by_user) }
end
+ describe '.not_expired' do
+ let_it_be(:expiring_yesterday) { create(:group_member, expires_at: 1.day.ago) }
+ let_it_be(:expiring_today) { create(:group_member, expires_at: Date.today) }
+ let_it_be(:expiring_tomorrow) { create(:group_member, expires_at: 1.day.from_now) }
+ let_it_be(:not_expiring) { create(:group_member) }
+
+ subject { described_class.not_expired }
+
+ it { is_expected.not_to include(expiring_yesterday, expiring_today) }
+ it { is_expected.to include(expiring_tomorrow, not_expiring) }
+ end
+
+ describe '.last_ten_days_excluding_today' do
+ let_it_be(:created_today) { create(:group_member, created_at: Date.today.beginning_of_day) }
+ let_it_be(:created_yesterday) { create(:group_member, created_at: 1.day.ago) }
+ let_it_be(:created_eleven_days_ago) { create(:group_member, created_at: 11.days.ago) }
+
+ subject { described_class.last_ten_days_excluding_today }
+
+ it { is_expected.to include(created_yesterday) }
+ it { is_expected.not_to include(created_today, created_eleven_days_ago) }
+ end
+
describe '.search_invite_email' do
it 'returns only members the matching e-mail' do
create(:group_member, :invited)
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
new file mode 100644
index 00000000000..8a05d20fb33
--- /dev/null
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::DebianGroupPackages do
+ include HttpBasicAuthHelpers
+ include WorkhorseHelpers
+
+ include_context 'Debian repository shared context', :group do
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
+
+ it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release" }
+
+ it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release'
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/InRelease" }
+
+ it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
+ end
+
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+
+ it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages'
+ end
+
+ describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
+ let(:url) { "/groups/#{group.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+
+ it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File'
+ end
+ end
+end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
new file mode 100644
index 00000000000..d2f208d0079
--- /dev/null
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::DebianProjectPackages do
+ include HttpBasicAuthHelpers
+ include WorkhorseHelpers
+
+ include_context 'Debian repository shared context', :project do
+ describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release.gpg' do
+ let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
+
+ it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
+ end
+
+ describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release' do
+ let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release" }
+
+ it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Release'
+ end
+
+ describe 'GET projects/:id/-/packages/debian/dists/*distribution/InRelease' do
+ let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/InRelease" }
+
+ it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
+ end
+
+ describe 'GET projects/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
+ let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+
+ it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Packages'
+ end
+
+ describe 'GET projects/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
+ let(:url) { "/projects/#{project.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+
+ it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO File'
+ end
+
+ describe 'PUT projects/:id/-/packages/debian/incoming/:file_name' do
+ let(:method) { :put }
+ let(:url) { "/projects/#{project.id}/-/packages/debian/incoming/#{file_name}" }
+
+ it_behaves_like 'Debian project repository PUT endpoint', :created, nil
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
index 51564de6041..929e41516f1 100644
--- a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
@@ -3,12 +3,11 @@
require 'spec_helper'
RSpec.describe MergeRequestPollCachedWidgetEntity do
- include ProjectForksHelper
using RSpec::Parameterized::TableSyntax
- let(:project) { create :project, :repository }
- let(:resource) { create(:merge_request, source_project: project, target_project: project) }
- let(:user) { create(:user) }
+ let_it_be(:project, refind: true) { create :project, :repository }
+ let_it_be(:resource, refind: true) { create(:merge_request, source_project: project, target_project: project) }
+ let_it_be(:user) { create(:user) }
let(:request) { double('request', current_user: user, project: project) }
@@ -174,8 +173,6 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
end
context 'when auto merge is not enabled' do
- let(:resource) { create(:merge_request) }
-
it 'returns auto merge related information' do
expect(subject[:auto_merge_enabled]).to be_falsy
end
@@ -215,16 +212,5 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
expect(subject[:commits_without_merge_commits].size).to eq(12)
end
end
-
- context 'when merge request is not mergeable' do
- before do
- allow(resource).to receive(:mergeable?).and_return(false)
- end
-
- it 'does not have default_squash_commit_message and commits_without_merge_commits' do
- expect(subject[:default_squash_commit_message]).to eq(nil)
- expect(subject[:commits_without_merge_commits]).to eq(nil)
- end
- end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index b42a4f6ad3f..e0f6ab68034 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -155,7 +155,7 @@ RSpec.describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expected_queries = Gitlab.ee? ? 43 : 40
+ expected_queries = Gitlab.ee? ? 39 : 36
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
@@ -176,7 +176,7 @@ RSpec.describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-foss/issues/46368
- expected_queries = Gitlab.ee? ? 49 : 46
+ expected_queries = Gitlab.ee? ? 42 : 39
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
@@ -199,11 +199,10 @@ RSpec.describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- # 99 queries by default + 2 related to preloading
- # :source_pipeline and :source_job
# Existing numbers are high and require performance optimization
+ # Ongoing issue:
# https://gitlab.com/gitlab-org/gitlab/-/issues/225156
- expected_queries = Gitlab.ee? ? 95 : 86
+ expected_queries = Gitlab.ee? ? 85 : 76
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
index 77645298bc7..2936d6fae4d 100644
--- a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
+++ b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
@@ -43,12 +43,12 @@ RSpec.shared_context 'Pipeline Processing Service Tests With Yaml' do
{
pipeline: pipeline.status,
stages: pipeline.stages.pluck(:name, :status).to_h,
- jobs: pipeline.statuses.latest.pluck(:name, :status).to_h
+ jobs: pipeline.latest_statuses.pluck(:name, :status).to_h
}
end
def event_on_jobs(event, job_names)
- statuses = pipeline.statuses.latest.by_name(job_names).to_a
+ statuses = pipeline.latest_statuses.by_name(job_names).to_a
expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts
statuses.each { |status| status.public_send("#{event}!") }
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
new file mode 100644
index 00000000000..03d7ca71f1f
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -0,0 +1,309 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'Debian repository shared context' do |object_type|
+ before do
+ stub_feature_flags(debian_packages: true)
+ end
+
+ if object_type == :project
+ let(:project) { create(:project, :public) }
+ elsif object_type == :group
+ let(:group) { create(:group, :public) }
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ let(:distribution) { 'bullseye' }
+ let(:component) { 'main' }
+ let(:architecture) { 'amd64' }
+ let(:source_package) { 'sample' }
+ let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] }
+ let(:package_name) { 'libsample0' }
+ let(:package_version) { '1.2.3~alpha2-1' }
+ let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" }
+
+ let(:method) { :get }
+
+ let(:workhorse_params) do
+ if method == :put
+ file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}")
+ { file: file_upload }
+ else
+ {}
+ end
+ end
+
+ let(:params) { workhorse_params }
+
+ let(:auth_headers) { {} }
+ let(:workhorse_headers) do
+ if method == :put
+ workhorse_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
+ { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
+ else
+ {}
+ end
+ end
+
+ let(:headers) { auth_headers.merge(workhorse_headers) }
+
+ let(:send_rewritten_field) { true }
+
+ subject do
+ if method == :put
+ workhorse_finalize(
+ api(url),
+ method: method,
+ file_key: :file,
+ params: params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+ else
+ send method, api(url), headers: headers, params: params
+ end
+ end
+end
+
+RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token|
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+
+ let(:auth_headers) do
+ if user_role == :anonymous
+ {}
+ elsif auth_method == :token
+ { 'Private-Token' => token }
+ else
+ basic_auth_header(user.username, token)
+ end
+ end
+end
+
+RSpec.shared_context 'Debian repository project access' do |project_visibility_level, user_role, user_token, auth_method|
+ include_context 'Debian repository auth headers', user_role, user_token, auth_method do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian project repository GET request' do |user_role, add_member, status, body|
+ context "for user type #{user_role}" do
+ before do
+ project.send("add_#{user_role}", user) if add_member && user_role != :anonymous
+ end
+
+ and_body = body.nil? ? '' : ' and expected body'
+
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add_member, status, body|
+ context "for user type #{user_role}" do
+ before do
+ project.send("add_#{user_role}", user) if add_member && user_role != :anonymous
+ end
+
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :created
+ it 'creates package files' do
+ pending "Debian package creation not implemented"
+ expect { subject }
+ .to change { project.packages.debian.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
+ else
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects Debian access with unknown project id' do
+ context 'with an unknown project' do
+ let(:project) { double(id: non_existing_record_id) }
+
+ context 'as anonymous' do
+ it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
+ end
+
+ context 'as authenticated user' do
+ subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian project repository GET endpoint' do |success_status, success_body|
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ 'PUBLIC' | :developer | true | true | success_status | success_body
+ 'PUBLIC' | :guest | true | true | success_status | success_body
+ 'PUBLIC' | :developer | true | false | success_status | success_body
+ 'PUBLIC' | :guest | true | false | success_status | success_body
+ 'PUBLIC' | :developer | false | true | success_status | success_body
+ 'PUBLIC' | :guest | false | true | success_status | success_body
+ 'PUBLIC' | :developer | false | false | success_status | success_body
+ 'PUBLIC' | :guest | false | false | success_status | success_body
+ 'PUBLIC' | :anonymous | false | true | success_status | success_body
+ 'PRIVATE' | :developer | true | true | success_status | success_body
+ 'PRIVATE' | :guest | true | true | :forbidden | nil
+ 'PRIVATE' | :developer | true | false | :not_found | nil
+ 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | false | true | :not_found | nil
+ 'PRIVATE' | :guest | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :not_found | nil
+ 'PRIVATE' | :guest | false | false | :not_found | nil
+ 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ end
+
+ with_them do
+ include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Debian project repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown project id'
+end
+
+RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body|
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ 'PUBLIC' | :developer | true | true | success_status | nil
+ 'PUBLIC' | :guest | true | true | :forbidden | nil
+ 'PUBLIC' | :developer | true | false | :unauthorized | nil
+ 'PUBLIC' | :guest | true | false | :unauthorized | nil
+ 'PUBLIC' | :developer | false | true | :forbidden | nil
+ 'PUBLIC' | :guest | false | true | :forbidden | nil
+ 'PUBLIC' | :developer | false | false | :unauthorized | nil
+ 'PUBLIC' | :guest | false | false | :unauthorized | nil
+ 'PUBLIC' | :anonymous | false | true | :unauthorized | nil
+ 'PRIVATE' | :developer | true | true | success_status | nil
+ 'PRIVATE' | :guest | true | true | :forbidden | nil
+ 'PRIVATE' | :developer | true | false | :not_found | nil
+ 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | false | true | :not_found | nil
+ 'PRIVATE' | :guest | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :not_found | nil
+ 'PRIVATE' | :guest | false | false | :not_found | nil
+ 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ end
+
+ with_them do
+ include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Debian project repository PUT request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown project id'
+end
+
+RSpec.shared_context 'Debian repository group access' do |group_visibility_level, user_role, user_token, auth_method|
+ include_context 'Debian repository auth headers', user_role, user_token, auth_method do
+ before do
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility_level, false))
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian group repository GET request' do |user_role, add_member, status, body|
+ context "for user type #{user_role}" do
+ before do
+ group.send("add_#{user_role}", user) if add_member && user_role != :anonymous
+ end
+
+ and_body = body.nil? ? '' : ' and expected body'
+
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects Debian access with unknown group id' do
+ context 'with an unknown group' do
+ let(:group) { double(id: non_existing_record_id) }
+
+ context 'as anonymous' do
+ it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
+ end
+
+ context 'as authenticated user' do
+ subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status, success_body|
+ context 'with valid group' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:group_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ 'PUBLIC' | :developer | true | true | success_status | success_body
+ 'PUBLIC' | :guest | true | true | success_status | success_body
+ 'PUBLIC' | :developer | true | false | success_status | success_body
+ 'PUBLIC' | :guest | true | false | success_status | success_body
+ 'PUBLIC' | :developer | false | true | success_status | success_body
+ 'PUBLIC' | :guest | false | true | success_status | success_body
+ 'PUBLIC' | :developer | false | false | success_status | success_body
+ 'PUBLIC' | :guest | false | false | success_status | success_body
+ 'PUBLIC' | :anonymous | false | true | success_status | success_body
+ 'PRIVATE' | :developer | true | true | success_status | success_body
+ 'PRIVATE' | :guest | true | true | :forbidden | nil
+ 'PRIVATE' | :developer | true | false | :not_found | nil
+ 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | false | true | :not_found | nil
+ 'PRIVATE' | :guest | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :not_found | nil
+ 'PRIVATE' | :guest | false | false | :not_found | nil
+ 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ end
+
+ with_them do
+ include_context 'Debian repository group access', params[:group_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Debian group repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown group id'
+end