Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-02-19 12:12:26 +00:00
parent fe2f83b699
commit addf13e0d0
111 changed files with 2382 additions and 691 deletions

View File

@ -1,45 +1,4 @@
.review-docs:
extends:
- .default-retry
- .docs:rules:review-docs
image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine
stage: review
needs: []
variables:
# We're cloning the repo instead of downloading the script for now
# because some repos are private and CI_JOB_TOKEN cannot access files.
# See https://gitlab.com/gitlab-org/gitlab/issues/191273
GIT_DEPTH: 1
# By default, deploy the Review App using the `main` branch of the `gitlab-org/gitlab-docs` project
DOCS_BRANCH: main
environment:
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14236/diffs#note_40140693
auto_stop_in: 2 weeks
url: http://${DOCS_BRANCH}-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}.${DOCS_REVIEW_APPS_DOMAIN}/${DOCS_GITLAB_REPO_SUFFIX}
on_stop: review-docs-cleanup
before_script:
- source ./scripts/utils.sh
- install_gitlab_gem
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy:
extends: .review-docs
script:
- ./scripts/trigger-build.rb docs deploy
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
extends: .review-docs
environment:
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
action: stop
script:
- ./scripts/trigger-build.rb docs cleanup
.review-docs-hugo:
extends:
- .default-retry
- .docs:rules:review-docs
@ -51,25 +10,25 @@ review-docs-cleanup:
# By default, deploy the Review App using the `main` branch of the `gitlab-org/technical-writing/docs-gitlab-com` project
DOCS_BRANCH: main
environment:
name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
auto_stop_in: 2 weeks
url: https://new.docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
on_stop: review-docs-hugo-cleanup
url: https://docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
on_stop: review-docs-cleanup
before_script:
- source ./scripts/utils.sh
- install_gitlab_gem
# Deploy documentation review app by using GitLab Docs Hugo project (gitlab-org/technical-writing/docs-gitlab-com)
review-docs-hugo-deploy:
extends: .review-docs-hugo
# Deploy documentation review app by using GitLab Docs project (gitlab-org/technical-writing/docs-gitlab-com)
review-docs-deploy:
extends: .review-docs
script:
- ./scripts/trigger-build.rb docs-hugo deploy
# Cleanup remote environment of gitlab-org/technical-writing/docs-gitlab-com
review-docs-hugo-cleanup:
extends: .review-docs-hugo
review-docs-cleanup:
extends: .review-docs
environment:
name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
action: stop
script:
- ./scripts/trigger-build.rb docs-hugo cleanup

View File

@ -1125,12 +1125,9 @@ Gitlab/BoundedContexts:
- 'app/models/preloaders/commit_status_preloader.rb'
- 'app/models/preloaders/environments/deployment_preloader.rb'
- 'app/models/preloaders/group_policy_preloader.rb'
- 'app/models/preloaders/group_root_ancestor_preloader.rb'
- 'app/models/preloaders/labels_preloader.rb'
- 'app/models/preloaders/merge_request_diff_preloader.rb'
- 'app/models/preloaders/namespace_root_ancestor_preloader.rb'
- 'app/models/preloaders/project_policy_preloader.rb'
- 'app/models/preloaders/project_root_ancestor_preloader.rb'
- 'app/models/preloaders/projects/notes_preloader.rb'
- 'app/models/preloaders/runner_manager_policy_preloader.rb'
- 'app/models/preloaders/user_max_access_level_in_groups_preloader.rb'

View File

@ -3475,7 +3475,6 @@ RSpec/FeatureCategory:
- 'spec/presenters/blobs/notebook_presenter_spec.rb'
- 'spec/presenters/blobs/unfold_presenter_spec.rb'
- 'spec/presenters/ci/bridge_presenter_spec.rb'
- 'spec/presenters/ci/build_presenter_spec.rb'
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
- 'spec/presenters/ci/group_variable_presenter_spec.rb'
- 'spec/presenters/ci/pipeline_artifacts/code_coverage_presenter_spec.rb'
@ -3678,7 +3677,6 @@ RSpec/FeatureCategory:
- 'spec/serializers/blob_entity_spec.rb'
- 'spec/serializers/build_action_entity_spec.rb'
- 'spec/serializers/build_artifact_entity_spec.rb'
- 'spec/serializers/build_details_entity_spec.rb'
- 'spec/serializers/build_trace_entity_spec.rb'
- 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb'
- 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb'

View File

@ -12,7 +12,6 @@ import DomElementListener from '~/vue_shared/components/dom_element_listener.vue
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
import UserDate from '~/vue_shared/components/user_date.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert, VARIANT_DANGER } from '~/alert';
import { EVENT_SUCCESS, FIELDS, FORM_SELECTOR, INITIAL_PAGE, PAGE_SIZE } from './constants';
@ -41,7 +40,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
lastUsedHelpLink: helpPagePath('/user/profile/personal_access_tokens.md', {
anchor: 'view-token-usage-information',
}),
@ -117,10 +115,6 @@ export default {
ignoredFields.push('role');
}
if (!this.glFeatures.patIp) {
ignoredFields.push('lastUsedIps');
}
const fields = FIELDS.filter(({ key }) => !ignoredFields.includes(key));
// Remove the sortability of the columns if backend pagination is on.

View File

@ -0,0 +1,292 @@
<script>
import { GlButton, GlFormGroup, GlFormInput, GlAnimatedUploadIcon } from '@gitlab/ui';
import { kebabCase } from 'lodash';
import validation from '~/vue_shared/directives/validation';
import csrf from '~/lib/utils/csrf';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
import NewProjectDestinationSelect from '~/projects/new_v2/components/project_destination_select.vue';
const feedbackMap = {
valueMissing: {
isInvalid: (el) => el.validity?.valueMissing,
},
nameStartPattern: {
isInvalid: (el) => el.validity?.patternMismatch && !START_RULE.reg.test(el.value),
message: START_RULE.msg,
},
nameContainsPattern: {
isInvalid: (el) => el.validity?.patternMismatch && !CONTAINS_RULE.reg.test(el.value),
message: CONTAINS_RULE.msg,
},
};
const initFormField = ({ value = null, required = true } = {}) => ({
value,
required,
state: null,
feedback: null,
});
export default {
components: {
GlButton,
GlFormGroup,
GlFormInput,
GlAnimatedUploadIcon,
FileIcon,
MultiStepFormTemplate,
NewProjectDestinationSelect,
},
directives: {
validation: validation(feedbackMap),
},
props: {
backButtonPath: {
type: String,
required: true,
},
namespaceFullPath: {
type: String,
required: true,
},
namespaceId: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
importGitlabProjectPath: {
type: String,
required: true,
},
},
data() {
const form = {
state: false,
showValidation: false,
fields: {
name: initFormField(),
path: initFormField(),
},
};
return {
file: null,
filePreviewURL: null,
form,
animateUploadIcon: false,
dropzoneState: true,
};
},
computed: {
formattedFileSize() {
return numberToHumanSize(this.file.size);
},
},
watch: {
// eslint-disable-next-line func-names
'form.fields.name.value': function (newVal) {
this.form.fields.path.value = kebabCase(newVal);
},
},
methods: {
setFile() {
this.file = this.$refs.fileUpload.files['0'];
const fileUrlReader = new FileReader();
fileUrlReader.readAsDataURL(this.file);
fileUrlReader.onload = (e) => {
this.filePreviewURL = e.target?.result;
};
this.dropzoneState = true;
},
onDropzoneMouseEnter() {
this.animateUploadIcon = true;
},
onDropzoneMouseLeave() {
this.animateUploadIcon = false;
},
openFileUpload() {
this.$refs.fileUpload.click();
},
onSubmit() {
if (!this.form.state) {
this.form.showValidation = true;
}
if (this.file === null) {
this.dropzoneState = false;
}
if (!this.form.state || !this.dropzoneState) {
return;
}
this.$refs.form.submit();
},
},
csrf,
projectNamePattern: `(${START_RULE.reg.source})|(${CONTAINS_RULE.reg.source})`,
validFileMimetypes: ['application/gzip'],
};
</script>
<template>
<form
ref="form"
:action="importGitlabProjectPath"
enctype="multipart/form-data"
method="post"
@submit.prevent="onSubmit"
>
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<multi-step-form-template
:title="__('Import an exported GitLab project')"
:current-step="3"
:steps-total="3"
>
<template #form>
<gl-form-group
:label="__('Project name')"
label-for="name"
:description="
s__(
'ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces.',
)
"
:invalid-feedback="form.fields.name.feedback"
data-testid="project-name-form-group"
>
<gl-form-input
id="name"
v-model="form.fields.name.value"
v-validation:[form.showValidation]
:validation-message="s__('ProjectsNew|Please enter a valid project name.')"
:state="form.fields.name.state"
:pattern="$options.projectNamePattern"
name="name"
required
:placeholder="s__('ProjectsNew|My awesome project')"
data-testid="project-name"
/>
</gl-form-group>
<div class="gl-flex gl-flex-col gl-gap-4 sm:gl-flex-row">
<gl-form-group
:label="s__('ProjectsNew|Choose a group')"
class="sm:gl-w-1/2"
label-for="namespace"
>
<new-project-destination-select
toggle-aria-labelled-by="namespace"
:namespace-full-path="namespaceFullPath"
:namespace-id="namespaceId"
:root-url="rootPath"
/>
</gl-form-group>
<div class="gl-mt-2 gl-hidden gl-pt-6 sm:gl-block">{{ __('/') }}</div>
<gl-form-group
:label="s__('ProjectsNew|Project slug')"
label-for="path"
class="sm:gl-w-1/2"
:invalid-feedback="form.fields.path.feedback"
>
<gl-form-input
id="path"
v-model="form.fields.path.value"
v-validation:[form.showValidation]
:validation-message="s__('ProjectsNew|Please enter a valid project slug.')"
:state="form.fields.path.state"
name="path"
required
:placeholder="s__('ProjectsNew|my-awesome-project')"
data-testid="project-slug"
/>
</gl-form-group>
</div>
<p class="-gl-mt-3 gl-text-base gl-leading-normal gl-text-subtle">
{{
s__(
"ProjectsNew|To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.",
)
}}
</p>
<gl-form-group
:label="s__('ProjectsNew|GitLab project export')"
label-for="file-button"
:invalid-feedback="s__('ProjectsNew|Please upload a valid GitLab project export file.')"
:state="dropzoneState"
data-testid="project-file-form-group"
>
<button
id="file-button"
class="upload-dropzone-card upload-dropzone-border gl-mb-0 gl-h-full gl-w-full gl-items-center gl-justify-center gl-bg-default gl-px-5 gl-py-4"
type="button"
data-testid="dropzone-button"
@click="openFileUpload"
@mouseenter="onDropzoneMouseEnter"
@mouseleave="onDropzoneMouseLeave"
>
<div
v-if="file"
class="gl-flex gl-w-full gl-flex-col gl-items-center gl-justify-center gl-gap-3"
>
<file-icon :file-name="file.name" :size="24" class="gl-flex" />
<span>
{{ file.name }}
&middot;
<span class="gl-text-subtle">{{ formattedFileSize }}</span>
</span>
</div>
<div v-else class="gl-flex gl-items-center gl-justify-center gl-gap-3 gl-text-center">
<gl-animated-upload-icon :is-on="animateUploadIcon" />
<span>{{ __('Drop or upload file to attach') }}</span>
</div>
<input
ref="fileUpload"
type="file"
name="file"
hidden
:accept="$options.validFileMimetypes"
required
:multiple="false"
data-testid="dropzone-input"
@change="setFile"
/>
</button>
</gl-form-group>
</template>
<template #back>
<gl-button
category="primary"
variant="default"
:href="backButtonPath"
data-testid="back-button"
>
{{ __('Go back') }}
</gl-button>
</template>
<template #next>
<gl-button
type="submit"
category="primary"
variant="confirm"
data-testid="next-button"
@click.prevent="onSubmit"
>
{{ __('Import project') }}
</gl-button>
</template>
</multi-step-form-template>
</form>
</template>

View File

