Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a075011d5b
commit
13e210a5d6
|
|
@ -978,14 +978,18 @@ Gitlab/BoundedContexts:
|
|||
- 'app/models/board_project_recent_visit.rb'
|
||||
- 'app/models/bulk_import.rb'
|
||||
- 'app/models/bulk_imports/batch_tracker.rb'
|
||||
- 'app/models/bulk_imports/configuration.rb'
|
||||
- 'app/models/bulk_imports/entity.rb'
|
||||
- 'app/models/bulk_imports/export.rb'
|
||||
- 'app/models/bulk_imports/export_batch.rb'
|
||||
- 'app/models/bulk_imports/export_status.rb'
|
||||
- 'app/models/bulk_imports/export_upload.rb'
|
||||
- 'app/models/bulk_imports/failure.rb'
|
||||
- 'app/models/bulk_imports/file_transfer.rb'
|
||||
- 'app/models/bulk_imports/file_transfer/base_config.rb'
|
||||
- 'app/models/bulk_imports/file_transfer/group_config.rb'
|
||||
- 'app/models/bulk_imports/file_transfer/project_config.rb'
|
||||
- 'app/models/bulk_imports/tracker.rb'
|
||||
- 'app/models/chat_name.rb'
|
||||
- 'app/models/chat_team.rb'
|
||||
- 'app/models/ci_platform_metric.rb'
|
||||
|
|
|
|||
|
|
@ -1861,7 +1861,6 @@ Style/InlineDisableAnnotation:
|
|||
- 'ee/spec/controllers/concerns/gitlab_subscriptions/seat_count_alert_spec.rb'
|
||||
- 'ee/spec/controllers/concerns/routable_actions_spec.rb'
|
||||
- 'ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb'
|
||||
- 'ee/spec/elastic/migrate/20230503064300_backfill_project_permissions_in_blobs_using_permutations_spec.rb'
|
||||
- 'ee/spec/factories/package_metadata/pm_licenses.rb'
|
||||
- 'ee/spec/factories/security_scans.rb'
|
||||
- 'ee/spec/features/dashboards/todos_spec.rb'
|
||||
|
|
|
|||
|
|
@ -39,12 +39,9 @@ export default {
|
|||
<filtered-search-bar />
|
||||
|
||||
<gl-empty-state v-if="abuseReports.length == 0" :title="s__('AbuseReports|No reports found')" />
|
||||
<abuse-report-row
|
||||
v-for="(report, index) in abuseReports"
|
||||
v-else
|
||||
:key="index"
|
||||
:report="report"
|
||||
/>
|
||||
<ul v-else class="gl-pl-0">
|
||||
<abuse-report-row v-for="(report, index) in abuseReports" :key="index" :report="report" />
|
||||
</ul>
|
||||
|
||||
<gl-pagination
|
||||
v-if="showPagination"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
|
|||
import { formType } from '~/boards/constants';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isMetaKey } from '~/lib/utils/common_utils';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import { updateHistory, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { s__, __ } from '~/locale';
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ export default {
|
|||
async switchBoardKeyEvent(boardId, e) {
|
||||
if (isMetaKey(e)) {
|
||||
e.stopPropagation();
|
||||
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
|
||||
visitUrl(`${this.boardBaseUrl}/${boardId}`, true);
|
||||
}
|
||||
},
|
||||
switchBoardGroup(value) {
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ export default {
|
|||
pinnedFileStatus: '',
|
||||
codequalityData: {},
|
||||
sastData: {},
|
||||
keydownTime: undefined,
|
||||
listenersAttached: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -234,6 +236,7 @@ export default {
|
|||
'showWhitespace',
|
||||
'targetBranchName',
|
||||
'branchName',
|
||||
'showTreeList',
|
||||
]),
|
||||
...mapGetters('diffs', [
|
||||
'whichCollapsedTypes',
|
||||
|
|
@ -303,6 +306,9 @@ export default {
|
|||
resourceId() {
|
||||
return convertToGraphQLId('MergeRequest', this.getNoteableData.id);
|
||||
},
|
||||
renderFileTree() {
|
||||
return this.renderDiffFiles && this.showTreeList;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
commit(newCommit, oldCommit) {
|
||||
|
|
@ -337,6 +343,7 @@ export default {
|
|||
this.adjustView();
|
||||
this.subscribeToVirtualScrollingEvents();
|
||||
},
|
||||
renderFileTree: 'adjustView',
|
||||
isLoading: 'adjustView',
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -432,6 +439,7 @@ export default {
|
|||
'setFileByFile',
|
||||
'disableVirtualScroller',
|
||||
'fetchPinnedFile',
|
||||
'toggleTreeList',
|
||||
]),
|
||||
...mapActions('findingsDrawer', ['setDrawer']),
|
||||
closeDrawer() {
|
||||
|
|
@ -607,6 +615,8 @@ export default {
|
|||
}
|
||||
},
|
||||
setEventListeners() {
|
||||
if (this.listenersAttached) return;
|
||||
|
||||
Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));
|
||||
Mousetrap.bind(keysFor(MR_NEXT_FILE_IN_DIFF), () => this.jumpToFile(+1));
|
||||
|
||||
|
|
@ -619,32 +629,36 @@ export default {
|
|||
);
|
||||
}
|
||||
|
||||
let keydownTime;
|
||||
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
||||
keydownTime = new Date().getTime();
|
||||
this.keydownTime = new Date().getTime();
|
||||
});
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
if (keydownTime) {
|
||||
const delta = new Date().getTime() - keydownTime;
|
||||
window.addEventListener('blur', this.handleBrowserFindActivation);
|
||||
|
||||
// To make sure the user is using the find function we need to wait for blur
|
||||
// and max 1000ms to be sure it the search box is filtered
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
this.disableVirtualScroller();
|
||||
|
||||
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
|
||||
api.trackRedisCounterEvent('diff_searches');
|
||||
}
|
||||
}
|
||||
});
|
||||
this.listenersAttached = true;
|
||||
},
|
||||
removeEventListeners() {
|
||||
Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF));
|
||||
Mousetrap.unbind(keysFor(MR_NEXT_FILE_IN_DIFF));
|
||||
Mousetrap.unbind(keysFor(MR_COMMITS_NEXT_COMMIT));
|
||||
Mousetrap.unbind(keysFor(MR_COMMITS_PREVIOUS_COMMIT));
|
||||
Mousetrap.unbind(['ctrl+f', 'command+f']);
|
||||
Mousetrap.unbind(['ctrl+f', 'command+f', 'mod+f', 'mod+g']);
|
||||
window.removeEventListener('blur', this.handleBrowserFindActivation);
|
||||
this.listenersAttached = false;
|
||||
},
|
||||
handleBrowserFindActivation() {
|
||||
if (!this.keydownTime) return;
|
||||
|
||||
const delta = new Date().getTime() - this.keydownTime;
|
||||
|
||||
// To make sure the user is using the find function we need to wait for blur
|
||||
// and max 1000ms to be sure it the search box is filtered
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
this.disableVirtualScroller();
|
||||
|
||||
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
|
||||
api.trackRedisCounterEvent('diff_searches');
|
||||
}
|
||||
},
|
||||
jumpToFile(step) {
|
||||
const targetIndex = this.currentDiffIndex + step;
|
||||
|
|
@ -709,6 +723,10 @@ export default {
|
|||
this.trackEvent(types[event.name]);
|
||||
}
|
||||
},
|
||||
fileTreeToggled() {
|
||||
this.toggleTreeList();
|
||||
this.adjustView();
|
||||
},
|
||||
},
|
||||
howToMergeDocsPath: helpPagePath('user/project/merge_requests/merge_request_troubleshooting.md', {
|
||||
anchor: 'check-out-merge-requests-locally-through-the-head-ref',
|
||||
|
|
@ -738,7 +756,7 @@ export default {
|
|||
:data-can-create-note="getNoteableData.current_user.can_create_note"
|
||||
class="files d-flex gl-mt-2"
|
||||
>
|
||||
<diffs-file-tree :render-diff-files="renderDiffFiles" @toggled="adjustView" />
|
||||
<diffs-file-tree :visible="renderFileTree" @toggled="fileTreeToggled" />
|
||||
<div class="col-12 col-md-auto diff-files-holder">
|
||||
<commit-widget v-if="commit" :commit="commit" :collapsible="false" />
|
||||
<gl-alert
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { Mousetrap } from '~/lib/mousetrap';
|
||||
import { keysFor, MR_TOGGLE_FILE_BROWSER } from '~/behaviors/shortcuts/keybindings';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
|
|
@ -18,7 +16,7 @@ export default {
|
|||
minTreeWidth: MIN_TREE_WIDTH,
|
||||
maxTreeWidth: window.innerWidth / 2,
|
||||
props: {
|
||||
renderDiffFiles: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -32,33 +30,29 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['showTreeList']),
|
||||
renderFileTree() {
|
||||
return this.renderDiffFiles && this.showTreeList;
|
||||
},
|
||||
hideFileStats() {
|
||||
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
renderFileTree() {
|
||||
this.$emit('toggled');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
Mousetrap.bind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggleTreeList);
|
||||
Mousetrap.bind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggle);
|
||||
},
|
||||
beforeDestroy() {
|
||||
Mousetrap.unbind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggleTreeList);
|
||||
Mousetrap.unbind(keysFor(MR_TOGGLE_FILE_BROWSER), this.toggle);
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['cacheTreeListWidth', 'toggleTreeList']),
|
||||
toggle() {
|
||||
this.$emit('toggled');
|
||||
},
|
||||
cacheTreeListWidth(size) {
|
||||
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="renderFileTree" :style="{ width: `${treeWidth}px` }" class="diff-tree-list gl-px-5">
|
||||
<div v-if="visible" :style="{ width: `${treeWidth}px` }" class="diff-tree-list gl-px-5">
|
||||
<panel-resizer
|
||||
:size.sync="treeWidth"
|
||||
:start-size="treeWidth"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
DIFF_VIEW_COOKIE_NAME,
|
||||
MR_TREE_SHOW_KEY,
|
||||
TREE_LIST_STORAGE_KEY,
|
||||
TREE_LIST_WIDTH_STORAGE_KEY,
|
||||
OLD_LINE_KEY,
|
||||
NEW_LINE_KEY,
|
||||
TYPE_KEY,
|
||||
|
|
@ -759,10 +758,6 @@ export const toggleFileFinder = ({ commit }, visible) => {
|
|||
commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
|
||||
};
|
||||
|
||||
export const cacheTreeListWidth = (_, size) => {
|
||||
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
|
||||
};
|
||||
|
||||
export const receiveFullDiffError = ({ commit }, filePath) => {
|
||||
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
|
||||
createAlert({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import VueApollo from 'vue-apollo';
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
import { createAlert } from '~/alert';
|
||||
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
|
||||
import { parseUrlPathname } from '~/lib/utils/url_utility';
|
||||
import { parseUrlPathname, visitUrl } from '~/lib/utils/url_utility';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
||||
|
|
@ -282,7 +282,7 @@ export default class MergeRequestTabs {
|
|||
|
||||
if (isMetaClick(e)) {
|
||||
const targetLink = e.currentTarget.getAttribute('href');
|
||||
window.open(targetLink, '_blank');
|
||||
visitUrl(targetLink, true);
|
||||
} else if (action) {
|
||||
const href = e.currentTarget.getAttribute('href');
|
||||
this.tabShown(action, href);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import Raphael from './raphael';
|
||||
|
||||
|
|
@ -238,7 +239,7 @@ export default class BranchGraph {
|
|||
opacity: 0,
|
||||
cursor: 'pointer',
|
||||
})
|
||||
.click(() => window.open(options.commit_url.replace('%s', commit.id), '_blank'))
|
||||
.click(() => visitUrl(options.commit_url.replace('%s', commit.id), true))
|
||||
.hover(
|
||||
function () {
|
||||
this.tooltip = r.commitTooltip(x + 5, y, commit);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,16 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
maxDateRange: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
dateOptions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => TIME_RANGE_OPTIONS,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -28,7 +38,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
dateRangeOptions() {
|
||||
return TIME_RANGE_OPTIONS.map((option) => {
|
||||
return this.dateOptions.map((option) => {
|
||||
const dateRange = periodToDate(option.value);
|
||||
return {
|
||||
value: option.value,
|
||||
|
|
@ -95,6 +105,7 @@ export default {
|
|||
:default-start-date="dateRange.startDate"
|
||||
:default-end-date="dateRange.endDate"
|
||||
:default-max-date="defaultMaxDate"
|
||||
:max-date-range="maxDateRange"
|
||||
@input="onCustomRangeSelected"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<ul class="gl-pl-0">
|
||||
<image-list-row
|
||||
v-for="(listItem, index) in images"
|
||||
:key="index"
|
||||
|
|
@ -35,5 +35,5 @@ export default {
|
|||
:expiration-policy="expirationPolicy"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ export default {
|
|||
<manifests-empty-state v-else-if="manifests.length === 0" />
|
||||
|
||||
<div v-else data-testid="main-area">
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<ul class="gl-pl-0">
|
||||
<manifest-row
|
||||
v-for="(manifest, index) in manifests"
|
||||
:key="index"
|
||||
:dependency-proxy-image-prefix="dependencyProxyImagePrefix"
|
||||
:manifest="manifest"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
<div class="gl-display-flex gl-justify-content-center">
|
||||
<gl-keyset-pagination
|
||||
v-bind="pagination"
|
||||
|
|
|
|||
|
|
@ -188,17 +188,19 @@ export default {
|
|||
</template>
|
||||
|
||||
<template v-else-if="hasVersions">
|
||||
<package-list-row
|
||||
v-for="v in packageEntity.versions"
|
||||
:key="v.id"
|
||||
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
|
||||
name: packageEntity.name,
|
||||
...v,
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
:package-link="v.id.toString()"
|
||||
:disable-delete="true"
|
||||
:show-package-type="false"
|
||||
/>
|
||||
<ul class="gl-pl-0">
|
||||
<package-list-row
|
||||
v-for="v in packageEntity.versions"
|
||||
:key="v.id"
|
||||
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
|
||||
name: packageEntity.name,
|
||||
...v,
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
:package-link="v.id.toString()"
|
||||
:disable-delete="true"
|
||||
:show-package-type="false"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<p v-else class="gl-mt-3" data-testid="no-versions-message">
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default {
|
|||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div data-testid="packages-table">
|
||||
<ul data-testid="packages-table" class="gl-pl-0">
|
||||
<packages-list-row
|
||||
v-for="packageEntity in list"
|
||||
:key="packageEntity.id"
|
||||
|
|
@ -84,7 +84,7 @@ export default {
|
|||
:is-group="isGroupPage"
|
||||
@packageToDelete="setItemToBeDeleted"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<gl-pagination
|
||||
v-model="currentPage"
|
||||
|
|
|
|||
|
|
@ -117,14 +117,14 @@ export default {
|
|||
</gl-button>
|
||||
</div>
|
||||
|
||||
<div v-for="(item, index) in items" :key="index">
|
||||
<ul v-for="(item, index) in items" :key="index" class="gl-pl-0">
|
||||
<slot
|
||||
:select-item="selectItem"
|
||||
:is-selected="isSelected"
|
||||
:item="item"
|
||||
:first="!hiddenDelete && index === 0"
|
||||
></slot>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div class="gl-display-flex gl-justify-content-center">
|
||||
<gl-keyset-pagination
|
||||
|
|
|
|||
|
|
@ -77,3 +77,5 @@ export const REQUIRED_ICON_CLASS = 'gl-fill-green-500';
|
|||
export const NOT_REQUIRED_ICON_CLASS = 'gl-text-red-500';
|
||||
|
||||
export const DELETE_RULE_MODAL_ID = 'delete-branch-rule-modal';
|
||||
|
||||
export const projectUsersOptions = { push_code: true, active: true };
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ import { helpPagePath } from '~/helpers/help_page_helper';
|
|||
import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
|
||||
import { createAlert } from '~/alert';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import editBranchRuleMutation from 'ee_else_ce/projects/settings/branch_rules/mutations/edit_branch_rule.mutation.graphql';
|
||||
import deleteBranchRuleMutation from '../../mutations/branch_rule_delete.mutation.graphql';
|
||||
import editBranchRuleMutation from '../../mutations/edit_branch_rule.mutation.graphql';
|
||||
import { getAccessLevels } from '../../../utils';
|
||||
import BranchRuleModal from '../../../components/branch_rule_modal.vue';
|
||||
import Protection from './protection.vue';
|
||||
import RuleDrawer from './rule_drawer.vue';
|
||||
import {
|
||||
I18N,
|
||||
ALL_BRANCHES_WILDCARD,
|
||||
|
|
@ -61,6 +62,7 @@ export default {
|
|||
GlModal,
|
||||
GlButton,
|
||||
BranchRuleModal,
|
||||
RuleDrawer,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: {
|
||||
|
|
@ -115,6 +117,8 @@ export default {
|
|||
statusChecks: [],
|
||||
branchRule: {},
|
||||
matchingBranchesCount: null,
|
||||
isAllowedToMergeDrawerOpen: false,
|
||||
isRuleUpdating: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -214,18 +218,39 @@ export default {
|
|||
});
|
||||
});
|
||||
},
|
||||
editBranchRule({ name }) {
|
||||
openAllowedToMergeDrawer() {
|
||||
this.isAllowedToMergeDrawerOpen = true;
|
||||
},
|
||||
closeAllowedToMergeDrawer() {
|
||||
this.isAllowedToMergeDrawerOpen = false;
|
||||
},
|
||||
editBranchRule({ name = this.branchRule.name, branchProtection = null, toastMessage = '' }) {
|
||||
this.isRuleUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: editBranchRuleMutation,
|
||||
variables: {
|
||||
id: this.branchRule.id,
|
||||
name,
|
||||
input: {
|
||||
id: this.branchRule.id,
|
||||
name,
|
||||
...(branchProtection && { branchProtection }),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(visitUrl(setUrlParams({ branch: name })))
|
||||
.then(() => {
|
||||
const isRedirectNeeded = !branchProtection;
|
||||
if (isRedirectNeeded) {
|
||||
visitUrl(setUrlParams({ branch: name }));
|
||||
} else {
|
||||
this.closeAllowedToMergeDrawer();
|
||||
this.$toast.show(toastMessage);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
createAlert({ message: this.$options.i18n.updateBranchRuleError });
|
||||
})
|
||||
.finally(() => {
|
||||
this.isRuleUpdating = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -260,7 +285,7 @@ export default {
|
|||
<gl-button
|
||||
v-if="glFeatures.editBranchRules && !isPredefinedRule"
|
||||
v-gl-modal="$options.editModalId"
|
||||
data-testid="edit-rule-button"
|
||||
data-testid="edit-rule-name-button"
|
||||
size="small"
|
||||
>{{ $options.i18n.edit }}</gl-button
|
||||
>
|
||||
|
|
@ -283,7 +308,6 @@ export default {
|
|||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
|
||||
<!-- Allowed to push -->
|
||||
<protection
|
||||
class="gl-mt-3"
|
||||
|
|
@ -309,6 +333,23 @@ export default {
|
|||
:empty-state-copy="$options.i18n.allowedToMergeEmptyState"
|
||||
is-edit-available
|
||||
data-testid="allowed-to-merge-content"
|
||||
@edit="openAllowedToMergeDrawer"
|
||||
/>
|
||||
|
||||
<rule-drawer
|
||||
:is-open="isAllowedToMergeDrawerOpen"
|
||||
:roles="mergeAccessLevels.roles"
|
||||
:users="mergeAccessLevels.users"
|
||||
:groups="mergeAccessLevels.groups"
|
||||
:is-loading="isRuleUpdating"
|
||||
:title="s__('BranchRules|Edit allowed to merge')"
|
||||
@editRule="
|
||||
editBranchRule({
|
||||
branchProtection: { mergeAccessLevels: $event },
|
||||
toastMessage: s__('BranchRules|Allowed to merge updated'),
|
||||
})
|
||||
"
|
||||
@close="closeAllowedToMergeDrawer"
|
||||
/>
|
||||
|
||||
<!-- Force push -->
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export default {
|
|||
<gl-button
|
||||
v-if="glFeatures.editBranchRules && isEditAvailable"
|
||||
size="small"
|
||||
data-testid="edit-button"
|
||||
data-testid="edit-rule-button"
|
||||
@click="$emit('edit')"
|
||||
>{{ __('Edit') }}</gl-button
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
<script>
|
||||
import { GlDrawer, GlButton, GlFormGroup } from '@gitlab/ui';
|
||||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { projectUsersOptions } from './constants';
|
||||
|
||||
export default {
|
||||
DRAWER_Z_INDEX,
|
||||
projectUsersOptions,
|
||||
components: {
|
||||
GlDrawer,
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
ItemsSelector: () =>
|
||||
import('ee_component/projects/settings/branch_rules/components/view/items_selector.vue'),
|
||||
},
|
||||
props: {
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
users: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updatedGroups: [],
|
||||
updatedUsers: [],
|
||||
isRuleUpdated: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
getDrawerHeaderHeight() {
|
||||
return getContentWrapperHeight();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleRuleDataUpdate(namespace, items) {
|
||||
this.isRuleUpdated = true;
|
||||
this[namespace] = items;
|
||||
},
|
||||
formatItemsData(items, keyName, type) {
|
||||
return items.map((item) => ({ [keyName]: convertToGraphQLId(type, item.id) }));
|
||||
},
|
||||
getRuleEditData() {
|
||||
return [
|
||||
...this.formatItemsData(this.updatedUsers, 'userId', 'User'), // eslint-disable-line @gitlab/require-i18n-strings
|
||||
];
|
||||
},
|
||||
formatItemsIds(items) {
|
||||
return items.map((item) => ({ ...item, id: getIdFromGraphQLId(item.id) }));
|
||||
},
|
||||
editRule() {
|
||||
this.$emit('editRule', this.getRuleEditData());
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-drawer
|
||||
:header-height="getDrawerHeaderHeight"
|
||||
:z-index="$options.DRAWER_Z_INDEX"
|
||||
:open="isOpen"
|
||||
@ok="editRule()"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template #title>
|
||||
<h2 class="gl-font-size-h2 gl-mt-0">{{ title }}</h2>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
:disabled="!isRuleUpdated"
|
||||
:loading="isLoading"
|
||||
data-testid="save-allowed-to-merge"
|
||||
@click="editRule()"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button variant="confirm" category="secondary" @click="$emit('close')">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
<template #default>
|
||||
<gl-form-group class="gl-border-none">
|
||||
<items-selector
|
||||
type="users"
|
||||
:items="formatItemsIds(users)"
|
||||
is-project-only-namespace
|
||||
:users-options="$options.projectUsersOptions"
|
||||
@change="handleRuleDataUpdate('updatedUsers', $event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
</gl-drawer>
|
||||
</template>
|
||||
|
|
@ -1,9 +1,24 @@
|
|||
mutation editBrachRule($id: ProjectsBranchRuleID!, $name: String!) {
|
||||
branchRuleUpdate(input: { id: $id, name: $name }) {
|
||||
mutation editBrachRule($input: BranchRuleUpdateInput!) {
|
||||
branchRuleUpdate(input: $input) {
|
||||
errors
|
||||
branchRule {
|
||||
id
|
||||
name
|
||||
branchProtection {
|
||||
allowForcePush
|
||||
pushAccessLevels {
|
||||
nodes {
|
||||
accessLevel
|
||||
accessLevelDescription
|
||||
}
|
||||
}
|
||||
mergeAccessLevels {
|
||||
nodes {
|
||||
accessLevel
|
||||
accessLevelDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export default {
|
|||
components: {
|
||||
GlFormTextarea,
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
|
@ -19,11 +23,6 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messageText: this.value,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -40,13 +39,13 @@ export default {
|
|||
</div>
|
||||
<gl-form-textarea
|
||||
:id="inputId"
|
||||
v-model="messageText"
|
||||
:value="value"
|
||||
class="form-control js-gfm-input gl-mb-3 commit-message-edit !gl-font-monospace"
|
||||
dir="auto"
|
||||
required="required"
|
||||
rows="7"
|
||||
max-rows="32"
|
||||
@input="$emit('input', messageText)"
|
||||
@input="(val) => $emit('input', val)"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ export default {
|
|||
<ul class="border-top commits-list flex-list gl-list-none gl-p-0 gl-pt-4">
|
||||
<commit-edit
|
||||
v-if="shouldShowSquashEdit"
|
||||
:value="squashCommitMessage"
|
||||
v-model="squashCommitMessage"
|
||||
:label="__('Squash commit message')"
|
||||
input-id="squash-message-edit"
|
||||
class="gl-m-0! gl-p-0!"
|
||||
|
|
@ -617,7 +617,7 @@ export default {
|
|||
</commit-edit>
|
||||
<commit-edit
|
||||
v-if="shouldShowMergeEdit"
|
||||
:value="commitMessage"
|
||||
v-model="commitMessage"
|
||||
:label="__('Merge commit message')"
|
||||
input-id="merge-message-edit"
|
||||
class="gl-m-0! gl-p-0!"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
usersQueryOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
isProjectOnlyNamespace: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -59,6 +69,9 @@ export default {
|
|||
config() {
|
||||
return CONFIG[this.type];
|
||||
},
|
||||
showNamespaceDropdown() {
|
||||
return this.config.showNamespaceDropdown && !this.isProjectOnlyNamespace;
|
||||
},
|
||||
namespaceDropdownText() {
|
||||
return parseBoolean(this.isProjectNamespace)
|
||||
? this.$options.i18n.projectGroups
|
||||
|
|
@ -105,7 +118,7 @@ export default {
|
|||
}
|
||||
},
|
||||
async fetchUsersBySearchTerm(search) {
|
||||
const users = await Api.projectUsers(this.projectPath, search);
|
||||
const users = await Api.projectUsers(this.projectPath, search, this.usersQueryOptions);
|
||||
|
||||
return users?.map((user) => ({
|
||||
text: user.name,
|
||||
|
|
@ -228,10 +241,11 @@ export default {
|
|||
</gl-collapsible-listbox>
|
||||
|
||||
<gl-collapsible-listbox
|
||||
v-if="config.showNamespaceDropdown"
|
||||
v-if="showNamespaceDropdown"
|
||||
v-model="isProjectNamespace"
|
||||
:toggle-text="namespaceDropdownText"
|
||||
:items="$options.namespaceOptions"
|
||||
data-testid="namespace-dropdown"
|
||||
@select="handleSelectNamespace"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
<script>
|
||||
import { GlButton, GlForm, GlFormGroup, GlFormInput, GlModal } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { isSafeURL } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlModal,
|
||||
},
|
||||
props: {
|
||||
edit: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
imageFiles: {
|
||||
type: [Array, FileList],
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
imageId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
urlText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalUrl: this.url,
|
||||
modalUrlText: this.urlText,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUploadingImage']),
|
||||
title() {
|
||||
return this.edit
|
||||
? sprintf(this.$options.i18n.editTitle, { filename: this.filename })
|
||||
: this.$options.i18n.uploadTitle;
|
||||
},
|
||||
isUrlValid() {
|
||||
return this.modalUrl === '' || isSafeURL(this.modalUrl);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['uploadImage', 'updateImage']),
|
||||
clear() {
|
||||
this.modalUrl = this.url;
|
||||
this.modalUrlText = this.urlText;
|
||||
this.$emit('hidden');
|
||||
},
|
||||
async submit() {
|
||||
if (!this.isUrlValid) {
|
||||
this.$refs.urlInput.$el.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.edit) {
|
||||
await this.updateImage({
|
||||
imageId: this.imageId,
|
||||
url: this.modalUrl,
|
||||
urlText: this.modalUrlText,
|
||||
});
|
||||
this.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.uploadImage({
|
||||
files: this.imageFiles,
|
||||
url: this.modalUrl,
|
||||
urlText: this.modalUrlText,
|
||||
});
|
||||
this.clear();
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
cancel: __('Cancel'),
|
||||
description: s__(
|
||||
"Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead.",
|
||||
),
|
||||
invalidUrlMessage: __('Invalid URL'),
|
||||
textInputLabel: __('Text (optional)'),
|
||||
urlInputLabel: __('Link (optional)'),
|
||||
urlInputDescription: s__('Incidents|Must start with http:// or https://'),
|
||||
editTitle: s__('Incident|Editing %{filename}'),
|
||||
uploadTitle: s__('Incidents|Add image details'),
|
||||
update: __('Update'),
|
||||
upload: __('Upload'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
modal-id="metric-image-details-modal"
|
||||
size="sm"
|
||||
:title="title"
|
||||
:visible="visible"
|
||||
@hidden="clear"
|
||||
>
|
||||
<p v-if="!edit" data-testid="metric-image-details-modal-description">
|
||||
{{ $options.i18n.description }}
|
||||
</p>
|
||||
|
||||
<gl-form
|
||||
id="metric-image-details-modal-form"
|
||||
data-testid="metric-image-details-modal-form"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.textInputLabel"
|
||||
label-for="metric-image-details-modal-text-input"
|
||||
>
|
||||
<gl-form-input
|
||||
id="metric-image-details-modal-text-input"
|
||||
v-model="modalUrlText"
|
||||
data-testid="metric-image-details-modal-text-input"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
data-testid="metric-image-details-url-form-group"
|
||||
:label="$options.i18n.urlInputLabel"
|
||||
label-for="metric-image-details-modal-url-input"
|
||||
:description="$options.i18n.urlInputDescription"
|
||||
:invalid-feedback="$options.i18n.invalidUrlMessage"
|
||||
:state="isUrlValid"
|
||||
>
|
||||
<gl-form-input
|
||||
id="metric-image-details-modal-url-input"
|
||||
ref="urlInput"
|
||||
v-model="modalUrl"
|
||||
data-testid="metric-image-details-modal-url-input"
|
||||
:state="isUrlValid"
|
||||
lazy
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-form>
|
||||
|
||||
<template #modal-footer>
|
||||
<gl-button category="primary" variant="default" @click="clear">
|
||||
{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
form="metric-image-details-modal-form"
|
||||
:loading="isUploadingImage"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
type="submit"
|
||||
>
|
||||
{{ edit ? $options.i18n.update : $options.i18n.upload }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
@ -1,99 +1,48 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal } from '@gitlab/ui';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { s__ } from '~/locale';
|
||||
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
|
||||
import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
|
||||
import MetricImageDetailsModal from './metric_image_details_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
MetricImagesTable,
|
||||
MetricImageDetailsModal,
|
||||
UploadDropzone,
|
||||
},
|
||||
inject: ['canUpdate', 'projectId', 'iid'],
|
||||
data() {
|
||||
return {
|
||||
currentFiles: [],
|
||||
isModalUrlValid: true,
|
||||
modalVisible: false,
|
||||
modalUrl: '',
|
||||
modalUrlText: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['metricImages', 'isLoadingMetricImages', 'isUploadingImage']),
|
||||
actionPrimaryProps() {
|
||||
return {
|
||||
text: this.$options.i18n.modalUpload,
|
||||
attributes: {
|
||||
loading: this.isUploadingImage,
|
||||
disabled: this.isUploadingImage || !this.isModalUrlValid,
|
||||
category: 'primary',
|
||||
variant: 'confirm',
|
||||
},
|
||||
};
|
||||
},
|
||||
...mapState(['metricImages', 'isLoadingMetricImages']),
|
||||
},
|
||||
mounted() {
|
||||
this.setInitialData({ modelIid: this.iid, projectId: this.projectId });
|
||||
this.fetchImages();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchImages', 'uploadImage', 'setInitialData']),
|
||||
...mapActions(['fetchImages', 'setInitialData']),
|
||||
clearInputs() {
|
||||
this.modalVisible = false;
|
||||
this.modalUrl = '';
|
||||
this.modalUrlText = '';
|
||||
this.currentFile = false;
|
||||
this.currentFiles = [];
|
||||
},
|
||||
openMetricDialog(files) {
|
||||
this.modalVisible = true;
|
||||
this.currentFiles = files;
|
||||
},
|
||||
async onUpload() {
|
||||
if (!this.isModalUrlValid) return;
|
||||
|
||||
try {
|
||||
await this.uploadImage({
|
||||
files: this.currentFiles,
|
||||
url: this.modalUrl,
|
||||
urlText: this.modalUrlText,
|
||||
});
|
||||
// Error case handled within action
|
||||
} finally {
|
||||
this.clearInputs();
|
||||
}
|
||||
},
|
||||
onModalUrlInput(value) {
|
||||
if (value === '') this.validateModalUrl();
|
||||
},
|
||||
validateModalUrl() {
|
||||
try {
|
||||
this.isModalUrlValid = Boolean(new URL(this.modalUrl));
|
||||
} catch (err) {
|
||||
this.isModalUrlValid = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
modalUpload: __('Upload'),
|
||||
modalCancel: __('Cancel'),
|
||||
modalTitle: s__('Incidents|Add image details'),
|
||||
modalDescription: s__(
|
||||
"Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead.",
|
||||
),
|
||||
dropDescription: s__(
|
||||
'Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident',
|
||||
),
|
||||
textInputLabel: __('Text (optional)'),
|
||||
urlInputLabel: __('Link (optional)'),
|
||||
urlInputDescription: s__('Incidents|Must start with http:// or https://'),
|
||||
invalidUrlMessage: __('Invalid URL'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -103,40 +52,11 @@ export default {
|
|||
<div v-if="isLoadingMetricImages">
|
||||
<gl-loading-icon class="gl-p-5" size="sm" />
|
||||
</div>
|
||||
<gl-modal
|
||||
modal-id="upload-metric-modal"
|
||||
size="sm"
|
||||
:action-primary="actionPrimaryProps"
|
||||
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
|
||||
text: $options.i18n.modalCancel,
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
:title="$options.i18n.modalTitle"
|
||||
<metric-image-details-modal
|
||||
:image-files="currentFiles"
|
||||
:visible="modalVisible"
|
||||
@hidden="clearInputs"
|
||||
@primary.prevent="onUpload"
|
||||
>
|
||||
<p>{{ $options.i18n.modalDescription }}</p>
|
||||
<gl-form-group :label="$options.i18n.textInputLabel" label-for="upload-text-input">
|
||||
<gl-form-input id="upload-text-input" v-model="modalUrlText" />
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
id="upload-url-group"
|
||||
:label="$options.i18n.urlInputLabel"
|
||||
label-for="upload-url-input"
|
||||
:description="$options.i18n.urlInputDescription"
|
||||
:invalid-feedback="$options.i18n.invalidUrlMessage"
|
||||
:state="isModalUrlValid"
|
||||
>
|
||||
<gl-form-input
|
||||
id="upload-url-input"
|
||||
v-model="modalUrl"
|
||||
:state="isModalUrlValid"
|
||||
@blur="validateModalUrl"
|
||||
@input="onModalUrlInput"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-modal>
|
||||
/>
|
||||
<metric-images-table v-for="metric in metricImages" :key="metric.id" v-bind="metric" />
|
||||
<upload-dropzone
|
||||
v-if="canUpdate"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<script>
|
||||
import {
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlCard,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
|
|
@ -13,6 +11,7 @@ import {
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { __, s__ } from '~/locale';
|
||||
import MetricImageDetailsModal from './metric_image_details_modal.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
|
|
@ -20,20 +19,18 @@ export default {
|
|||
modalDescription: s__('Incident|Are you sure you wish to delete this image?'),
|
||||
modalCancel: __('Cancel'),
|
||||
modalTitle: s__('Incident|Deleting %{filename}'),
|
||||
editModalUpdate: __('Update'),
|
||||
editModalTitle: s__('Incident|Editing %{filename}'),
|
||||
editIconTitle: s__('Incident|Edit image text or link'),
|
||||
deleteIconTitle: s__('Incident|Delete image'),
|
||||
editButtonLabel: __('Edit'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlCard,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
MetricImageDetailsModal,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -67,11 +64,8 @@ export default {
|
|||
return {
|
||||
isCollapsed: false,
|
||||
isDeleting: false,
|
||||
isUpdating: false,
|
||||
modalVisible: false,
|
||||
editModalVisible: false,
|
||||
modalUrl: this.url,
|
||||
modalUrlText: this.urlText,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -86,17 +80,6 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
updateActionPrimaryProps() {
|
||||
return {
|
||||
text: this.$options.i18n.editModalUpdate,
|
||||
attributes: {
|
||||
loading: this.isUpdating,
|
||||
disabled: this.isUpdating,
|
||||
category: 'primary',
|
||||
variant: 'confirm',
|
||||
},
|
||||
};
|
||||
},
|
||||
arrowIconName() {
|
||||
return this.isCollapsed ? 'chevron-right' : 'chevron-down';
|
||||
},
|
||||
|
|
@ -110,16 +93,10 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['deleteImage', 'updateImage']),
|
||||
...mapActions(['deleteImage']),
|
||||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
resetEditFields() {
|
||||
this.modalUrl = this.url;
|
||||
this.modalUrlText = this.urlText;
|
||||
this.editModalVisible = false;
|
||||
this.modalVisible = false;
|
||||
},
|
||||
async onDelete() {
|
||||
try {
|
||||
this.isDeleting = true;
|
||||
|
|
@ -129,21 +106,6 @@ export default {
|
|||
this.modalVisible = false;
|
||||
}
|
||||
},
|
||||
async onUpdate() {
|
||||
try {
|
||||
this.isUpdating = true;
|
||||
await this.updateImage({
|
||||
imageId: this.id,
|
||||
url: this.modalUrl,
|
||||
urlText: this.modalUrlText,
|
||||
});
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
this.modalUrl = '';
|
||||
this.modalUrlText = '';
|
||||
this.editModalVisible = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -164,7 +126,7 @@ export default {
|
|||
text: $options.i18n.modalCancel,
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
@primary.prevent="onDelete"
|
||||
@hidden="resetEditFields"
|
||||
@hidden="modalVisible = false"
|
||||
>
|
||||
<template #modal-title>
|
||||
<gl-sprintf :message="$options.i18n.modalTitle">
|
||||
|
|
@ -176,46 +138,15 @@ export default {
|
|||
<p>{{ $options.i18n.modalDescription }}</p>
|
||||
</gl-modal>
|
||||
|
||||
<gl-modal
|
||||
modal-id="edit-metric-modal"
|
||||
size="sm"
|
||||
:action-primary="updateActionPrimaryProps"
|
||||
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
|
||||
text: $options.i18n.modalCancel,
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
<metric-image-details-modal
|
||||
edit
|
||||
:image-id="id"
|
||||
:filename="filename"
|
||||
:url="url"
|
||||
:url-text="urlText"
|
||||
:visible="editModalVisible"
|
||||
data-testid="metric-image-edit-modal"
|
||||
@hidden="resetEditFields"
|
||||
@primary.prevent="onUpdate"
|
||||
>
|
||||
<template #modal-title>
|
||||
<gl-sprintf :message="$options.i18n.editModalTitle">
|
||||
<template #filename>
|
||||
{{ filename }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
|
||||
<gl-form-group :label="__('Text (optional)')" label-for="upload-text-input">
|
||||
<gl-form-input
|
||||
id="upload-text-input"
|
||||
v-model="modalUrlText"
|
||||
data-testid="metric-image-text-field"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
:label="__('Link (optional)')"
|
||||
label-for="upload-url-input"
|
||||
:description="s__('Incidents|Must start with http or https')"
|
||||
>
|
||||
<gl-form-input
|
||||
id="upload-url-input"
|
||||
v-model="modalUrl"
|
||||
data-testid="metric-image-url-field"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-modal>
|
||||
@hidden="editModalVisible = false"
|
||||
/>
|
||||
|
||||
<template #header>
|
||||
<div class="gl-w-full gl-display-flex gl-flex-direction-row gl-justify-content-space-between">
|
||||
|
|
@ -242,7 +173,7 @@ export default {
|
|||
v-if="canUpdate"
|
||||
v-gl-tooltip.bottom
|
||||
icon="pencil"
|
||||
:aria-label="__('Edit')"
|
||||
:aria-label="$options.i18n.editButtonLabel"
|
||||
:title="$options.i18n.editIconTitle"
|
||||
data-testid="edit-button"
|
||||
@click="editModalVisible = true"
|
||||
|
|
@ -251,7 +182,7 @@ export default {
|
|||
v-if="canUpdate"
|
||||
v-gl-tooltip.bottom
|
||||
icon="remove"
|
||||
:aria-label="__('Delete')"
|
||||
:aria-label="$options.i18n.modalDelete"
|
||||
:title="$options.i18n.deleteIconTitle"
|
||||
data-testid="delete-button"
|
||||
@click="modalVisible = true"
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<li
|
||||
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
|
||||
:class="optionalClasses"
|
||||
>
|
||||
|
|
@ -159,5 +159,5 @@ export default {
|
|||
</div>
|
||||
<div class="gl-w-9"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Projects::MirrorsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if push_mirror_create? && Feature.enabled?(:use_remote_mirror_create_service, project)
|
||||
if push_mirror_create?
|
||||
service = ::RemoteMirrors::CreateService.new(project, current_user, push_mirror_attributes)
|
||||
result = service.execute
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
# projects to a GitLab instance. It associates the import with the responsible
|
||||
# user.
|
||||
class BulkImport < ApplicationRecord
|
||||
include AfterCommitQueue
|
||||
|
||||
MIN_MAJOR_VERSION = 14
|
||||
MIN_MINOR_VERSION_FOR_PROJECT = 4
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ class BulkImport < ApplicationRecord
|
|||
state :finished, value: 2
|
||||
state :timeout, value: 3
|
||||
state :failed, value: -1
|
||||
state :canceled, value: -2
|
||||
|
||||
event :start do
|
||||
transition created: :started
|
||||
|
|
@ -44,11 +47,21 @@ class BulkImport < ApplicationRecord
|
|||
transition any => :failed
|
||||
end
|
||||
|
||||
event :cancel do
|
||||
transition any => :canceled
|
||||
end
|
||||
|
||||
# rubocop:disable Style/SymbolProc
|
||||
after_transition any => [:finished, :failed, :timeout] do |bulk_import|
|
||||
bulk_import.update_has_failures
|
||||
end
|
||||
# rubocop:enable Style/SymbolProc
|
||||
|
||||
after_transition any => [:canceled] do |bulk_import|
|
||||
bulk_import.run_after_commit do
|
||||
bulk_import.propagate_cancel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def source_version_info
|
||||
|
|
@ -74,11 +87,17 @@ class BulkImport < ApplicationRecord
|
|||
update!(has_failures: true)
|
||||
end
|
||||
|
||||
def propagate_cancel
|
||||
return unless entities.any?
|
||||
|
||||
entities.each(&:cancel)
|
||||
end
|
||||
|
||||
def supports_batched_export?
|
||||
source_version_info >= self.class.min_gl_version_for_migration_in_batches
|
||||
end
|
||||
|
||||
def completed?
|
||||
finished? || failed? || timeout?
|
||||
finished? || failed? || timeout? || canceled?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ module BulkImports
|
|||
state :timeout, value: 3
|
||||
state :failed, value: -1
|
||||
state :skipped, value: -2
|
||||
state :canceled, value: -3
|
||||
|
||||
event :start do
|
||||
transition created: :started
|
||||
|
|
@ -53,6 +54,10 @@ module BulkImports
|
|||
event :cleanup_stale do
|
||||
transition [:created, :started] => :timeout
|
||||
end
|
||||
|
||||
event :cancel do
|
||||
transition any => :canceled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
# The tree structure of the entities results in the same structure for imported
|
||||
# Groups and Projects.
|
||||
class BulkImports::Entity < ApplicationRecord
|
||||
include AfterCommitQueue
|
||||
|
||||
self.table_name = 'bulk_import_entities'
|
||||
|
||||
FailedError = Class.new(StandardError)
|
||||
|
|
@ -71,6 +73,7 @@ class BulkImports::Entity < ApplicationRecord
|
|||
state :finished, value: 2
|
||||
state :timeout, value: 3
|
||||
state :failed, value: -1
|
||||
state :canceled, value: -2
|
||||
|
||||
event :start do
|
||||
transition created: :started
|
||||
|
|
@ -90,11 +93,21 @@ class BulkImports::Entity < ApplicationRecord
|
|||
transition started: :timeout
|
||||
end
|
||||
|
||||
event :cancel do
|
||||
transition any => :canceled
|
||||
end
|
||||
|
||||
# rubocop:disable Style/SymbolProc
|
||||
after_transition any => [:finished, :failed, :timeout] do |entity|
|
||||
entity.update_has_failures
|
||||
end
|
||||
# rubocop:enable Style/SymbolProc
|
||||
|
||||
after_transition any => [:canceled] do |entity|
|
||||
entity.run_after_commit do
|
||||
entity.propagate_cancel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.all_human_statuses
|
||||
|
|
@ -221,6 +234,10 @@ class BulkImports::Entity < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def propagate_cancel
|
||||
trackers.each(&:cancel)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_parent_is_a_group
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BulkImports::Tracker < ApplicationRecord
|
||||
include AfterCommitQueue
|
||||
|
||||
self.table_name = 'bulk_import_trackers'
|
||||
|
||||
alias_attribute :pipeline_name, :relation
|
||||
|
|
@ -52,6 +54,7 @@ class BulkImports::Tracker < ApplicationRecord
|
|||
state :timeout, value: 4
|
||||
state :failed, value: -1
|
||||
state :skipped, value: -2
|
||||
state :canceled, value: -3
|
||||
|
||||
event :start do
|
||||
transition enqueued: :started
|
||||
|
|
@ -83,6 +86,10 @@ class BulkImports::Tracker < ApplicationRecord
|
|||
transition any => :failed
|
||||
end
|
||||
|
||||
event :cancel do
|
||||
transition any => :canceled
|
||||
end
|
||||
|
||||
event :cleanup_stale do
|
||||
transition [:created, :started] => :timeout
|
||||
end
|
||||
|
|
@ -90,6 +97,12 @@ class BulkImports::Tracker < ApplicationRecord
|
|||
after_transition any => [:finished, :failed] do |tracker|
|
||||
BulkImports::ObjectCounter.persist!(tracker)
|
||||
end
|
||||
|
||||
after_transition any => [:canceled] do |tracker|
|
||||
tracker.run_after_commit do
|
||||
tracker.propagate_cancel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def checksums
|
||||
|
|
@ -111,6 +124,10 @@ class BulkImports::Tracker < ApplicationRecord
|
|||
pipeline_class.relation.to_sym
|
||||
end
|
||||
|
||||
def propagate_cancel
|
||||
batches.each(&:cancel)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cached_checksums
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
module Ci
|
||||
class Partition < Ci::ApplicationRecord
|
||||
MAX_PARTITION_SIZE = 100.gigabytes
|
||||
|
||||
validates :id, :status, presence: true
|
||||
validates :status, uniqueness: { if: ->(partition) { partition.status_changed? && partition.current? } }
|
||||
|
||||
state_machine :status, initial: :preparing do
|
||||
state :preparing, value: 0
|
||||
|
|
@ -13,6 +16,14 @@ module Ci
|
|||
event :ready do
|
||||
transition preparing: :ready
|
||||
end
|
||||
|
||||
event :switch_writes do
|
||||
transition ready: :current
|
||||
end
|
||||
|
||||
before_transition [:ready] => :current do
|
||||
Ci::Partition.with_status(:current).update_all(status: Ci::Partition.statuses[:active])
|
||||
end
|
||||
end
|
||||
|
||||
scope :id_after, ->(partition_id) { where(arel_table[:id].gt(partition_id)) }
|
||||
|
|
@ -29,6 +40,14 @@ module Ci
|
|||
def create_next!
|
||||
create!(id: last.id.next, status: statuses[:preparing])
|
||||
end
|
||||
|
||||
def next_available(partition_id)
|
||||
Ci::Partition
|
||||
.with_status(:ready)
|
||||
.id_after(partition_id)
|
||||
.order(id: :asc)
|
||||
.first
|
||||
end
|
||||
end
|
||||
|
||||
def above_threshold?(threshold)
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ module Packages
|
|||
|
||||
validates :file_type, presence: true
|
||||
validates :file_type, inclusion: { in: %w[unknown] },
|
||||
if: -> { package_file&.package&.debian_incoming? || package_file&.package&.processing? }
|
||||
if: -> { package_file&.package&.incoming? || package_file&.package&.processing? }
|
||||
validates :file_type,
|
||||
inclusion: { in: %w[source dsc deb udeb buildinfo changes ddeb] },
|
||||
if: -> { package_file&.package&.debian_package? && !package_file&.package&.processing? }
|
||||
if: -> { !package_file&.package&.incoming? && !package_file&.package&.processing? }
|
||||
|
||||
validates :component,
|
||||
presence: true,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ class Packages::Debian::GroupDistribution < ApplicationRecord
|
|||
include Packages::Debian::Distribution
|
||||
|
||||
def packages
|
||||
Packages::Package
|
||||
::Packages::Debian::Package
|
||||
.for_projects(group.all_projects.public_only)
|
||||
.debian
|
||||
.with_debian_codename(codename)
|
||||
.with_codename(codename)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Debian
|
||||
class Package < Packages::Package
|
||||
INSTALLABLE_STATUSES = [:default, :hidden].freeze
|
||||
|
||||
self.allow_legacy_sti_class = true
|
||||
|
||||
has_one :publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
|
||||
has_one :distribution, through: :publication, source: :distribution, inverse_of: :packages,
|
||||
class_name: 'Packages::Debian::ProjectDistribution'
|
||||
|
||||
accepts_nested_attributes_for :publication
|
||||
|
||||
delegate :codename, :suite, to: :distribution, prefix: :distribution
|
||||
|
||||
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :version?
|
||||
validates :name, inclusion: { in: [Packages::Debian::INCOMING_PACKAGE_NAME] }, unless: :version?
|
||||
|
||||
validates :version,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.debian_version_regex },
|
||||
if: :version?
|
||||
validate :forbidden_changes
|
||||
|
||||
scope :with_codename, ->(codename) do
|
||||
joins(:distribution).where(Packages::Debian::ProjectDistribution.table_name => { codename: codename })
|
||||
end
|
||||
|
||||
scope :with_codename_or_suite, ->(codename_or_suite) do
|
||||
joins(:distribution)
|
||||
.where(Packages::Debian::ProjectDistribution.table_name => { codename: codename_or_suite })
|
||||
.or(where(Packages::Debian::ProjectDistribution.table_name => { suite: codename_or_suite }))
|
||||
end
|
||||
|
||||
scope :preload_debian_file_metadata, -> { preload(package_files: :debian_file_metadatum) }
|
||||
|
||||
def self.incoming_package!
|
||||
default
|
||||
.with_version(nil)
|
||||
.find_by!(name: Packages::Debian::INCOMING_PACKAGE_NAME)
|
||||
end
|
||||
|
||||
def self.existing_packages_with(name:, version:)
|
||||
with_name(name)
|
||||
.with_version(version)
|
||||
.not_pending_destruction
|
||||
end
|
||||
|
||||
def incoming?
|
||||
name == Packages::Debian::INCOMING_PACKAGE_NAME && version.nil?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def forbidden_changes
|
||||
return unless persisted?
|
||||
|
||||
# Debian incoming
|
||||
return unless version_was.nil? || version.nil?
|
||||
|
||||
errors.add(:version, _('cannot be changed')) if version_changed?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,5 +8,5 @@ class Packages::Debian::ProjectDistribution < ApplicationRecord
|
|||
include Packages::Debian::Distribution
|
||||
|
||||
has_many :publications, class_name: 'Packages::Debian::Publication', inverse_of: :distribution, foreign_key: :distribution_id
|
||||
has_many :packages, class_name: 'Packages::Package', through: :publications
|
||||
has_many :packages, class_name: 'Packages::Debian::Package', through: :publications
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,23 +2,15 @@
|
|||
|
||||
class Packages::Debian::Publication < ApplicationRecord
|
||||
belongs_to :package,
|
||||
-> { where(package_type: :debian).where.not(version: nil) },
|
||||
inverse_of: :debian_publication,
|
||||
class_name: 'Packages::Package'
|
||||
-> { where.not(version: nil) },
|
||||
inverse_of: :publication,
|
||||
class_name: 'Packages::Debian::Package'
|
||||
belongs_to :distribution,
|
||||
inverse_of: :publications,
|
||||
class_name: 'Packages::Debian::ProjectDistribution',
|
||||
foreign_key: :distribution_id
|
||||
|
||||
validates :package, presence: true
|
||||
validate :valid_debian_package_type
|
||||
|
||||
validates :distribution, presence: true
|
||||
|
||||
private
|
||||
|
||||
def valid_debian_package_type
|
||||
return errors.add(:package, _('type must be Debian')) unless package&.debian?
|
||||
return errors.add(:package, _('must be a Debian package')) unless package.debian_package?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,14 +54,10 @@ class Packages::Package < ApplicationRecord
|
|||
has_one :terraform_module_metadatum, inverse_of: :package, class_name: 'Packages::TerraformModule::Metadatum'
|
||||
has_many :build_infos, inverse_of: :package
|
||||
has_many :pipelines, through: :build_infos, disable_joins: true
|
||||
has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
|
||||
has_one :debian_distribution, through: :debian_publication, source: :distribution, inverse_of: :packages, class_name: 'Packages::Debian::ProjectDistribution'
|
||||
has_many :matching_package_protection_rules, -> (package) { where(package_type: package.package_type).for_package_name(package.name) }, through: :project, source: :package_protection_rules
|
||||
|
||||
accepts_nested_attributes_for :debian_publication
|
||||
accepts_nested_attributes_for :maven_metadatum
|
||||
|
||||
delegate :codename, :suite, to: :debian_distribution, prefix: :debian_distribution
|
||||
delegate :target_sha, to: :composer_metadatum, prefix: :composer
|
||||
|
||||
validates :project, presence: true
|
||||
|
|
@ -84,8 +80,6 @@ class Packages::Package < ApplicationRecord
|
|||
validates :name, format: { with: Gitlab::Regex.npm_package_name_regex, message: Gitlab::Regex.npm_package_name_regex_message }, if: :npm?
|
||||
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
|
||||
validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module?
|
||||
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package?
|
||||
validates :name, inclusion: { in: [Packages::Debian::INCOMING_PACKAGE_NAME] }, if: :debian_incoming?
|
||||
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
|
||||
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
|
||||
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
|
||||
|
|
@ -97,11 +91,6 @@ class Packages::Package < ApplicationRecord
|
|||
presence: true,
|
||||
format: { with: Gitlab::Regex.generic_package_version_regex },
|
||||
if: :generic?
|
||||
validates :version,
|
||||
presence: true,
|
||||
format: { with: Gitlab::Regex.debian_version_regex },
|
||||
if: :debian_package?
|
||||
validate :forbidden_debian_changes, if: :debian?
|
||||
|
||||
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
|
||||
scope :with_name, ->(name) { where(name: name) }
|
||||
|
|
@ -147,14 +136,6 @@ class Packages::Package < ApplicationRecord
|
|||
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
|
||||
scope :including_dependency_links_with_nuget_metadatum, -> { includes(dependency_links: [:dependency, :nuget_metadatum]) }
|
||||
|
||||
scope :with_debian_codename, ->(codename) do
|
||||
joins(:debian_distribution).where(Packages::Debian::ProjectDistribution.table_name => { codename: codename })
|
||||
end
|
||||
scope :with_debian_codename_or_suite, ->(codename_or_suite) do
|
||||
joins(:debian_distribution).where(Packages::Debian::ProjectDistribution.table_name => { codename: codename_or_suite })
|
||||
.or(where(Packages::Debian::ProjectDistribution.table_name => { suite: codename_or_suite }))
|
||||
end
|
||||
scope :preload_debian_file_metadata, -> { preload(package_files: :debian_file_metadatum) }
|
||||
scope :with_composer_target, -> (target) do
|
||||
includes(:composer_metadatum)
|
||||
.joins(:composer_metadatum)
|
||||
|
|
@ -219,7 +200,8 @@ class Packages::Package < ApplicationRecord
|
|||
golang: 'Packages::Go::Package',
|
||||
rubygems: 'Packages::Rubygems::Package',
|
||||
conan: 'Packages::Conan::Package',
|
||||
rpm: 'Packages::Rpm::Package'
|
||||
rpm: 'Packages::Rpm::Package',
|
||||
debian: 'Packages::Debian::Package'
|
||||
}.freeze
|
||||
|
||||
def self.only_maven_packages_with_path(path, use_cte: false)
|
||||
|
|
@ -252,16 +234,6 @@ class Packages::Package < ApplicationRecord
|
|||
find_by!(name: name, version: version)
|
||||
end
|
||||
|
||||
def self.debian_incoming_package!
|
||||
find_by!(name: Packages::Debian::INCOMING_PACKAGE_NAME, version: nil, package_type: :debian, status: :default)
|
||||
end
|
||||
|
||||
def self.existing_debian_packages_with(name:, version:)
|
||||
debian.with_name(name)
|
||||
.with_version(version)
|
||||
.not_pending_destruction
|
||||
end
|
||||
|
||||
def self.pluck_names
|
||||
pluck(:name)
|
||||
end
|
||||
|
|
@ -319,14 +291,6 @@ class Packages::Package < ApplicationRecord
|
|||
terraform_module?
|
||||
end
|
||||
|
||||
def debian_incoming?
|
||||
debian? && version.nil?
|
||||
end
|
||||
|
||||
def debian_package?
|
||||
debian? && !version.nil?
|
||||
end
|
||||
|
||||
def package_settings
|
||||
project.namespace.package_settings
|
||||
end
|
||||
|
|
@ -417,13 +381,4 @@ class Packages::Package < ApplicationRecord
|
|||
|
||||
project.root_namespace.path == ::Packages::Npm.scope_of(name)
|
||||
end
|
||||
|
||||
def forbidden_debian_changes
|
||||
return unless persisted?
|
||||
|
||||
# Debian incoming
|
||||
if version_was.nil? || version.nil?
|
||||
errors.add(:version, _('cannot be changed')) if version_changed?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
module Ci
|
||||
module Partitions
|
||||
class CreateService
|
||||
MAX_PARTITION_SIZE = 100.gigabytes
|
||||
HEADROOM_PARTITIONS = 3
|
||||
|
||||
def initialize(partition)
|
||||
|
|
@ -26,7 +25,7 @@ module Ci
|
|||
end
|
||||
|
||||
def above_threshold?
|
||||
partition.above_threshold?(MAX_PARTITION_SIZE)
|
||||
partition.above_threshold?(Ci::Partition::MAX_PARTITION_SIZE)
|
||||
end
|
||||
|
||||
def headroom_available?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
module Partitions
|
||||
class SyncService
|
||||
def initialize(partition)
|
||||
@partition = partition
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless Feature.enabled?(:ci_partitioning_automation, :instance)
|
||||
return unless partition
|
||||
|
||||
sync_available_partitions_statuses!
|
||||
|
||||
next_ci_partition = next_available_partition
|
||||
return unless next_ci_partition.present? && above_threshold?
|
||||
|
||||
next_ci_partition.switch_writes!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :partition
|
||||
|
||||
def above_threshold?
|
||||
partition.above_threshold?(Ci::Partition::MAX_PARTITION_SIZE)
|
||||
end
|
||||
|
||||
def sync_available_partitions_statuses!
|
||||
Ci::Partition.id_after(partition.id).each do |partition|
|
||||
partition.ready! if partition.all_partitions_exist?
|
||||
end
|
||||
end
|
||||
|
||||
def next_available_partition
|
||||
Ci::Partition.next_available(partition.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -107,7 +107,7 @@ module Packages
|
|||
end
|
||||
|
||||
def incoming
|
||||
@package_file.package.project.packages.debian_incoming_package!
|
||||
::Packages::Debian::Package.for_projects(@package_file.package.project).incoming_package!
|
||||
end
|
||||
strong_memoize_attr(:incoming)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ module Packages
|
|||
cleanup_temp_package
|
||||
end
|
||||
|
||||
::Packages::Debian::GenerateDistributionWorker.perform_async(:project, package.debian_distribution.id)
|
||||
::Packages::Debian::GenerateDistributionWorker.perform_async(:project, package.distribution.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -90,10 +90,9 @@ module Packages
|
|||
end
|
||||
|
||||
def package
|
||||
packages = temp_package.project
|
||||
.packages
|
||||
.existing_debian_packages_with(name: package_name, version: package_version)
|
||||
package = packages.with_debian_codename_or_suite(package_distribution)
|
||||
packages = ::Packages::Debian::Package.for_projects(temp_package.project)
|
||||
.existing_packages_with(name: package_name, version: package_version)
|
||||
package = packages.with_codename_or_suite(package_distribution)
|
||||
.first
|
||||
|
||||
unless package
|
||||
|
|
@ -101,7 +100,7 @@ module Packages
|
|||
|
||||
if package_in_other_distribution
|
||||
raise ArgumentError, "Debian package #{package_name} #{package_version} exists " \
|
||||
"in distribution #{package_in_other_distribution.debian_distribution.codename}"
|
||||
"in distribution #{package_in_other_distribution.distribution.codename}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -153,7 +152,7 @@ module Packages
|
|||
return unless using_temporary_package?
|
||||
|
||||
package.update!(
|
||||
debian_publication_attributes: { distribution_id: distribution.id }
|
||||
publication_attributes: { distribution_id: distribution.id }
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Users
|
||||
class ActivityService
|
||||
LEASE_TIMEOUT = 1.minute.to_i
|
||||
|
||||
def initialize(author:, namespace: nil, project: nil)
|
||||
@user = if author.respond_to?(:username)
|
||||
author
|
||||
|
|
@ -30,6 +32,11 @@ module Users
|
|||
today = Date.today
|
||||
return if user.last_activity_on == today
|
||||
|
||||
lease = Gitlab::ExclusiveLease.new("activity_service:#{user.id}", timeout: LEASE_TIMEOUT)
|
||||
# Skip transaction checks for exclusive lease as it is breaking system specs.
|
||||
# See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/441536
|
||||
return unless Gitlab::ExclusiveLease.skipping_transaction_check { lease.try_obtain }
|
||||
|
||||
user.update_attribute(:last_activity_on, today)
|
||||
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event('unique_active_user', values: user.id)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ module BulkImports
|
|||
|
||||
def run
|
||||
return batch.skip! if tracker.failed? || tracker.finished?
|
||||
return cancel_batch if tracker.canceled?
|
||||
|
||||
logger.info(log_attributes(message: 'Batch tracker started'))
|
||||
batch.start!
|
||||
|
|
@ -149,5 +150,11 @@ module BulkImports
|
|||
}.merge(extra)
|
||||
)
|
||||
end
|
||||
|
||||
def cancel_batch
|
||||
batch.cancel!
|
||||
|
||||
logger.info(log_attributes(message: 'Batch tracker canceled'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ module BulkImports
|
|||
attr_reader :pipeline_tracker, :entity
|
||||
|
||||
def run
|
||||
return if pipeline_tracker.canceled?
|
||||
return skip_tracker if entity.failed?
|
||||
return cancel_tracker if entity.canceled?
|
||||
|
||||
raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed?
|
||||
raise(Pipeline::ExpiredError, 'Empty export status on source instance') if empty_export_timeout?
|
||||
|
|
@ -183,6 +185,12 @@ module BulkImports
|
|||
pipeline_tracker.update!(status_event: 'skip', jid: jid)
|
||||
end
|
||||
|
||||
def cancel_tracker
|
||||
logger.info(log_attributes(message: 'Canceling pipeline due to canceled entity'))
|
||||
|
||||
pipeline_tracker.update!(status_event: 'cancel', jid: jid)
|
||||
end
|
||||
|
||||
def log_attributes(extra = {})
|
||||
logger.default_attributes.merge(extra)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ module Ci
|
|||
|
||||
def perform
|
||||
Ci::Partitions::SetupDefaultService.new.execute
|
||||
|
||||
ci_partition_current = Ci::Partition.current
|
||||
return unless ci_partition_current
|
||||
|
||||
Ci::Partitions::CreateService.new(ci_partition_current).execute
|
||||
Ci::Partitions::SyncService.new(ci_partition_current).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: use_remote_mirror_create_service
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455515
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149263
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455631
|
||||
milestone: '17.0'
|
||||
group: group::source code
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -4,8 +4,9 @@ classes:
|
|||
- Elastic::IndexSetting
|
||||
feature_categories:
|
||||
- global_search
|
||||
description: TODO
|
||||
description: Describes the settings (such as number of shards and replicas) for each Elasticsearch or Opensearch index
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56344
|
||||
milestone: '13.11'
|
||||
gitlab_schema: gitlab_main
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442659
|
||||
exempt_from_sharding: true # data is specific to each cell's Elasticsearch cluster, no customer data
|
||||
|
|
@ -13,5 +13,6 @@ feature_categories:
|
|||
- user_management
|
||||
description: Stores events created by users interacting with various product features
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/a847501fd2ffc1c4becc7d0d352d80168d9b3568
|
||||
milestone: "2.2"
|
||||
milestone: '2.2'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462801
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
table_name: packages_packages
|
||||
classes:
|
||||
- Packages::Conan::Package
|
||||
- Packages::Debian::Package
|
||||
- Packages::Go::Package
|
||||
- Packages::MlModel::Package
|
||||
- Packages::Package
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ description: Stores log of push events
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12463
|
||||
milestone: '9.5'
|
||||
gitlab_schema: gitlab_main
|
||||
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462802
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqueIndexOnStatusToCiPartition < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.1'
|
||||
|
||||
TABLE_NAME = :ci_partitions
|
||||
INDEX_NAME = :index_ci_partitions_on_current_status
|
||||
CURRENT_STATUS = 2
|
||||
|
||||
def up
|
||||
add_concurrent_index(TABLE_NAME, :status, unique: true, where: "status = #{CURRENT_STATUS}", name: INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(TABLE_NAME, INDEX_NAME)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
121da77b8cb11e6b16b02bb5eea3ba8d3e7c83fbc5dde23ab94a8ec7c126f95a
|
||||
|
|
@ -25017,6 +25017,8 @@ CREATE INDEX index_ci_namespace_mirrors_on_traversal_ids_unnest ON ci_namespace_
|
|||
|
||||
CREATE UNIQUE INDEX index_ci_namespace_monthly_usages_on_namespace_id_and_date ON ci_namespace_monthly_usages USING btree (namespace_id, date);
|
||||
|
||||
CREATE UNIQUE INDEX index_ci_partitions_on_current_status ON ci_partitions USING btree (status) WHERE (status = 2);
|
||||
|
||||
CREATE INDEX index_ci_pending_builds_id_on_protected_partial ON ci_pending_builds USING btree (id) WHERE (protected = true);
|
||||
|
||||
CREATE UNIQUE INDEX index_ci_pending_builds_on_build_id ON ci_pending_builds USING btree (build_id);
|
||||
|
|
|
|||
|
|
@ -306,3 +306,37 @@ curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
|
|||
"source_title": "Issue title"
|
||||
}
|
||||
```
|
||||
|
||||
## Cancel a migration
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/438281) in GitLab 17.1.
|
||||
|
||||
Cancel a direct transfer migration. Requires administrator access.
|
||||
|
||||
```plaintext
|
||||
POST /bulk_imports/:id/cancel
|
||||
```
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports/1/cancel"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"status": "canceled",
|
||||
"source_type": "gitlab",
|
||||
"created_at": "2021-06-18T09:45:55.358Z",
|
||||
"updated_at": "2021-06-18T09:46:27.003Z"
|
||||
}
|
||||
```
|
||||
|
||||
Possible response status codes:
|
||||
|
||||
| Status | Description |
|
||||
|--------|---------------------------------|
|
||||
| 200 | Migration successfully canceled |
|
||||
| 401 | Unauthorized |
|
||||
| 403 | Forbidden |
|
||||
| 404 | Migration not found |
|
||||
| 503 | Service unavailable |
|
||||
|
|
|
|||
|
|
@ -449,7 +449,8 @@ GET /users/:user_id/projects
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
|
||||
"_links": {
|
||||
|
|
@ -580,7 +581,8 @@ GET /users/:user_id/projects
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
|
||||
"_links": {
|
||||
|
|
@ -702,7 +704,8 @@ Example response:
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
|
||||
"_links": {
|
||||
|
|
@ -822,7 +825,8 @@ Example response:
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
|
||||
"_links": {
|
||||
|
|
@ -958,7 +962,8 @@ Example response:
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
|
||||
"_links": {
|
||||
|
|
@ -1078,7 +1083,8 @@ Example response:
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
|
||||
"_links": {
|
||||
|
|
@ -1265,7 +1271,8 @@ GET /projects/:id
|
|||
"pipeline_artifacts_size": 0,
|
||||
"packages_size": 0,
|
||||
"snippets_size": 0,
|
||||
"uploads_size": 0
|
||||
"uploads_size": 0,
|
||||
"container_registry_size": 0
|
||||
},
|
||||
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
|
||||
"_links": {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,12 @@ TL;DR
|
|||
|
||||
## 3. Proposal
|
||||
|
||||
There is an [investigation](https://gitlab.com/gitlab-org/gitlab/-/issues/458338) to solve this problem using [direct transfer](../../../../user/group/import/index.md).
|
||||
|
||||
## 4. Evaluation
|
||||
|
||||
## 4.1. Pros
|
||||
|
||||
## 4.2. Cons
|
||||
|
||||
Direct transfer does not migrate users and users cannot exist on more than one Cell. This means in Cells 1.0, for migrations across Cells, any user contributions will be assigned to the user performing the import.
|
||||
|
|
|
|||
|
|
@ -131,13 +131,39 @@ Installation instructions will be added to the Developer documentation. [issue](
|
|||
|
||||
_This list will expand in the near future, but the overall architecture will be the same_
|
||||
|
||||
### Self Hosted models fitting into the current architecture
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant GitLab
|
||||
participant AIGateway as AI Gateway
|
||||
participant SelfHostedModel as Self Hosted Model
|
||||
participant GitLabAIVendor as GitLab AI Vendor
|
||||
|
||||
User ->> GitLab: Send request
|
||||
GitLab ->> GitLab: Check if self-hosted model is configured
|
||||
alt Self-hosted model configured
|
||||
GitLab ->> AIGateway: Create prompt and send request
|
||||
AIGateway ->> SelfHostedModel: Perform API request to AI model
|
||||
SelfHostedModel -->> AIGateway: Respond to the prompt
|
||||
AIGateway -->> GitLab: Forward AI response
|
||||
else
|
||||
GitLab ->> AIGateway: Create prompt and send request
|
||||
AIGateway ->> GitLabAIVendor: Perform API request to AI model
|
||||
GitLabAIVendor -->> AIGateway: Respond to the prompt
|
||||
AIGateway -->> GitLab: Forward AI response
|
||||
end
|
||||
GitLab -->> User: Forward AI response
|
||||
```
|
||||
|
||||
### GitLab Duo Feature Support
|
||||
|
||||
| Feature | Default Model | [Mistral AI 7B v0.1](https://huggingface.co/mistralai/Mistral-7B-v0.1) | [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) |
|
||||
|---------------------|------------------|----------------------------------------------------------------|---------------------|
|
||||
| GitLab Duo Chat | Anthropic Claude-2 <br/> Vertex AI Codey textembedding-gecko | Not planned | Not planned |
|
||||
| Code Completion | Vertex AI Codey code-gecko | ✅ | ✅ |
|
||||
| Code Generation | Anthropic Claude-2 | ✅ | ✅ |
|
||||
| Code Generation | Anthropic Claude-3 | ✅ | ✅ |
|
||||
| Git Suggestions | Vertex AI Codey codechat-bison | Not planned | Not planned |
|
||||
| Discussion Summary | Vertex AI Codey text-bison | Not planned | Not planned |
|
||||
| Issue Description Generation | Anthropic Claude-2 | Not planned | Not planned |
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ On GitLab Dedicated this feature is not available.
|
|||
|
||||
AI Impact analytics displays SDLC metrics for a group or project in the month-to-date and the past six months. You can use this table to observe how changes in the AI usage metric correlate with changes in other metrics.
|
||||
|
||||
The metric **Monthly Code Suggestions Usage rate** is calculated as the number of monthly unique Code Suggestions users divided by total monthly [unique contributors](../../user/profile/contributions_calendar.md#user-contribution-events). GitLab considers the total monthly unique code contributors, meaning only users with `pushed` events are included in the calculation.
|
||||
The metric **Monthly Code Suggestions Usage rate** represents users that engage with Code Suggestions every month. It is calculated as the number of monthly unique Code Suggestions users divided by total monthly [unique contributors](../../user/profile/contributions_calendar.md#user-contribution-events). Only unique code contributors, meaning users with `pushed` events, are included in the calculation.
|
||||
|
||||
The month-over-month comparison of the AI Usage unique users rate gives a more accurate indication of this metric, as it eliminates factors such as developer experience level and project type or complexity.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Use AsciiDoc files in your GitLab project, and understand AsciiDoc syntax."
|
||||
---
|
||||
|
||||
# AsciiDoc
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
description: "Learn about the GitLab tools for building, tracking, and delivering the code for your project."
|
||||
---
|
||||
|
||||
# Get started managing code
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "To speed up project creation in your group, build custom project templates and share them with your group."
|
||||
---
|
||||
|
||||
# Custom group-level project templates
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Manage Git access to projects by adding CA certificates to your top-level group, instead of individual groups."
|
||||
---
|
||||
|
||||
# Manage group's SSH certificates
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
description: "Build, automate, and customize changelogs in your GitLab project."
|
||||
---
|
||||
|
||||
# Changelogs
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
description: "Define custom Git attributes for your GitLab project to set options for file handling, display, locking, and storage."
|
||||
---
|
||||
|
||||
# Git attributes
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
description: "Syntax highlighting helps you read files in your GitLab project more easily, and identify what files contain."
|
||||
---
|
||||
|
||||
# Syntax Highlighting
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Integrate GitLab with Beyond Identity to verify GPG keys added to user accounts."
|
||||
---
|
||||
|
||||
# Beyond Identity
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Integrate GitLab with GitGuardian to get alerts for policy violations and security issues before they can be exploited."
|
||||
---
|
||||
|
||||
# GitGuardian
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Use protected tags in Git to control who can create tags, and prevent accidental tag updates or deletion."
|
||||
---
|
||||
|
||||
# Protected tags
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "How comma-separated values (CSV) files display in GitLab projects."
|
||||
---
|
||||
|
||||
# CSV files
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "How GeoJSON files are rendered when viewed in GitLab projects."
|
||||
---
|
||||
|
||||
# GeoJSON files
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
description: "GitLab projects display Jupyter Notebook files as clean, human-readable files instead of raw files."
|
||||
---
|
||||
# Jupyter Notebook files
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Troubleshooting problems with repository mirroring for GitLab projects."
|
||||
---
|
||||
|
||||
# Troubleshooting repository mirroring
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Sign commits in your GitLab repository with GPG (GNU Privacy Guard) keys."
|
||||
---
|
||||
|
||||
# Sign commits with GPG
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Why you should sign your GitLab commits cryptographically, and how to verify signed commits."
|
||||
---
|
||||
|
||||
# Signed commits
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Sign commits in your GitLab repository with SSH keys."
|
||||
---
|
||||
|
||||
# Sign commits with SSH keys
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "Sign commits and tags in your GitLab repository with X.509 certificates."
|
||||
---
|
||||
|
||||
# Sign commits and tags with X.509 certificates
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: "System notes track the history of changes made to an object, like a merge request or issue, in your GitLab project."
|
||||
---
|
||||
|
||||
# System notes
|
||||
|
|
|
|||
|
|
@ -348,11 +348,13 @@ To add a task to an iteration:
|
|||
1. Next to **Iteration**, select **Add to iteration**.
|
||||
1. From the dropdown list, select the iteration to be associated with the task.
|
||||
|
||||
## Set time tracking
|
||||
## Estimate and track spent time
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/438577) in GitLab 17.0.
|
||||
|
||||
To set time tracking, refer to the [time tracking page](project/time_tracking.md)
|
||||
You can estimate and track the time you spend on a task.
|
||||
|
||||
For more information, see [Time tracking](project/time_tracking.md).
|
||||
|
||||
## View task system notes
|
||||
|
||||
|
|
|
|||
|
|
@ -231,6 +231,31 @@ module API
|
|||
get ':import_id/entities/:entity_id/failures' do
|
||||
present paginate(bulk_import_entity.failures), with: Entities::BulkImports::EntityFailure
|
||||
end
|
||||
|
||||
desc 'Cancel GitLab Migration' do
|
||||
detail 'This feature was introduced in GitLab 17.1'
|
||||
success code: 200, model: Entities::BulkImport
|
||||
failure [
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' },
|
||||
{ code: 503, message: 'Service unavailable' }
|
||||
]
|
||||
end
|
||||
|
||||
params do
|
||||
requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration"
|
||||
end
|
||||
post ':import_id/cancel' do
|
||||
authenticated_as_admin!
|
||||
|
||||
bulk_import = BulkImport.find(params[:import_id])
|
||||
|
||||
bulk_import.cancel!
|
||||
|
||||
status :ok
|
||||
present bulk_import, with: Entities::BulkImport
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ module API
|
|||
expose :packages_size
|
||||
expose :snippets_size
|
||||
expose :uploads_size
|
||||
expose :container_registry_size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -98,30 +98,18 @@ module API
|
|||
use :mirror_branches_setting
|
||||
end
|
||||
post ':id/remote_mirrors' do
|
||||
if Feature.enabled?(:use_remote_mirror_create_service, user_project)
|
||||
service = ::RemoteMirrors::CreateService.new(
|
||||
user_project,
|
||||
current_user,
|
||||
declared_params(include_missing: false)
|
||||
)
|
||||
service = ::RemoteMirrors::CreateService.new(
|
||||
user_project,
|
||||
current_user,
|
||||
declared_params(include_missing: false)
|
||||
)
|
||||
|
||||
result = service.execute
|
||||
result = service.execute
|
||||
|
||||
if result.success?
|
||||
present result.payload[:remote_mirror], with: Entities::RemoteMirror
|
||||
else
|
||||
render_api_error!(result.message, 400)
|
||||
end
|
||||
if result.success?
|
||||
present result.payload[:remote_mirror], with: Entities::RemoteMirror
|
||||
else
|
||||
create_params = declared_params(include_missing: false)
|
||||
verify_mirror_branches_setting(create_params)
|
||||
new_mirror = user_project.remote_mirrors.create(create_params)
|
||||
|
||||
if new_mirror.persisted?
|
||||
present new_mirror, with: Entities::RemoteMirror
|
||||
else
|
||||
render_validation_error!(new_mirror)
|
||||
end
|
||||
render_api_error!(result.message, 400)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,5 +5,11 @@ module Gitlab
|
|||
def can?(...)
|
||||
Ability.allowed?(...)
|
||||
end
|
||||
|
||||
def can_any?(user, abilities, subject = :global, **opts)
|
||||
abilities.any? do |ability|
|
||||
can?(user, ability, subject, **opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -250,5 +250,3 @@ spotbugs-sast:
|
|||
- if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
|
||||
exists:
|
||||
- '**/*.groovy'
|
||||
- '**/*.scala'
|
||||
- '**/*.kt'
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def key
|
||||
key_path
|
||||
attributes[:key_path]
|
||||
end
|
||||
alias_method :key_path, :key
|
||||
|
||||
def events
|
||||
events_from_new_structure || events_from_old_structure || {}
|
||||
|
|
@ -34,6 +35,14 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def status
|
||||
attributes[:status]
|
||||
end
|
||||
|
||||
def value_json_schema
|
||||
attributes[:value_json_schema]
|
||||
end
|
||||
|
||||
def to_context
|
||||
return unless %w[redis redis_hll].include?(data_source)
|
||||
|
||||
|
|
@ -84,6 +93,10 @@ module Gitlab
|
|||
VALID_SERVICE_PING_STATUSES.include?(attributes[:status])
|
||||
end
|
||||
|
||||
def data_source
|
||||
attributes[:data_source]
|
||||
end
|
||||
|
||||
def internal_events?
|
||||
data_source == 'internal_events'
|
||||
end
|
||||
|
|
@ -154,14 +167,6 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def method_missing(method, *args)
|
||||
attributes[method] || super
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, *args)
|
||||
attributes[method].present? || super
|
||||
end
|
||||
|
||||
def events_from_new_structure
|
||||
events = attributes[:events]
|
||||
return unless events
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ module Gitlab
|
|||
include Gitlab::View::Presenter::Base
|
||||
delegator_override_with Gitlab::Routing.url_helpers
|
||||
delegator_override :can?
|
||||
delegator_override :can_any?
|
||||
delegator_override :declarative_policy_delegate
|
||||
delegator_override :present
|
||||
delegator_override :web_url
|
||||
|
|
|
|||
|
|
@ -147,14 +147,15 @@ class GroupSeeder
|
|||
@resource_count.times do |_|
|
||||
group = Group.find(group_id)
|
||||
|
||||
author = group.group_members.non_invite.sample.user
|
||||
epic_params = {
|
||||
title: FFaker::Lorem.sentence(6),
|
||||
description: FFaker::Lorem.paragraphs(3).join("\n\n"),
|
||||
author: group.group_members.non_invite.sample.user,
|
||||
author: author,
|
||||
group: group
|
||||
}
|
||||
|
||||
Epic.create!(epic_params)
|
||||
::Epics::CreateService.new(group: group, current_user: author, params: epic_params).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9038,6 +9038,9 @@ msgstr ""
|
|||
msgid "BranchRules|Allowed to merge (%{total})"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchRules|Allowed to merge updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchRules|Allowed to push and merge"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9113,6 +9116,9 @@ msgstr ""
|
|||
msgid "BranchRules|Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchRules|Edit allowed to merge"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchRules|From users with push access."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27199,9 +27205,6 @@ msgstr ""
|
|||
msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incidents|Must start with http or https"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incidents|Must start with http:// or https://"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62161,9 +62164,6 @@ msgstr ""
|
|||
msgid "mrWidget|Your password"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be a Debian package"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be a boolean value"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -62766,9 +62766,6 @@ msgstr ""
|
|||
msgid "two-factor authentication settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "type must be Debian"
|
||||
msgstr ""
|
||||
|
||||
msgid "type parameter is missing and is required"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -10,18 +10,17 @@ module RuboCop
|
|||
# # bad
|
||||
# def disable_project_joins_for_blob?
|
||||
# Elastic::DataMigrationService
|
||||
# .migration_has_finished?(:backfill_project_permissions_in_blobs_using_permutations)
|
||||
# .migration_has_finished?(:backfill_archived_on_issues)
|
||||
# end
|
||||
#
|
||||
# # good
|
||||
# def disable_project_joins_for_blob?
|
||||
# Elastic::DataMigrationService.migration_has_finished?(:backfill_project_permissions_in_blobs)
|
||||
# Elastic::DataMigrationService.migration_has_finished?(:backfill_archived_on_issues)
|
||||
# end
|
||||
|
||||
class AvoidCheckingFinishedOnDeprecatedMigrations < RuboCop::Cop::Base
|
||||
MSG = 'Migration is deprecated and can not be used with `migration_has_finished?`.'
|
||||
DEPRECATED_MIGRATIONS = [
|
||||
:backfill_project_permissions_in_blobs_using_permutations,
|
||||
:backfill_archived_on_issues
|
||||
].freeze
|
||||
|
||||
|
|
|
|||
|
|
@ -199,67 +199,6 @@ RSpec.describe Projects::MirrorsController, feature_category: :source_code_manag
|
|||
end
|
||||
end
|
||||
|
||||
context 'when feature flag "use_remote_mirror_create_service" is disabled' do
|
||||
before do
|
||||
stub_feature_flags(use_remote_mirror_create_service: false)
|
||||
end
|
||||
|
||||
context 'With valid URL for a push' do
|
||||
let(:remote_mirror_attributes) do
|
||||
{ "0" => { "enabled" => "0", url: 'https://updated.example.com' } }
|
||||
end
|
||||
|
||||
it 'processes a successful update' do
|
||||
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
|
||||
|
||||
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-push-remote-settings'))
|
||||
expect(flash[:notice]).to match(/successfully updated/)
|
||||
end
|
||||
|
||||
it 'creates a RemoteMirror object' do
|
||||
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
|
||||
end
|
||||
|
||||
context 'with json format' do
|
||||
it 'processes a successful update' do
|
||||
do_put(project, { remote_mirrors_attributes: remote_mirror_attributes }, { format: :json })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to include(
|
||||
'id' => project.id,
|
||||
'remote_mirrors_attributes' => a_kind_of(Array)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'With invalid URL for a push' do
|
||||
let(:remote_mirror_attributes) do
|
||||
{ "0" => { "enabled" => "0", url: 'ftp://invalid.invalid' } }
|
||||
end
|
||||
|
||||
it 'processes an unsuccessful update' do
|
||||
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
|
||||
|
||||
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-push-remote-settings'))
|
||||
expect(flash[:alert]).to match(/Only allowed schemes are/)
|
||||
end
|
||||
|
||||
it 'does not create a RemoteMirror object' do
|
||||
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
|
||||
end
|
||||
|
||||
context 'with json format' do
|
||||
it 'processes an unsuccessful update' do
|
||||
do_put(project, { remote_mirrors_attributes: remote_mirror_attributes }, { format: :json })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(json_response['remote_mirrors.url']).to include(/Only allowed schemes are/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user deletes the remote mirror' do
|
||||
let(:remote_mirror_attributes) do
|
||||
{ id: project.remote_mirrors.first.id, _destroy: 1 }
|
||||
|
|
|
|||
|
|
@ -33,5 +33,9 @@ FactoryBot.define do
|
|||
trait :skipped do
|
||||
status { -2 }
|
||||
end
|
||||
|
||||
trait :canceled do
|
||||
status { -3 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ FactoryBot.define do
|
|||
batched { true }
|
||||
end
|
||||
|
||||
trait :canceled do
|
||||
status { -3 }
|
||||
end
|
||||
|
||||
trait :stale do
|
||||
created_at { 1.day.ago }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :debian_package, class: 'Packages::Debian::Package' do
|
||||
project
|
||||
creator { project&.creator }
|
||||
status { :default }
|
||||
sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
|
||||
sequence(:version) { |n| "1.0-#{n}" }
|
||||
package_type { :debian }
|
||||
|
||||
trait :pending_destruction do
|
||||
status { :pending_destruction }
|
||||
end
|
||||
|
||||
transient do
|
||||
without_package_files { false }
|
||||
with_changes_file { false }
|
||||
file_metadatum_trait { processing? ? :unknown : :keep }
|
||||
published_in { :create }
|
||||
end
|
||||
|
||||
publication do
|
||||
if published_in == :create
|
||||
association(:debian_publication, package: instance)
|
||||
elsif published_in
|
||||
association(:debian_publication, package: instance, distribution: published_in)
|
||||
end
|
||||
end
|
||||
|
||||
package_files do
|
||||
package_files = []
|
||||
|
||||
unless without_package_files
|
||||
package_files.push(
|
||||
association(:debian_package_file, :source, file_metadatum_trait, package: instance),
|
||||
association(:debian_package_file, :dsc, file_metadatum_trait, package: instance),
|
||||
association(:debian_package_file, :deb, file_metadatum_trait, package: instance),
|
||||
association(:debian_package_file, :deb_dev, file_metadatum_trait, package: instance),
|
||||
association(:debian_package_file, :udeb, file_metadatum_trait, package: instance),
|
||||
association(:debian_package_file, :ddeb, file_metadatum_trait, package: instance),
|
||||
association(:debian_package_file, :buildinfo, file_metadatum_trait, package: instance)
|
||||
)
|
||||
end
|
||||
|
||||
if with_changes_file
|
||||
package_files.push(association(:debian_package_file, :changes, file_metadatum_trait, package: instance))
|
||||
end
|
||||
|
||||
package_files
|
||||
end
|
||||
|
||||
factory :debian_incoming do
|
||||
name { 'incoming' }
|
||||
version { nil }
|
||||
|
||||
transient do
|
||||
without_package_files { false }
|
||||
file_metadatum_trait { :unknown }
|
||||
published_in { nil }
|
||||
end
|
||||
end
|
||||
|
||||
factory :debian_temporary_with_files do
|
||||
status { :processing }
|
||||
|
||||
transient do
|
||||
without_package_files { false }
|
||||
with_changes_file { false }
|
||||
file_metadatum_trait { :unknown }
|
||||
published_in { nil }
|
||||
end
|
||||
end
|
||||
|
||||
factory :debian_temporary_with_changes do
|
||||
status { :processing }
|
||||
|
||||
transient do
|
||||
without_package_files { true }
|
||||
with_changes_file { true }
|
||||
file_metadatum_trait { :unknown }
|
||||
published_in { nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -43,80 +43,6 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
factory :debian_package do
|
||||
sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
|
||||
sequence(:version) { |n| "1.0-#{n}" }
|
||||
package_type { :debian }
|
||||
|
||||
transient do
|
||||
without_package_files { false }
|
||||
with_changes_file { false }
|
||||
file_metadatum_trait { processing? ? :unknown : :keep }
|
||||
published_in { :create }
|
||||
end
|
||||
|
||||
after :build do |package, evaluator|
|
||||
if evaluator.published_in == :create
|
||||
build(:debian_publication, package: package)
|
||||
elsif !evaluator.published_in.nil?
|
||||
create(:debian_publication, package: package, distribution: evaluator.published_in)
|
||||
end
|
||||
end
|
||||
|
||||
after :create do |package, evaluator|
|
||||
if evaluator.published_in == :create
|
||||
package.debian_publication.save!
|
||||
end
|
||||
|
||||
unless evaluator.without_package_files
|
||||
create :debian_package_file, :source, evaluator.file_metadatum_trait, package: package
|
||||
create :debian_package_file, :dsc, evaluator.file_metadatum_trait, package: package
|
||||
create :debian_package_file, :deb, evaluator.file_metadatum_trait, package: package
|
||||
create :debian_package_file, :deb_dev, evaluator.file_metadatum_trait, package: package
|
||||
create :debian_package_file, :udeb, evaluator.file_metadatum_trait, package: package
|
||||
create :debian_package_file, :ddeb, evaluator.file_metadatum_trait, package: package
|
||||
create :debian_package_file, :buildinfo, evaluator.file_metadatum_trait, package: package
|
||||
end
|
||||
|
||||
if evaluator.with_changes_file
|
||||
create :debian_package_file, :changes, evaluator.file_metadatum_trait, package: package
|
||||
end
|
||||
end
|
||||
|
||||
factory :debian_incoming do
|
||||
name { 'incoming' }
|
||||
version { nil }
|
||||
|
||||
transient do
|
||||
without_package_files { false }
|
||||
file_metadatum_trait { :unknown }
|
||||
published_in { nil }
|
||||
end
|
||||
end
|
||||
|
||||
factory :debian_temporary_with_files do
|
||||
status { :processing }
|
||||
|
||||
transient do
|
||||
without_package_files { false }
|
||||
with_changes_file { false }
|
||||
file_metadatum_trait { :unknown }
|
||||
published_in { nil }
|
||||
end
|
||||
end
|
||||
|
||||
factory :debian_temporary_with_changes do
|
||||
status { :processing }
|
||||
|
||||
transient do
|
||||
without_package_files { true }
|
||||
with_changes_file { true }
|
||||
file_metadatum_trait { :unknown }
|
||||
published_in { nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
factory :helm_package do
|
||||
sequence(:name) { |n| "package-#{n}" }
|
||||
sequence(:version) { |n| "v1.0.#{n}" }
|
||||
|
|
|
|||
|
|
@ -150,10 +150,6 @@ FactoryBot.define do
|
|||
avatar { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
end
|
||||
|
||||
trait :with_last_activity_on_today do
|
||||
last_activity_on { Date.today }
|
||||
end
|
||||
|
||||
trait :with_sign_ins do
|
||||
sign_in_count { 3 }
|
||||
current_sign_in_at { FFaker::Time.between(10.days.ago, 1.day.ago) }
|
||||
|
|
|
|||
|
|
@ -590,18 +590,42 @@ describe('diffs/components/app', () => {
|
|||
expect(wrapper.findComponent(DiffsFileTree).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass renderDiffFiles to file tree as true when files are present', () => {
|
||||
it('should pass visible to file tree as true when files are present', () => {
|
||||
createComponent({
|
||||
extendStore: ({ state }) => {
|
||||
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
|
||||
},
|
||||
});
|
||||
expect(wrapper.findComponent(DiffsFileTree).props('renderDiffFiles')).toBe(true);
|
||||
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass renderDiffFiles to file tree as false without files', () => {
|
||||
it('should pass visible to file tree as false without files', () => {
|
||||
createComponent({});
|
||||
expect(wrapper.findComponent(DiffsFileTree).props('renderDiffFiles')).toBe(false);
|
||||
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(false);
|
||||
});
|
||||
|
||||
it('should hide file tree when toggled', async () => {
|
||||
createComponent({
|
||||
extendStore: ({ state }) => {
|
||||
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
|
||||
},
|
||||
});
|
||||
wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(false);
|
||||
});
|
||||
|
||||
it('should show file tree when toggled', async () => {
|
||||
createComponent({
|
||||
extendStore: ({ state }) => {
|
||||
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
|
||||
},
|
||||
});
|
||||
wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
|
||||
await nextTick();
|
||||
wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,14 @@ import { Mousetrap } from '~/lib/mousetrap';
|
|||
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
|
||||
import TreeList from '~/diffs/components/tree_list.vue';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { SET_SHOW_TREE_LIST } from '~/diffs/store/mutation_types';
|
||||
import createDiffsStore from '../create_diffs_store';
|
||||
|
||||
describe('DiffsFileTree', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const createComponent = ({ renderDiffFiles = true, showTreeList = true } = {}) => {
|
||||
store = createDiffsStore();
|
||||
store.commit(`diffs/${SET_SHOW_TREE_LIST}`, showTreeList);
|
||||
const createComponent = ({ visible = true } = {}) => {
|
||||
wrapper = shallowMount(DiffsFileTree, {
|
||||
store,
|
||||
propsData: {
|
||||
renderDiffFiles,
|
||||
visible,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -35,7 +29,7 @@ describe('DiffsFileTree', () => {
|
|||
|
||||
describe('when renderDiffFiles and showTreeList are false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ renderDiffFiles: false, showTreeList: false });
|
||||
createComponent({ visible: false });
|
||||
});
|
||||
|
||||
it('tree list is hidden', () => {
|
||||
|
|
@ -44,18 +38,11 @@ describe('DiffsFileTree', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('emits toggled event', async () => {
|
||||
createComponent();
|
||||
store.commit(`diffs/${SET_SHOW_TREE_LIST}`, false);
|
||||
await nextTick();
|
||||
expect(wrapper.emitted('toggled')).toStrictEqual([[]]);
|
||||
});
|
||||
|
||||
it('toggles when "f" hotkey is pressed', async () => {
|
||||
createComponent();
|
||||
Mousetrap.trigger('f');
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(TreeList).exists()).toBe(false);
|
||||
expect(wrapper.emitted('toggled')).toStrictEqual([[]]);
|
||||
});
|
||||
|
||||
describe('size', () => {
|
||||
|
|
@ -84,13 +71,6 @@ describe('DiffsFileTree', () => {
|
|||
checkWidth(200);
|
||||
});
|
||||
|
||||
it('sets width of tree list', () => {
|
||||
createComponent({}, ({ state }) => {
|
||||
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
|
||||
});
|
||||
checkWidth(320);
|
||||
});
|
||||
|
||||
it('updates width', async () => {
|
||||
const WIDTH = 500;
|
||||
createComponent();
|
||||
|
|
|
|||
|
|
@ -8,12 +8,17 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import MergeRequestTabs, { getActionFromHref } from '~/merge_request_tabs';
|
||||
import Diff from '~/diff';
|
||||
import '~/lib/utils/common_utils';
|
||||
import '~/lib/utils/url_utility';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
jest.mock('~/lib/utils/webpack', () => ({
|
||||
resetServiceWorkersPublicPath: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
...jest.requireActual('~/lib/utils/url_utility'),
|
||||
visitUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MergeRequestTabs', () => {
|
||||
const testContext = {};
|
||||
const stubLocation = {};
|
||||
|
|
@ -127,7 +132,7 @@ describe('MergeRequestTabs', () => {
|
|||
testContext.class.bindEvents();
|
||||
$('.merge-request-tabs .commits-tab a').trigger(metakeyEvent);
|
||||
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
|
||||
});
|
||||
|
||||
it('opens page when commits badge is clicked', () => {
|
||||
|
|
@ -139,7 +144,7 @@ describe('MergeRequestTabs', () => {
|
|||
testContext.class.bindEvents();
|
||||
$('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent);
|
||||
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -151,7 +156,7 @@ describe('MergeRequestTabs', () => {
|
|||
|
||||
testContext.class.clickTab({ ...clickTabParams, metaKey: true });
|
||||
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
|
||||
});
|
||||
|
||||
it('opens page tab in a new browser tab with Cmd+Click - Mac', () => {
|
||||
|
|
@ -162,7 +167,7 @@ describe('MergeRequestTabs', () => {
|
|||
|
||||
testContext.class.clickTab({ ...clickTabParams, ctrlKey: true });
|
||||
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
|
||||
});
|
||||
|
||||
it('opens page tab in a new browser tab with Middle-click - Mac/PC', () => {
|
||||
|
|
@ -173,7 +178,7 @@ describe('MergeRequestTabs', () => {
|
|||
|
||||
testContext.class.clickTab({ ...clickTabParams, which: 2 });
|
||||
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(visitUrl).toHaveBeenCalledWith(expect.any(String), true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,22 +10,22 @@ describe('DateRangeFilter', () => {
|
|||
|
||||
let wrapper;
|
||||
|
||||
const defaultTimeRange = {
|
||||
value: '1h',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
const defaultProps = {
|
||||
selected: {
|
||||
value: '1h',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
const mount = (selected) => {
|
||||
const mount = (props = defaultProps) => {
|
||||
wrapper = shallowMountExtended(DateRangeFilter, {
|
||||
propsData: {
|
||||
selected,
|
||||
},
|
||||
propsData: props,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mount(defaultTimeRange);
|
||||
mount();
|
||||
});
|
||||
|
||||
const findDateRangesDropdown = () => wrapper.findComponent(DateRangesDropdown);
|
||||
|
|
@ -34,7 +34,7 @@ describe('DateRangeFilter', () => {
|
|||
it('renders the date ranges dropdown with the default selected value and options', () => {
|
||||
const dateRangesDropdown = findDateRangesDropdown();
|
||||
expect(dateRangesDropdown.exists()).toBe(true);
|
||||
expect(dateRangesDropdown.props('selected')).toBe(defaultTimeRange.value);
|
||||
expect(dateRangesDropdown.props('selected')).toBe(defaultProps.selected.value);
|
||||
expect(dateRangesDropdown.props('dateRangeOptions')).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
|
@ -101,8 +101,23 @@ describe('DateRangeFilter', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('renders dateRangeOptions based on dateOptions if specified', () => {
|
||||
mount({ ...defaultProps, dateOptions: [{ value: '7m', title: 'Last 7 minutes' }] });
|
||||
|
||||
expect(findDateRangesDropdown().props('dateRangeOptions')).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"endDate": 2024-04-23T04:00:00.000Z,
|
||||
"startDate": 2024-04-23T03:53:00.000Z,
|
||||
"text": "Last 7 minutes",
|
||||
"value": "7m",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not set the selected value if not specified', () => {
|
||||
mount(undefined);
|
||||
mount({ selected: undefined });
|
||||
|
||||
expect(findDateRangesDropdown().props('selected')).toBe('');
|
||||
});
|
||||
|
|
@ -112,7 +127,9 @@ describe('DateRangeFilter', () => {
|
|||
startDate: new Date('2022-01-01'),
|
||||
endDate: new Date('2022-01-02'),
|
||||
};
|
||||
mount({ value: 'custom', startDate: timeRange.startDate, endDate: timeRange.endDate });
|
||||
mount({
|
||||
selected: { value: 'custom', startDate: timeRange.startDate, endDate: timeRange.endDate },
|
||||
});
|
||||
|
||||
expect(findDateRangesPicker().exists()).toBe(true);
|
||||
expect(findDateRangesPicker().props('defaultStartDate')).toBe(timeRange.startDate);
|
||||
|
|
@ -153,17 +170,19 @@ describe('DateRangeFilter', () => {
|
|||
});
|
||||
|
||||
describe('start opened', () => {
|
||||
it('sets startOpend to true if custom date is selected without start and end date', () => {
|
||||
mount({ value: 'custom' });
|
||||
it('sets startOpened to true if custom date is selected without start and end date', () => {
|
||||
mount({ selected: { value: 'custom' } });
|
||||
|
||||
expect(findDateRangesPicker().props('startOpened')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets startOpend to false if custom date is selected with start and end date', () => {
|
||||
it('sets startOpened to false if custom date is selected with start and end date', () => {
|
||||
mount({
|
||||
value: 'custom',
|
||||
startDate: new Date('2022-01-01'),
|
||||
endDate: new Date('2022-01-02'),
|
||||
selected: {
|
||||
value: 'custom',
|
||||
startDate: new Date('2022-01-01'),
|
||||
endDate: new Date('2022-01-02'),
|
||||
},
|
||||
});
|
||||
|
||||
expect(findDateRangesPicker().props('startOpened')).toBe(false);
|
||||
|
|
@ -183,4 +202,16 @@ describe('DateRangeFilter', () => {
|
|||
'2024-04-24T00:00:00.000Z',
|
||||
);
|
||||
});
|
||||
it('sets max-date-range to maxDateRange', () => {
|
||||
mount({
|
||||
selected: {
|
||||
value: 'custom',
|
||||
startDate: new Date('2022-01-01'),
|
||||
endDate: new Date('2022-01-02'),
|
||||
},
|
||||
maxDateRange: 7,
|
||||
});
|
||||
|
||||
expect(findDateRangesPicker().props('maxDateRange')).toBe(7);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue