Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
83cddbd523
commit
65a0673d76
|
|
@ -222,6 +222,10 @@ export default {
|
|||
step21: () => import(/* webpackChunkName: 'hl-step21' */ 'highlight.js/lib/languages/step21'),
|
||||
stylus: () => import(/* webpackChunkName: 'hl-stylus' */ 'highlight.js/lib/languages/stylus'),
|
||||
subunit: () => import(/* webpackChunkName: 'hl-subunit' */ 'highlight.js/lib/languages/subunit'),
|
||||
svelte: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'hl-svelte' */ '~/vue_shared/components/source_viewer/languages/svelte'
|
||||
),
|
||||
swift: () => import(/* webpackChunkName: 'hl-swift' */ 'highlight.js/lib/languages/swift'),
|
||||
taggerscript: () =>
|
||||
import(/* webpackChunkName: 'hl-taggerscript' */ 'highlight.js/lib/languages/taggerscript'),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import DiffCodeQualityItem from './diff_code_quality_item.vue';
|
||||
import DiffInlineFindingsItem from './diff_inline_findings_item.vue';
|
||||
|
||||
export default {
|
||||
components: { DiffCodeQualityItem },
|
||||
components: { DiffInlineFindingsItem },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
|
|
@ -22,7 +22,7 @@ export default {
|
|||
{{ title }}
|
||||
</h4>
|
||||
<ul class="gl-list-style-none gl-mb-0 gl-p-0">
|
||||
<diff-code-quality-item
|
||||
<diff-inline-findings-item
|
||||
v-for="finding in findings"
|
||||
:key="finding.description"
|
||||
:finding="finding"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default {
|
|||
:size="12"
|
||||
:name="enhancedFinding.name"
|
||||
:class="enhancedFinding.class"
|
||||
class="codequality-severity-icon"
|
||||
class="inline-findings-severity-icon"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
|
|
@ -68,7 +68,7 @@ export default {
|
|||
:size="12"
|
||||
:name="severityIcon(drawer.severity)"
|
||||
:class="severityClass(drawer.severity)"
|
||||
class="codequality-severity-icon"
|
||||
class="inline-findings-severity-icon"
|
||||
/>
|
||||
|
||||
{{ drawer.severity }}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/h
|
|||
import LineHighlighter from '~/blob/line_highlighter';
|
||||
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
|
||||
import Tracking from '~/tracking';
|
||||
import { TEXT_FILE_TYPE } from '../constants';
|
||||
|
||||
/*
|
||||
* This mixin is intended to be used as an interface between our highlight worker and Vue components
|
||||
|
|
@ -37,8 +36,8 @@ export default {
|
|||
this.trackEvent(EVENT_LABEL_FALLBACK, language);
|
||||
this?.onError();
|
||||
},
|
||||
initHighlightWorker({ rawTextBlob, language, simpleViewer, fileType }) {
|
||||
if (simpleViewer?.fileType !== TEXT_FILE_TYPE || !this.glFeatures.highlightJsWorker) return;
|
||||
initHighlightWorker({ rawTextBlob, language, fileType }) {
|
||||
if (language !== 'json' || !this.glFeatures.highlightJsWorker) return;
|
||||
|
||||
if (this.isUnsupportedLanguage(language)) {
|
||||
this.handleUnsupportedLanguage(language);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
<script>
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
|
||||
import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
|
||||
import { STATUS_OPEN, STATUS_CLOSED, STATUS_ALL } from '~/issues/constants';
|
||||
import {
|
||||
OPERATORS_IS_NOT,
|
||||
OPERATORS_IS_NOT_OR,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_USER } from '~/graphql_shared/constants';
|
||||
import searchUsersQuery from '~/issues/list/queries/search_users.query.graphql';
|
||||
import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
|
||||
import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
|
||||
import getServiceDeskIssuesQuery from '../queries/get_service_desk_issues.query.graphql';
|
||||
import getServiceDeskIssuesCounts from '../queries/get_service_desk_issues_counts.query.graphql';
|
||||
import {
|
||||
|
|
@ -13,7 +24,21 @@ import {
|
|||
noSearchNoFilterTitle,
|
||||
searchPlaceholder,
|
||||
SERVICE_DESK_BOT_USERNAME,
|
||||
MAX_LIST_SIZE,
|
||||
STATUS_OPEN,
|
||||
STATUS_CLOSED,
|
||||
STATUS_ALL,
|
||||
WORKSPACE_PROJECT,
|
||||
} from '../constants';
|
||||
import {
|
||||
searchWithinTokenBase,
|
||||
assigneeTokenBase,
|
||||
milestoneTokenBase,
|
||||
labelTokenBase,
|
||||
releaseTokenBase,
|
||||
reactionTokenBase,
|
||||
confidentialityTokenBase,
|
||||
} from '../search_tokens';
|
||||
import InfoBanner from './info_banner.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -29,7 +54,12 @@ export default {
|
|||
IssuableList,
|
||||
InfoBanner,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: [
|
||||
'releasesPath',
|
||||
'autocompleteAwardEmojisPath',
|
||||
'hasIterationsFeature',
|
||||
'groupPath',
|
||||
'emptyStateSvgPath',
|
||||
'isProject',
|
||||
'isSignedIn',
|
||||
|
|
@ -41,7 +71,6 @@ export default {
|
|||
return {
|
||||
serviceDeskIssues: [],
|
||||
serviceDeskIssuesCounts: {},
|
||||
searchTokens: [],
|
||||
sortOptions: [],
|
||||
state: STATUS_OPEN,
|
||||
issuesError: null,
|
||||
|
|
@ -112,8 +141,134 @@ export default {
|
|||
isInfoBannerVisible() {
|
||||
return this.isServiceDeskSupported && this.hasAnyIssues;
|
||||
},
|
||||
hasOrFeature() {
|
||||
return this.glFeatures.orIssuableQueries;
|
||||
},
|
||||
searchTokens() {
|
||||
const preloadedUsers = [];
|
||||
|
||||
if (gon.current_user_id) {
|
||||
preloadedUsers.push({
|
||||
id: convertToGraphQLId(TYPENAME_USER, gon.current_user_id),
|
||||
name: gon.current_user_fullname,
|
||||
username: gon.current_username,
|
||||
avatar_url: gon.current_user_avatar_url,
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = [
|
||||
{
|
||||
...searchWithinTokenBase,
|
||||
},
|
||||
{
|
||||
...assigneeTokenBase,
|
||||
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
|
||||
fetchUsers: this.fetchUsers,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
|
||||
preloadedUsers,
|
||||
},
|
||||
{
|
||||
...milestoneTokenBase,
|
||||
fetchMilestones: this.fetchMilestones,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
|
||||
},
|
||||
{
|
||||
...labelTokenBase,
|
||||
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
|
||||
fetchLabels: this.fetchLabels,
|
||||
fetchLatestLabels: this.glFeatures.frontendCaching ? this.fetchLatestLabels : null,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.isProject) {
|
||||
tokens.push({
|
||||
...releaseTokenBase,
|
||||
fetchReleases: this.fetchReleases,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-release`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isSignedIn) {
|
||||
tokens.push({
|
||||
...reactionTokenBase,
|
||||
fetchEmojis: this.fetchEmojis,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
|
||||
});
|
||||
|
||||
tokens.push({
|
||||
...confidentialityTokenBase,
|
||||
});
|
||||
}
|
||||
|
||||
tokens.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return tokens;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.cache = {};
|
||||
},
|
||||
methods: {
|
||||
fetchWithCache(path, cacheName, searchKey, search) {
|
||||
if (this.cache[cacheName]) {
|
||||
const data = search
|
||||
? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
|
||||
: this.cache[cacheName].slice(0, MAX_LIST_SIZE);
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
return axios.get(path).then(({ data }) => {
|
||||
this.cache[cacheName] = data;
|
||||
return data.slice(0, MAX_LIST_SIZE);
|
||||
});
|
||||
},
|
||||
fetchUsers(search) {
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: searchUsersQuery,
|
||||
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
|
||||
})
|
||||
.then(({ data }) =>
|
||||
data[WORKSPACE_PROJECT]?.[`${WORKSPACE_PROJECT}Members`].nodes.map(
|
||||
(member) => member.user,
|
||||
),
|
||||
);
|
||||
},
|
||||
fetchMilestones(search) {
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: searchMilestonesQuery,
|
||||
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
|
||||
})
|
||||
.then(({ data }) => data[WORKSPACE_PROJECT]?.milestones.nodes);
|
||||
},
|
||||
fetchEmojis(search) {
|
||||
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
|
||||
},
|
||||
fetchReleases(search) {
|
||||
return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search);
|
||||
},
|
||||
fetchLabelsWithFetchPolicy(search, fetchPolicy = fetchPolicies.CACHE_FIRST) {
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: searchLabelsQuery,
|
||||
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
|
||||
fetchPolicy,
|
||||
})
|
||||
.then(({ data }) => data[WORKSPACE_PROJECT]?.labels.nodes)
|
||||
.then((labels) =>
|
||||
// TODO remove once we can search by title-only on the backend
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/346353
|
||||
labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())),
|
||||
);
|
||||
},
|
||||
fetchLabels(search) {
|
||||
return this.fetchLabelsWithFetchPolicy(search);
|
||||
},
|
||||
fetchLatestLabels(search) {
|
||||
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
|
||||
},
|
||||
handleClickTab(state) {
|
||||
if (this.state === state) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { __, s__ } from '~/locale';
|
||||
|
||||
export const SERVICE_DESK_BOT_USERNAME = 'support-bot';
|
||||
export const MAX_LIST_SIZE = 10;
|
||||
export const STATUS_ALL = 'all';
|
||||
export const STATUS_CLOSED = 'closed';
|
||||
export const STATUS_OPEN = 'opened';
|
||||
export const WORKSPACE_PROJECT = 'project';
|
||||
|
||||
export const errorFetchingCounts = __('An error occurred while getting issue counts');
|
||||
export const errorFetchingIssues = __('An error occurred while loading issues');
|
||||
|
|
@ -15,3 +20,7 @@ export const infoBannerUserNote = s__(
|
|||
);
|
||||
export const enableServiceDesk = s__('ServiceDesk|Enable Service Desk');
|
||||
export const learnMore = __('Learn more');
|
||||
export const titles = __('Titles');
|
||||
export const descriptions = __('Descriptions');
|
||||
export const no = __('No');
|
||||
export const yes = __('Yes');
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ export async function mountServiceDeskListApp() {
|
|||
}
|
||||
|
||||
const {
|
||||
projectDataReleasesPath,
|
||||
projectDataAutocompleteAwardEmojisPath,
|
||||
projectDataHasIterationsFeature,
|
||||
projectDataHasIssueWeightsFeature,
|
||||
projectDataHasIssuableHealthStatusFeature,
|
||||
projectDataGroupPath,
|
||||
projectDataEmptyStateSvgPath,
|
||||
projectDataFullPath,
|
||||
projectDataIsProject,
|
||||
|
|
@ -36,6 +42,12 @@ export async function mountServiceDeskListApp() {
|
|||
defaultClient: await gqlClient(),
|
||||
}),
|
||||
provide: {
|
||||
releasesPath: projectDataReleasesPath,
|
||||
autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath,
|
||||
hasIterationsFeature: parseBoolean(projectDataHasIterationsFeature),
|
||||
hasIssueWeightsFeature: parseBoolean(projectDataHasIssueWeightsFeature),
|
||||
hasIssuableHealthStatusFeature: parseBoolean(projectDataHasIssuableHealthStatusFeature),
|
||||
groupPath: projectDataGroupPath,
|
||||
emptyStateSvgPath: projectDataEmptyStateSvgPath,
|
||||
fullPath: projectDataFullPath,
|
||||
isProject: parseBoolean(projectDataIsProject),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
import { GlFilteredSearchToken } from '@gitlab/ui';
|
||||
import {
|
||||
OPERATORS_IS,
|
||||
TOKEN_TITLE_ASSIGNEE,
|
||||
TOKEN_TITLE_CONFIDENTIAL,
|
||||
TOKEN_TITLE_LABEL,
|
||||
TOKEN_TITLE_MILESTONE,
|
||||
TOKEN_TITLE_MY_REACTION,
|
||||
TOKEN_TITLE_RELEASE,
|
||||
TOKEN_TITLE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
TOKEN_TYPE_LABEL,
|
||||
TOKEN_TYPE_MILESTONE,
|
||||
TOKEN_TYPE_RELEASE,
|
||||
TOKEN_TYPE_MY_REACTION,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import { titles, descriptions, yes, no } from './constants';
|
||||
|
||||
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
|
||||
const EmojiToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
|
||||
const LabelToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
|
||||
const MilestoneToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
|
||||
const ReleaseToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue');
|
||||
|
||||
export const searchWithinTokenBase = {
|
||||
type: TOKEN_TYPE_SEARCH_WITHIN,
|
||||
title: TOKEN_TITLE_SEARCH_WITHIN,
|
||||
icon: 'search',
|
||||
token: GlFilteredSearchToken,
|
||||
unique: true,
|
||||
operators: OPERATORS_IS,
|
||||
options: [
|
||||
{ icon: 'title', value: 'TITLE', title: titles },
|
||||
{
|
||||
icon: 'text-description',
|
||||
value: 'DESCRIPTION',
|
||||
title: descriptions,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const assigneeTokenBase = {
|
||||
type: TOKEN_TYPE_ASSIGNEE,
|
||||
title: TOKEN_TITLE_ASSIGNEE,
|
||||
icon: 'user',
|
||||
token: UserToken,
|
||||
dataType: 'user',
|
||||
};
|
||||
|
||||
export const milestoneTokenBase = {
|
||||
type: TOKEN_TYPE_MILESTONE,
|
||||
title: TOKEN_TITLE_MILESTONE,
|
||||
icon: 'clock',
|
||||
token: MilestoneToken,
|
||||
shouldSkipSort: true,
|
||||
};
|
||||
|
||||
export const labelTokenBase = {
|
||||
type: TOKEN_TYPE_LABEL,
|
||||
title: TOKEN_TITLE_LABEL,
|
||||
icon: 'labels',
|
||||
token: LabelToken,
|
||||
};
|
||||
|
||||
export const releaseTokenBase = {
|
||||
type: TOKEN_TYPE_RELEASE,
|
||||
title: TOKEN_TITLE_RELEASE,
|
||||
icon: 'rocket',
|
||||
token: ReleaseToken,
|
||||
};
|
||||
|
||||
export const reactionTokenBase = {
|
||||
type: TOKEN_TYPE_MY_REACTION,
|
||||
title: TOKEN_TITLE_MY_REACTION,
|
||||
icon: 'thumb-up',
|
||||
token: EmojiToken,
|
||||
unique: true,
|
||||
};
|
||||
|
||||
export const confidentialityTokenBase = {
|
||||
type: TOKEN_TYPE_CONFIDENTIAL,
|
||||
title: TOKEN_TITLE_CONFIDENTIAL,
|
||||
icon: 'eye-slash',
|
||||
token: GlFilteredSearchToken,
|
||||
unique: true,
|
||||
operators: OPERATORS_IS,
|
||||
options: [
|
||||
{ icon: 'eye-slash', value: 'yes', title: yes },
|
||||
{ icon: 'eye', value: 'no', title: no },
|
||||
],
|
||||
};
|
||||
|
|
@ -97,6 +97,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
|
|||
sql: 'sql',
|
||||
stan: 'stan',
|
||||
stata: 'stata',
|
||||
svelte: 'svelte',
|
||||
swift: 'swift',
|
||||
tap: 'tap',
|
||||
tcl: 'tcl',
|
||||
|
|
@ -151,3 +152,5 @@ export const LEGACY_FALLBACKS = ['python', 'haml'];
|
|||
export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';
|
||||
|
||||
export const CODEOWNERS_LANGUAGE = 'codeowners';
|
||||
|
||||
export const SVELTE_LANGUAGE = 'svelte';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Language: Svelte.js
|
||||
Requires: xml, javascript, typescript, css, scss
|
||||
Description: Components of Svelte Framework
|
||||
*/
|
||||
|
||||
export default (hljs) => {
|
||||
return {
|
||||
subLanguage: 'xml',
|
||||
contains: [
|
||||
hljs.COMMENT('<!--', '-->', {
|
||||
relevance: 11,
|
||||
}),
|
||||
{
|
||||
begin: /^(\s*)(<script.*(lang="ts").*>)/gm,
|
||||
end: /^(\s*)(<\/script>)/gm,
|
||||
subLanguage: 'typescript',
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
relevance: 20,
|
||||
contains: [
|
||||
// special svelte $ syntax
|
||||
{
|
||||
begin: /^(\s*)(\$:)/gm,
|
||||
end: /(\s*)/gm,
|
||||
className: 'keyword',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
begin: /^(\s*)(<script(\s*context="module")?.*>)/gm,
|
||||
end: /^(\s*)(<\/script>)/gm,
|
||||
subLanguage: 'javascript',
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
relevance: 15,
|
||||
contains: [
|
||||
// special svelte $ syntax
|
||||
{
|
||||
begin: /^(\s*)(\$:)/gm,
|
||||
end: /(\s*)/gm,
|
||||
className: 'keyword',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
begin: /^(\s*)(<style.*(lang="scss"|type="text\/scss").*>)/gm,
|
||||
end: /^(\s*)(<\/style>)/gm,
|
||||
subLanguage: 'scss',
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
relevance: 20,
|
||||
},
|
||||
{
|
||||
begin: /^(\s*)(<style.*>)/gm,
|
||||
end: /^(\s*)(<\/style>)/gm,
|
||||
subLanguage: 'css',
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
relevance: 15,
|
||||
},
|
||||
{
|
||||
begin: /\{/gm,
|
||||
end: /}/gm,
|
||||
subLanguage: 'javascript',
|
||||
contains: [
|
||||
{
|
||||
begin: /[{]/,
|
||||
end: /[}]/,
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
begin: /([#:/@])(if|else|each|await|then|catch|debug|html)/gm,
|
||||
className: 'keyword',
|
||||
relevance: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
LEGACY_FALLBACKS,
|
||||
CODEOWNERS_FILE_NAME,
|
||||
CODEOWNERS_LANGUAGE,
|
||||
SVELTE_LANGUAGE,
|
||||
} from './constants';
|
||||
import Chunk from './components/chunk.vue';
|
||||
import { registerPlugins } from './plugins/index';
|
||||
|
|
@ -56,9 +57,15 @@ export default {
|
|||
return this.content.split(/\r?\n/);
|
||||
},
|
||||
language() {
|
||||
return this.blob.name === this.$options.codeownersFileName
|
||||
? this.$options.codeownersLanguage
|
||||
: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()];
|
||||
if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
|
||||
// override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
|
||||
return SVELTE_LANGUAGE;
|
||||
} else if (this.blob.name === this.$options.codeownersFileName) {
|
||||
// override for codeowners files
|
||||
return this.$options.codeownersLanguage;
|
||||
}
|
||||
|
||||
return ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()];
|
||||
},
|
||||
lineNumbers() {
|
||||
return this.splitContent.length;
|
||||
|
|
@ -168,12 +175,36 @@ export default {
|
|||
// If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint)
|
||||
return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
|
||||
},
|
||||
async loadSubLanguages(languageDefinition) {
|
||||
if (!languageDefinition?.contains) return;
|
||||
|
||||
// generate list of languages to load
|
||||
const languages = new Set(
|
||||
languageDefinition.contains
|
||||
.filter((component) => Boolean(component.subLanguage))
|
||||
.map((component) => component.subLanguage),
|
||||
);
|
||||
|
||||
if (languageDefinition.subLanguage) {
|
||||
languages.add(languageDefinition.subLanguage);
|
||||
}
|
||||
|
||||
// load all sub-languages at once
|
||||
await Promise.all(
|
||||
[...languages].map(async (subLanguage) => {
|
||||
const subLanguageDefinition = await languageLoader[subLanguage]();
|
||||
this.hljs.registerLanguage(subLanguage, subLanguageDefinition.default);
|
||||
}),
|
||||
);
|
||||
},
|
||||
async loadLanguage() {
|
||||
let languageDefinition;
|
||||
|
||||
try {
|
||||
languageDefinition = await languageLoader[this.language]();
|
||||
this.hljs.registerLanguage(this.language, languageDefinition.default);
|
||||
|
||||
await this.loadSubLanguages(this.hljs.getLanguage(this.language));
|
||||
} catch (message) {
|
||||
this.$emit('error', message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -543,8 +543,8 @@ class Namespace < ApplicationRecord
|
|||
def changing_allow_descendants_override_disabled_shared_runners_is_allowed
|
||||
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
|
||||
|
||||
if shared_runners_enabled && !new_record?
|
||||
errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled'))
|
||||
if shared_runners_enabled && allow_descendants_override_disabled_shared_runners
|
||||
errors.add(:allow_descendants_override_disabled_shared_runners, _('can not be true if shared runners are enabled'))
|
||||
end
|
||||
|
||||
if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Nuget
|
||||
module V2
|
||||
class ServiceIndexPresenter
|
||||
include API::Helpers::RelatedResourcesHelpers
|
||||
|
||||
ROOT_ATTRIBUTES = {
|
||||
xmlns: 'http://www.w3.org/2007/app',
|
||||
'xmlns:atom' => 'http://www.w3.org/2005/Atom'
|
||||
}.freeze
|
||||
|
||||
def initialize(project_or_group)
|
||||
@project_or_group = project_or_group
|
||||
end
|
||||
|
||||
def xml
|
||||
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
||||
xml.service(ROOT_ATTRIBUTES.merge('xml:base' => xml_base)) do
|
||||
xml.workspace do
|
||||
xml['atom'].title('Default', type: 'text')
|
||||
xml.collection(href: 'Packages') do
|
||||
xml['atom'].title('Packages', type: 'text')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project_or_group
|
||||
|
||||
def xml_base
|
||||
base_path = case project_or_group
|
||||
when Project
|
||||
api_v4_projects_packages_nuget_v2_path(id: project_or_group.id)
|
||||
when Group
|
||||
api_v4_groups___packages_nuget_v2_path(id: project_or_group.id)
|
||||
end
|
||||
|
||||
expose_url(base_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,15 +18,7 @@ module Projects
|
|||
end
|
||||
|
||||
def project_members
|
||||
@project_members ||= sorted(get_project_members)
|
||||
end
|
||||
|
||||
def get_project_members
|
||||
members = Member.from_union([project_members_through_ancestral_groups,
|
||||
project_members_through_invited_groups,
|
||||
individual_project_members])
|
||||
|
||||
User.id_in(members.select(:user_id))
|
||||
@project_members ||= sorted(project.authorized_users)
|
||||
end
|
||||
|
||||
def all_members
|
||||
|
|
@ -34,33 +26,5 @@ module Projects
|
|||
|
||||
[{ username: "all", name: "All Project and Group Members", count: project_members.count }]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_members_through_invited_groups
|
||||
GroupMember
|
||||
.active_without_invites_and_requests
|
||||
.with_source_id(visible_groups.self_and_ancestors.pluck_primary_key)
|
||||
.select(*GroupMember.cached_column_list)
|
||||
end
|
||||
|
||||
def visible_groups
|
||||
visible_groups = project.invited_groups
|
||||
|
||||
unless project.team.member?(current_user)
|
||||
visible_groups = visible_groups.public_or_visible_to_user(current_user)
|
||||
end
|
||||
|
||||
visible_groups
|
||||
end
|
||||
|
||||
def project_members_through_ancestral_groups
|
||||
members = project.group.present? ? project.group.members_with_parents : Member.none
|
||||
members.select(*GroupMember.cached_column_list)
|
||||
end
|
||||
|
||||
def individual_project_members
|
||||
project.project_members.select(*GroupMember.cached_column_list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
- can_edit_max_page_size=can?(current_user, :update_max_pages_size)
|
||||
- can_enforce_https_only=Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
|
||||
- can_edit_max_page_size = can?(current_user, :update_max_pages_size)
|
||||
- can_enforce_https_only = Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
|
||||
- can_edit_unique_domain = Feature.enabled?(:pages_unique_domain, @project)
|
||||
|
||||
- return unless can_edit_max_page_size || can_enforce_https_only
|
||||
- return unless can_edit_max_page_size || can_enforce_https_only || can_edit_unique_domain
|
||||
= gitlab_ui_form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f|
|
||||
- if can_edit_max_page_size
|
||||
= render_if_exists 'shared/pages/max_pages_size_input', form: f
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
%p.gl-pl-6
|
||||
= s_("GitLabPages|When enabled, all attempts to visit your website through HTTP are automatically redirected to HTTPS using a response with status code 301. Requires a valid certificate for all domains. %{docs_link_start}Learn more.%{link_end}").html_safe % { docs_link_start: docs_link_start, link_end: link_end }
|
||||
|
||||
- if Feature.enabled?(:pages_unique_domain, @project)
|
||||
- if can_edit_unique_domain
|
||||
.form-group
|
||||
= f.fields_for :project_setting do |settings|
|
||||
= settings.gitlab_ui_checkbox_component :pages_unique_domain_enabled,
|
||||
|
|
|
|||
|
|
@ -273,6 +273,8 @@
|
|||
- 1
|
||||
- - groups_create_event
|
||||
- 1
|
||||
- - groups_enterprise_users_disassociate
|
||||
- 1
|
||||
- - groups_export_memberships
|
||||
- 1
|
||||
- - groups_schedule_bulk_repository_shard_moves
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddRuleIdxToScanResultPolicies < Gitlab::Database::Migration[2.1]
|
||||
enable_lock_retries!
|
||||
|
||||
CONSTRAINT_NAME = "check_scan_result_policies_rule_idx_positive"
|
||||
|
||||
def up
|
||||
add_column :scan_result_policies, :rule_idx, :smallint
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :scan_result_policies, :rule_idx
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddRuleIdxConstraintToScanResultPolicies < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
CONSTRAINT_NAME = "check_scan_result_policies_rule_idx_positive"
|
||||
|
||||
def up
|
||||
add_check_constraint :scan_result_policies, "rule_idx IS NULL OR rule_idx >= 0", CONSTRAINT_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_check_constraint :scan_result_policies, CONSTRAINT_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqueIndexToScanResultPoliciesOnPositionInConfiguration < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_scan_result_policies_on_position_in_configuration'
|
||||
COLUMNS = %i[security_orchestration_policy_configuration_id project_id orchestration_policy_idx rule_idx]
|
||||
|
||||
def up
|
||||
add_concurrent_index :scan_result_policies, COLUMNS, unique: true, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :scan_result_policies, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
5a4aff0810f41646e25614ad26a826434eb28e85a8f1393e4e1799ce6ab61ff7
|
||||
|
|
@ -0,0 +1 @@
|
|||
ff621297609cbbcb09108981daacfacf341d1e39bbbbe71fe140c6a0b7af40eb
|
||||
|
|
@ -0,0 +1 @@
|
|||
20e8e9e72d25ed720d4aa8c3045a8cc99c6e34cc9ac4f0973e39627300866f30
|
||||
|
|
@ -22432,7 +22432,9 @@ CREATE TABLE scan_result_policies (
|
|||
age_interval smallint,
|
||||
vulnerability_attributes jsonb DEFAULT '{}'::jsonb,
|
||||
project_id bigint,
|
||||
CONSTRAINT age_value_null_or_positive CHECK (((age_value IS NULL) OR (age_value >= 0)))
|
||||
rule_idx smallint,
|
||||
CONSTRAINT age_value_null_or_positive CHECK (((age_value IS NULL) OR (age_value >= 0))),
|
||||
CONSTRAINT check_scan_result_policies_rule_idx_positive CHECK (((rule_idx IS NULL) OR (rule_idx >= 0)))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE scan_result_policies_id_seq
|
||||
|
|
@ -32933,6 +32935,8 @@ CREATE UNIQUE INDEX index_sbom_sources_on_source_type_and_source ON sbom_sources
|
|||
|
||||
CREATE INDEX index_scan_result_policies_on_policy_configuration_id ON scan_result_policies USING btree (security_orchestration_policy_configuration_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_scan_result_policies_on_position_in_configuration ON scan_result_policies USING btree (security_orchestration_policy_configuration_id, project_id, orchestration_policy_idx, rule_idx);
|
||||
|
||||
CREATE INDEX index_scan_result_policies_on_project_id ON scan_result_policies USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_schema_inconsistencies_on_issue_id ON schema_inconsistencies USING btree (issue_id);
|
||||
|
|
|
|||
|
|
@ -80,13 +80,22 @@ This writes the downloaded file to `MyNuGetPkg.1.3.0.17.nupkg` in the current di
|
|||
|
||||
## Upload a package file
|
||||
|
||||
> Introduced in GitLab 12.8.
|
||||
> - Introduced in GitLab 12.8 for NuGet v3 feed.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2 for NuGet v2 feed.
|
||||
|
||||
Upload a NuGet package file:
|
||||
|
||||
```plaintext
|
||||
PUT projects/:id/packages/nuget
|
||||
```
|
||||
- For NuGet v3 feed:
|
||||
|
||||
```plaintext
|
||||
PUT projects/:id/packages/nuget
|
||||
```
|
||||
|
||||
- For NuGet V2 feed:
|
||||
|
||||
```plaintext
|
||||
PUT projects/:id/packages/nuget/v2
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ----------- |
|
||||
|
|
@ -95,12 +104,23 @@ PUT projects/:id/packages/nuget
|
|||
| `package_version` | string | yes | The version of the package. |
|
||||
| `package_filename`| string | yes | The name of the file. |
|
||||
|
||||
```shell
|
||||
curl --request PUT \
|
||||
--form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
|
||||
--user <username>:<personal_access_token> \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/nuget/"
|
||||
```
|
||||
- For NuGet v3 feed:
|
||||
|
||||
```shell
|
||||
curl --request PUT \
|
||||
--form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
|
||||
--user <username>:<personal_access_token> \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/nuget/"
|
||||
```
|
||||
|
||||
- For NuGet v2 feed:
|
||||
|
||||
```shell
|
||||
curl --request PUT \
|
||||
--form 'package=@path/to/mynugetpkg.1.3.0.17.nupkg' \
|
||||
--user <username>:<personal_access_token> \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"
|
||||
```
|
||||
|
||||
## Upload a symbol package file
|
||||
|
||||
|
|
@ -158,6 +178,37 @@ The examples in this document all use the project-level prefix.
|
|||
|
||||
## Service Index
|
||||
|
||||
### V2 source feed/protocol
|
||||
|
||||
Returns an XML document that represents the service index of the v2 NuGet source feed.
|
||||
Authentication is not required:
|
||||
|
||||
```plaintext
|
||||
GET <route-prefix>/v2
|
||||
```
|
||||
|
||||
Example Request:
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xml:base="https://gitlab.example.com/api/v4/projects/1/packages/nuget/v2">
|
||||
<workspace>
|
||||
<atom:title type="text">Default</atom:title>
|
||||
<collection href="Packages">
|
||||
<atom:title type="text">Packages</atom:title>
|
||||
</collection>
|
||||
</workspace>
|
||||
</service>
|
||||
```
|
||||
|
||||
### V3 source feed/protocol
|
||||
|
||||
> - Introduced in GitLab 12.6.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214674) to be public in GitLab 16.1.
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ EE: true
|
|||
uses system fonts for all text."
|
||||
- Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry.
|
||||
See the [complete list what comprises a GraphQL breaking change](api_graphql_styleguide.md#breaking-changes).
|
||||
- Any change that introduces an [advanced search migration](search/advanced_search_migration_styleguide.md#creating-a-new-advanced-search-migration)
|
||||
- Any change that introduces an [advanced search migration](search/advanced_search_migration_styleguide.md#create-a-new-advanced-search-migration)
|
||||
**must** have a changelog entry.
|
||||
- A fix for a regression introduced and then fixed in the same release (such as
|
||||
fixing a bug introduced during a monthly release candidate) **should not**
|
||||
|
|
|
|||
|
|
@ -6,13 +6,25 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Advanced search migration style guide
|
||||
|
||||
## Creating a new advanced search migration
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6.
|
||||
## Create a new advanced search migration
|
||||
|
||||
NOTE:
|
||||
This functionality is only supported for indices created in GitLab 13.0 and later.
|
||||
|
||||
### With a script
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414674) in GitLab 16.3.
|
||||
|
||||
Execute `scripts/elastic-migration` and follow the prompts to create:
|
||||
|
||||
- A migration file to define the migration: `ee/elastic/migrate/YYYYMMDDHHMMSS_migration_name.rb`
|
||||
- A spec file to test the migration: `ee/spec/elastic/migrate/YYYYMMDDHHMMSS_migration_name_spec.rb`
|
||||
- A dictionary file to identify the migration: `ee/elastic/docs/YYYYMMDDHHMMSS_migration_name.yml`
|
||||
|
||||
### Manually
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/234046) in GitLab 13.6.
|
||||
|
||||
In the [`ee/elastic/migrate/`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/elastic/migrate) folder, create a new file with the filename format `YYYYMMDDHHMMSS_migration_name.rb`. This format is the same for Rails database migrations.
|
||||
|
||||
```ruby
|
||||
|
|
|
|||
|
|
@ -527,7 +527,29 @@ With reindex migrations running in the background, there's no need for a manual
|
|||
intervention. This usually happens in situations where new features are added to
|
||||
advanced search, which means adding or changing the way content is indexed.
|
||||
|
||||
To confirm that the advanced search migrations ran, you can check with:
|
||||
### Migration dictionary files
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414674) in GitLab 16.3.
|
||||
|
||||
Every migration has a corresponding dictionary file in the `ee/elastic/docs/` folder with the following information:
|
||||
|
||||
```yaml
|
||||
name:
|
||||
version:
|
||||
description:
|
||||
group:
|
||||
milestone:
|
||||
introduced_by_url:
|
||||
obsolete:
|
||||
marked_obsolete_by_url:
|
||||
marked_obsolete_in_milestone:
|
||||
```
|
||||
|
||||
You can use this information, for example, to identify when a migration was introduced or was marked as obsolete.
|
||||
|
||||
### Check for pending migrations
|
||||
|
||||
To check for pending advanced search migrations, run this command:
|
||||
|
||||
```shell
|
||||
curl "$CLUSTER_URL/gitlab-production-migrations/_search?q=*" | jq .
|
||||
|
|
@ -566,7 +588,7 @@ This should return something similar to:
|
|||
}
|
||||
```
|
||||
|
||||
To debug issues with the migrations you can check the [`elasticsearch.log` file](../../administration/logs/index.md#elasticsearchlog).
|
||||
To debug issues with the migrations, check the [`elasticsearch.log`](../../administration/logs/index.md#elasticsearchlog) file.
|
||||
|
||||
### Retry a halted migration
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ Prerequisite:
|
|||
To view a list of dashboards (both built-in and custom) for a project:
|
||||
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. Select **Analyze > Dashboards**.
|
||||
1. Select **Analyze > Analytics dashboards**.
|
||||
1. From the list of available dashboards, select the dashboard you want to view.
|
||||
|
||||
## Change the location of dashboards
|
||||
|
|
@ -174,7 +174,7 @@ create a `line_chart.yaml` file with the following required fields:
|
|||
To create a custom dashboard:
|
||||
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. Select **Analyze > Dashboards**.
|
||||
1. Select **Analyze > Analytics dashboards**.
|
||||
1. Select **New dashboard**.
|
||||
1. In the **New dashboard** input, enter the name of the dashboard.
|
||||
1. From the **Add visualizations** list on the right, select the visualizations to add to the dashboard.
|
||||
|
|
@ -188,7 +188,7 @@ You can edit your custom dashboard's title and add or resize visualizations in t
|
|||
To edit an existing custom dashboard:
|
||||
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. Select **Analyze > Dashboards**.
|
||||
1. Select **Analyze > Analytics dashboards**.
|
||||
1. From the list of available dashboards, select a custom dashboard (one without the `By GitLab` label) you want to edit.
|
||||
1. Select **Edit**.
|
||||
1. Optional. Change the title of the dashboard.
|
||||
|
|
|
|||
|
|
@ -225,8 +225,7 @@ After you set up your identity provider to work with GitLab, you must configure
|
|||
1. In the **Default membership role** field, select the role to assign to new users.
|
||||
The default role is **Guest**. In [GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/214523)
|
||||
and later, group owners can set a default membership role other than **Guest**.
|
||||
To do so, [configure the SAML SSO for the group](#configure-gitlab). That role
|
||||
becomes the starting role of all users added to the group.
|
||||
That role becomes the starting role of all users added to the group.
|
||||
1. Select the **Enable SAML authentication for this group** checkbox.
|
||||
1. Optional. Select:
|
||||
- **Enforce SSO-only authentication for web activity for this group**.
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ You can now add a new source to NuGet with:
|
|||
- [Visual Studio](#add-a-source-with-visual-studio)
|
||||
- [.NET CLI](#add-a-source-with-the-net-cli)
|
||||
- [Configuration file](#add-a-source-with-a-configuration-file)
|
||||
- [Chocolatey CLI](#add-a-source-with-chocolatey-cli)
|
||||
|
||||
### Add a source with the NuGet CLI
|
||||
|
||||
|
|
@ -281,6 +282,22 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Re
|
|||
export GITLAB_PACKAGE_REGISTRY_PASSWORD=<gitlab_personal_access_token or deploy_token>
|
||||
```
|
||||
|
||||
### Add a source with Chocolatey CLI
|
||||
|
||||
You can add a source feed with the Chocolatey CLI. If you use Chocolatey CLI v1.x, you can add only a NuGet v2 source feed.
|
||||
|
||||
#### Configure a project-level endpoint
|
||||
|
||||
You need a project-level endpoint to publish NuGet packages to the Package Registry.
|
||||
|
||||
To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Registry as a source for Chocolatey:
|
||||
|
||||
- Add the Package Registry as a source with `choco`:
|
||||
|
||||
```shell
|
||||
choco source add -n=gitlab -s "'https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2'" -u=<gitlab_username or deploy_token_username> -p=<gitlab_personal_access_token or deploy_token>
|
||||
```
|
||||
|
||||
## Publish a NuGet package
|
||||
|
||||
Prerequisite:
|
||||
|
|
@ -385,6 +402,31 @@ updated:
|
|||
|
||||
1. Commit the changes and push it to your GitLab repository to trigger a new CI/CD build.
|
||||
|
||||
### Publish a NuGet package with Chocolatey CLI
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416404) in GitLab 16.2.
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- The project-level Package Registry is a source for Chocolatey.
|
||||
|
||||
To publish a package with the Chocolatey CLI:
|
||||
|
||||
```shell
|
||||
choco push <package_file> --source <source_url> --api-key <gitlab_personal_access_token, deploy_token or job token>
|
||||
```
|
||||
|
||||
In this command:
|
||||
|
||||
- `<package_file>` is your package filename and ends with `.nupkg`.
|
||||
- `<source_url>` is the URL of the NuGet v2 feed Package Registry.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
choco push MyPackage.1.0.0.nupkg --source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/v2" --api-key <gitlab_personal_access_token, deploy_token or job token>
|
||||
```
|
||||
|
||||
### Publishing a package with the same name or version
|
||||
|
||||
When you publish a package with the same name or version as an existing package,
|
||||
|
|
|
|||
|
|
@ -64,14 +64,29 @@ You also have access to the terminal and can install any necessary dependencies.
|
|||
|
||||
## Workspaces and projects
|
||||
|
||||
A workspace is scoped to a project. When you create a workspace, you must:
|
||||
Workspaces are scoped to a project. When you create a workspace, you must:
|
||||
|
||||
- Assign the workspace to a specific project.
|
||||
- Select a project with a `.devfile.yaml` file.
|
||||
|
||||
The workspace can then interact with the GitLab API based on the permissions granted to the current user.
|
||||
|
||||
## Deleting data associated with a workspace
|
||||
### Open and manage workspaces from a project
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125331) in GitLab 16.2.
|
||||
|
||||
To open a workspace from a file or the repository file list:
|
||||
|
||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||
1. In the upper right, select **Edit**.
|
||||
1. From the dropdown list, under **Your workspaces**, select the workspace.
|
||||
|
||||
From the dropdown list, you can also:
|
||||
|
||||
- Restart, stop, or terminate an existing workspace.
|
||||
- Create a new workspace.
|
||||
|
||||
### Deleting data associated with a workspace
|
||||
|
||||
When you delete a project, agent, user, or token associated with a workspace:
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ module API
|
|||
|
||||
included do
|
||||
# https://docs.microsoft.com/en-us/nuget/api/service-index
|
||||
desc 'The NuGet Service Index' do
|
||||
desc 'The NuGet V3 Feed Service Index' do
|
||||
detail 'This feature was introduced in GitLab 12.6'
|
||||
success code: 200, model: ::API::Entities::Nuget::ServiceIndex
|
||||
failure [
|
||||
|
|
@ -34,6 +34,33 @@ module API
|
|||
present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group_without_auth),
|
||||
with: ::API::Entities::Nuget::ServiceIndex
|
||||
end
|
||||
|
||||
desc 'The NuGet V2 Feed Service Index' do
|
||||
detail 'This feature was introduced in GitLab 16.2'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 404, message: 'Not Found' }
|
||||
]
|
||||
tags %w[nuget_packages]
|
||||
end
|
||||
namespace '/v2' do
|
||||
get format: :xml, urgency: :low do
|
||||
env['api.format'] = :xml
|
||||
content_type 'application/xml; charset=utf-8'
|
||||
# needed to allow browser default inline styles in xml response
|
||||
header 'Content-Security-Policy', "nonce-#{SecureRandom.base64(16)}"
|
||||
|
||||
track_package_event(
|
||||
'cli_metadata',
|
||||
:nuget,
|
||||
**snowplow_gitlab_standard_context_without_auth.merge(category: 'API::NugetPackages', feed: 'v2')
|
||||
)
|
||||
|
||||
present ::Packages::Nuget::V2::ServiceIndexPresenter
|
||||
.new(project_or_group_without_auth)
|
||||
.xml
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -98,6 +98,22 @@ module API
|
|||
created!
|
||||
end
|
||||
|
||||
def publish_package(symbol_package: false)
|
||||
upload_nuget_package_file(symbol_package: symbol_package) do |package|
|
||||
track_package_event(
|
||||
symbol_package ? 'push_symbol_package' : 'push_package',
|
||||
:nuget,
|
||||
**{ category: 'API::NugetPackages',
|
||||
project: package.project,
|
||||
namespace: package.project.namespace }.tap { |args| args[:feed] = 'v2' if request.path.include?('nuget/v2') }
|
||||
)
|
||||
end
|
||||
rescue ObjectStorage::RemoteStoreError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
|
||||
|
||||
forbidden!
|
||||
end
|
||||
|
||||
def required_permission
|
||||
:read_package
|
||||
end
|
||||
|
|
@ -179,7 +195,7 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# To support an additional authentication option for download endpoints,
|
||||
# To support an additional authentication option for publish endpoints,
|
||||
# we redefine the `authenticate_with` method by combining the previous
|
||||
# authentication option with the new one.
|
||||
authenticate_with do |accept|
|
||||
|
|
@ -191,7 +207,7 @@ module API
|
|||
|
||||
namespace '/nuget' do
|
||||
# https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
|
||||
desc 'The NuGet Package Publish endpoint' do
|
||||
desc 'The NuGet V3 Feed Package Publish endpoint' do
|
||||
detail 'This feature was introduced in GitLab 12.6'
|
||||
success code: 201
|
||||
failure [
|
||||
|
|
@ -207,19 +223,7 @@ module API
|
|||
use :file_params
|
||||
end
|
||||
put urgency: :low do
|
||||
upload_nuget_package_file do |package|
|
||||
track_package_event(
|
||||
'push_package',
|
||||
:nuget,
|
||||
category: 'API::NugetPackages',
|
||||
project: package.project,
|
||||
namespace: package.project.namespace
|
||||
)
|
||||
end
|
||||
rescue ObjectStorage::RemoteStoreError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
|
||||
|
||||
forbidden!
|
||||
publish_package
|
||||
end
|
||||
|
||||
desc 'The NuGet Package Authorize endpoint' do
|
||||
|
|
@ -252,19 +256,7 @@ module API
|
|||
use :file_params
|
||||
end
|
||||
put 'symbolpackage', urgency: :low do
|
||||
upload_nuget_package_file(symbol_package: true) do |package|
|
||||
track_package_event(
|
||||
'push_symbol_package',
|
||||
:nuget,
|
||||
category: 'API::NugetPackages',
|
||||
project: package.project,
|
||||
namespace: package.project.namespace
|
||||
)
|
||||
end
|
||||
rescue ObjectStorage::RemoteStoreError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
|
||||
|
||||
forbidden!
|
||||
publish_package(symbol_package: true)
|
||||
end
|
||||
|
||||
desc 'The NuGet Symbol Package Authorize endpoint' do
|
||||
|
|
@ -280,6 +272,42 @@ module API
|
|||
put 'symbolpackage/authorize', urgency: :low do
|
||||
authorize_nuget_upload
|
||||
end
|
||||
|
||||
namespace '/v2' do
|
||||
desc 'The NuGet V2 Feed Package Publish endpoint' do
|
||||
detail 'This feature was introduced in GitLab 16.2'
|
||||
success code: 201
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not Found' }
|
||||
]
|
||||
tags %w[nuget_packages]
|
||||
end
|
||||
|
||||
params do
|
||||
use :file_params
|
||||
end
|
||||
put do
|
||||
publish_package
|
||||
end
|
||||
|
||||
desc 'The NuGet V2 Feed Package Authorize endpoint' do
|
||||
detail 'This feature was introduced in GitLab 16.2'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not Found' }
|
||||
]
|
||||
tags %w[nuget_packages]
|
||||
end
|
||||
|
||||
put 'authorize', urgency: :low do
|
||||
authorize_nuget_upload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -404,9 +404,14 @@ module Gitlab
|
|||
|
||||
def revoke_token_family(token)
|
||||
return unless Feature.enabled?(:pat_reuse_detection)
|
||||
return unless access_token_rotation_request?
|
||||
|
||||
PersonalAccessTokens::RevokeTokenFamilyService.new(token).execute
|
||||
end
|
||||
|
||||
def access_token_rotation_request?
|
||||
current_request.path.match(%r{access_tokens/\d+/rotate$})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53985,6 +53985,9 @@ msgstr ""
|
|||
msgid "can not be set for this type of note"
|
||||
msgstr ""
|
||||
|
||||
msgid "can not be true if shared runners are enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "can only be changed by a group admin."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -54033,9 +54036,6 @@ msgstr ""
|
|||
msgid "cannot be changed if a personal project has container registry tags."
|
||||
msgstr ""
|
||||
|
||||
msgid "cannot be changed if shared runners are enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "cannot be changed since member is associated with a custom role"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ gem "warning", "~> 1.3"
|
|||
|
||||
gem 'confiner', '~> 0.4'
|
||||
|
||||
gem 'chemlab', '~> 0.10'
|
||||
gem 'chemlab', '~> 0.11', '>= 0.11.1'
|
||||
gem 'chemlab-library-www-gitlab-com', '~> 0.1', '>= 0.1.1'
|
||||
|
||||
# dependencies for jenkins client
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ GEM
|
|||
capybara-screenshot (1.0.26)
|
||||
capybara (>= 1.0, < 4)
|
||||
launchy
|
||||
chemlab (0.10.0)
|
||||
chemlab (0.11.1)
|
||||
colorize (~> 0.8)
|
||||
i18n (~> 1.8)
|
||||
rake (>= 12, < 14)
|
||||
|
|
@ -340,7 +340,7 @@ DEPENDENCIES
|
|||
allure-rspec (~> 2.20.0)
|
||||
capybara (~> 3.39.2)
|
||||
capybara-screenshot (~> 1.0.26)
|
||||
chemlab (~> 0.10)
|
||||
chemlab (~> 0.11, >= 0.11.1)
|
||||
chemlab-library-www-gitlab-com (~> 0.1, >= 0.1.1)
|
||||
confiner (~> 0.4)
|
||||
deprecation_toolkit (~> 2.0.3)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Generate an Elastic migration file, spec and dictionary record with the current timestamp.
|
||||
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
require 'uri'
|
||||
require 'readline'
|
||||
require 'active_support/core_ext/string'
|
||||
|
||||
class ElasticMigrationCreator
|
||||
attr_reader :options
|
||||
|
||||
Options = Struct.new(
|
||||
:name,
|
||||
:description,
|
||||
:group,
|
||||
:introduced_by_url,
|
||||
:milestone,
|
||||
:obsolete,
|
||||
:marked_obsolete_by_url,
|
||||
:marked_obsolete_in_milestone
|
||||
)
|
||||
|
||||
def initialize
|
||||
@options = Options.new
|
||||
end
|
||||
|
||||
def execute
|
||||
options.name = read_name
|
||||
options.description = read_description
|
||||
options.group = read_group
|
||||
options.introduced_by_url = read_introduced_by_url
|
||||
options.milestone = read_milestone
|
||||
|
||||
$stdout.puts "\e[32mcreated\e[0m #{file_path}"
|
||||
$stdout.puts "\e[32mcreated\e[0m #{spec_file_path}"
|
||||
$stdout.puts "\e[32mcreated\e[0m #{dictionary_file_path}"
|
||||
|
||||
write
|
||||
$stdout.puts "\n=> Please consult the documentation for Advanced Search Migrations: #{documentation_reference}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_name
|
||||
read_variable('name', 'Name of the migration in CamelCase').camelize
|
||||
end
|
||||
|
||||
def read_description
|
||||
read_variable('description', 'Description of what the migration does')
|
||||
end
|
||||
|
||||
def read_group
|
||||
read_variable('group', 'The group introducing a feature flag, like: `global search`')
|
||||
end
|
||||
|
||||
def read_milestone
|
||||
milestone = File.read('VERSION')
|
||||
milestone.gsub(/^(\d+\.\d+).*$/, '\1').chomp
|
||||
end
|
||||
|
||||
def read_variable(name, description)
|
||||
$stdout.puts "\n>> #{description}:"
|
||||
|
||||
loop do
|
||||
variable = Readline.readline('?> ', false)&.strip
|
||||
return variable unless variable.empty?
|
||||
|
||||
warn "Error: #{name} is required."
|
||||
end
|
||||
end
|
||||
|
||||
def read_introduced_by_url
|
||||
$stdout.puts
|
||||
$stdout.puts ">> URL of the MR introducing the migration (enter to skip):"
|
||||
|
||||
loop do
|
||||
introduced_by_url = Readline.readline('?> ', false)&.strip
|
||||
introduced_by_url = nil if introduced_by_url.empty?
|
||||
return introduced_by_url if introduced_by_url.nil? || introduced_by_url.start_with?('https://')
|
||||
|
||||
warn 'Error: URL needs to start with https://'
|
||||
end
|
||||
end
|
||||
|
||||
def write
|
||||
# create migration file
|
||||
FileUtils.mkdir_p(File.dirname(file_path))
|
||||
File.write(file_path, file_contents)
|
||||
|
||||
# create spec
|
||||
FileUtils.mkdir_p(File.dirname(spec_file_path))
|
||||
File.write(spec_file_path, spec_contents)
|
||||
|
||||
# create dictionary file
|
||||
FileUtils.mkdir_p(File.dirname(dictionary_file_path))
|
||||
File.write(dictionary_file_path, dictionary_contents)
|
||||
end
|
||||
|
||||
def timestamp
|
||||
@timestamp ||= Time.now.strftime('%Y%m%d%H%M%S')
|
||||
end
|
||||
|
||||
def file_name
|
||||
@file_name ||= "#{timestamp}_#{options.name.dup.underscore}"
|
||||
end
|
||||
|
||||
def file_path
|
||||
"ee/elastic/migrate/#{file_name}.rb"
|
||||
end
|
||||
|
||||
def spec_file_path
|
||||
"ee/spec/elastic/migrate/#{file_name}_spec.rb"
|
||||
end
|
||||
|
||||
def dictionary_file_path
|
||||
"ee/elastic/docs/#{file_name}.yml"
|
||||
end
|
||||
|
||||
def file_contents
|
||||
"# frozen_string_literal: true
|
||||
|
||||
class #{options.name} < Elastic::Migration
|
||||
end
|
||||
"
|
||||
end
|
||||
|
||||
def spec_contents
|
||||
"# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_relative 'migration_shared_examples'
|
||||
require File.expand_path('#{file_path}')
|
||||
|
||||
RSpec.describe #{options.name}, feature_category: :#{options.group.parameterize.underscore} do
|
||||
let(:version) { #{timestamp} }
|
||||
end
|
||||
"
|
||||
end
|
||||
|
||||
def dictionary_contents
|
||||
dictionary_config_hash.to_yaml
|
||||
end
|
||||
|
||||
def dictionary_config_hash
|
||||
{
|
||||
'name' => options.name,
|
||||
'version' => timestamp,
|
||||
'description' => options.description,
|
||||
'group' => "group::#{options.group}",
|
||||
'milestone' => options.milestone,
|
||||
'introduced_by_url' => options.introduced_by_url,
|
||||
'obsolete' => false,
|
||||
'marked_obsolete_by_url' => nil,
|
||||
'marked_obsolete_in_milestone' => nil
|
||||
}
|
||||
end
|
||||
|
||||
def documentation_reference
|
||||
'https://docs.gitlab.com/ee/development/search/advanced_search_migration_styleguide.html'
|
||||
end
|
||||
end
|
||||
|
||||
ElasticMigrationCreator.new.execute if $PROGRAM_NAME == __FILE__
|
||||
|
||||
# vim: ft=ruby
|
||||
|
|
@ -196,23 +196,28 @@ RSpec.describe Projects::AutocompleteSourcesController do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'only public members are returned for public project' do
|
||||
shared_examples 'returns all members of public project' do
|
||||
before do
|
||||
stub_feature_flags(disable_all_mention: false)
|
||||
end
|
||||
|
||||
it 'only returns public members' do
|
||||
it 'returns members including those from invited private groups' do
|
||||
get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
|
||||
|
||||
expect(members_by_username('all').symbolize_keys).to include(
|
||||
username: 'all',
|
||||
name: 'All Project and Group Members',
|
||||
count: 1)
|
||||
count: 2)
|
||||
|
||||
expect(members_by_username(user.username).symbolize_keys).to include(
|
||||
type: user.class.name,
|
||||
name: user.name,
|
||||
avatar_url: user.avatar_url)
|
||||
|
||||
expect(members_by_username(invited_private_member.username).symbolize_keys).to include(
|
||||
type: invited_private_member.class.name,
|
||||
name: invited_private_member.name,
|
||||
avatar_url: invited_private_member.avatar_url)
|
||||
end
|
||||
|
||||
context 'when `disable_all_mention` FF is enabled' do
|
||||
|
|
@ -234,7 +239,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
|
|||
let(:issuable_type) { private_issue.class.name }
|
||||
end
|
||||
|
||||
it_behaves_like 'only public members are returned for public project' do
|
||||
it_behaves_like 'returns all members of public project' do
|
||||
let(:issuable_type) { issue.class.name }
|
||||
end
|
||||
end
|
||||
|
|
@ -244,7 +249,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
|
|||
let(:issuable_type) { private_work_item.class.name }
|
||||
end
|
||||
|
||||
it_behaves_like 'only public members are returned for public project' do
|
||||
it_behaves_like 'returns all members of public project' do
|
||||
let(:issuable_type) { work_item.class.name }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,18 +65,14 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :allow_descendants_override_disabled_shared_runners do
|
||||
allow_descendants_override_disabled_shared_runners { true }
|
||||
end
|
||||
|
||||
trait :disabled_and_unoverridable do
|
||||
trait :shared_runners_disabled_and_unoverridable do
|
||||
shared_runners_disabled
|
||||
allow_descendants_override_disabled_shared_runners { false }
|
||||
end
|
||||
|
||||
trait :disabled_and_overridable do
|
||||
trait :shared_runners_disabled_and_overridable do
|
||||
shared_runners_disabled
|
||||
allow_descendants_override_disabled_shared_runners
|
||||
allow_descendants_override_disabled_shared_runners { true }
|
||||
end
|
||||
|
||||
trait :shared_runners_enabled do
|
||||
|
|
|
|||
|
|
@ -40,9 +40,5 @@ FactoryBot.define do
|
|||
trait :shared_runners_disabled do
|
||||
shared_runners_enabled { false }
|
||||
end
|
||||
|
||||
trait :allow_descendants_override_disabled_shared_runners do
|
||||
allow_descendants_override_disabled_shared_runners { true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
|
||||
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
|
||||
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
|
||||
import { multipleFindingsArrCodeQualityScale } from '../mock_data/diff_code_quality';
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ const findDescriptionLinkSection = () => wrapper.findByTestId('description-butto
|
|||
|
||||
describe('DiffCodeQuality', () => {
|
||||
const createWrapper = ({ glFeatures = {}, link = true } = {}) => {
|
||||
return shallowMountExtended(DiffCodeQualityItem, {
|
||||
return shallowMountExtended(DiffInlineFindingsItem, {
|
||||
propsData: {
|
||||
finding: codeQualityFinding,
|
||||
link,
|
||||
|
|
@ -30,7 +30,7 @@ describe('DiffCodeQuality', () => {
|
|||
expect(findIcon().exists()).toBe(true);
|
||||
|
||||
expect(findIcon().attributes()).toMatchObject({
|
||||
class: `codequality-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
|
||||
class: `inline-findings-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
|
||||
name: SEVERITY_ICONS[codeQualityFinding.severity],
|
||||
size: '12',
|
||||
});
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
|
||||
import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
|
||||
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
|
||||
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
|
||||
import { multipleCodeQualityNoSast } from '../mock_data/diff_code_quality';
|
||||
|
||||
let wrapper;
|
||||
const heading = () => wrapper.findByTestId('diff-inline-findings-heading');
|
||||
const diffCodeQualityItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
|
||||
const diffInlineFindingsItems = () => wrapper.findAllComponents(DiffInlineFindingsItem);
|
||||
|
||||
describe('DiffInlineFindings', () => {
|
||||
const createWrapper = () => {
|
||||
|
|
@ -23,10 +23,10 @@ describe('DiffInlineFindings', () => {
|
|||
expect(heading().text()).toBe(NEW_CODE_QUALITY_FINDINGS);
|
||||
});
|
||||
|
||||
it('renders the correct number of DiffCodeQualityItem components with correct props', () => {
|
||||
it('renders the correct number of DiffInlineFindingsItem components with correct props', () => {
|
||||
wrapper = createWrapper();
|
||||
expect(diffCodeQualityItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
|
||||
expect(diffCodeQualityItems().wrappers[0].props('finding')).toEqual(
|
||||
expect(diffInlineFindingsItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
|
||||
expect(diffInlineFindingsItems().wrappers[0].props('finding')).toEqual(
|
||||
wrapper.props('findings')[0],
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
|
|||
</span>
|
||||
|
||||
<gl-icon-stub
|
||||
class="codequality-severity-icon gl-text-orange-300"
|
||||
class="inline-findings-severity-icon gl-text-orange-300"
|
||||
data-testid="findings-drawer-severity-icon"
|
||||
name="severity-low"
|
||||
size="12"
|
||||
|
|
|
|||
|
|
@ -2,13 +2,8 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
|
||||
import highlightMixin from '~/repository/mixins/highlight_mixin';
|
||||
import LineHighlighter from '~/blob/line_highlighter';
|
||||
import Tracking from '~/tracking';
|
||||
import { TEXT_FILE_TYPE } from '~/repository/constants';
|
||||
import {
|
||||
EVENT_ACTION,
|
||||
EVENT_LABEL_FALLBACK,
|
||||
LINES_PER_CHUNK,
|
||||
} from '~/vue_shared/components/source_viewer/constants';
|
||||
import { LINES_PER_CHUNK } from '~/vue_shared/components/source_viewer/constants';
|
||||
|
||||
const lineHighlighter = new LineHighlighter();
|
||||
jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() }));
|
||||
|
|
@ -24,7 +19,7 @@ describe('HighlightMixin', () => {
|
|||
const hash = '#L50';
|
||||
const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
|
||||
const rawTextBlob = contentArray.join('\n');
|
||||
const languageMock = 'javascript';
|
||||
const languageMock = 'json';
|
||||
|
||||
const createComponent = ({ fileType = TEXT_FILE_TYPE, language = languageMock } = {}) => {
|
||||
const simpleViewer = { fileType };
|
||||
|
|
@ -50,26 +45,13 @@ describe('HighlightMixin', () => {
|
|||
describe('initHighlightWorker', () => {
|
||||
const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n');
|
||||
|
||||
it('does not instruct worker if file is not a text file', () => {
|
||||
it('does not instruct worker if file is not a JSON file', () => {
|
||||
workerMock.postMessage.mockClear();
|
||||
createComponent({ fileType: 'markdown' });
|
||||
createComponent({ language: 'javascript' });
|
||||
|
||||
expect(workerMock.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tracks event if a language is not supported and does not instruct worker', () => {
|
||||
const unsupportedLanguage = 'some_unsupported_language';
|
||||
const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
|
||||
|
||||
jest.spyOn(Tracking, 'event');
|
||||
workerMock.postMessage.mockClear();
|
||||
createComponent({ language: unsupportedLanguage });
|
||||
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
|
||||
expect(onErrorMock).toHaveBeenCalled();
|
||||
expect(workerMock.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('generates a chunk for the first 70 lines of raw text', () => {
|
||||
expect(splitIntoChunks).toHaveBeenCalledWith(languageMock, firstSeventyLines);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
|
||||
import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
|
||||
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
|
||||
import { TYPENAME_USER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { STATUS_CLOSED, STATUS_OPEN } from '~/service_desk/constants';
|
||||
import getServiceDeskIssuesQuery from '~/service_desk/queries/get_service_desk_issues.query.graphql';
|
||||
import getServiceDeskIssuesCountsQuery from '~/service_desk/queries/get_service_desk_issues_counts.query.graphql';
|
||||
import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
|
||||
import InfoBanner from '~/service_desk/components/info_banner.vue';
|
||||
import {
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
TOKEN_TYPE_LABEL,
|
||||
TOKEN_TYPE_MILESTONE,
|
||||
TOKEN_TYPE_MY_REACTION,
|
||||
TOKEN_TYPE_RELEASE,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import {
|
||||
getServiceDeskIssuesQueryResponse,
|
||||
getServiceDeskIssuesCountsQueryResponse,
|
||||
|
|
@ -24,6 +36,10 @@ describe('ServiceDeskListApp', () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
const defaultProvide = {
|
||||
releasesPath: 'releases/path',
|
||||
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
|
||||
hasIterationsFeature: true,
|
||||
groupPath: 'group/path',
|
||||
emptyStateSvgPath: 'empty-state.svg',
|
||||
isProject: true,
|
||||
isSignedIn: true,
|
||||
|
|
@ -34,28 +50,31 @@ describe('ServiceDeskListApp', () => {
|
|||
|
||||
const defaultQueryResponse = getServiceDeskIssuesQueryResponse;
|
||||
|
||||
const mockServiceDeskIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
|
||||
const mockServiceDeskIssuesCountsQueryResponse = jest
|
||||
const mockServiceDeskIssuesQueryResponseHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(defaultQueryResponse);
|
||||
const mockServiceDeskIssuesCountsQueryResponseHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(getServiceDeskIssuesCountsQueryResponse);
|
||||
|
||||
const findIssuableList = () => wrapper.findComponent(IssuableList);
|
||||
const findInfoBanner = () => wrapper.findComponent(InfoBanner);
|
||||
const findLabelsToken = () =>
|
||||
findIssuableList()
|
||||
.props('searchTokens')
|
||||
.find((token) => token.type === TOKEN_TYPE_LABEL);
|
||||
|
||||
const mountComponent = ({
|
||||
const createComponent = ({
|
||||
provide = {},
|
||||
data = {},
|
||||
serviceDeskIssuesQueryResponse = mockServiceDeskIssuesQueryResponse,
|
||||
serviceDeskIssuesCountsQueryResponse = mockServiceDeskIssuesCountsQueryResponse,
|
||||
stubs = {},
|
||||
mountFn = shallowMount,
|
||||
serviceDeskIssuesQueryResponseHandler = mockServiceDeskIssuesQueryResponseHandler,
|
||||
serviceDeskIssuesCountsQueryResponseHandler = mockServiceDeskIssuesCountsQueryResponseHandler,
|
||||
} = {}) => {
|
||||
const requestHandlers = [
|
||||
[getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponse],
|
||||
[getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponse],
|
||||
[getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponseHandler],
|
||||
[getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler],
|
||||
];
|
||||
|
||||
return mountFn(ServiceDeskListApp, {
|
||||
return shallowMount(ServiceDeskListApp, {
|
||||
apolloProvider: createMockApollo(
|
||||
requestHandlers,
|
||||
{},
|
||||
|
|
@ -75,15 +94,11 @@ describe('ServiceDeskListApp', () => {
|
|||
...defaultProvide,
|
||||
...provide,
|
||||
},
|
||||
data() {
|
||||
return data;
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent();
|
||||
wrapper = createComponent();
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -107,23 +122,82 @@ describe('ServiceDeskListApp', () => {
|
|||
expect(findInfoBanner().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render when Service Desk is not supported and has any number of issues', async () => {
|
||||
wrapper = createComponent({ provide: { isServiceDeskSupported: false } });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findInfoBanner().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render, when there are no issues', async () => {
|
||||
wrapper = mountComponent({ provide: { hasAnyIssues: false } });
|
||||
wrapper = createComponent({ provide: { hasAnyIssues: false } });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findInfoBanner().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Events', () => {
|
||||
describe('when "click-tab" event is emitted by IssuableList', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
describe('Tokens', () => {
|
||||
const mockCurrentUser = {
|
||||
id: 1,
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
avatar_url: 'avatar/url',
|
||||
};
|
||||
|
||||
findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
|
||||
describe('when user is signed out', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ provide: { isSignedIn: false } });
|
||||
});
|
||||
|
||||
it('updates ui to the new tab', () => {
|
||||
it('does not render My-Reaction or Confidential tokens', () => {
|
||||
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when all tokens are available', () => {
|
||||
beforeEach(() => {
|
||||
window.gon = {
|
||||
current_user_id: mockCurrentUser.id,
|
||||
current_user_fullname: mockCurrentUser.name,
|
||||
current_username: mockCurrentUser.username,
|
||||
current_user_avatar_url: mockCurrentUser.avatar_url,
|
||||
};
|
||||
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
it('renders all tokens alphabetically', () => {
|
||||
const preloadedUsers = [
|
||||
{ ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
|
||||
];
|
||||
|
||||
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
{ type: TOKEN_TYPE_RELEASE },
|
||||
{ type: TOKEN_TYPE_SEARCH_WITHIN },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Events', () => {
|
||||
describe('when "click-tab" event is emitted by IssuableList', () => {
|
||||
it('updates ui to the new tab', async () => {
|
||||
createComponent();
|
||||
|
||||
findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
|
||||
|
||||
await nextTick();
|
||||
expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
|
||||
});
|
||||
});
|
||||
|
|
@ -131,13 +205,13 @@ describe('ServiceDeskListApp', () => {
|
|||
|
||||
describe('Errors', () => {
|
||||
describe.each`
|
||||
error | mountOption | message
|
||||
${'fetching issues'} | ${'serviceDeskIssuesQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingIssues}
|
||||
${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingCounts}
|
||||
`('when there is an error $error', ({ mountOption, message }) => {
|
||||
error | responseHandler | message
|
||||
${'fetching issues'} | ${'serviceDeskIssuesQueryResponseHandler'} | ${ServiceDeskListApp.i18n.errorFetchingIssues}
|
||||
${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponseHandler'} | ${ServiceDeskListApp.i18n.errorFetchingCounts}
|
||||
`('when there is an error $error', ({ responseHandler, message }) => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
|
||||
wrapper = createComponent({
|
||||
[responseHandler]: jest.fn().mockRejectedValue(new Error('ERROR')),
|
||||
});
|
||||
return waitForPromises();
|
||||
});
|
||||
|
|
@ -148,4 +222,30 @@ describe('ServiceDeskListApp', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When providing token for labels', () => {
|
||||
it('passes function to fetchLatestLabels property if frontend caching is enabled', () => {
|
||||
wrapper = createComponent({
|
||||
provide: {
|
||||
glFeatures: {
|
||||
frontendCaching: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(typeof findLabelsToken().fetchLatestLabels).toBe('function');
|
||||
});
|
||||
|
||||
it('passes null to fetchLatestLabels property if frontend caching is disabled', () => {
|
||||
wrapper = createComponent({
|
||||
provide: {
|
||||
glFeatures: {
|
||||
frontendCaching: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findLabelsToken().fetchLatestLabels).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
LEGACY_FALLBACKS,
|
||||
CODEOWNERS_FILE_NAME,
|
||||
CODEOWNERS_LANGUAGE,
|
||||
SVELTE_LANGUAGE,
|
||||
} from '~/vue_shared/components/source_viewer/constants';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import LineHighlighter from '~/blob/line_highlighter';
|
||||
|
|
@ -120,6 +121,33 @@ describe('Source Viewer component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('sub-languages', () => {
|
||||
const languageDefinition = {
|
||||
subLanguage: 'xml',
|
||||
contains: [{ subLanguage: 'javascript' }, { subLanguage: 'typescript' }],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(hljs, 'getLanguage').mockReturnValue(languageDefinition);
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('registers the primary sub-language', () => {
|
||||
expect(hljs.registerLanguage).toHaveBeenCalledWith(
|
||||
languageDefinition.subLanguage,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(languageDefinition.contains)(
|
||||
'registers the rest of the sub-languages',
|
||||
({ subLanguage }) => {
|
||||
expect(hljs.registerLanguage).toHaveBeenCalledWith(subLanguage, expect.any(Function));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('registers json language definition if fileType is package_json', async () => {
|
||||
await createComponent({ language: 'json', fileType: 'package_json' });
|
||||
const languageDefinition = await import(`highlight.js/lib/languages/json`);
|
||||
|
|
@ -146,6 +174,18 @@ describe('Source Viewer component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('registers svelte language definition if file name ends with .svelte', async () => {
|
||||
await createComponent({ name: `component.${SVELTE_LANGUAGE}` });
|
||||
const languageDefinition = await import(
|
||||
'~/vue_shared/components/source_viewer/languages/svelte'
|
||||
);
|
||||
|
||||
expect(hljs.registerLanguage).toHaveBeenCalledWith(
|
||||
SVELTE_LANGUAGE,
|
||||
languageDefinition.default,
|
||||
);
|
||||
});
|
||||
|
||||
it('highlights the first chunk', () => {
|
||||
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
|
||||
expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
|
||||
|
|
|
|||
|
|
@ -218,9 +218,9 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
|
|||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:shared_runners_setting, :is_disabled_and_unoverridable) do
|
||||
:shared_runners_enabled | "false"
|
||||
:disabled_and_overridable | "false"
|
||||
:disabled_and_unoverridable | "true"
|
||||
:shared_runners_enabled | "false"
|
||||
:shared_runners_disabled_and_overridable | "false"
|
||||
:shared_runners_disabled_and_unoverridable | "true"
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -516,12 +516,32 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
|
|||
set_bearer_token(token_3.token)
|
||||
end
|
||||
|
||||
it 'revokes the latest rotated token' do
|
||||
expect(token_1).not_to be_revoked
|
||||
context 'with url related to access tokens' do
|
||||
before do
|
||||
set_header('SCRIPT_NAME', "/personal_access_tokens/#{token_3.id}/rotate")
|
||||
end
|
||||
|
||||
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
|
||||
it 'revokes the latest rotated token' do
|
||||
expect(token_1).not_to be_revoked
|
||||
|
||||
expect(token_1.reload).to be_revoked
|
||||
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
|
||||
|
||||
expect(token_1.reload).to be_revoked
|
||||
end
|
||||
end
|
||||
|
||||
context 'with url not related to access tokens' do
|
||||
before do
|
||||
set_header('SCRIPT_NAME', '/epics/1')
|
||||
end
|
||||
|
||||
it 'does not revoke the latest rotated token' do
|
||||
expect(token_1).not_to be_revoked
|
||||
|
||||
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::RevokedError)
|
||||
|
||||
expect(token_1.reload).not_to be_revoked
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer, feature_cate
|
|||
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
|
||||
|
||||
let_it_be(:group) do
|
||||
create(:group, :disabled_and_unoverridable).tap { |g| g.add_maintainer(user) }
|
||||
create(:group, :shared_runners_disabled_and_unoverridable).tap { |g| g.add_maintainer(user) }
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -2389,7 +2389,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
context 'when parent has shared runners disabled but allows override' do
|
||||
let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
||||
let(:parent) { create(:group, :shared_runners_disabled_and_overridable) }
|
||||
let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
|
||||
|
||||
it 'is valid' do
|
||||
|
|
@ -2415,7 +2415,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
|
|||
context 'when namespace is a group' do
|
||||
context 'without a parent' do
|
||||
context 'with shared runners disabled' do
|
||||
let(:namespace) { build(:group, :allow_descendants_override_disabled_shared_runners, :shared_runners_disabled) }
|
||||
let(:namespace) { build(:group, :shared_runners_disabled_and_overridable) }
|
||||
|
||||
it 'is valid' do
|
||||
expect(namespace).to be_valid
|
||||
|
|
@ -2423,13 +2423,13 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
context 'with shared runners enabled' do
|
||||
let(:namespace) { create(:namespace) }
|
||||
let(:namespace) { build(:group) }
|
||||
|
||||
it 'is invalid' do
|
||||
namespace.allow_descendants_override_disabled_shared_runners = true
|
||||
|
||||
expect(namespace).to be_invalid
|
||||
expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be changed if shared runners are enabled')
|
||||
expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('can not be true if shared runners are enabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2437,7 +2437,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
|
|||
context 'with a parent' do
|
||||
context 'when parent does not allow shared runners' do
|
||||
let(:parent) { create(:group, :shared_runners_disabled) }
|
||||
let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
|
||||
let(:group) { build(:group, :shared_runners_disabled_and_overridable, parent_id: parent.id) }
|
||||
|
||||
it 'is invalid' do
|
||||
expect(group).to be_invalid
|
||||
|
|
@ -2447,7 +2447,7 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
|
|||
|
||||
context 'when parent allows shared runners and setting to true' do
|
||||
let(:parent) { create(:group, shared_runners_enabled: true) }
|
||||
let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
|
||||
let(:group) { build(:group, :shared_runners_disabled_and_overridable, parent_id: parent.id) }
|
||||
|
||||
it 'is valid' do
|
||||
expect(group).to be_valid
|
||||
|
|
|
|||
|
|
@ -7026,10 +7026,10 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
where(:shared_runners_setting, :project_shared_runners_enabled, :valid_record) do
|
||||
:shared_runners_enabled | true | true
|
||||
:shared_runners_enabled | false | true
|
||||
:disabled_and_overridable | true | true
|
||||
:disabled_and_overridable | false | true
|
||||
:disabled_and_unoverridable | true | false
|
||||
:disabled_and_unoverridable | false | true
|
||||
:shared_runners_disabled_and_overridable | true | true
|
||||
:shared_runners_disabled_and_overridable | false | true
|
||||
:shared_runners_disabled_and_unoverridable | true | false
|
||||
:shared_runners_disabled_and_unoverridable | false | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Nuget::V2::ServiceIndexPresenter, feature_category: :package_registry do
|
||||
let_it_be(:project) { build_stubbed(:project) }
|
||||
let_it_be(:group) { build_stubbed(:group) }
|
||||
|
||||
describe '#xml' do
|
||||
let(:project_or_group) { project }
|
||||
let(:presenter) { described_class.new(project_or_group) }
|
||||
let(:xml_doc) { Nokogiri::XML::Document.parse(presenter.xml.to_xml) }
|
||||
let(:service_node) { xml_doc.at_xpath('//xmlns:service') }
|
||||
|
||||
it { expect(xml_doc.root.name).to eq('service') }
|
||||
|
||||
it 'includes the workspace and collection nodes' do
|
||||
workspace = xml_doc.at_xpath('//xmlns:service/xmlns:workspace')
|
||||
collection = xml_doc.at_xpath('//xmlns:service/xmlns:workspace/xmlns:collection')
|
||||
|
||||
expect(workspace).to be_present
|
||||
expect(workspace.children).to include(collection)
|
||||
expect(collection).to be_present
|
||||
end
|
||||
|
||||
it 'sets the appropriate XML namespaces on the root node' do
|
||||
expect(service_node.namespaces['xmlns']).to eq('http://www.w3.org/2007/app')
|
||||
expect(service_node.namespaces['xmlns:atom']).to eq('http://www.w3.org/2005/Atom')
|
||||
end
|
||||
|
||||
context 'when the presenter is initialized with a project' do
|
||||
it 'sets the XML base path correctly for a project scope' do
|
||||
expect(service_node['xml:base']).to include(expected_xml_base(project))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the presenter is initialized with a group' do
|
||||
let(:project_or_group) { group }
|
||||
|
||||
it 'sets the XML base path correctly for a group scope' do
|
||||
expect(service_node['xml:base']).to include(expected_xml_base(group))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expected_xml_base(project_or_group)
|
||||
case project_or_group
|
||||
when Project
|
||||
api_v4_projects_packages_nuget_v2_path(id: project_or_group.id)
|
||||
when Group
|
||||
api_v4_groups___packages_nuget_v2_path(id: project_or_group.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -31,6 +31,12 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/groups/:id/-/packages/nuget/v2' do
|
||||
it_behaves_like 'handling nuget service requests', v2: true do
|
||||
let(:url) { "/groups/#{target.id}/-/packages/nuget/v2" }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do
|
||||
it_behaves_like 'handling nuget metadata requests with package name',
|
||||
example_names_with_status:
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
|
|||
it_behaves_like 'accept get request on private project with access to package registry for everyone'
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/nuget/v2' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/v2" }
|
||||
|
||||
it_behaves_like 'handling nuget service requests', v2: true
|
||||
|
||||
it_behaves_like 'accept get request on private project with access to package registry for everyone'
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
|
||||
|
||||
|
|
@ -183,75 +191,39 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
|
|||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { put api(url), headers: headers }
|
||||
|
||||
it_behaves_like 'nuget authorize upload endpoint'
|
||||
it_behaves_like 'nuget authorize upload endpoint' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:id/packages/nuget' do
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let_it_be(:file_name) { 'package.nupkg' }
|
||||
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget" }
|
||||
let(:headers) { {} }
|
||||
let(:params) { { package: temp_file(file_name) } }
|
||||
let(:file_key) { :package }
|
||||
let(:send_rewritten_field) { true }
|
||||
|
||||
subject do
|
||||
workhorse_finalize(
|
||||
api(url),
|
||||
method: :put,
|
||||
file_key: file_key,
|
||||
params: params,
|
||||
headers: headers,
|
||||
send_rewritten_field: send_rewritten_field
|
||||
)
|
||||
it_behaves_like 'nuget upload endpoint' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget" }
|
||||
end
|
||||
|
||||
it_behaves_like 'nuget upload endpoint'
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage/authorize' do
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { put api(url), headers: headers }
|
||||
|
||||
it_behaves_like 'nuget authorize upload endpoint'
|
||||
it_behaves_like 'nuget authorize upload endpoint' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage' do
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let_it_be(:file_name) { 'package.snupkg' }
|
||||
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
|
||||
let(:headers) { {} }
|
||||
let(:params) { { package: temp_file(file_name) } }
|
||||
let(:file_key) { :package }
|
||||
let(:send_rewritten_field) { true }
|
||||
|
||||
subject do
|
||||
workhorse_finalize(
|
||||
api(url),
|
||||
method: :put,
|
||||
file_key: file_key,
|
||||
params: params,
|
||||
headers: headers,
|
||||
send_rewritten_field: send_rewritten_field
|
||||
)
|
||||
it_behaves_like 'nuget upload endpoint', symbol_package: true do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" }
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'nuget upload endpoint', symbol_package: true
|
||||
describe 'PUT /api/v4/projects/:id/packages/nuget/v2/authorize' do
|
||||
it_behaves_like 'nuget authorize upload endpoint' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/v2/authorize" }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:id/packages/nuget/v2' do
|
||||
it_behaves_like 'nuget upload endpoint' do
|
||||
let(:url) { "/projects/#{target.id}/packages/nuget/v2" }
|
||||
end
|
||||
end
|
||||
|
||||
def update_visibility_to(visibility)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,21 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
|||
.and change { sub_group.shared_runners_enabled }.from(false).to(true)
|
||||
.and change { project.shared_runners_enabled }.from(false).to(true)
|
||||
end
|
||||
|
||||
context 'when already allowing descendants to override' do
|
||||
let(:group) { create(:group, :shared_runners_disabled_and_overridable) }
|
||||
|
||||
it 'enables shared Runners for itself and descendants' do
|
||||
expect do
|
||||
expect(subject[:status]).to eq(:success)
|
||||
|
||||
reload_models(group, sub_group, project)
|
||||
end.to change { group.shared_runners_enabled }.from(false).to(true)
|
||||
.and change { group.allow_descendants_override_disabled_shared_runners }.from(true).to(false)
|
||||
.and change { sub_group.shared_runners_enabled }.from(false).to(true)
|
||||
.and change { project.shared_runners_enabled }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group has pending builds' do
|
||||
|
|
@ -101,7 +116,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
|||
|
||||
context 'disable shared Runners' do
|
||||
let!(:group) { create(:group) }
|
||||
let!(:sub_group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent: group) }
|
||||
let!(:sub_group) { create(:group, :shared_runners_disabled_and_overridable, parent: group) }
|
||||
let!(:sub_group2) { create(:group, parent: group) }
|
||||
let!(:project) { create(:project, group: group, shared_runners_enabled: true) }
|
||||
let!(:project2) { create(:project, group: sub_group2, shared_runners_enabled: true) }
|
||||
|
|
@ -124,7 +139,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
|||
end
|
||||
|
||||
context 'with override on self' do
|
||||
let(:group) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
||||
let(:group) { create(:group, :shared_runners_disabled_and_overridable) }
|
||||
|
||||
it 'disables it' do
|
||||
expect do
|
||||
|
|
@ -172,7 +187,7 @@ RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :groups_and
|
|||
end
|
||||
|
||||
context 'when ancestor disables shared Runners but allows to override' do
|
||||
let!(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
|
||||
let!(:parent) { create(:group, :shared_runners_disabled_and_overridable) }
|
||||
let!(:group) { create(:group, :shared_runners_disabled, parent: parent) }
|
||||
let!(:project) { create(:project, shared_runners_enabled: false, group: group) }
|
||||
|
||||
|
|
|
|||
|
|
@ -120,120 +120,88 @@ RSpec.describe Projects::ParticipantsService, feature_category: :groups_and_proj
|
|||
describe '#project_members' do
|
||||
subject(:usernames) { service.project_members.map { |member| member[:username] } }
|
||||
|
||||
shared_examples 'return project members' do
|
||||
context 'when there is a project in group namespace' do
|
||||
let_it_be(:public_group) { create(:group, :public) }
|
||||
let_it_be(:public_project) { create(:project, :public, namespace: public_group) }
|
||||
context 'when there is a project in group namespace' do
|
||||
let_it_be(:public_group) { create(:group, :public) }
|
||||
let_it_be(:public_project, reload: true) { create(:project, :public, namespace: public_group) }
|
||||
|
||||
let_it_be(:public_group_owner) { create(:user) }
|
||||
let_it_be(:public_group_owner) { create(:user) }
|
||||
|
||||
let(:service) { described_class.new(public_project, create(:user)) }
|
||||
let(:service) { described_class.new(public_project, create(:user)) }
|
||||
|
||||
before do
|
||||
public_group.add_owner(public_group_owner)
|
||||
end
|
||||
|
||||
it 'returns members of a group' do
|
||||
expect(usernames).to include(public_group_owner.username)
|
||||
end
|
||||
before do
|
||||
public_group.add_owner(public_group_owner)
|
||||
end
|
||||
|
||||
context 'when there is a private group and a public project' do
|
||||
let_it_be(:public_group) { create(:group, :public) }
|
||||
let_it_be(:private_group) { create(:group, :private, :nested) }
|
||||
let_it_be(:public_project) { create(:project, :public, namespace: public_group) }
|
||||
|
||||
let_it_be(:project_issue) { create(:issue, project: public_project) }
|
||||
|
||||
let_it_be(:public_group_owner) { create(:user) }
|
||||
let_it_be(:private_group_member) { create(:user) }
|
||||
let_it_be(:public_project_maintainer) { create(:user) }
|
||||
let_it_be(:private_group_owner) { create(:user) }
|
||||
|
||||
let_it_be(:group_ancestor_owner) { create(:user) }
|
||||
|
||||
before_all do
|
||||
public_group.add_owner public_group_owner
|
||||
private_group.add_developer private_group_member
|
||||
public_project.add_maintainer public_project_maintainer
|
||||
|
||||
private_group.add_owner private_group_owner
|
||||
private_group.parent.add_owner group_ancestor_owner
|
||||
end
|
||||
|
||||
context 'when the private group is invited to the public project' do
|
||||
before_all do
|
||||
create(:project_group_link, group: private_group, project: public_project)
|
||||
end
|
||||
|
||||
context 'when a user who is outside the public project and the private group is signed in' do
|
||||
let(:service) { described_class.new(public_project, create(:user)) }
|
||||
|
||||
it 'does not return the private group' do
|
||||
expect(usernames).not_to include(private_group.name)
|
||||
end
|
||||
|
||||
it 'does not return private group members' do
|
||||
expect(usernames).not_to include(private_group_member.username)
|
||||
end
|
||||
|
||||
it 'returns the project maintainer' do
|
||||
expect(usernames).to include(public_project_maintainer.username)
|
||||
end
|
||||
|
||||
it 'returns project members from an invited public group' do
|
||||
invited_public_group = create(:group, :public)
|
||||
invited_public_group.add_owner create(:user)
|
||||
|
||||
create(:project_group_link, group: invited_public_group, project: public_project)
|
||||
|
||||
expect(usernames).to include(invited_public_group.users.first.username)
|
||||
end
|
||||
|
||||
it 'does not return ancestors of the private group' do
|
||||
expect(usernames).not_to include(group_ancestor_owner.username)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when public project maintainer is signed in' do
|
||||
let(:service) { described_class.new(public_project, public_project_maintainer) }
|
||||
|
||||
it 'returns private group members' do
|
||||
expect(usernames).to include(private_group_member.username)
|
||||
end
|
||||
|
||||
it 'returns members of the ancestral groups of the private group' do
|
||||
expect(usernames).to include(group_ancestor_owner.username)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when private group owner is signed in' do
|
||||
let(:service) { described_class.new(public_project, private_group_owner) }
|
||||
|
||||
it 'returns private group members' do
|
||||
expect(usernames).to include(private_group_member.username)
|
||||
end
|
||||
|
||||
it 'returns ancestors of the the private group' do
|
||||
expect(usernames).to include(group_ancestor_owner.username)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the namespace owner of the public project is signed in' do
|
||||
let(:service) { described_class.new(public_project, public_group_owner) }
|
||||
|
||||
it 'returns private group members' do
|
||||
expect(usernames).to include(private_group_member.username)
|
||||
end
|
||||
|
||||
it 'does not return members of the ancestral groups of the private group' do
|
||||
expect(usernames).to include(group_ancestor_owner.username)
|
||||
end
|
||||
end
|
||||
end
|
||||
it 'returns members of a group' do
|
||||
expect(usernames).to include(public_group_owner.username)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'return project members'
|
||||
context 'when there is a private group and a public project' do
|
||||
let_it_be(:public_group) { create(:group, :public) }
|
||||
let_it_be(:private_group) { create(:group, :private, :nested) }
|
||||
let_it_be(:public_project, reload: true) { create(:project, :public, namespace: public_group) }
|
||||
|
||||
let_it_be(:project_issue) { create(:issue, project: public_project) }
|
||||
|
||||
let_it_be(:public_group_owner) { create(:user) }
|
||||
let_it_be(:private_group_member) { create(:user) }
|
||||
let_it_be(:public_project_maintainer) { create(:user) }
|
||||
let_it_be(:private_group_owner) { create(:user) }
|
||||
|
||||
let_it_be(:group_ancestor_owner) { create(:user) }
|
||||
|
||||
before_all do
|
||||
public_group.add_owner public_group_owner
|
||||
private_group.add_developer private_group_member
|
||||
public_project.add_maintainer public_project_maintainer
|
||||
|
||||
private_group.add_owner private_group_owner
|
||||
private_group.parent.add_owner group_ancestor_owner
|
||||
end
|
||||
|
||||
context 'when the private group is invited to the public project' do
|
||||
before_all do
|
||||
create(:project_group_link, group: private_group, project: public_project)
|
||||
end
|
||||
|
||||
let(:service) { described_class.new(public_project, create(:user)) }
|
||||
|
||||
it 'does not return the private group' do
|
||||
expect(usernames).not_to include(private_group.name)
|
||||
end
|
||||
|
||||
it 'returns private group members' do
|
||||
expect(usernames).to include(private_group_member.username)
|
||||
end
|
||||
|
||||
it 'returns the project maintainer' do
|
||||
expect(usernames).to include(public_project_maintainer.username)
|
||||
end
|
||||
|
||||
it 'returns project members from an invited public group' do
|
||||
invited_public_group = create(:group, :public)
|
||||
invited_public_group.add_owner create(:user)
|
||||
|
||||
create(:project_group_link, group: invited_public_group, project: public_project)
|
||||
|
||||
expect(usernames).to include(invited_public_group.users.first.username)
|
||||
end
|
||||
|
||||
it 'returns members of the ancestral groups of the private group' do
|
||||
expect(usernames).to include(group_ancestor_owner.username)
|
||||
end
|
||||
|
||||
it 'returns invited group members of the private group' do
|
||||
invited_group = create(:group, :public)
|
||||
create(:group_group_link, shared_group: private_group, shared_with_group: invited_group)
|
||||
|
||||
other_user = create(:user)
|
||||
invited_group.add_guest(other_user)
|
||||
|
||||
expect(usernames).to include(other_user.username)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -570,12 +570,12 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
|
|||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:project_shared_runners_enabled, :shared_runners_setting, :expected_shared_runners_enabled) do
|
||||
true | :disabled_and_unoverridable | false
|
||||
false | :disabled_and_unoverridable | false
|
||||
true | :disabled_and_overridable | true
|
||||
false | :disabled_and_overridable | false
|
||||
true | :shared_runners_enabled | true
|
||||
false | :shared_runners_enabled | false
|
||||
true | :shared_runners_disabled_and_unoverridable | false
|
||||
false | :shared_runners_disabled_and_unoverridable | false
|
||||
true | :shared_runners_disabled_and_overridable | true
|
||||
false | :shared_runners_disabled_and_overridable | false
|
||||
true | :shared_runners_enabled | true
|
||||
false | :shared_runners_enabled | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'handling nuget service requests' do
|
||||
RSpec.shared_examples 'handling nuget service requests' do |v2: false|
|
||||
subject { get api(url) }
|
||||
|
||||
it { is_expected.to have_request_urgency(v2 ? :low : :default) }
|
||||
|
||||
context 'with valid target' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
@ -20,15 +22,17 @@ RSpec.shared_examples 'handling nuget service requests' do
|
|||
end
|
||||
|
||||
with_them do
|
||||
let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
|
||||
|
||||
subject { get api(url) }
|
||||
let(:snowplow_gitlab_standard_context) do
|
||||
snowplow_context(user_role: :anonymous).tap do |ctx|
|
||||
ctx[:feed] = 'v2' if v2
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], v2
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
|
||||
RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true, v2 = false|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
|
|
@ -28,15 +28,22 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
|
|||
|
||||
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata'
|
||||
|
||||
it 'returns a valid json response' do
|
||||
it 'returns a valid json or xml response' do
|
||||
subject
|
||||
|
||||
expect(response.media_type).to eq('application/json')
|
||||
expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
|
||||
expect(json_response).to be_a(Hash)
|
||||
if v2
|
||||
expect(response.media_type).to eq('application/xml')
|
||||
expect(body).to have_xpath('//service')
|
||||
.and have_xpath('//service/workspace')
|
||||
.and have_xpath('//service/workspace/collection[@href]')
|
||||
else
|
||||
expect(response.media_type).to eq('application/json')
|
||||
expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
|
||||
expect(json_response).to be_a(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid format' do
|
||||
context 'with invalid format', unless: v2 do
|
||||
let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" }
|
||||
|
||||
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
|
||||
|
|
@ -439,6 +446,13 @@ end
|
|||
|
||||
RSpec.shared_examples 'nuget authorize upload endpoint' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { put api(url), headers: headers }
|
||||
|
||||
it { is_expected.to have_request_urgency(:low) }
|
||||
|
||||
context 'with valid project' do
|
||||
where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
|
||||
|
|
@ -517,6 +531,26 @@ end
|
|||
|
||||
RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
include_context 'workhorse headers'
|
||||
|
||||
let(:headers) { {} }
|
||||
let(:file_name) { symbol_package ? 'package.snupkg' : 'package.nupkg' }
|
||||
let(:params) { { package: temp_file(file_name) } }
|
||||
let(:file_key) { :package }
|
||||
let(:send_rewritten_field) { true }
|
||||
|
||||
subject do
|
||||
workhorse_finalize(
|
||||
api(url),
|
||||
method: :put,
|
||||
file_key: file_key,
|
||||
params: params,
|
||||
headers: headers,
|
||||
send_rewritten_field: send_rewritten_field
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to have_request_urgency(:low) }
|
||||
|
||||
context 'with valid project' do
|
||||
where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
|
||||
|
|
@ -573,7 +607,12 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
|
|||
end
|
||||
|
||||
let(:headers) { user_headers.merge(workhorse_headers) }
|
||||
let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } }
|
||||
|
||||
let(:snowplow_gitlab_standard_context) do
|
||||
{ project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' }.tap do |ctx|
|
||||
ctx[:feed] = 'v2' if url.include?('nuget/v2')
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
|
||||
|
|
@ -604,4 +643,16 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
|
|||
|
||||
it_behaves_like 'returning response status', :bad_request
|
||||
end
|
||||
|
||||
context 'when ObjectStorage::RemoteStoreError is raised' do
|
||||
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(::Packages::CreatePackageFileService) do |instance|
|
||||
allow(instance).to receive(:execute).and_raise(ObjectStorage::RemoteStoreError)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', :forbidden
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'projects/pages/_pages_settings', feature_category: :pages do
|
||||
let_it_be(:project) { build_stubbed(:project, :repository) }
|
||||
let_it_be(:user) { build_stubbed(:user) }
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
allow(view).to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
context 'for pages unique domain' do
|
||||
it 'shows the unique domain toggle' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_content('Use unique domain')
|
||||
end
|
||||
|
||||
context 'when pages_unique_domain feature flag is disabled' do
|
||||
it 'does not show the unique domain toggle' do
|
||||
stub_feature_flags(pages_unique_domain: false)
|
||||
|
||||
# We have to use `view.render` because `render` causes issues
|
||||
# https://github.com/rails/rails/issues/41320
|
||||
expect(view.render('projects/pages/pages_settings')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -285,6 +285,9 @@ func configureRoutes(u *upstream) {
|
|||
// NuGet Artifact Repository
|
||||
u.route("PUT", apiProjectPattern+`/packages/nuget/`, mimeMultipartUploader),
|
||||
|
||||
// NuGet v2 Artifact Repository
|
||||
u.route("PUT", apiProjectPattern+`/packages/nuget/v2`, mimeMultipartUploader),
|
||||
|
||||
// PyPI Artifact Repository
|
||||
u.route("POST", apiProjectPattern+`/packages/pypi`, mimeMultipartUploader),
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ func TestAcceleratedUpload(t *testing.T) {
|
|||
{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},
|
||||
{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true},
|
||||
{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true},
|
||||
{"PUT", "/api/v4/projects/9001/packages/nuget/v2/files", true},
|
||||
{"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v2/files", true},
|
||||
{"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v2/files", true},
|
||||
{"POST", `/api/v4/groups/import`, true},
|
||||
{"POST", `/api/v4/groups/import/`, true},
|
||||
{"POST", `/api/v4/projects/import`, true},
|
||||
|
|
@ -289,6 +292,8 @@ func TestUnacceleratedUploads(t *testing.T) {
|
|||
{"POST", `/api/v4/projects/group/project/wikis/attachments`},
|
||||
{"PUT", "/api/v4/projects/group/subgroup/project/packages/nuget/v1/files"},
|
||||
{"PUT", "/api/v4/projects/group/project/packages/nuget/v1/files"},
|
||||
{"POST", "/api/v4/projects/group/subgroup/project/packages/nuget/v2/files"},
|
||||
{"POST", "/api/v4/projects/group/project/packages/nuget/v2/files"},
|
||||
{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
|
||||
{"POST", `/api/v4/projects/group/project/packages/pypi`},
|
||||
{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
|
||||
|
|
|
|||
Loading…
Reference in New Issue