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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-09 06:09:18 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-09 06:09:18 +0300
commit9c8d620e48c59fe3d10f9c4b50f91124d7c09182 (patch)
treec629ebcedd29c2ca756af2367218f6723ac3d58d /app
parent1c0289261b8d67e983b5d3ed1ef23fd800deab98 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue15
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue32
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue357
-rw-r--r--app/assets/javascripts/boards/constants.js6
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js18
-rw-r--r--app/graphql/mutations/boards/lists/base.rb28
-rw-r--r--app/graphql/mutations/boards/lists/base_create.rb55
-rw-r--r--app/graphql/mutations/boards/lists/create.rb51
-rw-r--r--app/models/concerns/packages/debian/architecture.rb6
-rw-r--r--app/models/concerns/packages/debian/component.rb6
-rw-r--r--app/models/concerns/packages/debian/component_file.rb101
-rw-r--r--app/models/concerns/packages/debian/distribution.rb8
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/operations/feature_flag.rb26
-rw-r--r--app/models/packages/debian/group_component_file.rb9
-rw-r--r--app/models/packages/debian/project_component_file.rb9
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/boards/lists/base_create_service.rb71
-rw-r--r--app/services/boards/lists/create_service.rb63
-rw-r--r--app/uploaders/packages/debian/component_file_uploader.rb27
21 files changed, 739 insertions, 154 deletions
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 879f62ee6ff..b5f6d03d6d3 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -5,8 +5,8 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { getParameterByName } from '~/lib/utils/common_utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import boardsStore from '~/boards/stores/boards_store';
import { fullLabelId, fullBoardId } from '../boards_util';
+import { formType } from '../constants';
import updateBoardMutation from '../graphql/board_update.mutation.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@@ -26,12 +26,6 @@ const boardDefaults = {
hide_closed_list: false,
};
-const formType = {
- new: 'new',
- delete: 'delete',
- edit: 'edit',
-};
-
export default {
i18n: {
[formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
@@ -100,11 +94,14 @@ export default {
type: Object,
required: true,
},
+ currentPage: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
- currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
@@ -256,7 +253,7 @@ export default {
}
},
cancel() {
- boardsStore.showPage('');
+ this.$emit('cancel');
},
resetFormState() {
if (this.isNewForm) {
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index fcd1c3fdceb..da76d21fe39 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -12,11 +12,12 @@ import {
import httpStatusCodes from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import projectQuery from '../graphql/project_boards.query.graphql';
import groupQuery from '../graphql/group_boards.query.graphql';
-import boardsStore from '../stores/boards_store';
+import eventHub from '../eventhub';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
@@ -35,6 +36,7 @@ export default {
directives: {
GlModalDirective,
},
+ inject: ['fullPath', 'recentBoardsEndpoint'],
props: {
currentBoard: {
type: Object,
@@ -99,12 +101,11 @@ export default {
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
- state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
- store: boardsStore,
filterTerm: '',
+ currentPage: '',
};
},
computed: {
@@ -114,16 +115,13 @@ export default {
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
- currentPage() {
- return this.state.currentPage;
- },
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
board() {
- return this.state.currentBoard;
+ return this.currentBoard;
},
showDelete() {
return this.boards.length > 1;
@@ -148,11 +146,17 @@ export default {
},
},
created() {
- boardsStore.setCurrentBoard(this.currentBoard);
+ eventHub.$on('showBoardModal', this.showPage);
+ },
+ beforeDestroy() {
+ eventHub.$off('showBoardModal', this.showPage);
},
methods: {
showPage(page) {
- boardsStore.showPage(page);
+ this.currentPage = page;
+ },
+ cancel() {
+ this.showPage('');
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
@@ -161,7 +165,7 @@ export default {
this.$apollo.addSmartQuery('boards', {
variables() {
- return { fullPath: this.state.endpoints.fullPath };
+ return { fullPath: this.fullPath };
},
query() {
return this.groupId ? groupQuery : projectQuery;
@@ -179,8 +183,10 @@ export default {
});
this.loadingRecentBoards = true;
- boardsStore
- .recentBoards()
+ // Follow up to fetch recent boards using GraphQL
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/300985
+ axios
+ .get(this.recentBoardsEndpoint)
.then((res) => {
this.recentBoards = res.data;
})
@@ -346,6 +352,8 @@ export default {
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
+ :current-page="currentPage"
+ @cancel="cancel"
/>
</span>
</div>
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
new file mode 100644
index 00000000000..3483f5c1281
--- /dev/null
+++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
@@ -0,0 +1,357 @@
+<script>
+import { throttle } from 'lodash';
+import {
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlModalDirective,
+} from '@gitlab/ui';
+
+import httpStatusCodes from '~/lib/utils/http_status';
+
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import projectQuery from '../graphql/project_boards.query.graphql';
+import groupQuery from '../graphql/group_boards.query.graphql';
+
+import boardsStore from '../stores/boards_store';
+import BoardForm from './board_form.vue';
+
+const MIN_BOARDS_TO_VIEW_RECENT = 10;
+
+export default {
+ name: 'BoardsSelector',
+ components: {
+ BoardForm,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ props: {
+ currentBoard: {
+ type: Object,
+ required: true,
+ },
+ throttleDuration: {
+ type: Number,
+ default: 200,
+ required: false,
+ },
+ boardBaseUrl: {
+ type: String,
+ required: true,
+ },
+ hasMissingBoards: {
+ type: Boolean,
+ required: true,
+ },
+ canAdminBoard: {
+ type: Boolean,
+ required: true,
+ },
+ multipleIssueBoardsAvailable: {
+ type: Boolean,
+ required: true,
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ scopedIssueBoardFeatureEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ weights: {
+ type: Array,
+ required: true,
+ },
+ enabledScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ hasScrollFade: false,
+ loadingBoards: 0,
+ loadingRecentBoards: false,
+ scrollFadeInitialized: false,
+ boards: [],
+ recentBoards: [],
+ state: boardsStore.state,
+ throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
+ contentClientHeight: 0,
+ maxPosition: 0,
+ store: boardsStore,
+ filterTerm: '',
+ };
+ },
+ computed: {
+ parentType() {
+ return this.groupId ? 'group' : 'project';
+ },
+ loading() {
+ return this.loadingRecentBoards || Boolean(this.loadingBoards);
+ },
+ currentPage() {
+ return this.state.currentPage;
+ },
+ filteredBoards() {
+ return this.boards.filter((board) =>
+ board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
+ );
+ },
+ board() {
+ return this.state.currentBoard;
+ },
+ showDelete() {
+ return this.boards.length > 1;
+ },
+ scrollFadeClass() {
+ return {
+ 'fade-out': !this.hasScrollFade,
+ };
+ },
+ showRecentSection() {
+ return (
+ this.recentBoards.length &&
+ this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
+ !this.filterTerm.length
+ );
+ },
+ },
+ watch: {
+ filteredBoards() {
+ this.scrollFadeInitialized = false;
+ this.$nextTick(this.setScrollFade);
+ },
+ },
+ created() {
+ boardsStore.setCurrentBoard(this.currentBoard);
+ },
+ methods: {
+ showPage(page) {
+ boardsStore.showPage(page);
+ },
+ cancel() {
+ this.showPage('');
+ },
+ loadBoards(toggleDropdown = true) {
+ if (toggleDropdown && this.boards.length > 0) {
+ return;
+ }
+
+ this.$apollo.addSmartQuery('boards', {
+ variables() {
+ return { fullPath: this.state.endpoints.fullPath };
+ },
+ query() {
+ return this.groupId ? groupQuery : projectQuery;
+ },
+ loadingKey: 'loadingBoards',
+ update(data) {
+ if (!data?.[this.parentType]) {
+ return [];
+ }
+ return data[this.parentType].boards.edges.map(({ node }) => ({
+ id: getIdFromGraphQLId(node.id),
+ name: node.name,
+ }));
+ },
+ });
+
+ this.loadingRecentBoards = true;
+ boardsStore
+ .recentBoards()
+ .then((res) => {
+ this.recentBoards = res.data;
+ })
+ .catch((err) => {
+ /**
+ * If user is unauthorized we'd still want to resolve the
+ * request to display all boards.
+ */
+ if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
+ this.recentBoards = []; // recent boards are empty
+ return;
+ }
+ throw err;
+ })
+ .then(() => this.$nextTick()) // Wait for boards list in DOM
+ .then(() => {
+ this.setScrollFade();
+ })
+ .catch(() => {})
+ .finally(() => {
+ this.loadingRecentBoards = false;
+ });
+ },
+ isScrolledUp() {
+ const { content } = this.$refs;
+
+ if (!content) {
+ return false;
+ }
+
+ const currentPosition = this.contentClientHeight + content.scrollTop;
+
+ return currentPosition < this.maxPosition;
+ },
+ initScrollFade() {
+ const { content } = this.$refs;
+
+ if (!content) {
+ return;
+ }
+
+ this.scrollFadeInitialized = true;
+
+ this.contentClientHeight = content.clientHeight;
+ this.maxPosition = content.scrollHeight;
+ },
+ setScrollFade() {
+ if (!this.scrollFadeInitialized) this.initScrollFade();
+
+ this.hasScrollFade = this.isScrolledUp();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="boards-switcher js-boards-selector gl-mr-3">
+ <span class="boards-selector-wrapper js-boards-selector-wrapper">
+ <gl-dropdown
+ data-qa-selector="boards_dropdown"
+ toggle-class="dropdown-menu-toggle js-dropdown-toggle"
+ menu-class="flex-column dropdown-extended-height"
+ :text="board.name"
+ @show="loadBoards"
+ >
+ <p class="gl-new-dropdown-header-top" @mousedown.prevent>
+ {{ s__('IssueBoards|Switch board') }}
+ </p>
+ <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
+
+ <div
+ v-if="!loading"
+ ref="content"
+ data-qa-selector="boards_dropdown_content"
+ class="dropdown-content flex-fill"
+ @scroll.passive="throttledSetScrollFade"
+ >
+ <gl-dropdown-item
+ v-show="filteredBoards.length === 0"
+ class="gl-pointer-events-none text-secondary"
+ >
+ {{ s__('IssueBoards|No matching boards found') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-section-header v-if="showRecentSection">
+ {{ __('Recent') }}
+ </gl-dropdown-section-header>
+
+ <template v-if="showRecentSection">
+ <gl-dropdown-item
+ v-for="recentBoard in recentBoards"
+ :key="`recent-${recentBoard.id}`"
+ class="js-dropdown-item"
+ :href="`${boardBaseUrl}/${recentBoard.id}`"
+ >
+ {{ recentBoard.name }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-divider v-if="showRecentSection" />
+
+ <gl-dropdown-section-header v-if="showRecentSection">
+ {{ __('All') }}
+ </gl-dropdown-section-header>
+
+ <gl-dropdown-item
+ v-for="otherBoard in filteredBoards"
+ :key="otherBoard.id"
+ class="js-dropdown-item"
+ :href="`${boardBaseUrl}/${otherBoard.id}`"
+ >
+ {{ otherBoard.name }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
+ {{
+ s__(
+ 'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
+ )
+ }}
+ </gl-dropdown-item>
+ </div>
+
+ <div
+ v-show="filteredBoards.length > 0"
+ class="dropdown-content-faded-mask"
+ :class="scrollFadeClass"
+ ></div>
+
+ <gl-loading-icon v-if="loading" />
+
+ <div v-if="canAdminBoard">
+ <gl-dropdown-divider />
+
+ <gl-dropdown-item
+ v-if="multipleIssueBoardsAvailable"
+ v-gl-modal-directive="'board-config-modal'"
+ data-qa-selector="create_new_board_button"
+ @click.prevent="showPage('new')"
+ >
+ {{ s__('IssueBoards|Create new board') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item
+ v-if="showDelete"
+ v-gl-modal-directive="'board-config-modal'"
+ class="text-danger js-delete-board"
+ @click.prevent="showPage('delete')"
+ >
+ {{ s__('IssueBoards|Delete board') }}
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown>
+
+ <board-form
+ v-if="currentPage"
+ :labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
+ :project-id="projectId"
+ :group-id="groupId"
+ :can-admin-board="canAdminBoard"
+ :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
+ :weights="weights"
+ :enable-scoped-labels="enabledScopedLabels"
+ :current-board="currentBoard"
+ :current-page="state.currentPage"
+ @cancel="cancel"
+ />
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 723aef4875d..f45c5d8fbcd 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -21,6 +21,12 @@ export const ListTypeTitles = {
label: __('Label'),
};
+export const formType = {
+ new: 'new',
+ delete: 'delete',
+ edit: 'edit',
+};
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 5e8dd81438b..9a86e4ae3eb 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -358,5 +358,6 @@ export default () => {
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
});
};
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 738c8fb927e..fda56546697 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,8 +1,12 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { mapGetters } from 'vuex';
+import store from '~/boards/stores';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
+import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
Vue.use(VueApollo);
@@ -16,11 +20,15 @@ export default (params = {}) => {
el: boardsSwitcherElement,
components: {
BoardsSelector,
+ BoardsSelectorDeprecated,
},
+ mixins: [glFeatureFlagMixin()],
apolloProvider,
+ store,
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
+ recentBoardsEndpoint: params.recentBoardsEndpoint,
},
data() {
const { dataset } = boardsSwitcherElement;
@@ -39,8 +47,16 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
+ computed: {
+ ...mapGetters(['shouldUseGraphQL']),
+ },
render(createElement) {
- return createElement(BoardsSelector, {
+ if (this.shouldUseGraphQL) {
+ return createElement(BoardsSelector, {
+ props: this.boardsSelectorProps,
+ });
+ }
+ return createElement(BoardsSelectorDeprecated, {
props: this.boardsSelectorProps,
});
},
diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb
deleted file mode 100644
index 34c138bddc9..00000000000
--- a/app/graphql/mutations/boards/lists/base.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Mutations
- module Boards
- module Lists
- class Base < BaseMutation
- include Mutations::ResolvesIssuable
-
- argument :board_id, ::Types::GlobalIDType[::Board],
- required: true,
- description: 'Global ID of the issue board to mutate.'
-
- field :list,
- Types::BoardListType,
- null: true,
- description: 'List of the issue board.'
-
- authorize :admin_list
-
- private
-
- def find_object(id:)
- GitlabSchema.object_from_id(id, expected_type: ::Board)
- end
- end
- end
- end
-end
diff --git a/app/graphql/mutations/boards/lists/base_create.rb b/app/graphql/mutations/boards/lists/base_create.rb
new file mode 100644
index 00000000000..a21c7feece3
--- /dev/null
+++ b/app/graphql/mutations/boards/lists/base_create.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Lists
+ class BaseCreate < BaseMutation
+ argument :backlog, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Create the backlog list.'
+
+ argument :label_id, ::Types::GlobalIDType[::Label],
+ required: false,
+ description: 'Global ID of an existing label.'
+
+ def ready?(**args)
+ if args.slice(*mutually_exclusive_args).size != 1
+ arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
+ raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
+ end
+
+ super
+ end
+
+ def resolve(**args)
+ board = authorized_find!(id: args[:board_id])
+ params = create_list_params(args)
+
+ response = create_list(board, params)
+
+ {
+ list: response.success? ? response.payload[:list] : nil,
+ errors: response.errors
+ }
+ end
+
+ private
+
+ def create_list(board, params)
+ raise NotImplementedError
+ end
+
+ def create_list_params(args)
+ params = args.slice(*mutually_exclusive_args).with_indifferent_access
+ params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
+
+ params
+ end
+
+ def mutually_exclusive_args
+ [:backlog, :label_id]
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb
index 9eb9a4d4b87..f3aae9ac9c8 100644
--- a/app/graphql/mutations/boards/lists/create.rb
+++ b/app/graphql/mutations/boards/lists/create.rb
@@ -3,59 +3,32 @@
module Mutations
module Boards
module Lists
- class Create < Base
+ class Create < BaseCreate
graphql_name 'BoardListCreate'
- argument :backlog, GraphQL::BOOLEAN_TYPE,
- required: false,
- description: 'Create the backlog list.'
+ argument :board_id, ::Types::GlobalIDType[::Board],
+ required: true,
+ description: 'Global ID of the issue board to mutate.'
- argument :label_id, ::Types::GlobalIDType[::Label],
- required: false,
- description: 'Global ID of an existing label.'
+ field :list,
+ Types::BoardListType,
+ null: true,
+ description: 'Issue list in the issue board.'
- def ready?(**args)
- if args.slice(*mutually_exclusive_args).size != 1
- arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
- raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
- end
+ authorize :admin_list
- super
- end
-
- def resolve(**args)
- board = authorized_find!(id: args[:board_id])
- params = create_list_params(args)
-
- response = create_list(board, params)
+ private
- {
- list: response.success? ? response.payload[:list] : nil,
- errors: response.errors
- }
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Board)
end
- private
-
def create_list(board, params)
create_list_service =
::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
create_list_service.execute(board)
end
-
- # Overridden in EE
- def create_list_params(args)
- params = args.slice(*mutually_exclusive_args).with_indifferent_access
- params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id
-
- params
- end
-
- # Overridden in EE
- def mutually_exclusive_args
- [:backlog, :label_id]
- end
end
end
end
diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb
index 4aa633e0357..760ebb49980 100644
--- a/app/models/concerns/packages/debian/architecture.rb
+++ b/app/models/concerns/packages/debian/architecture.rb
@@ -7,6 +7,12 @@ module Packages
included do
belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures
+ # files must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :files,
+ class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile",
+ foreign_key: :architecture_id,
+ inverse_of: :architecture,
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :distribution,
presence: true
diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb
index e37110231ce..7b342c7b684 100644
--- a/app/models/concerns/packages/debian/component.rb
+++ b/app/models/concerns/packages/debian/component.rb
@@ -7,6 +7,12 @@ module Packages
included do
belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :components
+ # files must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :files,
+ class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile",
+ foreign_key: :component_id,
+ inverse_of: :component,
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :distribution,
presence: true
diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb
new file mode 100644
index 00000000000..3cc2c291e96
--- /dev/null
+++ b/app/models/concerns/packages/debian/component_file.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ module ComponentFile
+ extend ActiveSupport::Concern
+
+ included do
+ include Sortable
+ include FileStoreMounter
+
+ def self.container_foreign_key
+ "#{container_type}_id".to_sym
+ end
+
+ def self.distribution_class
+ "::Packages::Debian::#{container_type.capitalize}Distribution".constantize
+ end
+
+ belongs_to :component, class_name: "Packages::Debian::#{container_type.capitalize}Component", inverse_of: :files
+ belongs_to :architecture, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", inverse_of: :files, optional: true
+
+ enum file_type: { packages: 1, source: 2, di_packages: 3 }
+ enum compression_type: { gz: 1, bz2: 2, xz: 3 }
+
+ validates :component, presence: true
+ validates :file_type, presence: true
+ validates :architecture, presence: true, unless: :source?
+ validates :architecture, absence: true, if: :source?
+ validates :file, length: { minimum: 0, allow_nil: false }
+ validates :size, presence: true
+ validates :file_store, presence: true
+ validates :file_md5, presence: true
+ validates :file_sha256, presence: true
+
+ scope :with_container, ->(container) do
+ joins(component: :distribution)
+ .where("packages_debian_#{container_type}_distributions" => { container_foreign_key => container.id })
+ end
+
+ scope :with_codename_or_suite, ->(codename_or_suite) do
+ joins(component: :distribution)
+ .merge(distribution_class.with_codename_or_suite(codename_or_suite))
+ end
+
+ scope :with_component_name, ->(component_name) do
+ joins(:component)
+ .where("packages_debian_#{container_type}_components" => { name: component_name })
+ end
+
+ scope :with_file_type, ->(file_type) { where(file_type: file_type) }
+
+ scope :with_architecture_name, ->(architecture_name) do
+ left_outer_joins(:architecture)
+ .where("packages_debian_#{container_type}_architectures" => { name: architecture_name })
+ end
+
+ scope :with_compression_type, ->(compression_type) { where(compression_type: compression_type) }
+ scope :with_file_sha256, ->(file_sha256) { where(file_sha256: file_sha256) }
+
+ scope :preload_distribution, -> { includes(component: :distribution) }
+
+ mount_file_store_uploader Packages::Debian::ComponentFileUploader
+
+ before_validation :update_size_from_file
+
+ def file_name
+ case file_type
+ when 'di_packages'
+ 'Packages'
+ else
+ file_type.capitalize
+ end
+ end
+
+ def relative_path
+ case file_type
+ when 'packages'
+ "#{component.name}/binary-#{architecture.name}/#{file_name}#{extension}"
+ when 'source'
+ "#{component.name}/source/#{file_name}#{extension}"
+ when 'di_packages'
+ "#{component.name}/debian-installer/binary-#{architecture.name}/#{file_name}#{extension}"
+ end
+ end
+
+ private
+
+ def extension
+ return '' unless compression_type
+
+ ".#{compression_type}"
+ end
+
+ def update_size_from_file
+ self.size ||= file.size
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 546d866d670..08fb9ccf3ea 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -18,10 +18,16 @@ module Packages
belongs_to container_type
belongs_to :creator, class_name: 'User'
+ # component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :components,
class_name: "Packages::Debian::#{container_type.capitalize}Component",
foreign_key: :distribution_id,
- inverse_of: :distribution
+ inverse_of: :distribution,
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :component_files,
+ through: :components,
+ source: :files,
+ class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile"
has_many :architectures,
class_name: "Packages::Debian::#{container_type.capitalize}Architecture",
foreign_key: :distribution_id,
diff --git a/app/models/group.rb b/app/models/group.rb
index aa79d379fac..ed8ce67015b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -75,7 +75,7 @@ class Group < Namespace
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
- # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads
+ # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
accepts_nested_attributes_for :variables, allow_destroy: true
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 442f9d36c43..be3f719ddb3 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -6,6 +6,7 @@ module Operations
include AtomicInternalId
include IidRoutes
include Limitable
+ include Referable
self.table_name = 'operations_feature_flags'
self.limit_scope = :project
@@ -65,6 +66,31 @@ module Operations
.reorder(:id)
.references(:operations_scopes)
end
+
+ def reference_prefix
+ '[feature_flag:'
+ end
+
+ def reference_pattern
+ @reference_pattern ||= %r{
+ #{Regexp.escape(reference_prefix)}(#{::Project.reference_pattern}\/)?(?<feature_flag>\d+)#{Regexp.escape(reference_postfix)}
+ }x
+ end
+
+ def link_reference_pattern
+ @link_reference_pattern ||= super("feature_flags", /(?<feature_flag>\d+)\/edit/)
+ end
+
+ def reference_postfix
+ ']'
+ end
+ end
+
+ def to_reference(from = nil, full: false)
+ project
+ .to_reference_base(from, full: full)
+ .then { |reference_base| reference_base.present? ? "#{reference_base}/" : nil }
+ .then { |reference_base| "#{self.class.reference_prefix}#{reference_base}#{iid}#{self.class.reference_postfix}" }
end
def related_issues(current_user, preload:)
diff --git a/app/models/packages/debian/group_component_file.rb b/app/models/packages/debian/group_component_file.rb
new file mode 100644
index 00000000000..333aab044a4
--- /dev/null
+++ b/app/models/packages/debian/group_component_file.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::GroupComponentFile < ApplicationRecord
+ def self.container_type
+ :group
+ end
+
+ include Packages::Debian::ComponentFile
+end
diff --git a/app/models/packages/debian/project_component_file.rb b/app/models/packages/debian/project_component_file.rb
new file mode 100644
index 00000000000..60ac29f91c2
--- /dev/null
+++ b/app/models/packages/debian/project_component_file.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::ProjectComponentFile < ApplicationRecord
+ def self.container_type
+ :project
+ end
+
+ include Packages::Debian::ComponentFile
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index ab70ed56913..d2b996b6911 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -200,7 +200,7 @@ class Project < ApplicationRecord
# Packages
has_many :packages, class_name: 'Packages::Package'
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
- # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads
+ # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
diff --git a/app/services/boards/lists/base_create_service.rb b/app/services/boards/lists/base_create_service.rb
new file mode 100644
index 00000000000..8399b1cc149
--- /dev/null
+++ b/app/services/boards/lists/base_create_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Boards
+ module Lists
+ # This class is used by issue and epic board lists
+ # for creating new list
+ class BaseCreateService < Boards::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute(board)
+ list = case type
+ when :backlog
+ create_backlog(board)
+ else
+ target = target(board)
+ position = next_position(board)
+
+ return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
+
+ create_list(board, type, target, position)
+ end
+
+ return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
+
+ ServiceResponse.success(payload: { list: list })
+ end
+
+ private
+
+ def type
+ # We don't ever expect to have more than one list
+ # type param at once.
+ if params.key?('backlog')
+ :backlog
+ else
+ :label
+ end
+ end
+
+ def target(board)
+ strong_memoize(:target) do
+ available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+
+ def available_labels
+ ::Labels::AvailableLabelsService.new(current_user, parent, {})
+ .available_labels
+ end
+
+ def next_position(board)
+ max_position = board.lists.movable.maximum(:position)
+ max_position.nil? ? 0 : max_position.succ
+ end
+
+ def create_list(board, type, target, position)
+ board.lists.create(create_list_attributes(type, target, position))
+ end
+
+ def create_list_attributes(type, target, position)
+ { type => target, list_type: type, position: position }
+ end
+
+ def create_backlog(board)
+ return board.lists.backlog.first if board.lists.backlog.exists?
+
+ board.lists.create(list_type: :backlog, position: nil)
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index a21ceee083f..37fe0a815bd 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -2,68 +2,7 @@
module Boards
module Lists
- class CreateService < Boards::BaseService
- include Gitlab::Utils::StrongMemoize
-
- def execute(board)
- list = case type
- when :backlog
- create_backlog(board)
- else
- target = target(board)
- position = next_position(board)
-
- return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
-
- create_list(board, type, target, position)
- end
-
- return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
-
- ServiceResponse.success(payload: { list: list })
- end
-
- private
-
- def type
- # We don't ever expect to have more than one list
- # type param at once.
- if params.key?('backlog')
- :backlog
- else
- :label
- end
- end
-
- def target(board)
- strong_memoize(:target) do
- available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
- end
- end
-
- def available_labels
- ::Labels::AvailableLabelsService.new(current_user, parent, {})
- .available_labels
- end
-
- def next_position(board)
- max_position = board.lists.movable.maximum(:position)
- max_position.nil? ? 0 : max_position.succ
- end
-
- def create_list(board, type, target, position)
- board.lists.create(create_list_attributes(type, target, position))
- end
-
- def create_list_attributes(type, target, position)
- { type => target, list_type: type, position: position }
- end
-
- def create_backlog(board)
- return board.lists.backlog.first if board.lists.backlog.exists?
-
- board.lists.create(list_type: :backlog, position: nil)
- end
+ class CreateService < Boards::Lists::BaseCreateService
end
end
end
diff --git a/app/uploaders/packages/debian/component_file_uploader.rb b/app/uploaders/packages/debian/component_file_uploader.rb
new file mode 100644
index 00000000000..e4d637fecac
--- /dev/null
+++ b/app/uploaders/packages/debian/component_file_uploader.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+class Packages::Debian::ComponentFileUploader < GitlabUploader
+ extend Workhorse::UploadPath
+ include ObjectStorage::Concern
+
+ storage_options Gitlab.config.packages
+
+ after :store, :schedule_background_upload
+
+ alias_method :upload, :model
+
+ def filename
+ model.file_name
+ end
+
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ raise ObjectNotReadyError, 'Package model not ready' unless model.id && model.component.distribution.container_id
+
+ Gitlab::HashedPath.new("debian_#{model.class.container_type}_component_file", model.id, root_hash: model.component.distribution.container_id)
+ end
+end