@ -0,0 +1,49 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import importFromGitlabExportApp from './import_from_gitlab_export_app.vue';
export function initGitLabImportProjectForm() {
const el = document.getElementById('js-import-gitlab-project-root');
if (!el) {
return null;
}
const {
backButtonPath,
namespaceFullPath,
namespaceId,
rootPath,
importGitlabProjectPath,
userNamespaceId,
canCreateProject,
rootUrl,
} = el.dataset;
const props = {
backButtonPath,
namespaceFullPath,
namespaceId,
rootPath,
importGitlabProjectPath,
};
const provide = {
userNamespaceId,
canCreateProject,
rootUrl,
};
return new Vue({
el,
name: 'ImportGitLabProjectRoot',
apolloProvider: new VueApollo({
defaultClient: createDefaultClient(),
}),
provide,
render(h) {
return h(importFromGitlabExportApp, { props });
},
});
}

View File

@ -1,5 +1,7 @@
import initGitLabImportProject from '~/projects/project_import_gitlab_project';
import { initNewProjectUrlSelect } from '~/projects/new';
import { initGitLabImportProjectForm } from '~/import/gitlab_project';
initNewProjectUrlSelect();
initGitLabImportProject();
initGitLabImportProjectForm();

View File

@ -1,19 +1,17 @@
<script>
import { GlButton, GlTruncate, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
import { PATH_SEPARATOR } from '~/lib/utils/url_utility';
import { GlCollapsibleListbox } from '@gitlab/ui';
import { joinPaths, PATH_SEPARATOR } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import { __, s__, n__ } from '~/locale';
import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
import eventHub from '../event_hub';
export default {
components: {
GlButton,
GlTruncate,
GlCollapsibleListbox,
GlIcon,
},
mixins: [Tracking.mixin()],
apollo: {
@ -49,6 +47,11 @@ export default {
required: false,
default: '',
},
toggleAriaLabelledBy: {
type: String,
required: false,
default: '',
},
groupsOnly: {
type: Boolean,
required: false,
@ -155,6 +158,19 @@ export default {
this.shouldSkipQuery = false;
}
},
handleDropdownItemClick(namespaceId) {
const namespace = this.allItems.find((item) => item.id === namespaceId);
if (namespace) {
eventHub.$emit('update-visibility', {
name: namespace.name,
visibility: namespace.visibility,
showPath: namespace.webUrl,
editPath: joinPaths(namespace.webUrl, '-', 'edit'),
});
}
this.setNamespace(namespace);
},
handleSelectTemplate(id, fullPath) {
this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
this.setNamespace({ id, fullPath });
@ -187,31 +203,34 @@ export default {
</script>
<template>
<gl-collapsible-listbox
searchable
fluid-width
:searching="loading"
:items="items"
:toggle-text="dropdownText"
:no-results-text="$options.i18n.emptySearchResult"
class="gl-w-full"
@show="trackDropdownShow"
@shown="handleDropdownShown"
@search="onSearch"
>
<template #toggle>
<gl-button :class="dropdownPlaceholderClass">
<gl-truncate
:text="dropdownText"
position="start"
class="gl-mr-auto gl-overflow-hidden"
with-tooltip
/>
<gl-icon class="gl-button-icon dropdown-chevron !gl-ml-2 !gl-mr-0" name="chevron-down" />
</gl-button>
</template>
<template #search-summary-sr-only>
{{ searchSummary }}
</template>
</gl-collapsible-listbox>
<div>
<gl-collapsible-listbox
searchable
fluid-width
:searching="loading"
:items="items"
:toggle-text="dropdownText"
toggle-class="gl-w-full"
:toggle-aria-labelled-by="toggleAriaLabelledBy"
:no-results-text="$options.i18n.emptySearchResult"
class="project-destination-select gl-w-full gl-max-w-full"
@show="trackDropdownShow"
@shown="handleDropdownShown"
@select="handleDropdownItemClick"
@search="onSearch"
>
<template #search-summary-sr-only>
{{ searchSummary }}
</template>
</gl-collapsible-listbox>
<input type="hidden" name="project[selected_namespace_id]" :value="selectedNamespace.id" />
<input
id="project[namespace_id]"
type="hidden"
name="namespace_id"
:value="selectedNamespace.id || userNamespaceUniqueId"
/>
</div>
</template>

View File

@ -25,20 +25,22 @@ export default () => {
const $projectPath = document.querySelector('.js-path-name');
const { name, path } = prepareParameters();
// get the project name from the URL and set it as input value
$projectName.value = name;
if ($projectName || $projectPath) {
// get the project name from the URL and set it as input value
$projectName.value = name;
// get the path url and append it in the input
$projectPath.value = path;
// get the path url and append it in the input
$projectPath.value = path;
// generate slug when project name changes
$projectName.addEventListener('keyup', () => {
projectNew.onProjectNameChange($projectName, $projectPath);
hasUserDefinedProjectName = $projectName.value.trim().length > 0;
});
// generate slug when project name changes
$projectName.addEventListener('keyup', () => {
projectNew.onProjectNameChange($projectName, $projectPath);
hasUserDefinedProjectName = $projectName.value.trim().length > 0;
});
// generate project name from the slug if one isn't set
$projectPath.addEventListener('keyup', () =>
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
);
// generate project name from the slug if one isn't set
$projectPath.addEventListener('keyup', () =>
projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName),
);
}
};

View File

@ -1,5 +1,7 @@
<script>
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { InternalEvents } from '~/tracking';
export default {
components: {
@ -7,16 +9,26 @@ export default {
GlButtonGroup,
GlDisclosureDropdownItem,
},
mixins: [InternalEvents.mixin()],
props: {
ideItem: {
type: Object,
required: true,
},
},
computed: {
shortcutsDisabled() {
return shouldDisableShortcuts();
},
},
methods: {
closeDropdown() {
this.$emit('close-dropdown');
},
trackAndClose({ action, label }) {
this.trackEvent(action, { label });
this.closeDropdown();
},
},
};
</script>
@ -41,5 +53,16 @@ export default {
</gl-button>
</gl-button-group>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item v-else-if="ideItem.href" :item="ideItem" @action="closeDropdown" />
<gl-disclosure-dropdown-item
v-else-if="ideItem.href"
:item="ideItem"
@action="trackAndClose(ideItem.tracking)"
>
<template #list-item>
<span class="gl-mb-2 gl-flex gl-items-center gl-justify-between">
<span>{{ ideItem.text }}</span>
<kbd v-if="ideItem.shortcut && !shortcutsDisabled" class="flat">{{ ideItem.shortcut }}</kbd>
</span>
</template>
</gl-disclosure-dropdown-item>
</template>

View File

@ -2,6 +2,7 @@
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { getHTTPProtocol } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import { GO_TO_PROJECT_WEBIDE, keysFor } from '~/behaviors/shortcuts/keybindings';
import CodeDropdownCloneItem from './code_dropdown_clone_item.vue';
import CodeDropdownDownloadItems from './code_dropdown_download_items.vue';
import CodeDropdownIdeItem from './code_dropdown_ide_item.vue';
@ -36,6 +37,16 @@ export default {
required: false,
default: '',
},
webIdeUrl: {
type: String,
required: false,
default: '',
},
gitpodUrl: {
type: String,
required: false,
default: '',
},
currentPath: {
type: String,
required: false,
@ -46,6 +57,16 @@ export default {
required: false,
default: () => [],
},
showWebIdeButton: {
type: Boolean,
required: false,
default: true,
},
showGitpodButton: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
httpLabel() {
@ -58,22 +79,52 @@ export default {
httpUrlEncoded() {
return encodeURIComponent(this.httpUrl);
},
webIdeActionShortcutKey() {
return keysFor(GO_TO_PROJECT_WEBIDE)[0];
},
webIdeAction() {
return {
text: __('Web IDE'),
shortcut: this.webIdeActionShortcutKey,
tracking: {
action: 'click_consolidated_edit',
label: 'web_ide',
},
href: this.webIdeUrl,
extraAttrs: {
target: '_blank',
},
};
},
gitPodAction() {
return {
text: __('GitPod'),
tracking: {
action: 'click_consolidated_edit',
label: 'gitpod',
},
href: this.gitpodUrl,
extraAttrs: {
target: '_blank',
},
};
},
ideGroup() {
const groups = [
/* eslint-disable-next-line @gitlab/require-i18n-strings */
this.createIdeGroup('Visual Studio Code', VSCODE_BASE_URL),
this.createIdeGroup('IntelliJ IDEA', JETBRAINS_BASE_URL),
];
const actions = [];
if (this.xcodeUrl) {
groups.push({
/* eslint-disable-next-line @gitlab/require-i18n-strings */
text: 'Xcode',
href: this.xcodeUrl,
});
if (this.showWebIdeButton) actions.push(this.webIdeAction);
if (this.showGitpodButton) actions.push(this.gitPodAction);
if (this.httpUrl || this.sshUrl) {
actions.push(this.createIdeGroup(__('Visual Studio Code'), VSCODE_BASE_URL));
actions.push(this.createIdeGroup(__('IntelliJ IDEA'), JETBRAINS_BASE_URL));
}
return groups.filter((group) => group.items?.length || group.href);
if (this.xcodeUrl) {
actions.push({ text: __('Xcode'), href: this.xcodeUrl });
}
return actions;
},
sourceCodeGroup() {
return this.directoryDownloadLinks.map((link) => ({
@ -107,7 +158,7 @@ export default {
...(this.sshUrl
? [
{
text: 'SSH',
text: __('SSH'),
href: `${baseUrl}${this.sshUrlEncoded}`,
},
]
@ -115,7 +166,7 @@ export default {
...(this.httpUrl
? [
{
text: 'HTTPS',
text: __('HTTPS'),
href: `${baseUrl}${this.httpUrlEncoded}`,
},
]

View File

@ -323,8 +323,12 @@ export default {
:http-url="httpUrl"
:kerberos-url="kerberosUrl"
:xcode-url="xcodeUrl"
:web-ide-url="webIDEUrl"
:gitpod-url="gitpodUrl"
:current-path="currentPath"
:directory-download-links="downloadLinks"
:show-web-ide-button="showWebIdeButton"
:show-gitpod-button="showGitpodButton"
/>
<repository-overflow-menu v-if="comparePath" />
</div>

View File

@ -2,6 +2,10 @@ import { s__ } from '~/locale';
export const BASE_IMPORT_TABLE_ROW_GRID_CLASSES = 'gl-grid-cols-[repeat(2,1fr),200px,200px]';
export const SOURCE_TYPE_GROUP = 'group';
export const SOURCE_TYPE_PROJECT = 'project';
export const SOURCE_TYPE_FILE = 'file';
export const IMPORT_HISTORY_TABLE_STATUS = {
inProgress: 'started',
complete: 'finished',

View File

@ -0,0 +1,21 @@
import { basic } from 'jest/vue_shared/components/import_history_table/mock_data';
import ImportHistoryTableSource from './import_history_table_source.vue';
export default {
component: ImportHistoryTableSource,
title: 'vue_shared/import/import_history_table_source',
};
const defaultProps = {
item: basic.items[0],
};
const Template = (args, { argTypes }) => ({
components: { ImportHistoryTableSource },
props: Object.keys(argTypes),
template: `<import-history-table-source v-bind="$props"/>`,
});
export const Default = Template.bind({});
Default.args = defaultProps;

View File

@ -0,0 +1,55 @@
<script>
import { GlIcon, GlLink, GlTruncate } from '@gitlab/ui';
import { SOURCE_TYPE_GROUP, SOURCE_TYPE_PROJECT, SOURCE_TYPE_FILE } from './constants';
/**
* A basic formatter for showing the source information of an import
*/
export default {
name: 'ImportHistoryTableSource',
components: {
GlIcon,
GlLink,
GlTruncate,
},
props: {
/**
* Should accept the data that comes form the BulkImport API
*/
item: {
type: Object,
required: true,
},
},
computed: {
sourceIconName() {
switch (this.item.entity_type) {
case SOURCE_TYPE_PROJECT:
return 'project';
case SOURCE_TYPE_FILE:
return 'project';
case SOURCE_TYPE_GROUP:
return 'group';
default:
return '';
}
},
isFile() {
return this.item.entity_type === SOURCE_TYPE_FILE;
},
},
};
</script>
<template>
<div class="gl-flex gl-items-start gl-gap-3 gl-pt-1">
<gl-icon :name="sourceIconName" class="gl-mt-1 gl-flex-shrink-0" />
<span v-if="isFile">{{ item.fileName }}</span>
<gl-link
v-else
class="gl-overflow-hidden !gl-text-default hover:gl-underline"
:href="item.source_full_path"
>
<gl-truncate :text="item.source_full_path" position="middle" with-tooltip />
</gl-link>
</div>
</template>

View File

@ -10,6 +10,7 @@ import Tracking from '~/tracking';
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
import { keysFor, GO_TO_PROJECT_WEBIDE } from '~/behaviors/shortcuts/keybindings';
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
export const i18n = {
@ -32,7 +33,7 @@ export default {
ConfirmForkModal,
},
i18n,
mixins: [Tracking.mixin()],
mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
props: {
isFork: {
type: Boolean,
@ -141,13 +142,15 @@ export default {
};
},
computed: {
hideIDEActionsInDirectoryView() {
return this.glFeatures.directoryCodeDropdownUpdates && !this.isBlob;
},
actions() {
return [
this.pipelineEditorAction,
this.webIdeAction,
this.editAction,
this.gitpodAction,
].filter((action) => action);
return this.hideIDEActionsInDirectoryView
? [this.pipelineEditorAction, this.editAction].filter(Boolean)
: [this.pipelineEditorAction, this.webIdeAction, this.editAction, this.gitpodAction].filter(
Boolean,
);
},
hasActions() {
return this.actions.length > 0;

View File

@ -557,3 +557,8 @@
@apply gl-line-clamp-2 gl-whitespace-normal;
margin-bottom: 0;
}
// stylelint-disable-next-line gitlab/no-gl-class
.project-destination-select .gl-button-text {
flex-grow: 1;
}

View File

@ -8,9 +8,6 @@ module UserSettings
feature_category :system_access
before_action :check_personal_access_tokens_enabled
before_action do
push_frontend_feature_flag(:pat_ip, current_user)
end
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:ics) }
def index

View File

@ -17,7 +17,7 @@ module Resolvers
private
def unconditional_includes
[:trigger_requests]
[:trigger_requests, :pipelines]
end
end
end

View File

@ -268,7 +268,11 @@ module Types
end
def triggered
object.try(:trigger_request)
if Feature.enabled?(:ci_read_trigger_from_ci_pipeline, object.project)
object.pipeline.trigger_id.present?
else
object.try(:trigger_request).present?
end
end
def manual_variables

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
module ErrorTracking
# rubocop:disable Graphql/AuthorizeTypes -- we already authorize the work item itself
class StackTraceContextType < BaseObject
graphql_name 'WorkItemWidgetErrorTrackingStackTraceContext'
description 'Represents details about a line of code of the stack trace'
field :line_number, GraphQL::Types::Int,
null: true,
description: 'Line number of code.', method: :first
field :line, GraphQL::Types::String,
null: true,
description: 'Line of code.', method: :last
end
# rubocop:enable Graphql/AuthorizeTypes
end
end
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
module ErrorTracking
# Disabling widget level authorization as it might be too granular
# and we already authorize the parent work item
# rubocop:disable Graphql/AuthorizeTypes -- reason above
class StackTraceType < BaseObject
graphql_name 'ErrorTrackingStackTrace'
description 'Represents a stack trace'
connection_type_class Types::CountableConnectionType
field :filename, GraphQL::Types::String,
null: true,
description: 'Filename of the stack trace.'
field :absolute_path, GraphQL::Types::String,
null: true,
description: 'Absolute path of the stack trace.', hash_key: "absPath"
field :function, GraphQL::Types::String,
null: true,
description: 'Name of the function where the error occured.'
field :line_number, GraphQL::Types::Int,
null: true,
description: 'Line number of the stack trace.', hash_key: "lineNo"
field :column_number, GraphQL::Types::Int,
null: true,
description: 'Column number of the stack trace.', hash_key: "colNo"
field :context, [Types::WorkItems::Widgets::ErrorTracking::StackTraceContextType],
null: true,
description: 'Context of the stack trace.', hash_key: "context"
end
# rubocop:enable Graphql/AuthorizeTypes
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
class ErrorTrackingStatusEnum < BaseEnum
graphql_name 'ErrorTrackingStatus'
description 'Status of the error tracking service'
value 'SUCCESS', value: :success, description: 'Successfuly fetch the stack trace.'
value 'ERROR', value: :error, description: 'Error tracking service respond with an error.'
value 'NOT_FOUND', value: :not_found, description: 'Sentry issue not found.'
value 'RETRY', value: :retry, description: 'Error tracking service is not ready.'
end
end
end
end

View File

@ -13,7 +13,56 @@ module Types
implements ::Types::WorkItems::WidgetInterface
field :identifier, GraphQL::Types::BigInt, null: true,
description: 'Error tracking issue id.', method: :sentry_issue_identifier
description: 'Error tracking issue id.' \
'This field can only be resolved for one work item in any single request.',
method: :sentry_issue_identifier do
extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
end
field :stack_trace, ::Types::WorkItems::Widgets::ErrorTracking::StackTraceType.connection_type,
null: true,
description: 'Stack trace details of the error.' \
'This field can only be resolved for one work item in any single request.' do
extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
end
field :status, ErrorTrackingStatusEnum, null: true,
description: 'Response status of error service.' \
'This field can only be resolved for one work item in any single request.' do
extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: 1)
end
def stack_trace
return [] if object.sentry_issue_identifier.nil?
if latest_event_result[:status] == :success
Gitlab::ErrorTracking::StackTraceHighlightDecorator
.decorate(latest_event_result[:latest_event])
.stack_trace_entries
else
[]
end
end
def status
return :not_found if object.sentry_issue_identifier.nil?
if latest_event_result[:status] == :success
:success
elsif latest_event_result[:http_status] == :no_content
:retry
else
:error
end
end
private
def latest_event_result
@latest_event ||= ::ErrorTracking::IssueLatestEventService
.new(object.work_item.project, current_user, issue_id: object.sentry_issue_identifier)
.execute
end
end
# rubocop:enable Graphql/AuthorizeTypes
end

View File

@ -21,10 +21,6 @@ module BreadcrumbsHelper
@breadcrumb_title = title
end
def breadcrumb_list_item(link)
content_tag :li, link, class: 'gl-breadcrumb-item gl-inline-flex'
end
def add_to_breadcrumb_collapsed_links(link, location: :before)
@breadcrumb_collapsed_links ||= {}
@breadcrumb_collapsed_links[location] ||= []

View File

@ -41,29 +41,12 @@ module GroupsHelper
group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png')
end
def group_title(group)
@has_group_title = true
full_title = []
sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index|
if index > 0
add_to_breadcrumb_collapsed_links(
{ text: simple_sanitize(parent.name), href: group_path(parent), avatar_url: parent.try(:avatar_url) },
location: :before
)
else
full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
end
def push_group_breadcrumbs(group)
sorted_ancestors(group).with_route.reverse_each do |parent|
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent), parent.try(:avatar_url))
end
full_title << render("layouts/nav/breadcrumbs/collapsed_inline_list", location: :before, title: _("Show all breadcrumbs"))
full_title << breadcrumb_list_item(group_title_link(group))
push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group), group.try(:avatar_url))
full_title.join.html_safe
end
def projects_lfs_status(group)

View File

@ -88,14 +88,10 @@ module PageLayoutHelper
end
def header_title(title = nil, title_url = nil)
if title
@header_title = title
@header_title_url = title_url
else
return @header_title unless @header_title_url
return @header_title unless title
breadcrumb_list_item(link_to(@header_title, @header_title_url))
end
@header_title = title
@header_title_url = title_url
end
def sidebar(name = nil)

View File

@ -103,14 +103,18 @@ module ProjectsHelper
end
end
def project_title(project)
namespace_link = build_namespace_breadcrumb_link(project)
project_link = build_project_breadcrumb_link(project)
def push_project_breadcrumbs(project)
if project.group
push_group_breadcrumbs(project.group)
else
owner = project.namespace.owner
name = sanitize(owner.name, tags: [])
url = user_path(owner)
namespace_link = breadcrumb_list_item(namespace_link) unless project.group
project_link = breadcrumb_list_item project_link
push_to_schema_breadcrumb(name, url)
end
"#{namespace_link} #{project_link}".html_safe
push_to_schema_breadcrumb(simple_sanitize(project.name), project_path(project), project.try(:avatar_url))
end
def remove_project_message(project)
@ -1071,38 +1075,6 @@ module ProjectsHelper
}
end
def build_project_breadcrumb_link(project)
project_name = simple_sanitize(project.name)
push_to_schema_breadcrumb(project_name, project_path(project), project.try(:avatar_url))
link_to project_path(project), class: '!gl-inline-flex' do
if project.avatar_url && !Rails.env.test?
icon = render Pajamas::AvatarComponent.new(
project,
alt: project.name,
size: 16,
class: 'avatar-tile'
)
end
[icon, content_tag("span", project_name, class: "js-breadcrumb-item-text")].join.html_safe
end
end
def build_namespace_breadcrumb_link(project)
if project.group
group_title(project.group)
else
owner = project.namespace.owner
name = sanitize(owner.name, tags: [])
url = user_path(owner)
push_to_schema_breadcrumb(name, url)
link_to(name, url)
end
end
def delete_inactive_projects?
strong_memoize(:delete_inactive_projects_setting) do
::Gitlab::CurrentSettings.delete_inactive_projects?

View File

@ -170,20 +170,6 @@ module Ci
)
end
scope :eager_load_everything, -> do
includes(
[
{ pipeline: [:project, :user] },
:job_artifacts_archive,
:metadata,
:trigger_request,
:project,
:user,
:tags
]
)
end
scope :with_exposed_artifacts, -> do
joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
.includes(:metadata, :job_artifacts_metadata)

View File

@ -15,12 +15,11 @@ module Ci
has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
has_one :sourced_pipeline, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id, inverse_of: :source_job
has_one :trigger, through: :pipeline
belongs_to :trigger_request
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
accepts_nested_attributes_for :needs
scope :preload_needs, -> { preload(:needs) }
@ -268,6 +267,14 @@ module Ci
options[:manual_confirmation] if manual_job?
end
def trigger_short_token
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
trigger&.short_token
else
trigger_request&.trigger_short_token
end
end
private
def dependencies

View File

@ -37,12 +37,12 @@ module Ci
self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
end
def last_trigger_request
trigger_requests.last
end
def last_used
last_trigger_request.try(:created_at)
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
pipelines.last&.created_at
else
trigger_requests.last&.created_at
end
end
def short_token

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Namespaces
module Preloaders
class GroupRootAncestorPreloader < NamespaceRootAncestorPreloader
extend Gitlab::Utils::Override
private
override :join_sql
def join_sql
Group.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Namespaces
module Preloaders
class NamespaceRootAncestorPreloader
def initialize(namespaces, root_ancestor_preloads = [])
@namespaces = namespaces
@root_ancestor_preloads = root_ancestor_preloads
end
def execute
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.id as source_id')
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
root_ancestors_by_id = root_query.group_by(&:source_id)
@namespaces.each do |namespace|
namespace.root_ancestor = root_ancestors_by_id[namespace.id].first
end
end
private
def join_sql
Namespace.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Namespaces
module Preloaders
class ProjectRootAncestorPreloader
def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = [])
@projects = projects
@namespace_sti_name = namespace_sti_name
@root_ancestor_preloads = root_ancestor_preloads
end
def execute
return unless @projects.is_a?(ActiveRecord::Relation)
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.project_id as source_id')
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
root_ancestors_by_id = root_query.group_by(&:source_id)
ActiveRecord::Associations::Preloader.new(records: @projects, associations: :namespace).call
@projects.each do |project|
root_ancestor = root_ancestors_by_id[project.id]&.first
project.namespace.root_ancestor = root_ancestor if root_ancestor.present?
end
end
private
def join_sql
@projects
.joins(@namespace_sti_name)
.select('projects.id as project_id, namespaces.traversal_ids[1] as root_id')
.to_sql
end
end
end
end

View File

@ -1,14 +0,0 @@
# frozen_string_literal: true
module Preloaders
class GroupRootAncestorPreloader < NamespaceRootAncestorPreloader
extend Gitlab::Utils::Override
private
override :join_sql
def join_sql
Group.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
end
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
module Preloaders
class NamespaceRootAncestorPreloader
def initialize(namespaces, root_ancestor_preloads = [])
@namespaces = namespaces
@root_ancestor_preloads = root_ancestor_preloads
end
def execute
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.id as source_id')
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
root_ancestors_by_id = root_query.group_by(&:source_id)
@namespaces.each do |namespace|
namespace.root_ancestor = root_ancestors_by_id[namespace.id].first
end
end
private
def join_sql
Namespace.select('id, traversal_ids[1] as root_id').where(id: @namespaces.map(&:id)).to_sql
end
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
module Preloaders
class ProjectRootAncestorPreloader
def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = [])
@projects = projects
@namespace_sti_name = namespace_sti_name
@root_ancestor_preloads = root_ancestor_preloads
end
def execute
return unless @projects.is_a?(ActiveRecord::Relation)
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
.select('namespaces.*, root_query.project_id as source_id')
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
root_ancestors_by_id = root_query.group_by(&:source_id)
ActiveRecord::Associations::Preloader.new(records: @projects, associations: :namespace).call
@projects.each do |project|
root_ancestor = root_ancestors_by_id[project.id]&.first
project.namespace.root_ancestor = root_ancestor if root_ancestor.present?
end
end
private
def join_sql
@projects
.joins(@namespace_sti_name)
.select('projects.id as project_id, namespaces.traversal_ids[1] as root_id')
.to_sql
end
end
end

View File

@ -13,13 +13,19 @@ module Ci
end
def trigger_variables
return [] unless trigger_request
@trigger_variables ||=
if pipeline.variables.any?
if ::Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project)
return [] if pipeline.trigger_id.blank?
pipeline.variables.map(&:to_hash_variable)
else
trigger_request.user_variables
return [] unless trigger_request
if pipeline.variables.any?
pipeline.variables.map(&:to_hash_variable)
else
trigger_request.user_variables
end
end
end

View File

@ -90,7 +90,8 @@ class BuildDetailsEntity < Ci::JobEntity
raw_project_job_path(project, build)
end
expose :trigger, if: ->(*) { build.trigger_request } do
expose :trigger,
if: ->(*) { Feature.enabled?(:ci_read_trigger_from_ci_pipeline, project) ? build.trigger : build.trigger_request } do
expose :trigger_short_token, as: :short_token
expose :trigger_variables, as: :variables, using: TriggerVariableEntity

View File

@ -39,6 +39,7 @@ class PipelineSerializer < BaseSerializer
:cancelable_statuses,
:retryable_builds,
:stages,
:trigger,
:trigger_requests,
:user,
(:latest_statuses if preload_statuses),

View File

@ -63,7 +63,6 @@ module PersonalAccessTokens
end
def last_used_ip_needs_update?
return false unless Feature.enabled?(:pat_ip, @personal_access_token.user)
return false unless Gitlab::IpAddressState.current
return true if @personal_access_token.last_used_at.nil?

View File

@ -2,27 +2,40 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
= render ::Layouts::PageHeadingComponent.new('') do |c|
- c.with_heading do
%span.gl-inline-flex.gl-items-center.gl-gap-3
= sprite_icon('tanuki', size: 32)
= _('Import an exported GitLab project')
- if Feature.enabled?(:new_project_creation_form, @user)
- add_page_specific_style 'page_bundles/projects'
- namespace_id = namespace_id_from(params)
#js-import-gitlab-project-root{ data: {
back_button_path: new_project_path(anchor: 'import_project'),
namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path || current_user.namespace.full_path,
namespace_id: namespace_id_from(params) || @current_user_group&.id,
import_gitlab_project_path: import_gitlab_project_path,
root_path: root_path,
user_namespace_id: current_user.namespace_id,
can_create_project: current_user.can_create_project?.to_s,
root_url: root_url,
} }
- else
= render ::Layouts::PageHeadingComponent.new('') do |c|
- c.with_heading do
%span.gl-inline-flex.gl-items-center.gl-gap-3
= sprite_icon('tanuki', size: 32)
= _('Import an exported GitLab project')
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
= render 'import/shared/new_project_form'
.row
.form-group.col-md-12
= _("To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.")
.row
.form-group.col-sm-12
= label_tag :file, _('GitLab project export'), class: 'label-bold'
.form-group
= file_field_tag :file, class: ''
.row
.col-sm-12.gl-mt-5
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
= _('Import project')
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
= render 'import/shared/new_project_form'
.row
.form-group.col-md-12
= _("To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.")
.row
.form-group.col-sm-12
= label_tag :file, _('GitLab project export'), class: 'label-bold'
.form-group
= file_field_tag :file, class: ''
.row
.col-sm-12.gl-mt-5
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
= _('Import project')
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= _('Cancel')

View File

@ -1,6 +1,6 @@
- page_title @group.name
- page_description @group.description_html unless page_description
- header_title group_title(@group) unless header_title
- push_group_breadcrumbs(@group)
- nav "group"
- display_subscription_banner!
- base_layout = local_assigns[:base_layout]

View File

@ -1,12 +0,0 @@
- dropdown_location = local_assigns.fetch(:location, nil)
- button_tooltip = local_assigns.fetch(:title, _("Show all breadcrumbs"))
- if defined?(@breadcrumb_collapsed_links) && @breadcrumb_collapsed_links.key?(dropdown_location)
%li.expander.gl-breadcrumb-item.gl-inline-flex
= render Pajamas::ButtonComponent.new(icon: 'ellipsis_h',
button_options: { class: 'button-ellipsis-horizontal js-breadcrumbs-collapsed-expander gl-ml-0', type: "button", data: { container: 'body' }, "aria-label": button_tooltip, title: button_tooltip })
- @breadcrumb_collapsed_links[dropdown_location].each_with_index do |item, index|
%li.gl-breadcrumb-item{ :class => "!gl-hidden" }
= link_to item[:href] do
- if item[:avatar_url]
= render Pajamas::AvatarComponent.new(item[:avatar_url], alt: item[:text], class: "avatar-tile", size: 16)
= item[:text]

View File

@ -1,6 +1,6 @@
- page_title @project.full_name
- page_description @project.description_html unless page_description
- header_title project_title(@project) unless header_title
- push_project_breadcrumbs(@project)
- nav "project"
- page_itemtype 'http://schema.org/SoftwareSourceCode'
- display_subscription_banner!

View File

@ -1,6 +1,6 @@
---
description: "Selects an editor in the Edit dropdown menu"
category: default
internal_events: true
action: click_consolidated_edit
extra_properties:
identifiers:
@ -14,4 +14,3 @@ tiers:
additional_properties:
label:
description: "The editor selected in the Edit dropdown menu"

View File

@ -1,9 +1,9 @@
---
name: pat_ip
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428577
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161076
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428577
milestone: '17.8'
group: group::authentication
type: beta
default_enabled: true
name: ci_read_trigger_from_ci_pipeline
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502767
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180728
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/508601
milestone: '17.10'
group: group::ci platform
type: gitlab_com_derisk
default_enabled: false

View File

@ -5,6 +5,5 @@ feature_category: continuous_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163429
milestone: '17.4'
queued_migration_version: 20240827095907
# Replace with the approximate date you think it's best to ensure the completion of this BBM.
finalize_after: '2024-09-25'
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250218232001'

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class FinalizeHkBackfillCiBuildNeedsProjectId < Gitlab::Database::Migration[2.2]
milestone '17.10'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_ci
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillCiBuildNeedsProjectId',
table_name: :ci_build_needs,
column_name: :id,
job_arguments: [:project_id, :p_ci_builds, :project_id, :build_id, :partition_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1 @@
0ac37c0e9f2bc5df498fc0c1b095e42606eba52f120deb7e6e0df599eeab0a61

View File

@ -8,7 +8,7 @@ title: GitLab Duo Self-Hosted
{{< details >}}
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Offering: GitLab Self-Managed
{{< /details >}}

View File

@ -8,7 +8,7 @@ title: Configure GitLab to access GitLab Duo Self-Hosted
{{< details >}}
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Offering: GitLab Self-Managed
{{< /details >}}

View File

@ -8,7 +8,7 @@ title: Enable logging for self-hosted models
{{< details >}}
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Offering: GitLab Self-Managed
{{< /details >}}

View File

@ -8,7 +8,7 @@ title: GitLab Duo Self-Hosted supported platforms
{{< details >}}
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Offering: GitLab Self-Managed
{{< /details >}}

View File

@ -8,7 +8,7 @@ title: Supported GitLab Duo Self-Hosted models and hardware requirements
{{< details >}}
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Offering: GitLab Self-Managed
{{< /details >}}

View File

@ -8,7 +8,7 @@ title: Troubleshooting GitLab Duo Self-Hosted
{{< details >}}
- Tier: Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Tier: Ultimate with GitLab Duo Enterprise - [Start a GitLab Duo Enterprise trial on a paid Ultimate subscription](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
- Offering: GitLab Self-Managed
{{< /details >}}

View File

@ -15385,6 +15385,30 @@ The edge type for [`EpicList`](#epiclist).
| <a id="epiclistedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="epiclistedgenode"></a>`node` | [`EpicList`](#epiclist) | The item at the end of the edge. |
#### `ErrorTrackingStackTraceConnection`
The connection type for [`ErrorTrackingStackTrace`](#errortrackingstacktrace).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="errortrackingstacktraceconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="errortrackingstacktraceconnectionedges"></a>`edges` | [`[ErrorTrackingStackTraceEdge]`](#errortrackingstacktraceedge) | A list of edges. |
| <a id="errortrackingstacktraceconnectionnodes"></a>`nodes` | [`[ErrorTrackingStackTrace]`](#errortrackingstacktrace) | A list of nodes. |
| <a id="errortrackingstacktraceconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ErrorTrackingStackTraceEdge`
The edge type for [`ErrorTrackingStackTrace`](#errortrackingstacktrace).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="errortrackingstacktraceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="errortrackingstacktraceedgenode"></a>`node` | [`ErrorTrackingStackTrace`](#errortrackingstacktrace) | The item at the end of the edge. |
#### `EscalationPolicyTypeConnection`
The connection type for [`EscalationPolicyType`](#escalationpolicytype).
@ -25463,6 +25487,21 @@ Check permissions for the current user on an epic.
| <a id="epicpermissionsreadepiciid"></a>`readEpicIid` | [`Boolean!`](#boolean) | If `true`, the user can perform `read_epic_iid` on this resource. |
| <a id="epicpermissionsupdateepic"></a>`updateEpic` | [`Boolean!`](#boolean) | If `true`, the user can perform `update_epic` on this resource. |
### `ErrorTrackingStackTrace`
Represents a stack trace.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="errortrackingstacktraceabsolutepath"></a>`absolutePath` | [`String`](#string) | Absolute path of the stack trace. |
| <a id="errortrackingstacktracecolumnnumber"></a>`columnNumber` | [`Int`](#int) | Column number of the stack trace. |
| <a id="errortrackingstacktracecontext"></a>`context` | [`[WorkItemWidgetErrorTrackingStackTraceContext!]`](#workitemwidgeterrortrackingstacktracecontext) | Context of the stack trace. |
| <a id="errortrackingstacktracefilename"></a>`filename` | [`String`](#string) | Filename of the stack trace. |
| <a id="errortrackingstacktracefunction"></a>`function` | [`String`](#string) | Name of the function where the error occured. |
| <a id="errortrackingstacktracelinenumber"></a>`lineNumber` | [`Int`](#int) | Line number of the stack trace. |
### `EscalationPolicyType`
Represents an escalation policy.
@ -39651,9 +39690,22 @@ Represents the error tracking widget.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgeterrortrackingidentifier"></a>`identifier` | [`BigInt`](#bigint) | Error tracking issue id. |
| <a id="workitemwidgeterrortrackingidentifier"></a>`identifier` | [`BigInt`](#bigint) | Error tracking issue id.This field can only be resolved for one work item in any single request. |
| <a id="workitemwidgeterrortrackingstacktrace"></a>`stackTrace` | [`ErrorTrackingStackTraceConnection`](#errortrackingstacktraceconnection) | Stack trace details of the error.This field can only be resolved for one work item in any single request. (see [Connections](#connections)) |
| <a id="workitemwidgeterrortrackingstatus"></a>`status` | [`ErrorTrackingStatus`](#errortrackingstatus) | Response status of error service.This field can only be resolved for one work item in any single request. |
| <a id="workitemwidgeterrortrackingtype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetErrorTrackingStackTraceContext`
Represents details about a line of code of the stack trace.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgeterrortrackingstacktracecontextline"></a>`line` | [`String`](#string) | Line of code. |
| <a id="workitemwidgeterrortrackingstacktracecontextlinenumber"></a>`lineNumber` | [`Int`](#int) | Line number of code. |
### `WorkItemWidgetHealthStatus`
Represents a health status widget.
@ -41402,6 +41454,17 @@ Epic ID wildcard values.
| <a id="epicwildcardidany"></a>`ANY` | Any epic is assigned. |
| <a id="epicwildcardidnone"></a>`NONE` | No epic is assigned. |
### `ErrorTrackingStatus`
Status of the error tracking service.
| Value | Description |
| ----- | ----------- |
| <a id="errortrackingstatuserror"></a>`ERROR` | Error tracking service respond with an error. |
| <a id="errortrackingstatusnot_found"></a>`NOT_FOUND` | Sentry issue not found. |
| <a id="errortrackingstatusretry"></a>`RETRY` | Error tracking service is not ready. |
| <a id="errortrackingstatussuccess"></a>`SUCCESS` | Successfuly fetch the stack trace. |
### `EscalationRuleStatus`
Escalation rule statuses.

View File

@ -30,10 +30,10 @@ A REST API request must start with the root endpoint and the path.
- The path must start with `/api/v4` (`v4` represents the API version).
In the following example, the API request retrieves the list of all projects on GitLab host
`example.com`:
`gitlab.example.com`:
```shell
curl "https://example.com/api/v4/projects"
curl "https://gitlab.example.com/api/v4/projects"
```
Access to some endpoints require authentication. For more information, see
@ -69,14 +69,14 @@ send the payload body:
- Query string:
```shell
curl --request POST "https://example.com/api/v4/projects?name=<example-name>&description=<example-description>"
curl --request POST "https://gitlab.example.com/api/v4/projects?name=<example-name>&description=<example-description>"
```
- Request payload (JSON):
```shell
curl --request POST --header "Content-Type: application/json" \
--data '{"name":"<example-name>", "description":"<example-description>"}' "https://example.com/api/v4/projects"
--data '{"name":"<example-name>", "description":"<example-description>"}' "https://gitlab.example.com/api/v4/projects"
```
URL encoded query strings have a length limitation. Requests that are too large

View File

@ -31,7 +31,7 @@ For example, this script uses a colon:
```yaml
job:
script:
- curl --request POST --header 'Content-Type: application/json' "https://gitlab/api/v4/projects"
- curl --request POST --header 'Content-Type: application/json' "https://gitlab.example.com/api/v4/projects"
```
To be considered valid YAML, you must wrap the entire command in single quotes. If
@ -41,7 +41,7 @@ if possible:
```yaml
job:
script:
- 'curl --request POST --header "Content-Type: application/json" "https://gitlab/api/v4/projects"'
- 'curl --request POST --header "Content-Type: application/json" "https://gitlab.example.com/api/v4/projects"'
```
You can verify the syntax is valid with the [CI Lint](../yaml/lint.md) tool.

View File

@ -66,6 +66,7 @@ Dashboards support the following filters:
- **Date range**: Date selector to filter data by date.
- **Anonymous users**: Toggle to include or exclude anonymous users from the dataset.
- **Project**: Dropdown list to filter data by project.
#### Dashboard status
@ -132,6 +133,8 @@ To create a built-in analytics dashboard:
enabled: true
dateRange:
enabled: true
projects:
enabled: true
```
Refer to the `DashboardFilters` type in the [`ee/app/validators/json_schemas/analytics_dashboard.json`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/analytics_dashboard.json) for a list of supported filters.

View File

@ -25,7 +25,7 @@ and thumbs-ups. React with emoji on:
- [Issues](project/issues/_index.md).
- [Tasks](tasks.md).
- [Merge requests](project/merge_requests/_index.md), [snippets](snippets.md).
- [Merge requests](project/merge_requests/_index.md) and [snippets](snippets.md).
- [Epics](group/epics/_index.md).
- [Objectives and key results](okrs.md).
- Anywhere else you can have a comment thread.

View File

@ -216,6 +216,7 @@ When you delete or block an enterprise user account, their personal access token
- In GitLab 16.0 and earlier, token usage information is updated every 24 hours.
- The frequency of token usage information updates [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/410168) in GitLab 16.1 from 24 hours to 10 minutes.
- Ability to view IP addresses [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/428577) in GitLab 17.8 [with a flag](../../administration/feature_flags.md) named `pat_ip`. Enabled by default in 17.9.
- Ability to view IP addresses made [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/513302) in GitLab 17.10. Feature flag `pat_ip` removed.
{{< /history >}}

View File

@ -73,7 +73,7 @@ module API
authenticate!
authorize! :admin_build, user_project
triggers = user_project.triggers.includes(:trigger_requests)
triggers = user_project.triggers.includes(:trigger_requests, :pipelines)
present paginate(triggers), with: Entities::Trigger, current_user: current_user
end

View File

@ -50,7 +50,7 @@ module API
group_projects = projects_for_group_preload(projects_relation)
groups = group_projects.map(&:namespace)
Preloaders::GroupRootAncestorPreloader.new(groups).execute
::Namespaces::Preloaders::GroupRootAncestorPreloader.new(groups).execute
group_projects.each do |project|
project.group = project.namespace

View File

@ -5,6 +5,7 @@ module BulkImports
module Pipelines
class MembersPipeline
include Pipeline
include HexdigestCacheStrategy
GROUP_MEMBER_RELATIONS = %i[direct inherited shared_from_groups].freeze
PROJECT_MEMBER_RELATIONS = %i[direct inherited invited_groups shared_into_ancestors].freeze
@ -16,12 +17,24 @@ module BulkImports
transformer Import::BulkImports::Common::Transformers::SourceUserMemberAttributesTransformer
def extract(context)
graphql_extractor.extract(context)
extracted_data = graphql_extractor.extract(context)
# add source_xid to each entry to ensure uniqueness when caching
extracted_data.each do |entry|
entry['source_xid'] = context.source_xid
entry['entity_type'] = context.entity_type
end
extracted_data
end
def load(_context, data)
return unless data
# Remove source_xid and entity_type since we don't use them in membership creation
data.delete('source_xid')
data.delete('entity_type')
if data[:source_user]
create_placeholder_membership(data)
else

View File

@ -7,6 +7,8 @@ module BulkImports
attr_reader :tracker
delegate :source_xid, :entity_type, to: :entity
def initialize(tracker, extra = {})
@tracker = tracker
@extra = extra

View File

@ -49,15 +49,19 @@ module Ci
end
def build_payload(job)
base_payload = { cell_id: Gitlab.config.cell.id }
base_payload.merge(extra_payload(job)).compact_blank
base_payload = { scoped_user_id: job.scoped_user&.id }.compact_blank
base_payload.merge(routable_payload(job))
end
def extra_payload(job)
# Creating routing information for routable tokens https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/routable_tokens/
def routable_payload(job)
{
scoped_user_id: job.scoped_user&.id,
organization_id: job.project.organization_id
}
c: Gitlab.config.cell.id,
o: job.project.organization_id,
u: job.user_id,
p: job.project_id,
g: job.project.group&.id
}.compact_blank.transform_values { |id| id.to_s(36) }
end
def token_prefix
@ -101,14 +105,30 @@ module Ci
strong_memoize_attr :scoped_user
def cell_id
@jwt.payload['cell_id']
decode(@jwt.payload['c'])
end
strong_memoize_attr :cell_id
def organization
job&.project&.organization
def organization_id
decode(@jwt.payload['o'])
end
def project_id
decode(@jwt.payload['p'])
end
def user_id
decode(@jwt.payload['u'])
end
def group_id
decode(@jwt.payload['g'])
end
private
def decode(encoded_value)
encoded_value&.to_i(36)
end
strong_memoize_attr :organization
end
end
end

View File

@ -48,26 +48,26 @@ module Gitlab
'de' => 97,
'en' => 100,
'eo' => 0,
'es' => 38,
'es' => 40,
'fil_PH' => 0,
'fr' => 98,
'fr' => 97,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 84,
'ja' => 99,
'ko' => 30,
'it' => 85,
'ja' => 96,
'ko' => 47,
'nb_NO' => 16,
'nl_NL' => 0,
'pl_PL' => 2,
'pt_BR' => 92,
'ro_RO' => 50,
'ru' => 15,
'pt_BR' => 93,
'ro_RO' => 49,
'ru' => 54,
'si_LK' => 9,
'tr_TR' => 6,
'uk' => 38,
'zh_CN' => 89,
'uk' => 37,
'zh_CN' => 86,
'zh_HK' => 1,
'zh_TW' => 85
'zh_TW' => 81
}.freeze
private_constant :TRANSLATION_LEVELS

View File

@ -25,22 +25,29 @@ module Gitlab
def wrap_mentions_in_backticks(text)
return text unless text.present?
if MENTION_REGEX.match?(text)
text = MENTION_REGEX.replace_gsub(text) do |match|
case match[0]
when /^`/
match[0]
when /^ /
" `#{match[0].lstrip}`"
when /^\(/
"(`#{match[0].sub(/^./, '')}`"
else
"`#{match[0]}`"
resultant_array = []
split_array = text.split("\n")
split_array.each do |line|
if MENTION_REGEX.match?(line)
line = MENTION_REGEX.replace_gsub(line) do |match|
case match[0]
when /^`/
match[0]
when /^ /
" `#{match[0].lstrip}`"
when /^\(/
"(`#{match[0].sub(/^./, '')}`"
else
"`#{match[0]}`"
end
end
end
resultant_array << line
end
text
resultant_array.join("\n")
end
end
end

View File

@ -1021,6 +1021,7 @@ excluded_attributes:
- :pipeline_schedule_id
- :merge_request_id
- :external_pull_request_id
- :trigger_id
- :ci_ref_id
- :locked
pipeline_metadata:

View File

@ -21399,6 +21399,9 @@ msgstr ""
msgid "Drop or %{linkStart}upload%{linkEnd} files to attach"
msgstr ""
msgid "Drop or upload file to attach"
msgstr ""
msgid "Drop your designs to start your upload."
msgstr ""
@ -26441,6 +26444,9 @@ msgstr ""
msgid "GitLabPages|Your project is configured for GitLab Pages and the pipeline is running..."
msgstr ""
msgid "GitPod"
msgstr ""
msgid "Gitaly servers"
msgstr ""
@ -30991,6 +30997,9 @@ msgstr ""
msgid "Integration|Branches for which notifications are to be sent"
msgstr ""
msgid "IntelliJ IDEA"
msgstr ""
msgid "IntelliJ IDEA (HTTPS)"
msgstr ""
@ -46060,6 +46069,9 @@ msgstr ""
msgid "ProjectsNew|Get started with one of our popular project templates."
msgstr ""
msgid "ProjectsNew|GitLab project export"
msgstr ""
msgid "ProjectsNew|Gitea host URL"
msgstr ""
@ -46099,6 +46111,9 @@ msgstr ""
msgid "ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces."
msgstr ""
msgid "ProjectsNew|My awesome project"
msgstr ""
msgid "ProjectsNew|New project"
msgstr ""
@ -46117,6 +46132,15 @@ msgstr ""
msgid "ProjectsNew|Please enter a valid personal access token."
msgstr ""
msgid "ProjectsNew|Please enter a valid project name."
msgstr ""
msgid "ProjectsNew|Please enter a valid project slug."
msgstr ""
msgid "ProjectsNew|Please upload a valid GitLab project export file."
msgstr ""
msgid "ProjectsNew|Project Configuration"
msgstr ""
@ -46126,6 +46150,9 @@ msgstr ""
msgid "ProjectsNew|Project name"
msgstr ""
msgid "ProjectsNew|Project slug"
msgstr ""
msgid "ProjectsNew|Projects"
msgstr ""
@ -46144,6 +46171,9 @@ msgstr ""
msgid "ProjectsNew|Select a template"
msgstr ""
msgid "ProjectsNew|To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here."
msgstr ""
msgid "ProjectsNew|Unable to suggest a path. Please refresh and try again."
msgstr ""
@ -46168,6 +46198,9 @@ msgstr ""
msgid "ProjectsNew|https://mycompany.fogbugz.com"
msgstr ""
msgid "ProjectsNew|my-awesome-project"
msgstr ""
msgid "Projects|An error occurred deleting the project. Please refresh the page to try again."
msgstr ""
@ -54438,9 +54471,6 @@ msgstr ""
msgid "Show all activity"
msgstr ""
msgid "Show all breadcrumbs"
msgstr ""
msgid "Show all comments"
msgstr ""
@ -63740,6 +63770,9 @@ msgstr ""
msgid "Visit new homepage"
msgstr ""
msgid "Visual Studio Code"
msgstr ""
msgid "Visual Studio Code (HTTPS)"
msgstr ""

View File

@ -536,9 +536,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
end
context 'when requesting triggered job JSON' do
let(:trigger) { create(:ci_trigger, project: project) }
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
let_it_be(:trigger) { create(:ci_trigger, project: project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, trigger: trigger) }
let_it_be(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
let_it_be(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
let(:user) { developer }
before do

View File

@ -59,6 +59,10 @@ FactoryBot.define do
status { :created }
end
trait :triggered do
trigger { association :ci_trigger, project_id: project_id }
end
factory :ci_pipeline do
trait :invalid do
status { :failed }

View File

@ -45,29 +45,37 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id
end
end
describe 'with sub-groups' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
let(:project) { subgroup_project }
before do
stub_feature_flags(vscode_web_ide: true)
ide_visit(project)
end
it_behaves_like 'new Web IDE'
where(:directory_code_dropdown_updates) do
[true, false]
end
describe 'with vscode feature flag off' do
before do
stub_feature_flags(vscode_web_ide: false)
with_them do
describe 'with sub-groups' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
ide_visit(project)
let(:project) { subgroup_project }
before do
stub_feature_flags(vscode_web_ide: true)
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
ide_visit(project)
end
it_behaves_like 'new Web IDE'
end
it_behaves_like 'legacy Web IDE'
describe 'with vscode feature flag off' do
before do
stub_feature_flags(vscode_web_ide: false)
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
ide_visit(project)
end
it_behaves_like 'legacy Web IDE'
end
end
end

View File

@ -201,7 +201,8 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
click_link('.gitignore')
edit_in_web_ide
click_button 'Edit'
click_link_or_button 'Web IDE'
expect_fork_prompt

View File

@ -443,7 +443,9 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
end
describe 'Variables' do
let(:trigger_request) { create(:ci_trigger_request, project_id: project.id) }
let(:trigger) { create(:ci_trigger, project: project) }
let(:pipeline) { create(:ci_pipeline, trigger: trigger, project: project, sha: project.commit('HEAD').sha) }
let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
context 'when user is a maintainer' do
@ -459,14 +461,20 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
end
end
context 'when variables are stored in trigger_request' do
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
visit project_job_path(project, job)
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it_behaves_like 'no reveal button variables behavior'
context 'when variables are stored in trigger_request' do
before do
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
visit project_job_path(project, job)
end
it_behaves_like 'no reveal button variables behavior'
end
end
context 'when variables are stored in pipeline_variables' do
@ -504,14 +512,20 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
end
end
context 'when variables are stored in trigger_request' do
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
visit project_job_path(project, job)
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it_behaves_like 'reveal button variables behavior'
context 'when variables are stored in trigger_request' do
before do
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
visit project_job_path(project, job)
end
it_behaves_like 'reveal button variables behavior'
end
end
context 'when variables are stored in pipeline_variables' do

View File

@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :groups_and_projects do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:project1) { create(:project, :repository, :public) }
let_it_be(:project2) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
before do
@ -17,65 +18,143 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
end
context 'with developer user' do
before_all do
stub_feature_flags(blob_overflow_menu: false)
project.add_developer(user)
context 'when directory_code_dropdown_updates is true' do
before_all do
project1.add_developer(user)
end
before do
stub_feature_flags(blob_overflow_menu: false)
stub_feature_flags(directory_code_dropdown_updates: true)
end
it 'shows all the expected links' do
visit project_path(project1)
# The navigation bar
within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links in the navigation bar' do
expect(page).to have_link('New issue')
expect(page).to have_link('New merge request')
expect(page).to have_link('New snippet', href: new_project_snippet_path(project1))
end
find_new_menu_toggle.click
end
# The dropdown above the tree
page.within('.tree-controls') do
find('.add-to-tree').click
aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New file')
expect(page).to have_button('Upload file')
expect(page).to have_button('New directory')
expect(page).to have_link('New branch')
expect(page).to have_link('New tag')
end
end
# The Web IDE
within_testid('code-dropdown') do
click_button 'Code'
end
expect(page).to have_link('Web IDE')
end
it 'hides the links when the project is archived' do
project1.update!(archived: true)
visit project_path(project1)
within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project1))
end
find_new_menu_toggle.click
end
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
within_testid('code-dropdown') do
click_button('Code')
expect(page).not_to have_button('Edit')
expect(page).not_to have_link('Web IDE')
end
end
end
it 'shows all the expected links' do
visit project_path(project)
# The navigation bar
within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links in the navigation bar' do
expect(page).to have_link('New issue')
expect(page).to have_link('New merge request')
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
find_new_menu_toggle.click
context 'when directory_code_dropdown_updates is false' do
before_all do
project2.add_developer(user)
end
# The dropdown above the tree
page.within('.tree-controls') do
find('.add-to-tree').click
aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New file')
expect(page).to have_button('Upload file')
expect(page).to have_button('New directory')
expect(page).to have_link('New branch')
expect(page).to have_link('New tag')
end
before do
stub_feature_flags(blob_overflow_menu: false)
stub_feature_flags(directory_code_dropdown_updates: false)
end
# The Web IDE
click_button 'Edit'
expect(page).to have_button('Web IDE')
end
it 'shows all the expected links' do
visit project_path(project2)
it 'hides the links when the project is archived' do
project.update!(archived: true)
# The navigation bar
within_testid('super-sidebar') do
find_new_menu_toggle.click
visit project_path(project)
aggregate_failures 'dropdown links in the navigation bar' do
expect(page).to have_link('New issue')
expect(page).to have_link('New merge request')
expect(page).to have_link('New snippet', href: new_project_snippet_path(project2))
end
within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
find_new_menu_toggle.click
end
find_new_menu_toggle.click
# The dropdown above the tree
page.within('.repo-breadcrumb') do
find_by_testid('add-to-tree').click
aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New file')
expect(page).to have_button('Upload file')
expect(page).to have_button('New directory')
expect(page).to have_link('New branch')
expect(page).to have_link('New tag')
end
end
# The Web IDE
click_button 'Edit'
expect(page).to have_button('Web IDE')
end
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
it 'hides the links when the project is archived' do
project2.update!(archived: true)
expect(page).not_to have_button('Edit')
visit project_path(project2)
within_testid('super-sidebar') do
find_new_menu_toggle.click
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project2))
end
find_new_menu_toggle.click
end
expect(page).not_to have_selector('[data-testid="add-to-tree"]')
expect(page).not_to have_button('Edit')
end
end
end
@ -90,14 +169,33 @@ RSpec.describe 'Projects > Show > Collaboration links', :js, feature_category: :
end
with_them do
before do
project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
project.add_member(user, user_level)
visit project_path(project)
context 'when directory_code_dropdown_updates is true' do
before do
stub_feature_flags(directory_code_dropdown_updates: true)
project1.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
project1.add_member(user, user_level)
visit project_path(project1)
end
it "updates Web IDE link" do
within_testid('code-dropdown') do
click_button 'Code'
end
expect(page.has_link?('Web IDE')).to be(expect_ide_link)
end
end
it "updates Web IDE link" do
expect(page.has_button?('Edit')).to be(expect_ide_link)
context 'when directory_code_dropdown_updates is false' do
before do
stub_feature_flags(directory_code_dropdown_updates: false)
project2.project_feature.update!({ merge_requests_access_level: merge_requests_access_level })
project2.add_member(user, user_level)
visit project_path(project2)
end
it "updates Web IDE link" do
expect(page.has_button?('Edit')).to be(expect_ide_link)
end
end
end
end

View File

@ -8,69 +8,76 @@ RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_id
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
stub_feature_flags(vscode_web_ide: false)
project.add_maintainer(user)
sign_in(user)
visit project_tree_path(project, :master)
wait_for_requests
ide_visit_from_link
where(:directory_code_dropdown_updates) do
[true, false]
end
after do
set_cookie('new_repo', 'false')
end
with_them do
before do
stub_feature_flags(vscode_web_ide: false)
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
it 'creates directory in current directory' do
wait_for_all_requests
project.add_maintainer(user)
sign_in(user)
all('.ide-tree-actions button').last.click
page.within('.modal') do
find('.form-control').set('folder name')
click_button('Create directory')
end
expect(page).to have_content('folder name')
first('.ide-tree-actions button').click
page.within('.modal') do
find('.form-control').set('folder name/file name')
click_button('Create file')
end
wait_for_requests
find('.js-ide-commit-mode').click
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
# taller (as it is by default with chrome headless) then the button will not exist.
if page.has_css?('[data-testid="begin-commit-button"]')
find_by_testid('begin-commit-button').click
end
fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false)
wait_for_requests
page.within '.multi-file-commit-form' do
click_button('Commit')
visit project_tree_path(project, :master)
wait_for_requests
ide_visit_from_link
end
find('.js-ide-edit-mode').click
after do
set_cookie('new_repo', 'false')
end
expect(page).to have_content('folder name')
it 'creates directory in current directory' do
wait_for_all_requests
all('.ide-tree-actions button').last.click
page.within('.modal') do
find('.form-control').set('folder name')
click_button('Create directory')
end
expect(page).to have_content('folder name')
first('.ide-tree-actions button').click
page.within('.modal') do
find('.form-control').set('folder name/file name')
click_button('Create file')
end
wait_for_requests
find('.js-ide-commit-mode').click
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
# taller (as it is by default with chrome headless) then the button will not exist.
if page.has_css?('[data-testid="begin-commit-button"]')
find_by_testid('begin-commit-button').click
end
fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false)
wait_for_requests
page.within '.multi-file-commit-form' do
click_button('Commit')
wait_for_requests
end
find('.js-ide-edit-mode').click
expect(page).to have_content('folder name')
end
end
end

View File

@ -8,56 +8,64 @@ RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
stub_feature_flags(vscode_web_ide: false)
project.add_maintainer(user)
sign_in(user)
visit project_path(project)
wait_for_requests
ide_visit_from_link
where(:directory_code_dropdown_updates) do
[true, false]
end
after do
set_cookie('new_repo', 'false')
end
with_them do
before do
stub_feature_flags(vscode_web_ide: false)
stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates)
it 'creates file in current directory' do
wait_for_requests
first('.ide-tree-actions button').click
project.add_maintainer(user)
sign_in(user)
page.within('.modal') do
find('.form-control').set('file name')
click_button('Create file')
end
wait_for_requests
find('.js-ide-commit-mode').click
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
# taller (as it is by default with chrome headless) then the button will not exist.
if page.has_css?('[data-testid="begin-commit-button"]')
find_by_testid('begin-commit-button').click
end
fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false)
page.within '.multi-file-commit-form' do
click_button('Commit')
visit project_path(project)
wait_for_requests
ide_visit_from_link
end
find('.js-ide-edit-mode').click
after do
set_cookie('new_repo', 'false')
end
expect(page).to have_content('file name')
it 'creates file in current directory' do
wait_for_all_requests
first('.ide-tree-actions button').click
page.within('.modal') do
find('.form-control').set('file name')
click_button('Create file')
end
wait_for_requests
find('.js-ide-commit-mode').click
# Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT,
# (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is
# taller (as it is by default with chrome headless) then the button will not exist.
if page.has_css?('[data-testid="begin-commit-button"]')
find_by_testid('begin-commit-button').click
end
fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false)
page.within '.multi-file-commit-form' do
click_button('Commit')
wait_for_requests
end
find('.js-ide-edit-mode').click
expect(page).to have_content('file name')
end
end
end

View File

@ -140,14 +140,32 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
stub_feature_flags(vscode_web_ide: false)
end
it 'opens folder in IDE' do
visit project_tree_path(project, File.join('master', 'bar'))
ide_visit_from_link
context 'when directory_code_dropdown_updates is enabled' do
it 'opens folder in IDE' do
stub_feature_flags(directory_code_dropdown_updates: true)
wait_for_all_requests
find('.ide-file-list')
wait_for_requests
expect(page).to have_selector('.is-open', text: 'bar')
visit project_tree_path(project, File.join('master', 'bar'))
ide_visit_from_link
wait_for_all_requests
find('.ide-file-list')
wait_for_requests
expect(page).to have_selector('.is-open', text: 'bar')
end
end
context 'when directory_code_dropdown_updates is disabled' do
it 'opens folder in IDE' do
stub_feature_flags(directory_code_dropdown_updates: false)
visit project_tree_path(project, File.join('master', 'bar'))
ide_visit_from_link
wait_for_all_requests
find('.ide-file-list')
wait_for_requests
expect(page).to have_selector('.is-open', text: 'bar')
end
end
end

View File

@ -62,9 +62,6 @@ describe('~/access_tokens/components/access_token_table_app', () => {
initialActiveAccessTokens: defaultActiveAccessTokens,
noActiveTokensMessage,
showRole,
glFeatures: {
patIp: true,
},
...props,
},
});

View File

@ -0,0 +1,164 @@
import { nextTick } from 'vue';
import { GlAnimatedUploadIcon, GlFormInput } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import importFromGitlabExportApp from '~/import/gitlab_project/import_from_gitlab_export_app.vue';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('Import from GitLab export file app', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(importFromGitlabExportApp, {
propsData: {
backButtonPath: '/projects/new#import_project',
namespaceFullPath: 'root',
namespaceId: '1',
rootPath: '/',
importGitlabProjectPath: 'import/path',
},
stubs: {
GlFormInput,
},
});
};
beforeEach(() => {
createComponent();
});
const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate);
const findForm = () => wrapper.find('form');
const findProjectNameInput = () => wrapper.findByTestId('project-name');
const findProjectSlugInput = () => wrapper.findByTestId('project-slug');
const findDropzoneButton = () => wrapper.findByTestId('dropzone-button');
const findDropzoneInput = () => wrapper.findByTestId('dropzone-input');
const findAnimatedUploadIcon = () => wrapper.findComponent(GlAnimatedUploadIcon);
const findBackButton = () => wrapper.findByTestId('back-button');
const findNextButton = () => wrapper.findByTestId('next-button');
const setProjectName = async (projectName) => {
await findProjectNameInput().setValue(projectName);
await findProjectNameInput().trigger('blur');
await nextTick();
};
const uploadFile = async () => {
const file = new File(['foo'], 'foo.gz', { type: 'application/gzip', size: 1024 });
Object.defineProperty(findDropzoneInput().element, 'files', { value: [file] });
findDropzoneInput().trigger('change');
await nextTick();
};
describe('form', () => {
it('renders the multi step form correctly', () => {
expect(findMultiStepForm().props()).toMatchObject({
currentStep: 3,
stepsTotal: 3,
});
});
it('renders the form element correctly', () => {
const form = findForm();
expect(form.attributes('action')).toBe('import/path');
expect(form.find('input[type=hidden][name=authenticity_token]').attributes('value')).toBe(
'mock-csrf-token',
);
});
it('does not submit the form without requried fields', () => {
const submitSpy = jest.spyOn(findForm().element, 'submit');
findForm().trigger('submit');
expect(submitSpy).not.toHaveBeenCalled();
});
it('submits the form with valid form data', async () => {
const submitSpy = jest.spyOn(findForm().element, 'submit');
await setProjectName('test project');
uploadFile();
await nextTick();
findForm().trigger('submit');
expect(submitSpy).toHaveBeenCalledWith();
});
});
describe('validation', () => {
it('shows an error message when project name is cleared', async () => {
await setProjectName('');
const formGroup = wrapper.findByTestId('project-name-form-group');
expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter a valid project name.');
});
it('shows an error message when project name starts with invalid characters', async () => {
await setProjectName('#test');
const formGroup = wrapper.findByTestId('project-name-form-group');
expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
'Name must start with a letter, digit, emoji, or underscore.',
);
});
it('shows an error message when project name contains invalid characters', async () => {
await setProjectName('test?');
const formGroup = wrapper.findByTestId('project-name-form-group');
expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
'Name can contain only lowercase or uppercase letters, digits, emoji, spaces, dots, underscores, dashes, or pluses.',
);
});
it('shows an error message when there are no file uploaded', async () => {
findForm().trigger('submit');
await nextTick();
const formGroup = wrapper.findByTestId('project-file-form-group');
expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
'Please upload a valid GitLab project export file.',
);
});
});
describe('project slug', () => {
it('updates the project slug appropriately when updating project name', async () => {
await setProjectName('test project');
expect(findProjectSlugInput().props('value')).toBe('test-project');
});
});
describe('drop zone', () => {
it('renders a drop zone', () => {
expect(findDropzoneInput().exists()).toBe(true);
expect(findDropzoneButton().text()).toBe('Drop or upload file to attach');
expect(findAnimatedUploadIcon().exists()).toBe(true);
});
it('uploads a file', async () => {
await uploadFile();
expect(findDropzoneButton().text()).toContain('foo.gz');
expect(findAnimatedUploadIcon().exists()).toBe(false);
});
});
describe('back button', () => {
it('renders a back button', () => {
expect(findBackButton().attributes('href')).toBe('/projects/new#import_project');
});
});
describe('next button', () => {
it('renders a next button', () => {
expect(findNextButton().attributes('type')).toBe('submit');
});
});
});

View File

@ -1,15 +1,23 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlButtonGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
import CodeDropdownIdeItem from '~/repository/components/code_dropdown/code_dropdown_ide_item.vue';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
jest.mock('~/behaviors/shortcuts/shortcuts_toggle', () => ({
shouldDisableShortcuts: () => false,
}));
describe('CodeDropdownIdeItem', () => {
let wrapper;
const { bindInternalEventDocument } = useMockInternalEventsTracking();
const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const findAllGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAtIndex = (index) => findAllGlButtons().at(index);
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
const findKbd = () => wrapper.find('kbd');
const createComponent = (props = {}) => {
wrapper = shallowMount(CodeDropdownIdeItem, {
@ -56,6 +64,11 @@ describe('CodeDropdownIdeItem', () => {
type: 'button',
text: 'button 1',
href: '/link 1',
shortcut: '.',
tracking: {
action: 'click_consolidated_edit',
label: 'web_ide',
},
};
beforeEach(() => {
@ -71,9 +84,27 @@ describe('CodeDropdownIdeItem', () => {
expect(dropdownItem.props('item')).toStrictEqual(mockButtonItem);
});
it('renders shortcut if passed in', () => {
expect(findKbd().exists()).toBe(true);
expect(findKbd().text()).toBe(mockButtonItem.shortcut);
});
it('closes the dropdown on click', () => {
findDropdownItemAtIndex(0).vm.$emit('action');
expect(wrapper.emitted('close-dropdown')).toStrictEqual([[]]);
});
it('calls to track events if passed in', () => {
const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
findDropdownItemAtIndex(0).vm.$emit('action');
expect(trackEventSpy).toHaveBeenCalledTimes(1);
expect(trackEventSpy).toHaveBeenCalledWith(
'click_consolidated_edit',
{
label: 'web_ide',
},
undefined,
);
});
});
});

View File

@ -17,6 +17,8 @@ describe('Compact Code Dropdown coomponent', () => {
const httpUrl = 'http://foo.bar';
const httpsUrl = 'https://foo.bar';
const xcodeUrl = 'xcode://foo.bar';
const webIdeUrl = 'webIdeUrl://foo.bar';
const gitpodUrl = 'gitpodUrl://foo.bar';
const currentPath = null;
const directoryDownloadLinks = [
{ text: 'zip', path: `${httpUrl}/archive.zip` },
@ -28,6 +30,10 @@ describe('Compact Code Dropdown coomponent', () => {
sshUrl,
httpUrl,
xcodeUrl,
webIdeUrl,
gitpodUrl,
showWebIdeButton: true,
showGitpodButton: true,
currentPath,
directoryDownloadLinks,
};
@ -116,13 +122,19 @@ describe('Compact Code Dropdown coomponent', () => {
describe('ideGroup', () => {
it('should not render if ideGroup is empty', () => {
createComponent({ xcodeUrl: undefined, sshUrl: undefined, httpUrl: undefined });
createComponent({
xcodeUrl: undefined,
sshUrl: undefined,
httpUrl: undefined,
showWebIdeButton: false,
showGitpodButton: false,
});
expect(findCodeDropdownIdeItems().exists()).toBe(false);
});
it('renders with correct props', () => {
createComponent();
expect(findCodeDropdownIdeItems()).toHaveLength(3);
expect(findCodeDropdownIdeItems()).toHaveLength(5);
mockIdeItems.forEach((item, index) => {
const ideItem = findCodeDropdownIdeItemAtIndex(index);

View File

@ -1,4 +1,27 @@
export const mockIdeItems = [
{
extraAttrs: {
target: '_blank',
},
href: 'webIdeUrl://foo.bar',
shortcut: '.',
text: 'Web IDE',
tracking: {
action: 'click_consolidated_edit',
label: 'web_ide',
},
},
{
extraAttrs: {
target: '_blank',
},
href: 'gitpodUrl://foo.bar',
text: 'GitPod',
tracking: {
action: 'click_consolidated_edit',
label: 'gitpod',
},
},
{
items: [
{

View File

@ -183,6 +183,10 @@ describe('HeaderArea', () => {
httpUrl: headerAppInjected.httpUrl,
kerberosUrl: headerAppInjected.kerberosUrl,
xcodeUrl: headerAppInjected.xcodeUrl,
webIdeUrl: headerAppInjected.webIdeUrl,
gitpodUrl: headerAppInjected.gitpodUrl,
showWebIdeButton: headerAppInjected.showWebIdeButton,
showGitpodButton: headerAppInjected.showGitpodButton,
currentPath: defaultMockRoute.params.path,
directoryDownloadLinks: headerAppInjected.downloadLinks,
});

View File

@ -96,7 +96,10 @@ describe('vue_shared/components/web_ide_link', () => {
let wrapper;
let trackingSpy;
function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
function createComponent(
props,
{ mountFn = shallowMountExtended, slots = {}, featureFlagValue = false } = {},
) {
const fakeApollo = createMockApollo([
[getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)],
]);
@ -122,6 +125,11 @@ describe('vue_shared/components/web_ide_link', () => {
GlDisclosureDropdownItem,
},
apolloProvider: fakeApollo,
provide: {
glFeatures: {
directoryCodeDropdownUpdates: featureFlagValue,
},
},
});
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
@ -205,9 +213,33 @@ describe('vue_shared/components/web_ide_link', () => {
props: { showEditButton: false },
expectedActions: [ACTION_WEB_IDE],
},
])('for a set of props', ({ props, expectedActions }) => {
{
props: {
showWebIdeButton: true,
showGitpodButton: true,
gitpodEnabled: true,
isBlob: true,
},
expectedActions: [
{ ...ACTION_WEB_IDE, text: 'Open in Web IDE' },
ACTION_EDIT,
{ ...ACTION_GITPOD, text: 'Open in Gitpod' },
],
featureFlagValue: true,
},
{
props: {
showWebIdeButton: true,
showGitpodButton: true,
gitpodEnabled: true,
isBlob: false,
},
expectedActions: [ACTION_EDIT],
featureFlagValue: true,
},
])('for a set of props', ({ props, expectedActions, featureFlagValue }) => {
beforeEach(() => {
createComponent(props);
createComponent(props, { featureFlagValue });
});
it('renders the appropiate actions', () => {

View File

@ -89,4 +89,57 @@ RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do
is_expected.to eq("/#{project.full_path}/-/jobs/#{build.id}/artifacts/browse")
end
end
describe '#triggered' do
subject { resolve_field(:triggered, build, current_user: user, object_type: described_class) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
context 'when not triggered' do
let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, pipeline: pipeline, project: project, user: user) }
it 'returns false' do
expect(build.pipeline).to receive(:trigger_id).and_call_original
is_expected.to be(false)
end
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it 'returns false' do
expect(build).to receive(:trigger_request).and_call_original
is_expected.to be(false)
end
end
end
context 'when triggered' do
let_it_be(:trigger) { create(:ci_trigger, project: project) }
let_it_be(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
let_it_be(:pipeline) { create(:ci_empty_pipeline, trigger: trigger, project: project) }
let_it_be(:build) do
create(:ci_build, trigger_request: trigger_request, pipeline: pipeline, project: project, user: user)
end
it 'returns true' do
expect(build.pipeline).to receive(:trigger_id).and_call_original
is_expected.to be(true)
end
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it 'returns true' do
expect(build).to receive(:trigger_request).and_call_original
is_expected.to be(true)
end
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::ErrorTracking::StackTraceContextType, feature_category: :team_planning do
it 'exposes the expected fields' do
expected_fields = %i[line_number line]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::ErrorTracking::StackTraceType, feature_category: :team_planning do
it 'exposes the expected fields' do
expected_fields = %i[filename absolute_path line_number column_number function context]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ErrorTrackingStatus'], feature_category: :team_planning do
specify { expect(described_class.graphql_name).to eq('ErrorTrackingStatus') }
describe 'enum values' do
using RSpec::Parameterized::TableSyntax
where(:field_name, :field_value) do
'SUCCESS' | :success
'ERROR' | :error
'NOT_FOUND' | :not_found
'RETRY' | :retry
end
with_them do
it 'exposes correct available fields' do
expect(described_class.values[field_name].value).to eq(field_value)
end
end
end
end

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::ErrorTrackingType, feature_category: :team_planning do
it 'exposes the expected fields' do
expected_fields = %i[type identifier]
expected_fields = %i[type identifier stack_trace status]
expect(described_class).to have_graphql_fields(*expected_fields)
end

View File

@ -82,45 +82,30 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do
end
end
describe '#group_title' do
describe '#push_group_breadcrumbs' do
let_it_be(:group) { create(:group) }
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
subject { helper.group_title(very_deep_nested_group) }
subject { helper.push_group_breadcrumbs(very_deep_nested_group) }
context 'traversal queries' do
shared_examples 'correct ancestor order' do
it 'outputs the groups in the correct order' do
expect(subject)
.to match(%r{<li.*><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
end
end
before do
very_deep_nested_group.reload # make sure traversal_ids are reloaded
end
include_examples 'correct ancestor order'
end
it 'enqueues the elements in the breadcrumb schema list' do
expect(helper).to receive(:push_to_schema_breadcrumb).with(group.name, group_path(group), nil)
expect(helper).to receive(:push_to_schema_breadcrumb).with(nested_group.name, group_path(nested_group), nil)
expect(helper).to receive(:push_to_schema_breadcrumb).with(deep_nested_group.name, group_path(deep_nested_group), nil)
expect(helper).to receive(:push_to_schema_breadcrumb).with(very_deep_nested_group.name, group_path(very_deep_nested_group), nil)
it 'enqueues the elements in the breadcrumb schema list in the correct order' do
expect(helper).to receive(:push_to_schema_breadcrumb).with(group.name, group_path(group), nil).ordered
expect(helper).to receive(:push_to_schema_breadcrumb).with(nested_group.name, group_path(nested_group), nil).ordered
expect(helper).to receive(:push_to_schema_breadcrumb).with(deep_nested_group.name, group_path(deep_nested_group), nil).ordered
expect(helper).to receive(:push_to_schema_breadcrumb).with(very_deep_nested_group.name, group_path(very_deep_nested_group), nil).ordered
subject
end
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new do
helper.group_title(nested_group)
helper.push_group_breadcrumbs(nested_group)
end
expect do
helper.group_title(very_deep_nested_group)
helper.push_group_breadcrumbs(very_deep_nested_group)
end.not_to exceed_query_limit(control)
end
end

View File

@ -947,23 +947,28 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
end
end
describe '#project_title' do
subject { helper.project_title(project) }
describe '#push_project_breadcrumbs' do
subject { helper.push_project_breadcrumbs(project) }
it 'enqueues the elements in the breadcrumb schema list' do
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.namespace.name, user_path(project.owner))
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.name, project_path(project), nil)
it 'enqueues the elements in the breadcrumb schema list in the correct order' do
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.namespace.name, user_path(project.owner)).ordered
expect(helper).to receive(:push_to_schema_breadcrumb).with(project.name, project_path(project), nil).ordered
subject
end
context 'with malicious owner name' do
let(:malicious_owner_name) { 'a<a class="fixed-top" href=/api/v4' }
before do
allow_any_instance_of(User).to receive(:name).and_return('a<a class="fixed-top" href=/api/v4')
allow_any_instance_of(User).to receive(:name).and_return(malicious_owner_name)
end
it 'escapes the malicious owner name' do
expect(subject).not_to include('<a class="fixed-top" href="/api/v4"></a>')
expect(helper).not_to receive(:push_to_schema_breadcrumb).with(malicious_owner_name, user_path(project.owner))
expect(helper).to receive(:push_to_schema_breadcrumb).with('a', user_path(project.owner))
subject
end
end
end

View File

@ -75,6 +75,31 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
)
end
it 'creates member only once when source_xid and entity_type are the same' do
member = extracted_data(
email: member_user1.email,
id: member_user1.id
)
extracted = BulkImports::Pipeline::ExtractedData.new(
data: member.data,
page_info: { 'has_next_page' => false }
)
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
allow(extractor).to receive(:extract).and_return(extracted)
end
expect { pipeline.run }.to change(portable.members, :count).by(1)
# Run again with exact same configuration
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
allow(extractor).to receive(:extract).and_return(extracted)
end
expect { pipeline.run }.not_to change(portable.members, :count)
end
context 'when importer_user_mapping is enabled' do
let!(:import_source_user) do
create(:import_source_user,
@ -202,6 +227,15 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category
subject.load(context, member_data)
end
it 'removes source_xid and entity_type from data before creating member' do
data = member_data.merge('source_xid' => '123', 'entity_type' => 'group')
expect { pipeline.load(context, data) }.to change(portable.members, :count).by(1)
created_member = portable.members.last
expect(created_member.attributes).not_to include('source_xid', 'entity_type')
end
context 'when user_id is current user id' do
it 'does not create new membership' do
data = { user_id: user.id }

View File

@ -6,6 +6,7 @@ RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
let_it_be(:user) { create(:user) }
let_it_be(:job) { create(:ci_build, user: user) }
let(:cell_id) { 1 }
before do
allow(Gitlab::CurrentSettings)
@ -61,11 +62,38 @@ RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
subject(:decoded_token) { described_class.decode(encoded_token) }
before do
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
end
context 'with a valid token' do
let(:decoded_payload) { decoded_token.instance_variable_get(:@jwt).payload }
let(:expected_payload) do
{
"c" => cell_id.to_s(36),
"o" => job.project.organization_id.to_s(36),
"u" => user.id.to_s(36),
"p" => job.project_id.to_s(36)
}
end
it 'successfully decodes the token with subject' do
expect(decoded_token).to be_present
expect(decoded_token.job).to eq(job)
end
it 'successfully decodes the token with routable payload' do
expect(decoded_payload).to match(a_hash_including(expected_payload))
end
context 'when project belongs to a group' do
let_it_be(:job) { create(:ci_build, user: user, project: create(:project, :in_group)) }
it 'includes group id in routable payload' do
expect(decoded_payload)
.to match(a_hash_including(expected_payload.merge("g" => job.project.group.id.to_s(36))))
end
end
end
context 'when signing key is not available' do
@ -184,17 +212,58 @@ RSpec.describe Ci::JobToken::Jwt, feature_category: :secrets_management do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
before do
allow(Gitlab.config.cell).to receive(:id).and_return(cell_id)
end
it 'encodes the cell_id in the JWT payload' do
expect(decoded_token.cell_id).to eq(Gitlab.config.cell.id)
expect(decoded_token.cell_id).to eq(cell_id)
end
end
describe '#organization' do
describe '#organization_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'encodes the organization in the JWT payload' do
expect(decoded_token.organization).to eq(job.project.organization)
it 'encodes the organization_id in the JWT payload' do
expect(decoded_token.organization_id).to eq(job.project.organization_id)
end
end
describe '#project_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'encodes the project_id in the JWT payload' do
expect(decoded_token.project_id).to eq(job.project_id)
end
end
describe '#user_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
it 'encodes the user_id in the JWT payload' do
expect(decoded_token.user_id).to eq(job.user_id)
end
end
describe '#group_id' do
let(:encoded_token) { described_class.encode(job) }
let(:decoded_token) { described_class.decode(encoded_token) }
context 'when project belongs to a group' do
let_it_be(:job) { create(:ci_build, user: user, project: create(:project, :in_group)) }
it 'encodes the group_id in the JWT payload' do
expect(decoded_token.group_id).to eq(job.project.group.id)
end
end
context 'when project belongs to a personal namespace' do
it 'does not encode the group_id in the JWT payload' do
expect(decoded_token.group_id).to be_nil
end
end
end

View File

@ -93,4 +93,13 @@ RSpec.describe Gitlab::Import::UsernameMentionRewriter, feature_category: :impor
end
end
end
context 'when the text contains username in the new line' do
let(:original_text) { "Hello,\n@username is mentioned here.\nThis is the next line." }
let(:expected_text) { "Hello,\n`@username` is mentioned here.\nThis is the next line." }
it 'wraps the username in backticks and it should be properly formatted in the new line' do
expect(instance.wrap_mentions_in_backticks(original_text)).to eq(expected_text)
end
end
end

View File

@ -327,8 +327,8 @@ ci_pipelines: &pipeline_definition
- bridges
- processables
- generic_commit_statuses
- trigger_requests
- trigger
- trigger_requests
- variables
- auto_canceled_by
- auto_canceled_pipelines
@ -420,6 +420,7 @@ builds:
- resource_group
- metadata
- runner
- trigger
- trigger_request
- erased_by
- deployment
@ -495,6 +496,7 @@ bridges:
- deployment
- resource_group
- metadata
- trigger
- trigger_request
- downstream_pipeline
- upstream_pipeline

View File

@ -7,6 +7,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let_it_be_with_refind(:pipeline) { create(:ci_pipeline, project: project) }
describe 'associations' do
it { is_expected.to have_one(:trigger).through(:pipeline) }
it { is_expected.to belong_to(:trigger_request) }
end
@ -16,7 +17,6 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
it { is_expected.to delegate_method(:trigger_short_token).to(:trigger_request) }
end
describe '#clone' do
@ -89,7 +89,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let(:ignore_accessors) do
%i[type namespace lock_version target_url base_tags trace_sections
commit_id deployment erased_by_id project_id project_mirror
runner_id taggings tags trigger_request_id
runner_id taggings tags trigger_request_id trigger trigger_id
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
metadata runner_manager_build runner_manager runner_session trace_chunks
@ -195,7 +195,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
Ci::Build.attribute_names.map(&:to_sym) +
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
Ci::Build.reflect_on_all_associations.map(&:name) +
[:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens, :interruptible]
[:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens, :interruptible, :trigger]
current_accessors.uniq!
@ -678,4 +678,26 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
end
end
end
describe '#trigger_short_token' do
let_it_be(:pipeline) { create(:ci_pipeline, :triggered, project: project) }
let_it_be(:stage) { create(:ci_stage, project: project, pipeline: pipeline, name: 'test') }
let_it_be(:processable) { create(:ci_build, :triggered, stage_id: stage.id, pipeline: pipeline) }
it 'delegates to trigger' do
expect(processable.trigger).to receive(:short_token)
processable.trigger_short_token
end
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it 'delegates to trigger_request' do
expect(processable.trigger_request).to receive(:trigger_short_token)
processable.trigger_short_token
end
end
end
end

View File

@ -31,6 +31,50 @@ RSpec.describe Ci::Trigger, feature_category: :continuous_integration do
end
end
describe '#last_used' do
let_it_be(:project) { create :project }
let_it_be_with_refind(:trigger) { create(:ci_trigger, project: project) }
subject { trigger.last_used }
it { is_expected.to be_nil }
context 'when there is one pipeline' do
let_it_be(:pipeline1) { create(:ci_empty_pipeline, trigger: trigger, project: project, created_at: '2025-02-13') }
let_it_be(:build1) { create(:ci_build, pipeline: pipeline1, trigger_request: trigger_request1) }
let_it_be(:trigger_request1) { create(:ci_trigger_request, trigger: trigger, created_at: '2025-02-12') }
it { is_expected.to eq(pipeline1.reload.created_at) }
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it { is_expected.to eq(trigger_request1.reload.created_at) }
end
context 'when there are two pipelines' do
let_it_be(:pipeline2) do
create(:ci_empty_pipeline, trigger: trigger, project: project, created_at: '2025-02-11')
end
let_it_be(:build2) { create(:ci_build, pipeline: pipeline2, trigger_request: trigger_request2) }
let_it_be(:trigger_request2) { create(:ci_trigger_request, trigger: trigger, created_at: '2025-02-10') }
it { is_expected.to eq(pipeline2.reload.created_at) }
context 'when ff ci_read_trigger_from_ci_pipeline is disabled' do
before do
stub_feature_flags(ci_read_trigger_from_ci_pipeline: false)
end
it { is_expected.to eq(trigger_request2.reload.created_at) }
end
end
end
end
describe '#short_token' do
let(:trigger) { create(:ci_trigger) }

Some files were not shown because too many files have changed in this diff Show More