summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Doc_cleanup.md6
-rw-r--r--.rubocop_todo/gitlab/namespaced_class.yml1
-rw-r--r--app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue92
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue10
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql2
-rw-r--r--app/assets/javascripts/editor/schema/ci.json5
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js23
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue6
-rw-r--r--app/assets/javascripts/main.js8
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue25
-rw-r--r--app/assets/javascripts/members/guest_overage_confirm_action.js3
-rw-r--r--app/assets/javascripts/pages/users/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue6
-rw-r--r--app/assets/javascripts/repository/commits_service.js2
-rw-r--r--app/assets/javascripts/repository/utils/ref_switcher_utils.js7
-rw-r--r--app/assets/javascripts/super_sidebar/components/bottom_bar.vue24
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher.vue83
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue45
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue29
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue37
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue46
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue61
-rw-r--r--app/assets/javascripts/super_sidebar/mock_data.js70
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js22
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue6
-rw-r--r--app/assets/javascripts/users/profile/components/report_abuse_button.vue51
-rw-r--r--app/assets/javascripts/users/profile/index.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue22
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss22
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/utilities.scss8
-rw-r--r--app/controllers/abuse_reports_controller.rb21
-rw-r--r--app/graphql/types/description_version_type.rb19
-rw-r--r--app/graphql/types/notes/note_type.rb87
-rw-r--r--app/graphql/types/notes/system_note_metadata_type.rb22
-rw-r--r--app/helpers/nav_helper.rb4
-rw-r--r--app/models/abuse_report.rb12
-rw-r--r--app/models/ci/build.rb3
-rw-r--r--app/models/description_version.rb2
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/synthetic_note.rb2
-rw-r--r--app/models/system_note_metadata.rb6
-rw-r--r--app/policies/description_version_policy.rb5
-rw-r--r--app/views/abuse_reports/new.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml6
-rw-r--r--app/views/layouts/application.html.haml9
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--config/routes.rb6
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb12
-rw-r--r--db/migrate/20221204090437_add_category_to_abuse_report.rb7
-rw-r--r--db/schema_migrations/202212040904371
-rw-r--r--db/structure.sql3
-rw-r--r--doc/.vale/gitlab/HeadingDepth.yml4
-rw-r--r--doc/.vale/gitlab/SentenceLength.yml4
-rw-r--r--doc/.vale/gitlab/Spelling.yml2
-rw-r--r--doc/.vale/gitlab/Uppercase.yml6
-rw-r--r--doc/api/geo_nodes.md35
-rw-r--r--doc/api/graphql/reference/index.md55
-rw-r--r--doc/ci/yaml/index.md35
-rw-r--r--doc/development/documentation/topic_types/index.md2
-rw-r--r--doc/development/merge_request_diffs.md188
-rw-r--r--doc/user/profile/notifications.md65
-rw-r--r--lib/api/concerns/packages/debian_package_endpoints.rb4
-rw-r--r--lib/api/debian_group_packages.rb6
-rw-r--r--lib/api/debian_project_packages.rb8
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/cache.rb4
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--locale/gitlab.pot57
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/merge_request/show.rb1
-rw-r--r--scripts/gitlab_component_helpers.sh4
-rw-r--r--spec/features/abuse_report_spec.rb36
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb14
-rw-r--r--spec/frontend/abuse_reports/components/abuse_category_selector_spec.js125
-rw-r--r--spec/frontend/behaviors/markdown/render_gfm_spec.js9
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js12
-rw-r--r--spec/frontend/gfm_auto_complete/mock_data.js24
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js27
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js6
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js58
-rw-r--r--spec/frontend/members/guest_overage_confirm_action_spec.js7
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js8
-rw-r--r--spec/frontend/repository/commits_service_spec.js8
-rw-r--r--spec/frontend/repository/mock_data.js2
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js7
-rw-r--r--spec/frontend/users/profile/components/report_abuse_button_spec.js72
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap163
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js4
-rw-r--r--spec/graphql/types/description_version_type_spec.rb10
-rw-r--r--spec/graphql/types/notes/note_type_spec.rb3
-rw-r--r--spec/graphql/types/notes/system_note_metadata_type_spec.rb11
-rw-r--r--spec/helpers/nav_helper_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb21
-rw-r--r--spec/models/abuse_report_spec.rb20
-rw-r--r--spec/models/ci/build_spec.rb13
-rw-r--r--spec/requests/abuse_reports_controller_spec.rb74
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb29
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/cache_spec.rb13
-rw-r--r--workhorse/Makefile7
-rwxr-xr-xworkhorse/_support/detect-external-tests.sh11
-rw-r--r--workhorse/internal/upload/destination/destination_test.go69
-rw-r--r--workhorse/internal/upload/destination/objectstore/gocloud_object_test.go11
-rw-r--r--workhorse/internal/upload/destination/objectstore/multipart.go10
-rw-r--r--workhorse/internal/upload/destination/objectstore/multipart_test.go5
-rw-r--r--workhorse/internal/upload/destination/objectstore/object_test.go11
-rw-r--r--workhorse/internal/upload/destination/objectstore/s3_complete_multipart_api.go38
-rw-r--r--workhorse/internal/upload/destination/objectstore/s3_object_test.go11
-rw-r--r--workhorse/internal/upload/destination/objectstore/s3api/s3api.go37
-rw-r--r--workhorse/internal/upload/destination/objectstore/test/objectstore_stub.go10
-rw-r--r--workhorse/internal/upload/destination/upload_opts_test.go23
-rw-r--r--workhorse/internal/upload/object_storage_preparer_test.go7
-rw-r--r--workhorse/internal/zipartifacts/metadata_test.go12
-rw-r--r--yarn.lock8
127 files changed, 2104 insertions, 566 deletions
diff --git a/.gitlab/issue_templates/Doc_cleanup.md b/.gitlab/issue_templates/Doc_cleanup.md
index 3ea692ed1ac..1eb3829e281 100644
--- a/.gitlab/issue_templates/Doc_cleanup.md
+++ b/.gitlab/issue_templates/Doc_cleanup.md
@@ -1,5 +1,3 @@
-/labels ~"documentation" ~"docs-only" ~"documentation" ~"docs::improvement" ~"type::maintenance" ~"maintenance::refactor" ~"Seeking community contributions" ~"quick win" ~"Technical Writing"
-
<!--
* Use this template for documentation issues identified
* by [Vale](https://docs.gitlab.com/ee/development/documentation/testing.html#vale)
@@ -16,6 +14,8 @@ Do you want to work on this issue?
- **If the issue is unassigned**, in a comment, type `@docs-hackathon I would like to work on this issue` and a writer will assign it to you.
+ To be fair to others, do not ask for more than three issues at a time.
+
- **If the issue is assigned to someone already**, choose another issue. Do not open a merge request for this issue if you are not assigned.
## To resolve the issue
@@ -35,4 +35,4 @@ Thank you again for contributing to the GitLab documentation! :tada:
## Documentation issue
-
+/labels ~"documentation" ~"docs-only" ~"documentation" ~"docs::improvement" ~"type::maintenance" ~"maintenance::refactor" ~"Seeking community contributions" ~"quick win" ~"Technical Writing"
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index e49169bae67..bc5b49888cd 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -371,6 +371,7 @@ Gitlab/NamespacedClass:
- 'app/policies/deploy_keys_project_policy.rb'
- 'app/policies/deploy_token_policy.rb'
- 'app/policies/deployment_policy.rb'
+ - 'app/policies/description_version_policy.rb'
- 'app/policies/draft_note_policy.rb'
- 'app/policies/environment_policy.rb'
- 'app/policies/external_issue_policy.rb'
diff --git a/app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue b/app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue
new file mode 100644
index 00000000000..93477a01073
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue
@@ -0,0 +1,92 @@
+<script>
+import { GlButton, GlDrawer, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+export default {
+ name: 'AbuseCategorySelector',
+ csrf,
+ components: {
+ GlButton,
+ GlDrawer,
+ GlForm,
+ GlFormGroup,
+ GlFormRadioGroup,
+ },
+ inject: ['formSubmitPath', 'userId', 'reportedFromUrl'],
+ props: {
+ showDrawer: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ i18n: {
+ title: __('Report abuse to administrator'),
+ close: __('Close'),
+ label: s__('ReportAbuse|Why are you reporting this user?'),
+ next: __('Next'),
+ },
+ categoryOptions: [
+ { value: 'spam', text: s__("ReportAbuse|They're posting spam.") },
+ { value: 'offensive', text: s__("ReportAbuse|They're being offsensive or abusive.") },
+ { value: 'phishing', text: s__("ReportAbuse|They're phising.") },
+ { value: 'crypto', text: s__("ReportAbuse|They're crypto mining.") },
+ {
+ value: 'credentials',
+ text: s__("ReportAbuse|They're posting personal information or credentials."),
+ },
+ { value: 'copyright', text: s__("ReportAbuse|They're violating a copyright or trademark.") },
+ { value: 'malware', text: s__("ReportAbuse|They're posting malware.") },
+ { value: 'other', text: s__('ReportAbuse|Something else.') },
+ ],
+ data() {
+ return {
+ selected: '',
+ };
+ },
+ computed: {
+ drawerOffsetTop() {
+ const wrapperEl = document.querySelector('.content-wrapper');
+ return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
+ },
+ },
+ methods: {
+ closeDrawer() {
+ this.$emit('close-drawer');
+ },
+ },
+};
+</script>
+<template>
+ <gl-drawer :header-height="drawerOffsetTop" :open="showDrawer" @close="closeDrawer">
+ <template #title>
+ <h2
+ class="gl-font-size-h2 gl-mt-0 gl-mb-0 gl-line-height-24"
+ data-testid="category-drawer-title"
+ >
+ {{ $options.i18n.title }}
+ </h2>
+ </template>
+ <template #default>
+ <gl-form :action="formSubmitPath" method="post" class="gl-text-left">
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+
+ <input type="hidden" name="user_id" :value="userId" data-testid="input-user-id" />
+ <input type="hidden" name="ref_url" :value="reportedFromUrl" data-testid="input-referer" />
+
+ <gl-form-group :label="$options.i18n.label">
+ <gl-form-radio-group
+ v-model="selected"
+ :options="$options.categoryOptions"
+ name="abuse_report[category]"
+ required
+ />
+ </gl-form-group>
+
+ <gl-button type="submit" variant="confirm" data-testid="submit-form-button">
+ {{ $options.i18n.next }}
+ </gl-button>
+ </gl-form>
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index d712c90242c..ff301a99243 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -11,6 +11,7 @@ export default function initGFMInput($els) {
emojis: true,
members: enableGFM,
issues: enableGFM,
+ iterations: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 2eab5b84e3e..04b3599ea8c 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -18,6 +18,10 @@ function initPopovers(elements) {
// Render GitLab flavoured Markdown
export function renderGFM(element) {
+ if (!element) {
+ return;
+ }
+
const [
highlightEls,
krokiEls,
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 216796b357c..56461165588 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,9 +1,9 @@
<script>
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
},
props: {
schedule: {
@@ -24,7 +24,11 @@ export default {
<template>
<div>
- <ci-badge v-if="hasPipeline" :status="lastPipelineStatus" class="gl-vertical-align-middle" />
+ <ci-badge-link
+ v-if="hasPipeline"
+ :status="lastPipelineStatus"
+ class="gl-vertical-align-middle"
+ />
<span v-else data-testid="pipeline-schedule-status-text">
{{ s__('PipelineSchedules|None') }}
</span>
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index efa7909c913..e359344ab77 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { durationTimeFormatted } from '~/lib/utils/datetime_utility';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
@@ -11,7 +11,7 @@ import LinkCell from './cells/link_cell.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
GlTableLite,
LinkCell,
RunnerTags,
@@ -80,7 +80,7 @@ export default {
fixed
>
<template #cell(status)="{ item = {} }">
- <ci-badge v-if="item.detailedStatus" :status="item.detailedStatus" />
+ <ci-badge-link v-if="item.detailedStatus" :status="item.detailedStatus" />
</template>
<template #cell(job)="{ item = {} }">
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
index edfc22f644b..075dbb06190 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
@@ -8,7 +8,7 @@ query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String,
nodes {
id
detailedStatus {
- # fields for `<ci-badge>`
+ # fields for `<ci-badge-link>`
id
detailsPath
group
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 1f14bcd70bd..9a57548ddf3 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -997,6 +997,11 @@
"pull-push"
]
},
+ "unprotect": {
+ "type": "boolean",
+ "markdownDescription": "Use `unprotect: true` to set a cache to be shared between protected and unprotected branches.",
+ "default": false
+ },
"untracked": {
"type": "boolean",
"markdownDescription": "Use `untracked: true` to cache all files that are untracked in your Git repository. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cacheuntracked)",
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 293cd2df16f..81da8409873 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -39,8 +39,18 @@ export const CONTACTS_REMOVE_COMMAND = '/remove_contacts';
* @param string user input
* @return {string} escaped user input
*/
-function escape(string) {
- return lodashEscape(string).replace(/\$/g, '&dollar;');
+export function escape(string) {
+ // To prevent double (or multiple) enconding attack
+ // Decode the user input repeatedly prior to escaping the final decoded string.
+ let encodedString = string;
+ let decodedString = decodeURIComponent(encodedString);
+
+ while (decodedString !== encodedString) {
+ encodedString = decodeURIComponent(decodedString);
+ decodedString = decodeURIComponent(encodedString);
+ }
+
+ return lodashEscape(decodedString.replace(/\$/g, '&dollar;'));
}
export function showAndHideHelper($input, alias = '') {
@@ -106,6 +116,7 @@ export const defaultAutocompleteConfig = {
issues: true,
mergeRequests: true,
epics: true,
+ iterations: true,
milestones: true,
labels: true,
snippets: true,
@@ -209,6 +220,10 @@ class GfmAutoComplete {
[[referencePrefix]] = value.params;
if (/^[@%~]/.test(referencePrefix)) {
tpl += '<%- referencePrefix %>';
+ } else if (/^[*]/.test(referencePrefix)) {
+ // EE-ONLY
+ referencePrefix = '*iteration:';
+ tpl += '<%- referencePrefix %>';
}
}
}
@@ -883,7 +898,8 @@ class GfmAutoComplete {
const atSymbolsWithBar = Object.keys(controllers)
.join('|')
.replace(/[$]/, '\\$&')
- .replace(/([[\]:])/g, '\\$1');
+ .replace(/([[\]:])/g, '\\$1')
+ .replace(/([*])/g, '\\$1');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
@@ -912,6 +928,7 @@ GfmAutoComplete.atTypeMap = {
'#': 'issues',
'!': 'mergeRequests',
'&': 'epics',
+ '*iteration:': 'iterations',
'~': 'labels',
'%': 'milestones',
'/': 'commands',
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index d8c5c292f52..9ee4439b618 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -1,7 +1,7 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ActionsCell from './cells/actions_cell.vue';
import DurationCell from './cells/duration_cell.vue';
import JobCell from './cells/job_cell.vue';
@@ -14,7 +14,7 @@ export default {
},
components: {
ActionsCell,
- CiBadge,
+ CiBadgeLink,
DurationCell,
GlTable,
JobCell,
@@ -55,7 +55,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <ci-badge :status="item.detailedStatus" />
+ <ci-badge-link :status="item.detailedStatus" />
</template>
<template #cell(job)="{ item }">
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index df3b55ed2ad..21d0bda6b5d 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -103,6 +103,14 @@ function deferredInitialisation() {
initCopyCodeButton();
initGitlabVersionCheck();
+ // Init super sidebar
+ if (gon.use_new_navigation) {
+ // eslint-disable-next-line promise/catch-or-return
+ import('./super_sidebar/super_sidebar_bundle').then(({ initSuperSidebar }) => {
+ initSuperSidebar();
+ });
+ }
+
addSelectOnFocusBehaviour('.js-select-on-focus');
const glTooltipDelay = localStorage.getItem('gl-tooltip-delay');
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 6cd8bf57313..daf5e95e6ef 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -2,7 +2,9 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { mapActions } from 'vuex';
+import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
+import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
export default {
name: 'RoleDropdown',
@@ -50,22 +52,37 @@ export default {
return dispatch(`${this.namespace}/updateMemberRole`, payload);
},
}),
- handleSelect(value, name) {
- if (value === this.member.accessLevel.integerValue) {
+ async handleOverageConfirm(currentAccessLevel, value) {
+ return guestOverageConfirmAction({
+ currentAccessIntValue: currentAccessLevel,
+ dropdownIntValue: value,
+ });
+ },
+ async handleSelect(value, name) {
+ const currentAccessLevel = this.member.accessLevel.integerValue;
+ if (value === currentAccessLevel) {
return;
}
this.busy = true;
+ const confirmed = await this.handleOverageConfirm(currentAccessLevel, value);
+ if (!confirmed) {
+ this.busy = false;
+ return;
+ }
+
this.updateMemberRole({
memberId: this.member.id,
accessLevel: { integerValue: value, stringValue: name },
})
.then(() => {
this.$toast.show(s__('Members|Role updated successfully.'));
- this.busy = false;
})
- .catch(() => {
+ .catch((error) => {
+ Sentry.captureException(error);
+ })
+ .finally(() => {
this.busy = false;
});
},
diff --git a/app/assets/javascripts/members/guest_overage_confirm_action.js b/app/assets/javascripts/members/guest_overage_confirm_action.js
new file mode 100644
index 00000000000..2205c3ad792
--- /dev/null
+++ b/app/assets/javascripts/members/guest_overage_confirm_action.js
@@ -0,0 +1,3 @@
+export const guestOverageConfirmAction = () => {
+ return true;
+};
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index 5cbb7a06bc1..30c351359e4 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
+import { initReportAbuse } from '~/users/profile';
import UserTabs from './user_tabs';
function initUserProfile(action) {
@@ -19,3 +20,4 @@ const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
+initReportAbuse();
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
index c56537f4039..041b62e02ec 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__ } from '~/locale';
import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
@@ -12,7 +12,7 @@ export default {
fields: DEFAULT_FIELDS,
retry: __('Retry'),
components: {
- CiBadge,
+ CiBadgeLink,
GlButton,
GlLink,
GlTableLite,
@@ -72,7 +72,7 @@ export default {
<div
class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
>
- <ci-badge :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <ci-badge-link :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
<div class="gl-text-truncate">
<gl-link
:href="item.detailedStatus.detailsPath"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
index 936ae4da1ec..919694f8f85 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
@@ -1,12 +1,12 @@
<script>
import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
PipelinesTimeago,
},
mixins: [Tracking.mixin()],
@@ -38,7 +38,7 @@ export default {
<template>
<div>
- <ci-badge
+ <ci-badge-link
class="gl-mb-3"
:status="pipelineStatus"
:show-text="!isChildView"
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index f009c0310c5..d029f8cf89f 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -35,7 +35,7 @@ const fetchData = (projectPath, path, ref, offset) => {
gon.relative_url_root || '/',
projectPath,
'/-/refs/',
- ref,
+ encodeURIComponent(ref),
'/logs_tree/',
encodeURIComponent(removeLeadingSlash(path)),
);
diff --git a/app/assets/javascripts/repository/utils/ref_switcher_utils.js b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
index 8ff52104c93..ceb80d85f74 100644
--- a/app/assets/javascripts/repository/utils/ref_switcher_utils.js
+++ b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
@@ -24,7 +24,12 @@ export function generateRefDestinationPath(projectRootPath, selectedRef) {
[, namespace, , target] = match;
}
- const destinationPath = joinPaths(projectRootPath, namespace, selectedRef, target);
+ const destinationPath = joinPaths(
+ projectRootPath,
+ namespace,
+ encodeURIComponent(selectedRef),
+ target,
+ );
return `${destinationPath}${window.location.hash}`;
}
diff --git a/app/assets/javascripts/super_sidebar/components/bottom_bar.vue b/app/assets/javascripts/super_sidebar/components/bottom_bar.vue
new file mode 100644
index 00000000000..fea29458f45
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/bottom_bar.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ i18n: {
+ help: __('Help'),
+ new: __('New'),
+ },
+};
+</script>
+
+<template>
+ <div class="bottom-links gl-p-3">
+ <a href="#" class="gl-text-black-normal"
+ ><gl-icon name="question-o" class="gl-mr-3 gl-text-gray-300 gl-text-black-normal!" />{{
+ $options.i18n.help
+ }}</a
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
new file mode 100644
index 00000000000..f1ddb8290a0
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlAvatar, GlSearchBoxByType } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { contextSwitcherItems } from '../mock_data';
+import NavItem from './nav_item.vue';
+
+export default {
+ components: {
+ GlAvatar,
+ GlSearchBoxByType,
+ NavItem,
+ },
+ i18n: {
+ contextNavigation: s__('Navigation|Context navigation'),
+ switchTo: s__('Navigation|Switch to...'),
+ recentProjects: s__('Navigation|Recent projects'),
+ recentGroups: s__('Navigation|Recent groups'),
+ },
+ contextSwitcherItems,
+ viewAllProjectsItem: {
+ title: s__('Navigation|View all projects'),
+ link: '/projects',
+ icon: 'project',
+ },
+ viewAllGroupsItem: {
+ title: s__('Navigation|View all groups'),
+ link: '/groups',
+ icon: 'group',
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-search-box-by-type />
+ <nav :aria-label="$options.i18n.contextNavigation">
+ <ul class="gl-p-0 gl-list-style-none">
+ <li>
+ <div aria-hidden="true" class="gl-font-weight-bold gl-px-3 gl-py-3">
+ {{ $options.i18n.switchTo }}
+ </div>
+ <ul :aria-label="$options.i18n.switchTo" class="gl-p-0">
+ <nav-item :item="$options.contextSwitcherItems.yourWork" />
+ </ul>
+ </li>
+ <li>
+ <div aria-hidden="true" class="gl-font-weight-bold gl-px-3 gl-py-3">
+ {{ $options.i18n.recentProjects }}
+ </div>
+ <ul :aria-label="$options.i18n.recentProjects" class="gl-p-0">
+ <nav-item
+ v-for="project in $options.contextSwitcherItems.recentProjects"
+ :key="project.title"
+ :item="project"
+ >
+ <template #icon>
+ <gl-avatar shape="rect" :size="32" :src="project.avatar" />
+ </template>
+ </nav-item>
+ <nav-item :item="$options.viewAllProjectsItem" />
+ </ul>
+ </li>
+ <li>
+ <div aria-hidden="true" class="gl-font-weight-bold gl-px-3 gl-py-3">
+ {{ $options.i18n.recentGroups }}
+ </div>
+ <ul :aria-label="$options.i18n.recentGroups" class="gl-p-0">
+ <nav-item
+ v-for="project in $options.contextSwitcherItems.recentGroups"
+ :key="project.title"
+ :item="project"
+ >
+ <template #icon>
+ <gl-avatar shape="rect" :size="32" :src="project.avatar" />
+ </template>
+ </nav-item>
+ <nav-item :item="$options.viewAllGroupsItem" />
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
new file mode 100644
index 00000000000..b6f058f7aee
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlTruncate, GlAvatar, GlCollapseToggleDirective, GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTruncate,
+ GlAvatar,
+ GlIcon,
+ },
+ directives: {
+ CollapseToggle: GlCollapseToggleDirective,
+ },
+ props: {
+ context: {
+ type: Object,
+ required: true,
+ },
+ expanded: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ collapseIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ v-collapse-toggle.context-switcher
+ type="button"
+ class="context-switcher-toggle gl-bg-transparent gl-border-0 border-top border-bottom gl-border-gray-a-08 gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-pl-3 gl-pr-5 gl-h-8"
+ >
+ <gl-avatar :size="32" shape="rect" :src="context.avatar" class="gl-mr-3" />
+ <div class="gl-overflow-auto">
+ <gl-truncate :text="context.title" />
+ </div>
+ <span class="gl-flex-grow-1 gl-text-right">
+ <gl-icon :name="collapseIcon" />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
new file mode 100644
index 00000000000..873d7c48574
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ icon: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ href="#"
+ class="counter gl-relative gl-display-inline-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-black-normal gl-border gl-border-gray-a-08 gl-font-sm gl-font-weight-bold"
+ >
+ <gl-icon :name="icon" />
+ {{ count }}
+ </a>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
new file mode 100644
index 00000000000..4fd6918fd6f
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ name: 'NavItem',
+ components: {
+ GlIcon,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <a
+ :href="item.link"
+ class="gl-display-flex gl-pl-3 gl-py-3 gl-line-height-normal gl-text-black-normal gl-hover-bg-t-gray-a-08"
+ >
+ <div class="gl-mr-3">
+ <slot name="icon">
+ <gl-icon v-if="item.icon" :name="item.icon" />
+ </slot>
+ </div>
+ <div class="gl-pr-3">
+ {{ item.title }}
+ <div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500 gl-mt-1">
+ {{ item.subtitle }}
+ </div>
+ </div>
+ </a>
+ </li>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
new file mode 100644
index 00000000000..e5c29f966c1
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlCollapse } from '@gitlab/ui';
+import { user, counts, context } from '../mock_data';
+import UserBar from './user_bar.vue';
+import ContextSwitcherToggle from './context_switcher_toggle.vue';
+import ContextSwitcher from './context_switcher.vue';
+import BottomBar from './bottom_bar.vue';
+
+export default {
+ context,
+ user,
+ counts,
+ components: {
+ GlCollapse,
+ UserBar,
+ ContextSwitcherToggle,
+ ContextSwitcher,
+ BottomBar,
+ },
+ data() {
+ return {
+ contextSwitcherOpened: false,
+ };
+ },
+};
+</script>
+
+<template>
+ <aside
+ class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08 gl-z-index-9999"
+ data-testid="super-sidebar"
+ >
+ <user-bar :user="$options.user" :counts="$options.counts" />
+ <div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden">
+ <div class="gl-flex-grow-1 gl-overflow-auto">
+ <context-switcher-toggle :context="$options.context" :expanded="contextSwitcherOpened" />
+ <gl-collapse id="context-switcher" v-model="contextSwitcherOpened">
+ <context-switcher />
+ </gl-collapse>
+ </div>
+ <div class="gl-px-3">
+ <bottom-bar />
+ </div>
+ </div>
+ </aside>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
new file mode 100644
index 00000000000..00fcf70929c
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlAvatar, GlDropdown, GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import logo from '../../../../views/shared/_logo.svg';
+import Counter from './counter.vue';
+
+export default {
+ logo,
+ components: {
+ GlAvatar,
+ GlDropdown,
+ GlIcon,
+ NewNavToggle,
+ Counter,
+ },
+ directives: {
+ SafeHtml,
+ },
+ inject: ['rootPath', 'toggleNewNavEndpoint'],
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ counts: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="user-bar">
+ <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2 gl-gap-3">
+ <div class="gl-flex-grow-1">
+ <a v-safe-html="$options.logo" :href="rootPath"></a>
+ </div>
+ <gl-dropdown variant="link" no-caret>
+ <template #button-content>
+ <gl-icon name="plus" class="gl-vertical-align-middle gl-text-black-normal" />
+ </template>
+ </gl-dropdown>
+ <button class="gl-border-none">
+ <gl-icon name="search" class="gl-vertical-align-middle" />
+ </button>
+ <gl-dropdown data-testid="user-dropdown" variant="link" no-caret>
+ <template #button-content>
+ <gl-avatar :entity-name="user.name" :src="user.avatar_url" :size="32" />
+ </template>
+ <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled />
+ </gl-dropdown>
+ </div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2">
+ <counter icon="issues" :count="counts.assigned_issues" />
+ <counter icon="merge-request-open" :count="counts.assigned_merge_requests" />
+ <counter icon="todo-done" :count="counts.pending_todos" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/mock_data.js b/app/assets/javascripts/super_sidebar/mock_data.js
new file mode 100644
index 00000000000..b16a188b94f
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/mock_data.js
@@ -0,0 +1,70 @@
+import { s__ } from '~/locale';
+
+export const user = {
+ name: 'GitLab Bot',
+ avatar_url: '',
+};
+
+export const counts = {
+ assigned_issues: 0,
+ assigned_merge_requests: 4,
+ pending_todos: 12,
+};
+
+export const context = {
+ title: 'Typeahead.js',
+ link: '/',
+ avatar: 'https://gitlab.com/uploads/-/system/project/avatar/278964/project_avatar.png?width=32',
+};
+
+export const contextSwitcherItems = {
+ yourWork: { title: s__('Navigation|Your work'), link: '/', icon: 'work' },
+ recentProjects: [
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ title: 'Orange',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/project/avatar/4456656/pajamas-logo.png?width=64',
+ },
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ title: 'Lemon',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar: 'https://gitlab.com/uploads/-/system/project/avatar/7071551/GitLab_UI.png?width=64',
+ },
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ title: 'Coconut',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/project/avatar/4149988/SVGs_project.png?width=64',
+ },
+ ],
+ recentGroups: [
+ {
+ title: 'Developer Evangelism at GitLab',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/group/avatar/10087220/rainbow_tanuki.jpg?width=64',
+ },
+ {
+ title: 'security-products',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/group/avatar/11932235/gitlab-icon-rgb.png?width=64',
+ },
+ {
+ title: 'Tanuki-Workshops',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/group/avatar/5085244/Screenshot_2019-04-29_at_16.13.07.png?width=64',
+ },
+ ],
+};
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
new file mode 100644
index 00000000000..35aa6aff08c
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import SuperSidebar from './components/super_sidebar.vue';
+
+export const initSuperSidebar = () => {
+ const el = document.querySelector('.js-super-sidebar');
+
+ if (!el) return false;
+
+ const { rootPath, toggleNewNavEndpoint } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'SuperSidebarRoot',
+ provide: {
+ rootPath,
+ toggleNewNavEndpoint,
+ },
+ render(h) {
+ return h(SuperSidebar);
+ },
+ });
+};
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index b19f92aaeb4..c88c528a632 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -11,14 +11,14 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, sprintf } from '~/locale';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import StateActions from './states_table_actions.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
GlAlert,
GlBadge,
GlLink,
@@ -198,7 +198,7 @@ export default {
:id="`terraformJobStatusContainer${item.name}`"
class="gl-my-2"
>
- <ci-badge
+ <ci-badge-link
:id="`terraformJobStatus${item.name}`"
:status="pipelineDetailedStatus(item)"
class="gl-py-1"
diff --git a/app/assets/javascripts/users/profile/components/report_abuse_button.vue b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
new file mode 100644
index 00000000000..3008cdb6726
--- /dev/null
+++ b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+export default {
+ name: 'ReportAbuseButton',
+ components: {
+ GlButton,
+ AbuseCategorySelector,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['formSubmitPath', 'userId', 'reportedFromUrl'],
+ i18n: {
+ reportAbuse: __('Report abuse to administrator'),
+ },
+ data() {
+ return {
+ open: false,
+ };
+ },
+ computed: {
+ buttonTooltipText() {
+ return this.$options.i18n.reportAbuse;
+ },
+ },
+ methods: {
+ openDrawer() {
+ this.open = true;
+ },
+ closeDrawer() {
+ this.open = false;
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ <gl-button
+ v-gl-tooltip="buttonTooltipText"
+ category="primary"
+ :aria-label="buttonTooltipText"
+ icon="error"
+ @click="openDrawer"
+ />
+ <abuse-category-selector :show-drawer="open" @close-drawer="closeDrawer" />
+ </span>
+</template>
diff --git a/app/assets/javascripts/users/profile/index.js b/app/assets/javascripts/users/profile/index.js
new file mode 100644
index 00000000000..9246324a990
--- /dev/null
+++ b/app/assets/javascripts/users/profile/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import ReportAbuseButton from './components/report_abuse_button.vue';
+
+export const initReportAbuse = () => {
+ const el = document.getElementById('js-report-abuse');
+
+ if (!el) return false;
+
+ const { formSubmitPath, userId, reportedFromUrl } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: { formSubmitPath, userId, reportedFromUrl },
+ render(createElement) {
+ return createElement(ReportAbuseButton);
+ },
+ });
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index 9a3555d3e11..f7d6f7b4345 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -76,17 +76,17 @@ export default {
<div
:class="{
'gl-display-flex gl-align-items-center': actions.length,
- 'gl-md-display-flex gl-align-items-center': !actions.length,
+ 'gl-md-display-flex gl-align-items-center gl-flex-wrap gl-gap-3': !actions.length,
}"
- class="media-body"
+ class="media-body gl-line-height-24"
>
<slot></slot>
<div
:class="{
- 'state-container-action-buttons gl-flex-direction-column gl-flex-wrap gl-justify-content-end': !actions.length,
+ 'state-container-action-buttons gl-flex-wrap gl-lg-justify-content-end': !actions.length,
'gl-md-pt-0 gl-pt-3': hasActionsSlot,
}"
- class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3"
+ class="gl-display-flex gl-font-size-0 gl-gap-3"
>
<slot name="actions">
<actions v-if="actions.length" :tertiary-buttons="actions" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 8e1b18c63a4..a5d982fe221 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -88,25 +88,24 @@ export default {
</template>
<template v-if="!isLoading && !state.shouldBeRebased" #actions>
<gl-button
- v-if="userPermissions.canMerge"
+ v-if="showResolveButton"
+ :href="mr.conflictResolutionPath"
size="small"
variant="confirm"
- category="secondary"
- data-testid="merge-locally-button"
- class="js-check-out-modal-trigger gl-align-self-start"
- :class="{ 'gl-mr-2': showResolveButton }"
+ class="gl-align-self-start"
+ data-testid="resolve-conflicts-button"
>
- {{ s__('mrWidget|Resolve locally') }}
+ {{ s__('mrWidget|Resolve conflicts') }}
</gl-button>
<gl-button
- v-if="showResolveButton"
- :href="mr.conflictResolutionPath"
+ v-if="userPermissions.canMerge"
size="small"
variant="confirm"
- class="gl-mb-2 gl-md-mb-0 gl-align-self-start"
- data-testid="resolve-conflicts-button"
+ category="secondary"
+ data-testid="merge-locally-button"
+ class="js-check-out-modal-trigger gl-align-self-start"
>
- {{ s__('mrWidget|Resolve conflicts') }}
+ {{ s__('mrWidget|Resolve locally') }}
</gl-button>
</template>
</state-container>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 4ae4edf02c3..d687f0346c7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -179,27 +179,27 @@ export default {
</template>
<template v-if="!isLoading" #actions>
<gl-button
- v-if="showRebaseWithoutPipeline"
:loading="isMakingRequest"
variant="confirm"
size="small"
- category="secondary"
- data-testid="rebase-without-ci-button"
- class="gl-align-self-start gl-mr-2"
- @click="rebaseWithoutCi"
+ data-qa-selector="mr_rebase_button"
+ data-testid="standard-rebase-button"
+ class="gl-align-self-start"
+ @click="rebase"
>
- {{ __('Rebase without pipeline') }}
+ {{ __('Rebase') }}
</gl-button>
<gl-button
+ v-if="showRebaseWithoutPipeline"
:loading="isMakingRequest"
variant="confirm"
size="small"
- data-qa-selector="mr_rebase_button"
- data-testid="standard-rebase-button"
- class="gl-mb-2 gl-md-mb-0 gl-align-self-start"
- @click="rebase"
+ category="secondary"
+ data-testid="rebase-without-ci-button"
+ class="gl-align-self-start gl-mr-2"
+ @click="rebaseWithoutCi"
>
- {{ __('Rebase') }}
+ {{ __('Rebase without pipeline') }}
</gl-button>
</template>
</state-container>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 07db6b3c147..e60353578b0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -38,6 +38,7 @@
@import 'framework/sidebar';
@import 'framework/contextual_sidebar_header';
@import 'framework/contextual_sidebar';
+@import 'framework/super_sidebar';
@import 'framework/tables';
@import 'framework/notes';
@import 'framework/tabs';
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
new file mode 100644
index 00000000000..59a9df9ede0
--- /dev/null
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -0,0 +1,22 @@
+.super-sidebar {
+ top: 0;
+ width: $contextual-sidebar-width;
+
+ .user-bar {
+ background-color: $t-gray-a-04;
+
+ .tanuki-logo {
+ @include gl-vertical-align-middle;
+ }
+ }
+
+ .context-switcher-toggle {
+ &[aria-expanded='true'] {
+ background-color: $t-gray-a-08;
+ }
+ }
+}
+
+.with-performance-bar .super-sidebar {
+ top: $performance-bar-height;
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 4950561bcb7..2cc4ca55d19 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -1197,13 +1197,13 @@ $tabs-holder-z-index: 250;
}
.mr-section-container {
+ .media-body {
+ column-gap: 0;
+ }
+
.state-container-action-buttons {
@include media-breakpoint-up(md) {
flex-direction: row-reverse;
-
- .btn {
- margin-left: auto;
- }
}
}
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 714dd932147..48c268e1f2a 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -243,6 +243,14 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
+.gl-gap-2 {
+ gap: $gl-spacing-scale-2;
+}
+
+.gl-hover-bg-t-gray-a-08:hover {
+ background-color: $t-gray-a-08;
+}
+
/* End gitlab-ui#1709 */
/*
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 80aca7e21ce..0cf486f9afb 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -1,14 +1,21 @@
# frozen_string_literal: true
class AbuseReportsController < ApplicationController
- before_action :set_user, only: [:new]
+ before_action :set_user, :set_ref_url, only: [:new, :add_category]
feature_category :insider_threat
def new
- @abuse_report = AbuseReport.new
- @abuse_report.user_id = @user.id
- @ref_url = params.fetch(:ref_url, '')
+ @abuse_report = AbuseReport.new(user_id: @user.id)
+ end
+
+ def add_category
+ @abuse_report = AbuseReport.new(
+ user_id: @user.id,
+ category: report_params[:category]
+ )
+
+ render :new
end
def create
@@ -30,7 +37,7 @@ class AbuseReportsController < ApplicationController
private
def report_params
- params.require(:abuse_report).permit(:message, :user_id)
+ params.require(:abuse_report).permit(:message, :user_id, :category)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -44,4 +51,8 @@ class AbuseReportsController < ApplicationController
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def set_ref_url
+ @ref_url = params.fetch(:ref_url, '')
+ end
end
diff --git a/app/graphql/types/description_version_type.rb b/app/graphql/types/description_version_type.rb
new file mode 100644
index 00000000000..bee30597e4c
--- /dev/null
+++ b/app/graphql/types/description_version_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ class DescriptionVersionType < BaseObject
+ graphql_name 'DescriptionVersion'
+
+ authorize :read_issuable
+
+ field :id, ::Types::GlobalIDType[::DescriptionVersion],
+ null: false,
+ description: 'ID of the description version.'
+
+ field :description, GraphQL::Types::String,
+ null: true,
+ description: 'Content of the given description version.'
+ end
+end
+
+Types::DescriptionVersionType.prepend_mod
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
index 05629ea9223..7e09a3e91cf 100644
--- a/app/graphql/types/notes/note_type.rb
+++ b/app/graphql/types/notes/note_type.rb
@@ -11,54 +11,65 @@ module Types
implements(Types::ResolvableInterface)
- field :id, ::Types::GlobalIDType[::Note], null: false,
- description: 'ID of the note.'
+ field :id, ::Types::GlobalIDType[::Note],
+ null: false,
+ description: 'ID of the note.'
field :project, Types::ProjectType,
- null: true,
- description: 'Project associated with the note.'
+ null: true,
+ description: 'Project associated with the note.'
field :author, Types::UserType,
- null: false,
- description: 'User who wrote this note.'
+ null: false,
+ description: 'User who wrote this note.'
field :system, GraphQL::Types::Boolean,
- null: false,
- description: 'Indicates whether this note was created by the system or by a user.'
+ null: false,
+ description: 'Indicates whether this note was created by the system or by a user.'
field :system_note_icon_name,
- GraphQL::Types::String,
- null: true,
- description: 'Name of the icon corresponding to a system note.'
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the icon corresponding to a system note.'
field :body, GraphQL::Types::String,
- null: false,
- method: :note,
- description: 'Content of the note.'
-
- field :confidential, GraphQL::Types::Boolean, null: true,
- description: 'Indicates if this note is confidential.',
- method: :confidential?,
- deprecated: {
- reason: :renamed,
- replacement: 'internal',
- milestone: '15.5'
- }
-
- field :internal, GraphQL::Types::Boolean, null: true,
- description: 'Indicates if this note is internal.',
- method: :confidential?
-
- field :created_at, Types::TimeType, null: false,
- description: 'Timestamp of the note creation.'
- field :discussion, Types::Notes::DiscussionType, null: true,
- description: 'Discussion this note is a part of.'
- field :position, Types::Notes::DiffPositionType, null: true,
- description: 'Position of this note on a diff.'
- field :updated_at, Types::TimeType, null: false,
- description: "Timestamp of the note's last activity."
+ null: false,
+ method: :note,
+ description: 'Content of the note.'
+
+ field :confidential, GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates if this note is confidential.',
+ method: :confidential?,
+ deprecated: {
+ reason: :renamed,
+ replacement: 'internal',
+ milestone: '15.5'
+ }
+
+ field :internal, GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates if this note is internal.',
+ method: :confidential?
+
+ field :created_at, Types::TimeType,
+ null: false,
+ description: 'Timestamp of the note creation.'
+ field :discussion, Types::Notes::DiscussionType,
+ null: true,
+ description: 'Discussion this note is a part of.'
+ field :position, Types::Notes::DiffPositionType,
+ null: true,
+ description: 'Position of this note on a diff.'
+ field :updated_at, Types::TimeType,
+ null: false,
+ description: "Timestamp of the note's last activity."
field :url, GraphQL::Types::String,
- null: true,
- description: 'URL to view this Note in the Web UI.'
+ null: true,
+ description: 'URL to view this Note in the Web UI.'
+
+ field :system_note_metadata, Types::Notes::SystemNoteMetadataType,
+ null: true,
+ description: 'Metadata for the given note if it is a system note.'
markdown_field :body_html, null: true, method: :note
diff --git a/app/graphql/types/notes/system_note_metadata_type.rb b/app/graphql/types/notes/system_note_metadata_type.rb
new file mode 100644
index 00000000000..b3dd7e037f9
--- /dev/null
+++ b/app/graphql/types/notes/system_note_metadata_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Notes
+ class SystemNoteMetadataType < BaseObject
+ graphql_name 'SystemNoteMetadata'
+
+ authorize :read_note
+
+ field :id, ::Types::GlobalIDType[::SystemNoteMetadata],
+ null: false,
+ description: 'Global ID of the specific system note metadata.'
+
+ field :action, GraphQL::Types::String,
+ null: true,
+ description: 'System note metadata action.'
+ field :description_version, ::Types::DescriptionVersionType,
+ null: true,
+ description: 'Version of the changed description.'
+ end
+ end
+end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index bf3b132e33a..d0421cd5184 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -65,6 +65,10 @@ module NavHelper
%w(dev_ops_report usage_trends)
end
+ def show_super_sidebar?
+ Feature.enabled?(:super_sidebar_nav, current_user) && current_user&.use_new_navigation
+ end
+
private
def get_header_links
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index f1f22d94061..c90023dd9df 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -12,11 +12,23 @@ class AbuseReport < ApplicationRecord
validates :reporter, presence: true
validates :user, presence: true
validates :message, presence: true
+ validates :category, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
scope :by_user, ->(user) { where(user_id: user) }
scope :with_users, -> { includes(:reporter, :user) }
+ enum category: {
+ spam: 1,
+ offensive: 2,
+ phishing: 3,
+ crypto: 4,
+ credentials: 5,
+ copyright: 6,
+ malware: 7,
+ other: 8
+ }
+
# For CacheMarkdownField
alias_method :author, :reporter
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d5b9c338e39..111d2797fed 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -884,8 +884,9 @@ module Ci
return cache unless project.ci_separated_caches
- type_suffix = pipeline.protected_ref? ? 'protected' : 'non_protected'
cache.map do |entry|
+ type_suffix = !entry[:unprotect] && pipeline.protected_ref? ? 'protected' : 'non_protected'
+
entry.merge(key: "#{entry[:key]}-#{type_suffix}")
end
end
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index 96c8553c101..fb61b7f5fde 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -6,6 +6,8 @@ class DescriptionVersion < ApplicationRecord
validate :exactly_one_issuable
+ delegate :resource_parent, to: :issuable
+
def self.issuable_attrs
%i(issue merge_request).freeze
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index da07d8dd9fc..b0676c25f8e 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -166,8 +166,6 @@ class Milestone < ApplicationRecord
end
def self.states_count(projects, groups = nil)
- return STATE_COUNT_HASH unless projects || groups
-
counts = Milestone
.for_projects_and_groups(projects, groups)
.reorder(nil)
diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb
index a60c0d2f3bc..f88fa052665 100644
--- a/app/models/synthetic_note.rb
+++ b/app/models/synthetic_note.rb
@@ -14,7 +14,7 @@ class SyntheticNote < Note
discussion_id: event.discussion_id,
noteable: resource,
event: event,
- system_note_metadata: ::SystemNoteMetadata.new(action: action),
+ system_note_metadata: ::SystemNoteMetadata.new(action: action, id: event.discussion_id),
resource_parent: resource_parent
}
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 4e86036952b..36166bdbc9a 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -34,6 +34,12 @@ class SystemNoteMetadata < ApplicationRecord
belongs_to :note
belongs_to :description_version
+ delegate_missing_to :note
+
+ def declarative_policy_delegate
+ note
+ end
+
def icon_types
ICON_TYPES
end
diff --git a/app/policies/description_version_policy.rb b/app/policies/description_version_policy.rb
new file mode 100644
index 00000000000..9ee9df3278b
--- /dev/null
+++ b/app/policies/description_version_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class DescriptionVersionPolicy < BasePolicy
+ delegate { @subject.issuable }
+end
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index d5dfddef837..5c8db51d122 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -10,6 +10,7 @@
= form_errors(@abuse_report)
= f.hidden_field :user_id
+ = f.hidden_field :category
.form-group.row
.col-sm-2.col-form-label
= f.label :user_id
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index bb1d051f71f..f1d29e77e34 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,5 +1,9 @@
+- if show_super_sidebar?
+ - @left_sidebar = true
.layout-page.hide-when-top-nav-responsive-open{ class: page_with_sidebar_class }
- - if defined?(nav) && nav
+ - if show_super_sidebar?
+ %aside.js-super-sidebar.nav-sidebar{ data: { root_path: root_path, toggle_new_nav_endpoint: profile_preferences_url } }
+ - elsif defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper.content-wrapper-margin{ class: "#{@content_wrapper_class}" }
.mobile-overlay
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 455d18a5ae8..fa79219df4a 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -11,7 +11,14 @@
= render "layouts/visual_review" if ENV['REVIEW_APPS_ENABLED']
= render 'peek/bar'
= header_message
- = render partial: "layouts/header/default", locals: { project: @project, group: @group }
+
+ - if show_super_sidebar? # TODO: Move this CSS to a better place
+ :css
+ body {
+ --header-height: 0px;
+ }
+ - else
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
= render 'layouts/page', sidebar: sidebar, nav: nav
= footer_message
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 03ecf8cac22..19c68da835b 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -23,9 +23,7 @@
icon: 'error',
button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: s_('UserProfile|Already reported for abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- else
- = render Pajamas::ButtonComponent.new(href: new_abuse_report_path(user_id: @user.id, ref_url: request.referer),
- icon: 'error',
- button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: _('Report abuse to administrator'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
+ #js-report-abuse{ data: { form_submit_path: add_category_abuse_reports_path, user_id: @user.id, reported_from_url: user_url(@user) } }
- verified_gpg_keys = @user.gpg_keys.select(&:verified?)
- if verified_gpg_keys.any?
= render Pajamas::ButtonComponent.new(href: user_gpg_keys_path,
diff --git a/config/routes.rb b/config/routes.rb
index a9cb462b326..7569bce530a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -210,7 +210,11 @@ InitializerConnections.with_disabled_database_connections do
end
# Spam reports
- resources :abuse_reports, only: [:new, :create]
+ resources :abuse_reports, only: [:new, :create] do
+ collection do
+ post :add_category
+ end
+ end
# JWKS (JSON Web Key Set) endpoint
# Used by third parties to verify CI_JOB_JWT
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 4f6bfc5c82a..fa890531861 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -50,6 +50,17 @@ class Gitlab::Seeder::CycleAnalytics
end
def seed!
+ unless project.repository_exists?
+ puts
+ puts 'WARNING'
+ puts '======='
+ puts "Seeding #{self.class} is not possible because the given project (#{project.full_path}) doesn't have a repository."
+ puts 'Try specifying a project with working repository or omit the VSA_SEED_PROJECT_ID parameter so the seed script will automatically create one.'
+ puts
+
+ return
+ end
+
create_developers!
create_issues!
@@ -169,6 +180,7 @@ class Gitlab::Seeder::CycleAnalytics
)
project = FactoryBot.create(
:project,
+ :repository,
name: "Value Stream Management Project #{suffix}",
path: "vsmp-#{suffix}",
creator: admin,
diff --git a/db/migrate/20221204090437_add_category_to_abuse_report.rb b/db/migrate/20221204090437_add_category_to_abuse_report.rb
new file mode 100644
index 00000000000..e908f3354bb
--- /dev/null
+++ b/db/migrate/20221204090437_add_category_to_abuse_report.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddCategoryToAbuseReport < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :abuse_reports, :category, :integer, limit: 2, default: 1, null: false
+ end
+end
diff --git a/db/schema_migrations/20221204090437 b/db/schema_migrations/20221204090437
new file mode 100644
index 00000000000..3ae8d4c2067
--- /dev/null
+++ b/db/schema_migrations/20221204090437
@@ -0,0 +1 @@
+16bdaabcc19086652b0543dcdc7204305a920794fdab38c042d06bb2be76dde0 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a8b5547abaf..1ba63e4a20e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10602,7 +10602,8 @@ CREATE TABLE abuse_reports (
created_at timestamp without time zone,
updated_at timestamp without time zone,
message_html text,
- cached_markdown_version integer
+ cached_markdown_version integer,
+ category smallint DEFAULT 1 NOT NULL
);
CREATE SEQUENCE abuse_reports_id_seq
diff --git a/doc/.vale/gitlab/HeadingDepth.yml b/doc/.vale/gitlab/HeadingDepth.yml
index 7a3e5b4b552..5bbe667481c 100644
--- a/doc/.vale/gitlab/HeadingDepth.yml
+++ b/doc/.vale/gitlab/HeadingDepth.yml
@@ -1,5 +1,5 @@
---
-# Warning: gitlab.HeadingDepth
+# Suggestion: gitlab.HeadingDepth
#
# Checks that there are no headings greater than 3 levels
#
@@ -7,7 +7,7 @@
extends: existence
message: "Refactor the section or page to avoid headings greater than H5."
link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#headings-in-markdown
-level: warning
+level: suggestion
scope: raw
raw:
- '(?<=\n)#{5,}\s.*'
diff --git a/doc/.vale/gitlab/SentenceLength.yml b/doc/.vale/gitlab/SentenceLength.yml
index 69b0d27072e..48ebf02bc7f 100644
--- a/doc/.vale/gitlab/SentenceLength.yml
+++ b/doc/.vale/gitlab/SentenceLength.yml
@@ -1,5 +1,5 @@
---
-# Warning: gitlab.SentenceLength
+# Suggestion: gitlab.SentenceLength
#
# Counts words in a sentence and alerts if a sentence exceeds 25 words.
#
@@ -8,6 +8,6 @@ extends: occurrence
message: "Improve readability by using fewer than 25 words in this sentence."
scope: sentence
link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#language
-level: warning
+level: suggestion
max: 25
token: \b(\w+)\b
diff --git a/doc/.vale/gitlab/Spelling.yml b/doc/.vale/gitlab/Spelling.yml
index 92c5cb13b29..74d919831ac 100644
--- a/doc/.vale/gitlab/Spelling.yml
+++ b/doc/.vale/gitlab/Spelling.yml
@@ -10,7 +10,7 @@
#
# For a list of all options, see https://vale.sh/docs/topics/styles/
extends: spelling
-message: "Check the spelling of '%s'. If the spelling is correct, add this word to the spelling exception list."
+message: "Check the spelling of '%s'. If the spelling is correct, ask a Technical Writer to add this word to the spelling exception list."
level: warning
ignore:
- gitlab/spelling-exceptions.txt
diff --git a/doc/.vale/gitlab/Uppercase.yml b/doc/.vale/gitlab/Uppercase.yml
index 039ad7c5f03..724194695c4 100644
--- a/doc/.vale/gitlab/Uppercase.yml
+++ b/doc/.vale/gitlab/Uppercase.yml
@@ -1,13 +1,13 @@
---
-# Warning: gitlab.Uppercase
+# Suggestion: gitlab.Uppercase
#
# Checks for use of all uppercase letters with unknown reason.
#
# For a list of all options, see https://vale.sh/docs/topics/styles/
extends: conditional
-message: "Instead of uppercase for '%s', use lowercase or backticks (`) if possible. Otherwise, add this word or acronym to the rule's exception list."
+message: "Instead of uppercase for '%s', use lowercase or backticks (`) if possible. Otherwise, ask a Technical Writer to add this word or acronym to the rule's exception list."
link: https://docs.gitlab.com/ee/development/documentation/testing.html#vale-uppercase-acronym-test
-level: warning
+level: suggestion
ignorecase: false
# Ensures that the existence of 'first' implies the existence of 'second'.
first: '\b([A-Z]{3,5})\b'
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index 6b62e82f54d..5f9323016c0 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -198,22 +198,22 @@ _This can only be run against a primary Geo node._
PUT /geo_nodes/:id
```
-| Attribute | Type | Required | Description |
-|-----------------------------|---------|-----------|---------------------------------------------------------------------------|
-| `id` | integer | yes | The ID of the Geo node. |
-| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
-| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url`. |
-| `url` | string | yes | The user-facing URL of the Geo node. |
-| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.|
-| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
-| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
-| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
-| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
-| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. |
-| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
-| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
-| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
-| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. |
+| Attribute | Type | Required | Description |
+|-----------------------------|---------|---------|---------------------------------------------------------------------------|
+| `id` | integer | yes | The ID of the Geo node. |
+| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
+| `name` | string | no | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url`. |
+| `url` | string | no | The user-facing URL of the Geo node. |
+| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.|
+| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
+| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
+| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
+| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
+| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node should replicate blobs in Object Storage. |
+| `selective_sync_type` | string | no | Limit syncing to only specific groups or shards. Valid values: `"namespaces"`, `"shards"`, or `null`. |
+| `selective_sync_shards` | array | no | The repository storage for the projects synced if `selective_sync_type` == `shards`. |
+| `selective_sync_namespace_ids` | array | no | The IDs of groups that should be synced, if `selective_sync_type` == `namespaces`. |
+| `minimum_reverification_interval` | integer | no | The interval (in days) in which the repository verification is valid. Once expired, it is reverified. This has no effect when set on a secondary node. |
Example response:
@@ -255,9 +255,6 @@ in GitLab 14.9.
Removes the Geo node.
-NOTE:
-Only a Geo primary node accepts this request.
-
```plaintext
DELETE /geo_nodes/:id
```
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3b4f411e4a3..5a60ed3aaa7 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -12080,6 +12080,33 @@ Tags for a given deployment.
| <a id="deploymenttagname"></a>`name` | [`String`](#string) | Name of this git tag. |
| <a id="deploymenttagpath"></a>`path` | [`String`](#string) | Path for this tag. |
+### `DescriptionVersion`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="descriptionversioncandelete"></a>`canDelete` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 15.7. For backwards compatibility with REST API version and to be removed in a next iteration. |
+| <a id="descriptionversiondeletepath"></a>`deletePath` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.7. For backwards compatibility with REST API version and to be removed in a next iteration. |
+| <a id="descriptionversiondeleted"></a>`deleted` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 15.7. For backwards compatibility with REST API version and to be removed in a next iteration. |
+| <a id="descriptionversiondescription"></a>`description` | [`String`](#string) | Content of the given description version. |
+| <a id="descriptionversiondiffpath"></a>`diffPath` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.7. For backwards compatibility with REST API version and to be removed in a next iteration. |
+| <a id="descriptionversionid"></a>`id` | [`DescriptionVersionID!`](#descriptionversionid) | ID of the description version. |
+
+#### Fields with arguments
+
+##### `DescriptionVersion.diff`
+
+Description diff between versions.
+
+Returns [`String`](#string).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="descriptionversiondiffversionid"></a>`versionId` | [`DescriptionVersionID`](#descriptionversionid) | ID of a previous version to compare. If not specified first previous version is used. |
+
### `Design`
A single design.
@@ -16357,6 +16384,7 @@ Represents the network policy.
| <a id="noteresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. |
| <a id="notesystem"></a>`system` | [`Boolean!`](#boolean) | Indicates whether this note was created by the system or by a user. |
| <a id="notesystemnoteiconname"></a>`systemNoteIconName` | [`String`](#string) | Name of the icon corresponding to a system note. |
+| <a id="notesystemnotemetadata"></a>`systemNoteMetadata` | [`SystemNoteMetadata`](#systemnotemetadata) | Metadata for the given note if it is a system note. |
| <a id="noteupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of the note's last activity. |
| <a id="noteurl"></a>`url` | [`String`](#string) | URL to view this Note in the Web UI. |
| <a id="noteuserpermissions"></a>`userPermissions` | [`NotePermissions!`](#notepermissions) | Permissions for the current user on the resource. |
@@ -19548,9 +19576,18 @@ Represents a Suggested Reviewers result set.
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="suggestedreviewerstypereviewers"></a>`reviewers` | [`[String!]!`](#string) | List of reviewers. |
-| <a id="suggestedreviewerstypetopn"></a>`topN` | [`Int`](#int) | Number of reviewers returned. |
-| <a id="suggestedreviewerstypeversion"></a>`version` | [`String`](#string) | Suggested reviewer version. |
+| <a id="suggestedreviewerstypeaccepted"></a>`accepted` | [`[String!]`](#string) | List of accepted reviewer usernames. |
+| <a id="suggestedreviewerstypesuggested"></a>`suggested` | [`[String!]!`](#string) | List of suggested reviewer usernames. |
+
+### `SystemNoteMetadata`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="systemnotemetadataaction"></a>`action` | [`String`](#string) | System note metadata action. |
+| <a id="systemnotemetadatadescriptionversion"></a>`descriptionVersion` | [`DescriptionVersion`](#descriptionversion) | Version of the changed description. |
+| <a id="systemnotemetadataid"></a>`id` | [`SystemNoteMetadataID!`](#systemnotemetadataid) | Global ID of the specific system note metadata. |
### `TaskCompletionStatus`
@@ -23361,6 +23398,12 @@ A `DependencyProxyManifestID` is a global ID. It is encoded as a string.
An example `DependencyProxyManifestID` is: `"gid://gitlab/DependencyProxy::Manifest/1"`.
+### `DescriptionVersionID`
+
+A `DescriptionVersionID` is a global ID. It is encoded as a string.
+
+An example `DescriptionVersionID` is: `"gid://gitlab/DescriptionVersion/1"`.
+
### `DesignManagementDesignAtVersionID`
A `DesignManagementDesignAtVersionID` is a global ID. It is encoded as a string.
@@ -23698,6 +23741,12 @@ An example `SnippetID` is: `"gid://gitlab/Snippet/1"`.
Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text.
+### `SystemNoteMetadataID`
+
+A `SystemNoteMetadataID` is a global ID. It is encoded as a string.
+
+An example `SystemNoteMetadataID` is: `"gid://gitlab/SystemNoteMetadata/1"`.
+
### `TerraformStateID`
A `TerraformStateID` is a global ID. It is encoded as a string.
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index dffe409b193..b8457477c34 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -1103,10 +1103,16 @@ job:
### `cache`
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330047) in GitLab 15.0, caches are not shared between protected and unprotected branches.
+
Use `cache` to specify a list of files and directories to
cache between jobs. You can only use paths that are in the local working copy.
-Caching is shared between pipelines and jobs. Caches are restored before [artifacts](#artifacts).
+Caches are:
+
+- Shared between pipelines and jobs.
+- By default, not shared between [protected](../../user/project/protected_branches.md) and unprotected branches.
+- Restored before [artifacts](#artifacts).
Learn more about caches in [Caching in GitLab CI/CD](../caching/index.md).
@@ -1319,6 +1325,33 @@ rspec:
- binaries/
```
+#### `cache:unprotect`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362114) in GitLab 15.8.
+
+Use `cache:unprotect` to set a cache to be shared between [protected](../../user/project/protected_branches.md)
+and unprotected branches.
+
+WARNING:
+When set to `true`, users without access to protected branches can read and write to
+cache keys used by protected branches.
+
+**Keyword type**: Job keyword. You can use it only as part of a job or in the
+[`default` section](#default).
+
+**Possible inputs**:
+
+- `true` or `false` (default).
+
+**Example of `cache:untracked`**:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ unprotect: true
+```
+
#### `cache:when`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18969) in GitLab 13.5 and GitLab Runner v13.5.0.
diff --git a/doc/development/documentation/topic_types/index.md b/doc/development/documentation/topic_types/index.md
index 964b41303cb..cfc231c268a 100644
--- a/doc/development/documentation/topic_types/index.md
+++ b/doc/development/documentation/topic_types/index.md
@@ -28,7 +28,7 @@ If inline links are not sufficient, you can create a topic called **Related topi
and include an unordered list of related topics. This topic should be above the Troubleshooting section.
```markdown
-# Related topics
+## Related topics
- [Configure your pipeline](link-to-topic).
- [Trigger a pipeline manually](link-to-topic).
diff --git a/doc/development/merge_request_diffs.md b/doc/development/merge_request_diffs.md
new file mode 100644
index 00000000000..a3c8ada898e
--- /dev/null
+++ b/doc/development/merge_request_diffs.md
@@ -0,0 +1,188 @@
+---
+stage: Create
+group: Code Review
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Merge request diffs development guide **(FREE)**
+
+This document explains the backend design and flow of merge request diffs.
+It should help contributors:
+
+- Understand the code design.
+- Identify areas for improvement through contribution.
+
+It's intentional that it doesn't contain too many implementation details, as they
+can change often. The code better explains these details. The components
+mentioned here are the major parts of the application for how merge request diffs
+are generated, stored, and returned to users.
+
+NOTE:
+This page is a living document. Update it accordingly when the parts
+of the codebase touched in this document are changed or removed, or when new components
+are added.
+
+## Data model
+
+Four main ActiveRecord models represent what we collectively refer to
+as _diffs._ These database-backed records replicate data contained in the
+project's Git repository, and are in part a cache against excessive access requests
+to [Gitaly](gitaly.md). Additionally, they provide a logical place for:
+
+- Calculated and retrieved metadata about the pieces of the diff.
+- General class- and instance- based logic.
+
+```mermaid
+erDiagram
+ MergeRequest ||--|{ MergeRequestDiff: ""
+ MergeRequestDiff |{--|{ MergeRequestDiffCommit: ""
+ MergeRequestDiff |{--|| MergeRequestDiffDetail: ""
+ MergeRequestDiff |{--|{ MergeRequestDiffFile: ""
+ MergeRequestDiffCommit |{--|| MergeRequestDiffCommitUser: ""
+```
+
+### `MergeRequestDiff`
+
+`MergeRequestDiff` is defined in `app/models/merge_request_diff.rb`. This
+class holds metadata and context related to the diff resulting from a set of
+commits. It defines methods that are the primary means for interacting with diff
+contents, individual commits, and the files containing changes.
+
+```ruby
+#<MergeRequestDiff:0x00007fd1ed63b4d0
+ id: 28,
+ state: "collected",
+ merge_request_id: 28,
+ created_at: Tue, 06 Sep 2022 18:56:02.509469000 UTC +00:00,
+ updated_at: Tue, 06 Sep 2022 18:56:02.754201000 UTC +00:00,
+ base_commit_sha: "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ real_size: "9",
+ head_commit_sha: "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
+ start_commit_sha: "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ commits_count: 6,
+ external_diff: "diff-28",
+ external_diff_store: 1,
+ stored_externally: nil,
+ files_count: 9,
+ sorted: true,
+ diff_type: "regular",
+ verification_checksum: nil>
+```
+
+Diff content is usually accessed through this class. Logic is often applied
+to diff, file, and commit content before it is returned to a user.
+
+### `MergeRequestDiffCommit`
+
+`MergeRequestDiffCommit` is defined in `app/models/merge_request_diff_commit.rb`.
+This class corresponds to a single commit contained in its corresponding `MergeRequestDiff`,
+and holds header information about the commit.
+
+```ruby
+#<MergeRequestDiffCommit:0x00007fd1dfc6c4c0
+ authored_date: Wed, 06 Aug 2022 06:35:52.000000000 UTC +00:00,
+ committed_date: Wed, 06 Aug 2022 06:35:52.000000000 UTC +00:00,
+ merge_request_diff_id: 28,
+ relative_order: 0,
+ sha: "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
+ message: "Feature conflcit added\n\nSigned-off-by: Sample User <sample.user@example.com>\n",
+ trailers: {},
+ commit_author_id: 19,
+ committer_id: 19>
+```
+
+Every `MergeRequestDiffCommit` has a corresponding `MergeRequest::DiffCommitUser`
+record it `:belongs_to`, in ActiveRecord parlance. These records are `:commit_author`
+and `:committer`, and could be distinct individuals.
+
+### `MergeRequest::DiffCommitUser`
+
+`MergeRequest::DiffCommitUser` is defined in `app/models/merge_request/diff_commit_user.rb`.
+It captures the `name` and `email` of a given commit, but contains no connection
+itself to any `User` records.
+
+```ruby
+#<MergeRequest::DiffCommitUser:0x00007fd1dff7c930
+ id: 19,
+ name: "Sample User",
+ email: "sample.user@example.com">
+```
+
+### `MergeRequestDiffFile`
+
+`MergeRequestDiffFile` is defined in `app/models/merge_request_diff_file.rb`.
+This record of this class represents the diff of a single file contained in the
+`MergeRequestDiff`. It holds both meta and specific information about the file's
+relationship to the change, such as:
+
+- Whether it is added or renamed.
+- Its ordering in the diff.
+- The raw diff output itself.
+
+### `MergeRequestDiffDetail`
+
+`MergeRequestDiffDetail` is defined in `app/models/merge_request_diff_detail.rb`.
+This class provides verification information for Geo replication, but otherwise
+is not used for user-facing diffs.
+
+```ruby
+#<MergeRequestDiffFile:0x00007fd1ef7c9048
+ merge_request_diff_id: 28,
+ relative_order: 0,
+ new_file: true,
+ renamed_file: false,
+ deleted_file: false,
+ too_large: false,
+ a_mode: "0",
+ b_mode: "100644",
+ new_path: "files/ruby/feature.rb",
+ old_path: "files/ruby/feature.rb",
+ diff:
+ "@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n",
+ binary: false,
+ external_diff_offset: nil,
+ external_diff_size: nil>
+```
+
+## Flow
+
+These flowcharts should help explain the flow from the controllers down to the
+models for different features. This page is not intended to document the entirety
+of options for access and working with diffs, focusing solely on the most common.
+
+### `batch_diffs.json`
+
+The most common avenue for viewing diffs is the **Changes**
+tab in the top navigation bar of merge request pages in the GitLab UI. When selected, the
+diffs themselves are loaded via a paginated request to `/-/merge_requests/:id/batch_diffs.json`,
+which is served by [`Projects::MergeRequests::DiffsController#diffs_batch`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/controllers/projects/merge_requests/diffs_controller.rb):
+
+<!-- Don't delete the &nbsp; characters below. Mermaid returns a syntax error if they aren't included.-->
+
+```mermaid
+sequenceDiagram
+ Note over .#diffs_batch: Preload diffs and ivars
+ .#diffs_batch->>+.#define_diff_vars: &nbsp;
+ .#define_diff_vars ->>+ @merge_request: @merge_request_diffs =
+ Note right of @merge_request: An ordered collection of all diffs in MR
+ @merge_request-->>-.#define_diff_vars: &nbsp;
+ .#define_diff_vars ->>+ @merge_request: @merge_request_diff =
+ Note right of @merge_request: Most recent merge_request_diff (or commit)
+ @merge_request-->>-.#define_diff_vars: &nbsp;
+ .#define_diff_vars ->>+ .#define_diff_vars: @compare =
+ Note right of .#define_diff_vars:: param-filtered merge_request_diff(s)
+ .#define_diff_vars -->>- .#diffs_batch: &nbsp;
+ Note over .#diffs_batch: Preloading complete
+ .#diffs_batch->>+@merge_request: Calculate unfoldable diff lines
+ Note right of @merge_request: note_positions_for_paths.unfoldable
+ @merge_request-->>-.#diffs_batch: &nbsp;
+ Note over .#diffs_batch: Build options hash
+ Note over .#diffs_batch: Build cache_context
+ Note over .#diffs_batch: Unfold files in diff
+ .#diffs_batch->>+Gitlab_Diff_FileCollection_MergeRequestDiffBase: diffs.write_diff
+ Gitlab_Diff_FileCollection_MergeRequestDiffBase->>+Gitlab_Diff_HighlightCache: Highlight diff
+ Gitlab_Diff_HighlightCache -->>-Gitlab_Diff_FileCollection_MergeRequestDiffBase: Return highlighted diff
+ Note over Gitlab_Diff_FileCollection_MergeRequestDiffBase: Cache diff
+ Gitlab_Diff_FileCollection_MergeRequestDiffBase-->>-.#diffs_batch: &nbsp;
+ Note over .#diffs_batch: render JSON
+```
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index d0a420a4bbd..a97e511ec1c 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -248,40 +248,37 @@ enabled a feature flag for [moved sidebar actions](../project/merge_requests/ind
The following table presents the events that generate notifications for issues, merge requests, and
epics:
-| Event | Sent to |
-|------------------------|---------|
-| Change milestone issue | Subscribers and participants mentioned. |
-| Change milestone merge request | Subscribers and participants mentioned. |
-| Close epic | |
-| Close issue | |
-| Close merge request | |
-| Failed pipeline | The author of the pipeline. |
-| Fixed pipeline | The author of the pipeline. Enabled by default. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24309) in GitLab 13.1._ |
-| Issue due | Participants and Custom notification level with this event selected. |
-| Merge merge request | |
-| Merge when pipeline succeeds | Author, Participants, Watchers, Subscribers, and Custom notification level with this event selected. Custom notification level is ignored for Author, Watchers and Subscribers. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211961) in GitLab 13.4._ |
-| Merge request [marked as ready](../project/merge_requests/drafts.md) | Watchers and participants. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15332) in GitLab 13.10._ |
-| New epic | |
-| New issue | |
-| New merge request | |
-| New note | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
-| Push to merge request | Participants and Custom notification level with this event selected. |
-| Reassign issue | Participants, Watchers, Subscribers, Custom notification level with this event selected, and the old assignee. |
-| Reassign merge request | Participants, Watchers, Subscribers, Custom notification level with this event selected, and the old assignee. |
-| Remove milestone issue | Subscribers and participants mentioned. |
-| Remove milestone merge request | Subscribers and participants mentioned. |
-| Reopen epic | |
-| Reopen issue | |
-| Reopen merge request | |
-| Successful pipeline | The author of the pipeline, with Custom notification level for successful pipelines. If the pipeline failed previously, a "Fixed pipeline" message is sent for the first successful pipeline after the failure, and then a "Successful pipeline" message for any further successful pipelines. |
-
-If the title or description of an issue or merge request is
-changed, notifications are sent to any **new** mentions by username as
-if they had been mentioned in the original text.
-
-If an open merge request becomes unmergeable due to conflict, its author is notified about the cause.
-If a user has also set the merge request to automatically merge when pipeline succeeds,
-then that user is also notified.
+| Type | Event | Sent to |
+|------|-------|---------|
+| Epic | Closed | Subscribers and participants mentioned. |
+| Epic | New | Anyone mentioned by username in the description, with notification level "Mention" or higher. |
+| Epic | New note | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
+| Epic | Reopened | Subscribers and participants mentioned. |
+| Issue | Closed | Subscribers and participants mentioned. |
+| Issue | Due | Participants and Custom notification level with this event selected. |
+| Issue | Milestone changed | Subscribers and participants mentioned. |
+| Issue | Milestone removed | Subscribers and participants mentioned. |
+| Issue | New | Anyone mentioned by username in the description, with notification level "Mention" or higher. |
+| Issue | New note | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
+| Issue | Title or description changed | Any new mentions by username. |
+| Issue | Reassigned | Participants, Watchers, Subscribers, Custom notification level with this event selected, and the old assignee. |
+| Issue | Reopened | Subscribers and participants mentioned. |
+| Merge Request | Closed | Subscribers and participants mentioned. |
+| Merge Request | Conflict | Author and any user that has set the merge request to automatically merge when pipeline succeeds. |
+| Merge Request | [Marked as ready](../project/merge_requests/drafts.md) | Watchers and participants. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15332) in GitLab 13.10._ |
+| Merge Request | Merged | Subscribers and participants mentioned. |
+| Merge Request | Merged when pipeline succeeds | Author, Participants, Watchers, Subscribers, and Custom notification level with this event selected. Custom notification level is ignored for Author, Watchers and Subscribers. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211961) in GitLab 13.4._ |
+| Merge Request | Milestone changed | Subscribers and participants mentioned. |
+| Merge Request | Milestone removed | Subscribers and participants mentioned. |
+| Merge Request | New | Anyone mentioned by username in the description, with notification level "Mention" or higher. |
+| Merge Request | New note | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
+| Merge Request | Pushed | Participants and Custom notification level with this event selected. |
+| Merge Request | Reassigned | Participants, Watchers, Subscribers, Custom notification level with this event selected, and the old assignee. |
+| Merge Request | Reopened | Subscribers and participants mentioned. |
+| Merge Request | Title or description changed | Any new mentions by username. |
+| Pipeline | Failed | The author of the pipeline. |
+| Pipeline | Fixed | The author of the pipeline. Enabled by default. _[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24309) in GitLab 13.1._ |
+| Pipeline | Successful | The author of the pipeline, with Custom notification level for successful pipelines. If the pipeline failed previously, a "Fixed pipeline" message is sent for the first successful pipeline after the failure, and then a "Successful pipeline" message for any further successful pipelines. |
By default, you don't receive notifications for issues, merge requests, or epics created by yourself.
To always receive notifications on your own issues, merge requests, and so on, turn on
diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb
index 842250d351b..181759a7f38 100644
--- a/lib/api/concerns/packages/debian_package_endpoints.rb
+++ b/lib/api/concerns/packages/debian_package_endpoints.rb
@@ -35,10 +35,10 @@ module API
::Packages::Debian::DistributionsFinder.new(container, codename_or_suite: params[:distribution]).execute.last!
end
- def present_distribution_package_file!
+ def present_distribution_package_file!(project)
not_found! unless params[:package_name].start_with?(params[:letter])
- package_file = distribution_from!(user_project).package_files.with_file_name(params[:file_name]).last!
+ package_file = distribution_from!(project).package_files.with_file_name(params[:file_name]).last!
present_package_file!(package_file)
end
diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb
index 105a0955912..483d0dd9c90 100644
--- a/lib/api/debian_group_packages.rb
+++ b/lib/api/debian_group_packages.rb
@@ -12,10 +12,6 @@ module API
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
- def user_project
- @project ||= find_project!(params[:project_id])
- end
-
def project_or_group
user_group
end
@@ -55,7 +51,7 @@ module API
route_setting :authentication, authenticate_non_public: true
get 'pool/:distribution/:project_id/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do
- present_distribution_package_file!
+ present_distribution_package_file!(find_project!(params[:project_id]))
end
end
end
diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb
index 23a542e4183..353f64b8dd1 100644
--- a/lib/api/debian_project_packages.rb
+++ b/lib/api/debian_project_packages.rb
@@ -21,16 +21,16 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
def project_or_group
- user_project
+ user_project(action: :read_package)
end
end
after_validation do
require_packages_enabled!
- not_found! unless ::Feature.enabled?(:debian_packages, user_project)
+ not_found! unless ::Feature.enabled?(:debian_packages, project_or_group)
- authorize_read_package!
+ authorize_read_package!(project_or_group)
end
params do
@@ -58,7 +58,7 @@ module API
route_setting :authentication, authenticate_non_public: true
get 'pool/:distribution/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do
- present_distribution_package_file!
+ present_distribution_package_file!(project_or_group)
end
params do
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index a5481071fc5..a635f409109 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -9,7 +9,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[key untracked paths when policy].freeze
+ ALLOWED_KEYS = %i[key untracked paths when policy unprotect].freeze
ALLOWED_POLICY = %w[pull-push push pull].freeze
DEFAULT_POLICY = 'pull-push'
ALLOWED_WHEN = %w[on_success on_failure always].freeze
@@ -33,18 +33,22 @@ module Gitlab
entry :key, Entry::Key,
description: 'Cache key used to define a cache affinity.'
+ entry :unprotect, ::Gitlab::Config::Entry::Boolean,
+ description: 'Unprotect the cache from a protected ref.'
+
entry :untracked, ::Gitlab::Config::Entry::Boolean,
description: 'Cache all untracked files.'
entry :paths, Entry::Paths,
description: 'Specify which paths should be cached across builds.'
- attributes :policy, :when
+ attributes :policy, :when, :unprotect
def value
result = super
result[:key] = key_value
+ result[:unprotect] = unprotect || false
result[:policy] = policy || DEFAULT_POLICY
# Use self.when to avoid conflict with reserved word
result[:when] = self.when || DEFAULT_WHEN
diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb
index 781065a63db..409b6658cc0 100644
--- a/lib/gitlab/ci/pipeline/seed/build/cache.rb
+++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb
@@ -14,6 +14,7 @@ module Gitlab
@policy = local_cache.delete(:policy)
@untracked = local_cache.delete(:untracked)
@when = local_cache.delete(:when)
+ @unprotect = local_cache.delete(:unprotect)
@custom_key_prefix = custom_key_prefix
raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
@@ -25,7 +26,8 @@ module Gitlab
paths: @paths,
policy: @policy,
untracked: @untracked,
- when: @when
+ when: @when,
+ unprotect: @unprotect
}.compact
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 12cdcf445f7..5aad6f60d44 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -57,6 +57,7 @@ module Gitlab
gon.current_user_fullname = current_user.name
gon.current_user_avatar_url = current_user.avatar_url
gon.time_display_relative = current_user.time_display_relative
+ gon.use_new_navigation = Feature.enabled?(:super_sidebar_nav, current_user) && current_user&.use_new_navigation
end
# Initialize gon.features with any flags that should be
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6309e4f4075..6b4a352c8d7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -825,6 +825,9 @@ msgstr ""
msgid "%{level_name} is not allowed since the fork source project has lower visibility."
msgstr ""
+msgid "%{linkStart} Learn more%{linkEnd}."
+msgstr ""
+
msgid "%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it's ready."
msgstr ""
@@ -10971,6 +10974,9 @@ msgstr ""
msgid "Continue to the next step"
msgstr ""
+msgid "Continue with overages"
+msgstr ""
+
msgid "Continuous Integration and Deployment"
msgstr ""
@@ -27237,6 +27243,27 @@ msgstr ""
msgid "NavigationTheme|Red"
msgstr ""
+msgid "Navigation|Context navigation"
+msgstr ""
+
+msgid "Navigation|Recent groups"
+msgstr ""
+
+msgid "Navigation|Recent projects"
+msgstr ""
+
+msgid "Navigation|Switch to..."
+msgstr ""
+
+msgid "Navigation|View all groups"
+msgstr ""
+
+msgid "Navigation|View all projects"
+msgstr ""
+
+msgid "Navigation|Your work"
+msgstr ""
+
msgid "Nav|Help"
msgstr ""
@@ -34831,6 +34858,33 @@ msgstr ""
msgid "Report your license usage data to GitLab"
msgstr ""
+msgid "ReportAbuse|Something else."
+msgstr ""
+
+msgid "ReportAbuse|They're being offsensive or abusive."
+msgstr ""
+
+msgid "ReportAbuse|They're crypto mining."
+msgstr ""
+
+msgid "ReportAbuse|They're phising."
+msgstr ""
+
+msgid "ReportAbuse|They're posting malware."
+msgstr ""
+
+msgid "ReportAbuse|They're posting personal information or credentials."
+msgstr ""
+
+msgid "ReportAbuse|They're posting spam."
+msgstr ""
+
+msgid "ReportAbuse|They're violating a copyright or trademark."
+msgstr ""
+
+msgid "ReportAbuse|Why are you reporting this user?"
+msgstr ""
+
msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr ""
@@ -47337,6 +47391,9 @@ msgstr ""
msgid "You are about to delete this project containing:"
msgstr ""
+msgid "You are about to incur additional charges"
+msgstr ""
+
msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete."
msgstr ""
diff --git a/package.json b/package.json
index ef7eac995ad..f5c010e0d0b 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.0.1",
"@gitlab/svgs": "3.14.0",
- "@gitlab/ui": "52.6.0",
+ "@gitlab/ui": "52.6.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20221217175648",
"@rails/actioncable": "6.1.4-7",
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index aacff7c4172..bcce8aa0bfe 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -409,6 +409,7 @@ module QA
fill_element(:reply_field, '')
fill_element(:reply_field, initial_content.gsub(/(```suggestion:-0\+0\n).*(\n```)/, "\\1#{suggestion}\\2"))
click_element(:comment_now_button)
+ wait_for_requests
end
def apply_suggestion_with_message(message)
diff --git a/scripts/gitlab_component_helpers.sh b/scripts/gitlab_component_helpers.sh
index 0d72f940036..c46dbb57a58 100644
--- a/scripts/gitlab_component_helpers.sh
+++ b/scripts/gitlab_component_helpers.sh
@@ -87,12 +87,10 @@ function upload_package() {
function read_curl_package() {
local package_url="${1}"
- local token_header="${CURL_TOKEN_HEADER}"
- local token="${CI_JOB_TOKEN}"
echoinfo "Downloading from ${package_url} ..."
- curl --fail --silent --retry 3 --header "${token_header}: ${token}" "${package_url}"
+ curl --fail --silent --retry 3 "${package_url}"
}
function extract_package() {
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index fdd11b59938..87236530046 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -2,17 +2,43 @@
require 'spec_helper'
-RSpec.describe 'Abuse reports', feature_category: :not_owned do
- let(:another_user) { create(:user) }
+RSpec.describe 'Abuse reports', feature_category: :insider_threat do
+ let_it_be(:another_user) { create(:user) }
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project, author: another_user) }
before do
sign_in(create(:user))
end
- it 'report abuse' do
+ it 'report abuse from an issue', :js do
+ visit project_issue_path(project, issue)
+
+ click_button 'Issue actions'
+ click_link 'Report abuse to administrator'
+
+ wait_for_requests
+
+ fill_in 'abuse_report_message', with: 'This user sends spam'
+ click_button 'Send report'
+
+ expect(page).to have_content 'Thank you for your report'
+
visit user_path(another_user)
- click_link 'Report abuse'
+ expect(page).to have_button('Already reported for abuse')
+ end
+
+ it 'report abuse from profile', :js do
+ visit user_path(another_user)
+
+ click_button 'Report abuse to administrator'
+
+ choose "They're posting spam."
+ click_button 'Next'
+
+ wait_for_requests
fill_in 'abuse_report_message', with: 'This user sends spam'
click_button 'Send report'
@@ -21,6 +47,6 @@ RSpec.describe 'Abuse reports', feature_category: :not_owned do
visit user_path(another_user)
- expect(page).to have_button("Already reported for abuse")
+ expect(page).to have_button('Already reported for abuse')
end
end
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
index f040d801cfb..8e5cc7df053 100644
--- a/spec/features/nav/new_nav_toggle_spec.rb
+++ b/spec/features/nav/new_nav_toggle_spec.rb
@@ -48,14 +48,19 @@ RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
expect(user.reload.use_new_navigation).to eq true
end
+
+ it 'shows the old navigation' do
+ expect(page).to have_selector('.js-navbar')
+ expect(page).not_to have_selector('[data-testid="super-sidebar"]')
+ end
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
it 'allows to disable new nav', :aggregate_failures do
- within '.js-nav-user-dropdown' do
- find('a[data-toggle="dropdown"]').click
+ within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
+ find('button').click
expect(page).to have_content('Navigation redesign')
toggle = page.find('.gl-toggle.is-checked')
@@ -66,6 +71,11 @@ RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
expect(user.reload.use_new_navigation).to eq false
end
+
+ it 'shows the new navigation' do
+ expect(page).not_to have_selector('.js-navbar')
+ expect(page).to have_selector('[data-testid="super-sidebar"]')
+ end
end
end
end
diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
new file mode 100644
index 00000000000..4f66348f9cd
--- /dev/null
+++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
@@ -0,0 +1,125 @@
+import { GlDrawer, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ contentTop: jest.fn(),
+}));
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+describe('AbuseCategorySelector', () => {
+ let wrapper;
+
+ const ACTION_PATH = '/abuse_reports/add_category';
+ const USER_ID = '1';
+ const REPORTED_FROM_URL = 'http://example.com';
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(AbuseCategorySelector, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ formSubmitPath: ACTION_PATH,
+ userId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent({ showDrawer: true });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findTitle = () => wrapper.findByTestId('category-drawer-title');
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+
+ const findCSRFToken = () => findForm().find('input[name="authenticity_token"]');
+ const findUserId = () => wrapper.findByTestId('input-user-id');
+ const findReferer = () => wrapper.findByTestId('input-referer');
+
+ const findSubmitFormButton = () => wrapper.findByTestId('submit-form-button');
+
+ describe('Drawer', () => {
+ it('is open when prop showDrawer = true', () => {
+ expect(findDrawer().exists()).toBe(true);
+ expect(findDrawer().props('open')).toBe(true);
+ });
+
+ it('renders title', () => {
+ expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
+ });
+
+ it('emits close-drawer event', async () => {
+ await findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
+ });
+
+ describe('when props showDrawer = false', () => {
+ beforeEach(() => {
+ createComponent({ showDrawer: false });
+ });
+
+ it('hides the drawer', () => {
+ expect(findDrawer().props('open')).toBe(false);
+ });
+ });
+ });
+
+ describe('Select category form', () => {
+ it('renders POST form with path', () => {
+ expect(findForm().attributes()).toMatchObject({
+ method: 'post',
+ action: ACTION_PATH,
+ });
+ });
+
+ it('renders csrf token', () => {
+ expect(findCSRFToken().attributes('value')).toBe('mock-csrf-token');
+ });
+
+ it('renders label', () => {
+ expect(findFormGroup().exists()).toBe(true);
+ expect(findFormGroup().attributes('label')).toBe(wrapper.vm.$options.i18n.label);
+ });
+
+ it('renders radio group', () => {
+ expect(findRadioGroup().exists()).toBe(true);
+ expect(findRadioGroup().props('options')).toEqual(wrapper.vm.$options.categoryOptions);
+ expect(findRadioGroup().attributes('name')).toBe('abuse_report[category]');
+ expect(findRadioGroup().attributes('required')).not.toBeUndefined();
+ });
+
+ it('renders userId as a hidden fields', () => {
+ expect(findUserId().attributes()).toMatchObject({
+ type: 'hidden',
+ name: 'user_id',
+ value: USER_ID,
+ });
+ });
+
+ it('renders referer as a hidden fields', () => {
+ expect(findReferer().attributes()).toMatchObject({
+ type: 'hidden',
+ name: 'ref_url',
+ value: REPORTED_FROM_URL,
+ });
+ });
+
+ it('renders submit button', () => {
+ expect(findSubmitFormButton().exists()).toBe(true);
+ expect(findSubmitFormButton().text()).toBe(wrapper.vm.$options.i18n.next);
+ });
+ });
+});
diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js
new file mode 100644
index 00000000000..0bbb92282e5
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js
@@ -0,0 +1,9 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+describe('renderGFM', () => {
+ it('handles a missing element', () => {
+ expect(() => {
+ renderGFM();
+ }).not.toThrow();
+ });
+});
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 17bf465baf3..0821c59c8a0 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import PipelineScheduleLastPipeline from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
@@ -18,7 +18,7 @@ describe('Pipeline schedule last pipeline', () => {
});
};
- const findCIBadge = () => wrapper.findComponent(CiBadge);
+ const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
afterEach(() => {
@@ -28,8 +28,10 @@ describe('Pipeline schedule last pipeline', () => {
it('displays pipeline status', () => {
createComponent();
- expect(findCIBadge().exists()).toBe(true);
- expect(findCIBadge().props('status')).toBe(defaultProps.schedule.lastPipeline.detailedStatus);
+ expect(findCIBadgeLink().exists()).toBe(true);
+ expect(findCIBadgeLink().props('status')).toBe(
+ defaultProps.schedule.lastPipeline.detailedStatus,
+ );
expect(findStatusText().exists()).toBe(false);
});
@@ -37,6 +39,6 @@ describe('Pipeline schedule last pipeline', () => {
createComponent({ schedule: mockPipelineScheduleNodes[0] });
expect(findStatusText().text()).toBe('None');
- expect(findCIBadge().exists()).toBe(false);
+ expect(findCIBadgeLink().exists()).toBe(false);
});
});
diff --git a/spec/frontend/gfm_auto_complete/mock_data.js b/spec/frontend/gfm_auto_complete/mock_data.js
index 9c5a9d7ef3d..d58ccaf0f39 100644
--- a/spec/frontend/gfm_auto_complete/mock_data.js
+++ b/spec/frontend/gfm_auto_complete/mock_data.js
@@ -37,8 +37,8 @@ export const crmContactsMock = [
{
id: 1,
email: 'contact.1@email.com',
- firstName: 'Contact',
- lastName: 'One',
+ first_name: 'Contact',
+ last_name: 'One',
search: 'contact.1@email.com',
state: 'active',
set: false,
@@ -46,8 +46,8 @@ export const crmContactsMock = [
{
id: 2,
email: 'contact.2@email.com',
- firstName: 'Contact',
- lastName: 'Two',
+ first_name: 'Contact',
+ last_name: 'Two',
search: 'contact.2@email.com',
state: 'active',
set: false,
@@ -55,8 +55,8 @@ export const crmContactsMock = [
{
id: 3,
email: 'contact.3@email.com',
- firstName: 'Contact',
- lastName: 'Three',
+ first_name: 'Contact',
+ last_name: 'Three',
search: 'contact.3@email.com',
state: 'inactive',
set: false,
@@ -64,8 +64,8 @@ export const crmContactsMock = [
{
id: 4,
email: 'contact.4@email.com',
- firstName: 'Contact',
- lastName: 'Four',
+ first_name: 'Contact',
+ last_name: 'Four',
search: 'contact.4@email.com',
state: 'inactive',
set: true,
@@ -73,8 +73,8 @@ export const crmContactsMock = [
{
id: 5,
email: 'contact.5@email.com',
- firstName: 'Contact',
- lastName: 'Five',
+ first_name: 'Contact',
+ last_name: 'Five',
search: 'contact.5@email.com',
state: 'active',
set: true,
@@ -82,8 +82,8 @@ export const crmContactsMock = [
{
id: 5,
email: 'contact.6@email.com',
- firstName: 'Contact',
- lastName: 'Six',
+ first_name: 'Contact',
+ last_name: 'Six',
search: 'contact.6@email.com',
state: 'active',
set: undefined, // On purpose
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index eeef92d4183..cc2dc084e47 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GfmAutoComplete, {
+ escape,
membersBeforeSave,
highlighter,
CONTACT_STATE_ACTIVE,
@@ -21,6 +22,20 @@ import {
crmContactsMock,
} from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
+describe('escape', () => {
+ it.each`
+ xssPayload | escapedPayload
+ ${'<script>alert(1)</script>'} | ${'&lt;script&gt;alert(1)&lt;/script&gt;'}
+ ${'%3Cscript%3E alert(1) %3C%2Fscript%3E'} | ${'&lt;script&gt; alert(1) &lt;/script&gt;'}
+ ${'%253Cscript%253E alert(1) %253C%252Fscript%253E'} | ${'&lt;script&gt; alert(1) &lt;/script&gt;'}
+ `(
+ 'escapes the input string correctly accounting for multiple encoding',
+ ({ xssPayload, escapedPayload }) => {
+ expect(escape(xssPayload)).toBe(escapedPayload);
+ },
+ );
+});
+
describe('GfmAutoComplete', () => {
const fetchDataMock = { fetchData: jest.fn() };
let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock);
@@ -590,7 +605,7 @@ describe('GfmAutoComplete', () => {
id: 5,
title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
}),
- ).toBe('<li><small>5</small> &dollar;{search}&lt;script&gt;oh no &dollar;</li>');
+ ).toBe('<li><small>5</small> &amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</li>');
});
});
@@ -636,7 +651,7 @@ describe('GfmAutoComplete', () => {
availabilityStatus: '',
}),
).toBe(
- '<li>IMG my-group <small>&dollar;{search}&lt;script&gt;oh no &dollar;</small> <i class="icon"/></li>',
+ '<li>IMG my-group <small>&amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</small> <i class="icon"/></li>',
);
});
@@ -813,7 +828,7 @@ describe('GfmAutoComplete', () => {
const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
expect(GfmAutoComplete.Labels.templateFunction(color, title)).toBe(
- '<li><span class="dropdown-label-box" style="background: #123456"></span> &dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ '<li><span class="dropdown-label-box" style="background: #123456"></span> &amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</li>',
);
});
});
@@ -868,7 +883,7 @@ describe('GfmAutoComplete', () => {
const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
expect(GfmAutoComplete.Milestones.templateFunction(title, expired)).toBe(
- '<li>&dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ '<li>&amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</li>',
);
});
});
@@ -925,7 +940,9 @@ describe('GfmAutoComplete', () => {
const expectContacts = ({ input, output }) => {
triggerDropdown(input);
- expect(getDropdownItems()).toEqual(output.map((contact) => contact.email));
+ expect(getDropdownItems()).toEqual(
+ output.map((contact) => `${contact.first_name} ${contact.last_name} ${contact.email}`),
+ );
};
describe('with no contacts assigned', () => {
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
index 803df3df37f..3c4f2d624fe 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -2,14 +2,14 @@ import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { mockJobsNodes } from '../../mock_data';
describe('Jobs Table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
- const findStatusBadge = () => wrapper.findComponent(CiBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
const findJobStage = () => wrapper.findByTestId('job-stage-name');
const findJobName = () => wrapper.findByTestId('job-name');
@@ -43,7 +43,7 @@ describe('Jobs Table', () => {
});
it('displays job status', () => {
- expect(findStatusBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
it('displays the job stage and name', () => {
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index b254cce4d72..3815064b3f6 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -4,11 +4,14 @@ import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
+import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
import { member } from '../../mock_data';
Vue.use(Vuex);
+jest.mock('ee_else_ce/members/guest_overage_confirm_action');
describe('RoleDropdown', () => {
let wrapper;
@@ -63,12 +66,21 @@ describe('RoleDropdown', () => {
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ let originalGon;
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ gon.features = { showOverageOnRolePromotion: true };
+ });
+
afterEach(() => {
+ window.gon = originalGon;
wrapper.destroy();
});
describe('when dropdown is open', () => {
beforeEach(() => {
+ guestOverageConfirmAction.mockReturnValue(true);
createComponent();
return findDropdownToggle().trigger('click');
@@ -117,8 +129,12 @@ describe('RoleDropdown', () => {
await getDropdownItemByText('Developer').trigger('click');
expect(findDropdown().props('disabled')).toBe(true);
+ });
- await nextTick();
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
expect(findDropdown().props('disabled')).toBe(false);
});
@@ -148,4 +164,44 @@ describe('RoleDropdown', () => {
expect(findDropdown().props('right')).toBe(false);
});
+
+ describe('guestOverageConfirmAction', () => {
+ const mockConfirmAction = ({ confirmed }) => {
+ guestOverageConfirmAction.mockResolvedValueOnce(confirmed);
+ };
+
+ beforeEach(() => {
+ createComponent();
+
+ findDropdownToggle().trigger('click');
+ });
+
+ afterEach(() => {
+ guestOverageConfirmAction.mockReset();
+ });
+
+ describe('when guestOverageConfirmAction returns true', () => {
+ beforeEach(() => {
+ mockConfirmAction({ confirmed: true });
+
+ getDropdownItemByText('Reporter').trigger('click');
+ });
+
+ it('calls updateMemberRole', () => {
+ expect(actions.updateMemberRole).toHaveBeenCalled();
+ });
+ });
+
+ describe('when guestOverageConfirmAction returns false', () => {
+ beforeEach(() => {
+ mockConfirmAction({ confirmed: false });
+
+ getDropdownItemByText('Reporter').trigger('click');
+ });
+
+ it('does not call updateMemberRole', () => {
+ expect(actions.updateMemberRole).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/frontend/members/guest_overage_confirm_action_spec.js b/spec/frontend/members/guest_overage_confirm_action_spec.js
new file mode 100644
index 00000000000..d7ab54fa13b
--- /dev/null
+++ b/spec/frontend/members/guest_overage_confirm_action_spec.js
@@ -0,0 +1,7 @@
+import { guestOverageConfirmAction } from '~/members/guest_overage_confirm_action';
+
+describe('guestOverageConfirmAction', () => {
+ it('returns true', () => {
+ expect(guestOverageConfirmAction()).toBe(true);
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 740037a5ac8..9359bd9b95f 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -17,7 +17,7 @@ import {
TRACKING_CATEGORIES,
} from '~/pipelines/constants';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
jest.mock('~/pipelines/event_hub');
@@ -50,7 +50,7 @@ describe('Pipelines Table', () => {
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
- const findStatusBadge = () => wrapper.findComponent(CiBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
@@ -97,7 +97,7 @@ describe('Pipelines Table', () => {
describe('status cell', () => {
it('should render a status badge', () => {
- expect(findStatusBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
});
@@ -171,7 +171,7 @@ describe('Pipelines Table', () => {
});
it('tracks status badge click', () => {
- findStatusBadge().vm.$emit('ciStatusBadgeClick');
+ findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.table,
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
index de7c56f239a..b7343bf3a7e 100644
--- a/spec/frontend/repository/commits_service_spec.js
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -4,6 +4,7 @@ import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/co
import httpStatus from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
+import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from './mock_data';
jest.mock('~/flash');
@@ -39,10 +40,11 @@ describe('commits service', () => {
expect(axios.get).toHaveBeenCalledWith(testUrl, { params: { format: 'json', offset } });
});
- it('encodes the path correctly', async () => {
- await requestCommits(1, 'some-project', 'with $peci@l ch@rs/');
+ it('encodes the path and ref', async () => {
+ const encodedUrl = `/some-project/-/refs/${encodedRefWithSpecialCharMock}/logs_tree/with%20%24peci%40l%20ch%40rs%2F`;
+
+ await requestCommits(1, 'some-project', 'with $peci@l ch@rs/', refWithSpecialCharMock);
- const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F';
expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything());
});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index cda47a5b0a5..c1b5f89c37f 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -87,6 +87,8 @@ export const applicationInfoMock = { gitpodEnabled: true };
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';
+export const refWithSpecialCharMock = 'selected-#-ref';
+export const encodedRefWithSpecialCharMock = encodeURIComponent(refWithSpecialCharMock);
export const blobControlsDataMock = {
id: '1234',
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
index 3335059554f..4d0250fffbf 100644
--- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -1,5 +1,6 @@
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data';
const projectRootPath = 'root/Project1';
const currentRef = 'main';
@@ -19,4 +20,10 @@ describe('generateRefDestinationPath', () => {
setWindowLocation(currentPath);
expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
});
+
+ it('encodes the selected ref', () => {
+ const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
+
+ expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result);
+ });
});
diff --git a/spec/frontend/users/profile/components/report_abuse_button_spec.js b/spec/frontend/users/profile/components/report_abuse_button_spec.js
new file mode 100644
index 00000000000..bd39a089473
--- /dev/null
+++ b/spec/frontend/users/profile/components/report_abuse_button_spec.js
@@ -0,0 +1,72 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ReportAbuseButton from '~/users/profile/components/report_abuse_button.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+describe('ReportAbuseButton', () => {
+ let wrapper;
+
+ const ACTION_PATH = '/abuse_reports/add_category';
+ const USER_ID = '1';
+ const REPORTED_FROM_URL = 'http://example.com';
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(ReportAbuseButton, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ formSubmitPath: ACTION_PATH,
+ userId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findReportAbuseButton = () => wrapper.findComponent(GlButton);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
+ it('renders report abuse button', () => {
+ expect(findReportAbuseButton().exists()).toBe(true);
+
+ expect(findReportAbuseButton().props()).toMatchObject({
+ category: 'primary',
+ icon: 'error',
+ });
+
+ expect(findReportAbuseButton().attributes('aria-label')).toBe(
+ wrapper.vm.$options.i18n.reportAbuse,
+ );
+ });
+
+ it('renders abuse category selector with the drawer initially closed', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
+ });
+
+ describe('when button is clicked', () => {
+ beforeEach(async () => {
+ await findReportAbuseButton().vm.$emit('click');
+ });
+
+ it('opens the abuse category selector', () => {
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(true);
+ });
+
+ it('closes the abuse category selector', async () => {
+ await findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
deleted file mode 100644
index 4077564486c..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ /dev/null
@@ -1,163 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
-<div
- class="mr-widget-body media gl-display-flex gl-align-items-center"
->
- <div
- class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
- >
- <div
- class="gl-display-flex gl-m-auto"
- >
- <div
- class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2"
- >
- <div
- class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon"
- >
- <div
- class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto"
- >
- <div
- class="gl-display-flex gl-m-auto gl-translate-y-n50"
- >
- <svg
- aria-label="Scheduled "
- class="gl-display-block gl-icon s12"
- data-qa-selector="status_scheduled_icon"
- data-testid="status-scheduled-icon"
- role="img"
- >
- <use
- href="#status-scheduled"
- />
- </svg>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-w-full"
- >
- <div
- class="media-body gl-display-flex gl-align-items-center"
- >
-
- <h4
- class="gl-mr-3"
- data-testid="statusText"
- >
- Set by to be merged automatically when the pipeline succeeds
- </h4>
-
- <div
- class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3"
- >
- <div
- class="gl-display-flex gl-align-items-flex-start"
- >
- <div
- class="dropdown b-dropdown gl-dropdown gl-display-block gl-md-display-none! btn-group"
- lazy=""
- no-caret=""
- title="Options"
- >
- <!---->
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="dropdown-icon gl-icon s16"
- data-testid="ellipsis_v-icon"
- role="img"
- >
- <use
- href="#ellipsis_v"
- />
- </svg>
-
- <span
- class="gl-dropdown-button-text gl-sr-only"
- >
-
- </span>
-
- <svg
- aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
- data-testid="chevron-down-icon"
- role="img"
- >
- <use
- href="#chevron-down"
- />
- </svg>
- </button>
- <ul
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
- </div>
-
- <button
- class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Cancel auto-merge
-
- </span>
- </button>
- </div>
- </div>
- </div>
-
- <div
- class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
- >
- <button
- class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- title="Collapse merge details"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="chevron-lg-up-icon"
- role="img"
- >
- <use
- href="#chevron-lg-up"
- />
- </svg>
-
- <!---->
- </button>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 5b9f30dfb86..fef5fee5f19 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -128,14 +128,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
});
describe('template', () => {
- it('should have correct elements', () => {
- factory({
- ...defaultMrProps(),
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('should disable cancel auto merge button when the action is in progress', async () => {
factory({
...defaultMrProps(),
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index 07cbfe1e79b..4f24ec2d015 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -79,7 +79,7 @@ describe('CI Badge Link Component', () => {
const findIcon = () => wrapper.findComponent(CiIcon);
const createComponent = (propsData) => {
- wrapper = shallowMount(CiBadge, { propsData });
+ wrapper = shallowMount(CiBadgeLink, { propsData });
};
afterEach(() => {
diff --git a/spec/graphql/types/description_version_type_spec.rb b/spec/graphql/types/description_version_type_spec.rb
new file mode 100644
index 00000000000..36bb1af7f7b
--- /dev/null
+++ b/spec/graphql/types/description_version_type_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DescriptionVersion'], feature_category: :team_planning do
+ it { expect(described_class).to have_graphql_field(:id) }
+ it { expect(described_class).to have_graphql_field(:description) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_issuable) }
+end
diff --git a/spec/graphql/types/notes/note_type_spec.rb b/spec/graphql/types/notes/note_type_spec.rb
index cbf7f086dbe..dd364be5ae2 100644
--- a/spec/graphql/types/notes/note_type_spec.rb
+++ b/spec/graphql/types/notes/note_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Note'] do
+RSpec.describe GitlabSchema.types['Note'], feature_category: :team_planning do
it 'exposes the expected fields' do
expected_fields = %i[
author
@@ -24,6 +24,7 @@ RSpec.describe GitlabSchema.types['Note'] do
updated_at
user_permissions
url
+ system_note_metadata
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/notes/system_note_metadata_type_spec.rb b/spec/graphql/types/notes/system_note_metadata_type_spec.rb
new file mode 100644
index 00000000000..d243e926ff5
--- /dev/null
+++ b/spec/graphql/types/notes/system_note_metadata_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['SystemNoteMetadata'], feature_category: :team_planning do
+ it { expect(described_class).to have_graphql_field(:id) }
+ it { expect(described_class).to have_graphql_field(:action) }
+ it { expect(described_class).to have_graphql_field(:description_version) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_note) }
+end
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index 4a37e17fb08..adf784360c2 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -134,4 +134,62 @@ RSpec.describe NavHelper do
it { is_expected.to eq(true) }
end
end
+
+ describe '#show_super_sidebar?' do
+ shared_examples '#show_super_sidebar returns false' do
+ it 'returns false' do
+ expect(helper.show_super_sidebar?).to eq(false)
+ end
+ end
+
+ it 'returns false by default' do
+ allow(helper).to receive(:current_user).and_return(nil)
+
+ expect(helper.show_super_sidebar?).to be_falsy
+ end
+
+ context 'when used is signed-in' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ stub_feature_flags(super_sidebar_nav: new_nav_ff)
+ user.update!(use_new_navigation: user_preference)
+ end
+
+ context 'with feature flag off' do
+ let(:new_nav_ff) { false }
+
+ context 'when user has new nav disabled' do
+ let(:user_preference) { false }
+
+ it_behaves_like '#show_super_sidebar returns false'
+ end
+
+ context 'when user has new nav enabled' do
+ let(:user_preference) { true }
+
+ it_behaves_like '#show_super_sidebar returns false'
+ end
+ end
+
+ context 'with feature flag on' do
+ let(:new_nav_ff) { true }
+
+ context 'when user has new nav disabled' do
+ let(:user_preference) { false }
+
+ it_behaves_like '#show_super_sidebar returns false'
+ end
+
+ context 'when user has new nav enabled' do
+ let(:user_preference) { true }
+
+ it 'returns true' do
+ expect(helper.show_super_sidebar?).to eq(true)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 414cbb169b9..67252eed938 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -16,12 +16,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
let(:policy) { nil }
let(:key) { 'some key' }
let(:when_config) { nil }
+ let(:unprotect) { false }
let(:config) do
{
key: key,
untracked: true,
- paths: ['some/path/']
+ paths: ['some/path/'],
+ unprotect: unprotect
}.tap do |config|
config[:policy] = policy if policy
config[:when] = when_config if when_config
@@ -31,7 +33,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
describe '#value' do
shared_examples 'hash key value' do
it 'returns hash value' do
- expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success')
+ expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success', unprotect: false)
end
end
@@ -57,6 +59,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
end
+ context 'with option `unprotect` specified' do
+ let(:unprotect) { true }
+
+ it 'returns true' do
+ expect(entry.value).to match(a_hash_including(unprotect: true))
+ end
+ end
+
context 'with `policy`' do
where(:policy, :result) do
'pull-push' | 'pull-push'
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 69c0d05dcdd..c1b9bd58d98 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -631,7 +631,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho
it 'overrides default config' do
expect(entry[:image].value).to eq(name: 'some_image')
- expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success'])
+ expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success', unprotect: false])
end
end
@@ -646,7 +646,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho
it 'uses config from default entry' do
expect(entry[:image].value).to eq 'specified'
- expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success'])
+ expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success', unprotect: false])
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index c40589104cd..9722609aef6 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -127,7 +127,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success',
+ unprotect: false }],
job_variables: {},
root_variables_inheritance: true,
ignore: false,
@@ -142,7 +143,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success',
+ unprotect: false }],
job_variables: {},
root_variables_inheritance: true,
ignore: false,
@@ -158,7 +160,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" },
image: { name: "image:1.0" },
services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }],
- cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }],
+ cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success',
+ unprotect: false }],
only: { refs: %w(branches tags) },
job_variables: { 'VAR' => { value: 'job' } },
root_variables_inheritance: true,
@@ -206,7 +209,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', unprotect: false }],
job_variables: {},
root_variables_inheritance: true,
ignore: false,
@@ -219,7 +222,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
image: { name: 'image:1.0' },
services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }],
stage: 'test',
- cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }],
+ cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', unprotect: false }],
job_variables: { 'VAR' => { value: 'job' } },
root_variables_inheritance: true,
ignore: false,
@@ -274,7 +277,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
describe '#cache_value' do
it 'returns correct cache definition' do
- expect(root.cache_value).to eq([key: 'a', policy: 'pull-push', when: 'on_success'])
+ expect(root.cache_value).to eq([key: 'a', policy: 'pull-push', when: 'on_success', unprotect: false])
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
index fb8020bf43e..c264ea3bece 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
@@ -212,6 +212,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
paths: ['vendor/ruby'],
untracked: true,
policy: 'push',
+ unprotect: true,
when: 'on_success'
}
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index ae98d2e0cad..41c51340eb6 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1699,7 +1699,8 @@ module Gitlab
untracked: true,
key: 'key',
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
])
end
@@ -1723,7 +1724,8 @@ module Gitlab
untracked: true,
key: { files: ['file'] },
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
])
end
@@ -1749,14 +1751,16 @@ module Gitlab
untracked: true,
key: 'keya',
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
},
{
paths: ['logs/', 'binaries/'],
untracked: true,
key: 'key',
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
}
]
)
@@ -1783,7 +1787,8 @@ module Gitlab
untracked: true,
key: { files: ['file'] },
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
])
end
@@ -1808,7 +1813,8 @@ module Gitlab
untracked: true,
key: { files: ['file'], prefix: 'prefix' },
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
])
end
@@ -1831,7 +1837,8 @@ module Gitlab
untracked: false,
key: 'local',
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
])
end
end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 3871b18fdd5..32cf3b4c505 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AbuseReport do
+RSpec.describe AbuseReport, feature_category: :insider_threat do
let_it_be(:report, reload: true) { create(:abuse_report) }
let_it_be(:user, reload: true) { create(:admin) }
@@ -24,6 +24,7 @@ RSpec.describe AbuseReport do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:message) }
it { is_expected.to validate_uniqueness_of(:user_id).with_message('has already been reported') }
+ it { is_expected.to validate_presence_of(:category) }
end
describe '#remove_user' do
@@ -54,4 +55,21 @@ RSpec.describe AbuseReport do
report.notify
end
end
+
+ describe 'enums' do
+ let(:categories) do
+ {
+ spam: 1,
+ offensive: 2,
+ phishing: 3,
+ crypto: 4,
+ credentials: 5,
+ copyright: 6,
+ malware: 7,
+ other: 8
+ }
+ end
+
+ it { is_expected.to define_enum_for(:category).with_values(**categories) }
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a9b322a1a16..534875a9bba 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1136,6 +1136,19 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it do
is_expected.to all(a_hash_including(key: a_string_matching(/-protected$/)))
end
+
+ context 'and the cache has the `unprotect` option' do
+ let(:options) do
+ { cache: [
+ { key: "key", paths: ["public"], policy: "pull-push", unprotect: true },
+ { key: "key2", paths: ["public"], policy: "pull-push", unprotect: true }
+ ] }
+ end
+
+ it do
+ is_expected.to all(a_hash_including(key: a_string_matching(/-non_protected$/)))
+ end
+ end
end
context 'when pipeline is not on a protected ref' do
diff --git a/spec/requests/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb
index 510855d95e0..71ecf8444bf 100644
--- a/spec/requests/abuse_reports_controller_spec.rb
+++ b/spec/requests/abuse_reports_controller_spec.rb
@@ -40,6 +40,80 @@ RSpec.describe AbuseReportsController, feature_category: :users do
end
end
+ describe 'POST add_category', :aggregate_failures do
+ subject(:request) { post add_category_abuse_reports_path, params: request_params }
+
+ let(:abuse_category) { 'spam' }
+
+ context 'when user is reported for abuse' do
+ let(:ref_url) { 'http://example.com' }
+ let(:request_params) { { user_id: user.id, abuse_report: { category: abuse_category }, ref_url: ref_url } }
+
+ it 'renders new template' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:new)
+ end
+
+ it 'sets the instance variables' do
+ subject
+
+ expect(assigns(:abuse_report)).to be_kind_of(AbuseReport)
+ expect(assigns(:abuse_report)).to have_attributes(
+ user_id: user.id,
+ category: abuse_category
+ )
+ expect(assigns(:ref_url)).to eq(ref_url)
+ end
+ end
+
+ context 'when abuse_report is missing in params' do
+ let(:request_params) { { user_id: user.id } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ActionController::ParameterMissing)
+ end
+ end
+
+ context 'when user_id is missing in params' do
+ let(:request_params) { { abuse_report: { category: abuse_category } } }
+
+ it 'redirects the reporter to root_path' do
+ subject
+
+ expect(response).to redirect_to root_path
+ expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.'))
+ end
+ end
+
+ context 'when the user has already been deleted' do
+ let(:request_params) { { user_id: user.id, abuse_report: { category: abuse_category } } }
+
+ it 'redirects the reporter to root_path' do
+ user.destroy!
+
+ subject
+
+ expect(response).to redirect_to root_path
+ expect(flash[:alert]).to eq(_('Cannot create the abuse report. The user has been deleted.'))
+ end
+ end
+
+ context 'when the user has already been blocked' do
+ let(:request_params) { { user_id: user.id, abuse_report: { category: abuse_category } } }
+
+ it 'redirects the reporter to the user\'s profile' do
+ user.block
+
+ subject
+
+ expect(response).to redirect_to user
+ expect(flash[:alert]).to eq(_('Cannot create the abuse report. This user has been blocked.'))
+ end
+ end
+ end
+
describe 'POST create' do
context 'with valid attributes' do
it 'saves the abuse report' do
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index c27e165b39b..5258d26be17 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -5,7 +5,17 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
include HttpBasicAuthHelpers
include WorkhorseHelpers
- include_context 'Debian repository shared context', :project, true do
+ include_context 'Debian repository shared context', :project, false do
+ shared_examples 'accept GET request on private project with access to package registry for everyone' do
+ include_context 'Debian repository access', :private, :anonymous, :basic do
+ before do
+ container.project_feature.reload.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it_behaves_like 'Debian packages GET request', :success
+ end
+ end
+
context 'with invalid parameter' do
let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" }
@@ -16,54 +26,63 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/InRelease' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
- describe 'GET projects/:id/packages/debian/dists/*distribution/source/Sources' do
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/Sources' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
- describe 'GET projects/:id/packages/debian/dists/*distribution/source/by-hash/SHA256/:file_sha256' do
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do
@@ -90,6 +109,10 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
end
end
end
+
+ it_behaves_like 'accept GET request on private project with access to package registry for everyone' do
+ let(:file_name) { 'sample_1.2.3~alpha2.dsc' }
+ end
end
describe 'PUT projects/:id/packages/debian/:file_name' do
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index a59da706a8a..de35c943749 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -263,7 +263,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
GRAPHQL
end
- before do
+ before_all do
create_notes(item1, "some note1")
create_notes(item2, "some note2")
end
diff --git a/spec/services/ci/create_pipeline_service/cache_spec.rb b/spec/services/ci/create_pipeline_service/cache_spec.rb
index 82c3d374636..f9640f99031 100644
--- a/spec/services/ci/create_pipeline_service/cache_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cache_spec.rb
@@ -37,6 +37,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
paths: ['logs/', 'binaries/'],
policy: 'pull-push',
untracked: true,
+ unprotect: false,
when: 'on_success'
}
@@ -69,7 +70,8 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
key: /[a-f0-9]{40}/,
paths: ['logs/'],
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
}
expect(pipeline).to be_persisted
@@ -85,7 +87,8 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
key: /default/,
paths: ['logs/'],
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
}
expect(pipeline).to be_persisted
@@ -118,7 +121,8 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
key: /\$ENV_VAR-[a-f0-9]{40}/,
paths: ['logs/'],
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
}
expect(pipeline).to be_persisted
@@ -134,7 +138,8 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
key: /\$ENV_VAR-default/,
paths: ['logs/'],
policy: 'pull-push',
- when: 'on_success'
+ when: 'on_success',
+ unprotect: false
}
expect(pipeline).to be_persisted
diff --git a/workhorse/Makefile b/workhorse/Makefile
index a0412f5e2e1..4236a1a0d8e 100644
--- a/workhorse/Makefile
+++ b/workhorse/Makefile
@@ -144,7 +144,7 @@ testdata/scratch:
mkdir -p testdata/scratch
.PHONY: verify
-verify: lint vet detect-context detect-assert check-formatting staticcheck deps-check
+verify: lint vet detect-context detect-assert detect-external-tests check-formatting staticcheck deps-check
.PHONY: lint
lint:
@@ -167,6 +167,11 @@ detect-assert:
$(call message,Verify: $@)
_support/detect-assert.sh
+.PHONY: detect-external-tests
+detect-external-tests:
+ $(call message,Verify: $@)
+ _support/detect-external-tests.sh
+
.PHONY: check-formatting
check-formatting: install-goimports
$(call message,Verify: $@)
diff --git a/workhorse/_support/detect-external-tests.sh b/workhorse/_support/detect-external-tests.sh
new file mode 100755
index 00000000000..865bd1447e1
--- /dev/null
+++ b/workhorse/_support/detect-external-tests.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+go list -f '{{join .XTestGoFiles "\n"}}' ./... | awk '
+ { print }
+ END {
+ if(NR>0) {
+ print "Please avoid using external test packages (package foobar_test) in Workhorse."
+ print "See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107373."
+ exit(1)
+ }
+ }
+'
diff --git a/workhorse/internal/upload/destination/destination_test.go b/workhorse/internal/upload/destination/destination_test.go
index 97645be168f..b355935e347 100644
--- a/workhorse/internal/upload/destination/destination_test.go
+++ b/workhorse/internal/upload/destination/destination_test.go
@@ -1,4 +1,4 @@
-package destination_test
+package destination
import (
"context"
@@ -17,12 +17,11 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
)
func testDeadline() time.Time {
- return time.Now().Add(destination.DefaultObjectStoreTimeout)
+ return time.Now().Add(DefaultObjectStoreTimeout)
}
func requireFileGetsRemovedAsync(t *testing.T, filePath string) {
@@ -44,10 +43,10 @@ func TestUploadWrongSize(t *testing.T) {
tmpFolder := t.TempDir()
- opts := &destination.UploadOpts{LocalTempPath: tmpFolder}
- fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize+1, "upload", opts)
+ opts := &UploadOpts{LocalTempPath: tmpFolder}
+ fh, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize+1, "upload", opts)
require.Error(t, err)
- _, isSizeError := err.(destination.SizeError)
+ _, isSizeError := err.(SizeError)
require.True(t, isSizeError, "Should fail with SizeError")
require.Nil(t, fh)
}
@@ -58,10 +57,10 @@ func TestUploadWithKnownSizeExceedLimit(t *testing.T) {
tmpFolder := t.TempDir()
- opts := &destination.UploadOpts{LocalTempPath: tmpFolder, MaximumSize: test.ObjectSize - 1}
- fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", opts)
+ opts := &UploadOpts{LocalTempPath: tmpFolder, MaximumSize: test.ObjectSize - 1}
+ fh, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", opts)
require.Error(t, err)
- _, isSizeError := err.(destination.SizeError)
+ _, isSizeError := err.(SizeError)
require.True(t, isSizeError, "Should fail with SizeError")
require.Nil(t, fh)
}
@@ -72,9 +71,9 @@ func TestUploadWithUnknownSizeExceedLimit(t *testing.T) {
tmpFolder := t.TempDir()
- opts := &destination.UploadOpts{LocalTempPath: tmpFolder, MaximumSize: test.ObjectSize - 1}
- fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), -1, "upload", opts)
- require.Equal(t, err, destination.ErrEntityTooLarge)
+ opts := &UploadOpts{LocalTempPath: tmpFolder, MaximumSize: test.ObjectSize - 1}
+ fh, err := Upload(ctx, strings.NewReader(test.ObjectContent), -1, "upload", opts)
+ require.Equal(t, err, ErrEntityTooLarge)
require.Nil(t, fh)
}
@@ -94,7 +93,7 @@ func TestUploadWrongETag(t *testing.T) {
objectURL := ts.URL + test.ObjectPath
- opts := &destination.UploadOpts{
+ opts := &UploadOpts{
RemoteID: "test-file",
RemoteURL: objectURL,
PresignedPut: objectURL + "?Signature=ASignature",
@@ -110,7 +109,7 @@ func TestUploadWrongETag(t *testing.T) {
osStub.InitiateMultipartUpload(test.ObjectPath)
}
ctx, cancel := context.WithCancel(context.Background())
- fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", opts)
+ fh, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", opts)
require.Nil(t, fh)
require.Error(t, err)
require.Equal(t, 1, osStub.PutsCnt(), "File not uploaded")
@@ -146,7 +145,7 @@ func TestUpload(t *testing.T) {
for _, spec := range tests {
t.Run(spec.name, func(t *testing.T) {
- var opts destination.UploadOpts
+ var opts UploadOpts
var expectedDeletes, expectedPuts int
osStub, ts := test.StartObjectStore()
@@ -187,7 +186,7 @@ func TestUpload(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
+ fh, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
require.NoError(t, err)
require.NotNil(t, fh)
@@ -206,7 +205,7 @@ func TestUpload(t *testing.T) {
}
require.Equal(t, test.ObjectSize, fh.Size)
- if destination.FIPSEnabled() {
+ if FIPSEnabled() {
require.Empty(t, fh.MD5())
} else {
require.Equal(t, test.ObjectMD5, fh.MD5())
@@ -255,7 +254,7 @@ func TestUploadWithS3WorkhorseClient(t *testing.T) {
name: "unknown object size with limit",
objectSize: -1,
maxSize: test.ObjectSize - 1,
- expectedErr: destination.ErrEntityTooLarge,
+ expectedErr: ErrEntityTooLarge,
},
}
@@ -269,12 +268,12 @@ func TestUploadWithS3WorkhorseClient(t *testing.T) {
defer cancel()
remoteObject := "tmp/test-file/1"
- opts := destination.UploadOpts{
+ opts := UploadOpts{
RemoteID: "test-file",
Deadline: testDeadline(),
UseWorkhorseClient: true,
RemoteTempObjectID: remoteObject,
- ObjectStorageConfig: destination.ObjectStorageConfig{
+ ObjectStorageConfig: ObjectStorageConfig{
Provider: "AWS",
S3Credentials: s3Creds,
S3Config: s3Config,
@@ -282,7 +281,7 @@ func TestUploadWithS3WorkhorseClient(t *testing.T) {
MaximumSize: tc.maxSize,
}
- _, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), tc.objectSize, "upload", &opts)
+ _, err := Upload(ctx, strings.NewReader(test.ObjectContent), tc.objectSize, "upload", &opts)
if tc.expectedErr == nil {
require.NoError(t, err)
@@ -302,19 +301,19 @@ func TestUploadWithAzureWorkhorseClient(t *testing.T) {
defer cancel()
remoteObject := "tmp/test-file/1"
- opts := destination.UploadOpts{
+ opts := UploadOpts{
RemoteID: "test-file",
Deadline: testDeadline(),
UseWorkhorseClient: true,
RemoteTempObjectID: remoteObject,
- ObjectStorageConfig: destination.ObjectStorageConfig{
+ ObjectStorageConfig: ObjectStorageConfig{
Provider: "AzureRM",
URLMux: mux,
GoCloudConfig: config.GoCloudConfig{URL: "azblob://test-container"},
},
}
- _, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
+ _, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
require.NoError(t, err)
test.GoCloudObjectExists(t, bucketDir, remoteObject)
@@ -327,19 +326,19 @@ func TestUploadWithUnknownGoCloudScheme(t *testing.T) {
mux := new(blob.URLMux)
remoteObject := "tmp/test-file/1"
- opts := destination.UploadOpts{
+ opts := UploadOpts{
RemoteID: "test-file",
Deadline: testDeadline(),
UseWorkhorseClient: true,
RemoteTempObjectID: remoteObject,
- ObjectStorageConfig: destination.ObjectStorageConfig{
+ ObjectStorageConfig: ObjectStorageConfig{
Provider: "SomeCloud",
URLMux: mux,
GoCloudConfig: config.GoCloudConfig{URL: "foo://test-container"},
},
}
- _, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
+ _, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
require.Error(t, err)
}
@@ -351,7 +350,7 @@ func TestUploadMultipartInBodyFailure(t *testing.T) {
// this is the only way to get an in-body failure from our ObjectStoreStub
objectPath := "/bucket-but-no-object-key"
objectURL := ts.URL + objectPath
- opts := destination.UploadOpts{
+ opts := UploadOpts{
RemoteID: "test-file",
RemoteURL: objectURL,
PartSize: test.ObjectSize,
@@ -365,7 +364,7 @@ func TestUploadMultipartInBodyFailure(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- fh, err := destination.Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
+ fh, err := Upload(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, "upload", &opts)
require.Nil(t, fh)
require.Error(t, err)
require.EqualError(t, err, test.MultipartUploadInternalError().Error())
@@ -405,20 +404,20 @@ func TestUploadRemoteFileWithLimit(t *testing.T) {
testData: test.ObjectContent,
objectSize: -1,
maxSize: test.ObjectSize - 1,
- expectedErr: destination.ErrEntityTooLarge,
+ expectedErr: ErrEntityTooLarge,
},
{
name: "large object with unknown size with limit",
testData: string(make([]byte, 20000)),
objectSize: -1,
maxSize: 19000,
- expectedErr: destination.ErrEntityTooLarge,
+ expectedErr: ErrEntityTooLarge,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- var opts destination.UploadOpts
+ var opts UploadOpts
for _, remoteType := range remoteTypes {
osStub, ts := test.StartObjectStore()
@@ -454,7 +453,7 @@ func TestUploadRemoteFileWithLimit(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- fh, err := destination.Upload(ctx, strings.NewReader(tc.testData), tc.objectSize, "upload", &opts)
+ fh, err := Upload(ctx, strings.NewReader(tc.testData), tc.objectSize, "upload", &opts)
if tc.expectedErr == nil {
require.NoError(t, err)
@@ -468,7 +467,7 @@ func TestUploadRemoteFileWithLimit(t *testing.T) {
}
}
-func checkFileHandlerWithFields(t *testing.T, fh *destination.FileHandler, fields map[string]string, prefix string) {
+func checkFileHandlerWithFields(t *testing.T, fh *FileHandler, fields map[string]string, prefix string) {
key := func(field string) string {
if prefix == "" {
return field
@@ -482,7 +481,7 @@ func checkFileHandlerWithFields(t *testing.T, fh *destination.FileHandler, field
require.Equal(t, fh.RemoteURL, fields[key("remote_url")])
require.Equal(t, fh.RemoteID, fields[key("remote_id")])
require.Equal(t, strconv.FormatInt(test.ObjectSize, 10), fields[key("size")])
- if destination.FIPSEnabled() {
+ if FIPSEnabled() {
require.Empty(t, fields[key("md5")])
} else {
require.Equal(t, test.ObjectMD5, fields[key("md5")])
diff --git a/workhorse/internal/upload/destination/objectstore/gocloud_object_test.go b/workhorse/internal/upload/destination/objectstore/gocloud_object_test.go
index 55d886087be..5a6a4b90b34 100644
--- a/workhorse/internal/upload/destination/objectstore/gocloud_object_test.go
+++ b/workhorse/internal/upload/destination/objectstore/gocloud_object_test.go
@@ -1,4 +1,4 @@
-package objectstore_test
+package objectstore
import (
"context"
@@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
)
@@ -22,8 +21,8 @@ func TestGoCloudObjectUpload(t *testing.T) {
objectName := "test.png"
testURL := "azuretest://azure.example.com/test-container"
- p := &objectstore.GoCloudObjectParams{Ctx: ctx, Mux: mux, BucketURL: testURL, ObjectName: objectName}
- object, err := objectstore.NewGoCloudObject(p)
+ p := &GoCloudObjectParams{Ctx: ctx, Mux: mux, BucketURL: testURL, ObjectName: objectName}
+ object, err := NewGoCloudObject(p)
require.NotNil(t, object)
require.NoError(t, err)
@@ -48,8 +47,8 @@ func TestGoCloudObjectUpload(t *testing.T) {
if exists {
return fmt.Errorf("file %s is still present", objectName)
- } else {
- return nil
}
+
+ return nil
})
}
diff --git a/workhorse/internal/upload/destination/objectstore/multipart.go b/workhorse/internal/upload/destination/objectstore/multipart.go
index df336d2d901..900ca040dad 100644
--- a/workhorse/internal/upload/destination/objectstore/multipart.go
+++ b/workhorse/internal/upload/destination/objectstore/multipart.go
@@ -11,6 +11,8 @@ import (
"os"
"gitlab.com/gitlab-org/labkit/mask"
+
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/s3api"
)
// ErrNotEnoughParts will be used when writing more than size * len(partURLs)
@@ -51,7 +53,7 @@ func NewMultipart(partURLs []string, completeURL, abortURL, deleteURL string, pu
}
func (m *Multipart) Upload(ctx context.Context, r io.Reader) error {
- cmu := &CompleteMultipartUpload{}
+ cmu := &s3api.CompleteMultipartUpload{}
for i, partURL := range m.PartURLs {
src := io.LimitReader(r, m.partSize)
part, err := m.readAndUploadOnePart(ctx, partURL, m.PutHeaders, src, i+1)
@@ -91,7 +93,7 @@ func (m *Multipart) Delete() {
deleteURL(m.DeleteURL)
}
-func (m *Multipart) readAndUploadOnePart(ctx context.Context, partURL string, putHeaders map[string]string, src io.Reader, partNumber int) (*completeMultipartUploadPart, error) {
+func (m *Multipart) readAndUploadOnePart(ctx context.Context, partURL string, putHeaders map[string]string, src io.Reader, partNumber int) (*s3api.CompleteMultipartUploadPart, error) {
file, err := os.CreateTemp("", "part-buffer")
if err != nil {
return nil, fmt.Errorf("create temporary buffer file: %v", err)
@@ -118,7 +120,7 @@ func (m *Multipart) readAndUploadOnePart(ctx context.Context, partURL string, pu
if err != nil {
return nil, fmt.Errorf("upload part %d: %v", partNumber, err)
}
- return &completeMultipartUploadPart{PartNumber: partNumber, ETag: etag}, nil
+ return &s3api.CompleteMultipartUploadPart{PartNumber: partNumber, ETag: etag}, nil
}
func (m *Multipart) uploadPart(ctx context.Context, url string, headers map[string]string, body io.Reader, size int64) (string, error) {
@@ -142,7 +144,7 @@ func (m *Multipart) uploadPart(ctx context.Context, url string, headers map[stri
return part.ETag(), nil
}
-func (m *Multipart) complete(ctx context.Context, cmu *CompleteMultipartUpload) error {
+func (m *Multipart) complete(ctx context.Context, cmu *s3api.CompleteMultipartUpload) error {
body, err := xml.Marshal(cmu)
if err != nil {
return fmt.Errorf("marshal CompleteMultipartUpload request: %v", err)
diff --git a/workhorse/internal/upload/destination/objectstore/multipart_test.go b/workhorse/internal/upload/destination/objectstore/multipart_test.go
index 2a5161e42e7..00244a5c50b 100644
--- a/workhorse/internal/upload/destination/objectstore/multipart_test.go
+++ b/workhorse/internal/upload/destination/objectstore/multipart_test.go
@@ -1,4 +1,4 @@
-package objectstore_test
+package objectstore
import (
"context"
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
)
@@ -48,7 +47,7 @@ func TestMultipartUploadWithUpcaseETags(t *testing.T) {
deadline := time.Now().Add(testTimeout)
- m, err := objectstore.NewMultipart(
+ m, err := NewMultipart(
[]string{ts.URL}, // a single presigned part URL
ts.URL, // the complete multipart upload URL
"", // no abort
diff --git a/workhorse/internal/upload/destination/objectstore/object_test.go b/workhorse/internal/upload/destination/objectstore/object_test.go
index 24117891b6d..2b94cd9e3b1 100644
--- a/workhorse/internal/upload/destination/objectstore/object_test.go
+++ b/workhorse/internal/upload/destination/objectstore/object_test.go
@@ -1,4 +1,4 @@
-package objectstore_test
+package objectstore
import (
"context"
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
)
@@ -35,7 +34,7 @@ func testObjectUploadNoErrors(t *testing.T, startObjectStore osFactory, useDelet
defer cancel()
deadline := time.Now().Add(testTimeout)
- object, err := objectstore.NewObject(objectURL, deleteURL, putHeaders, test.ObjectSize)
+ object, err := NewObject(objectURL, deleteURL, putHeaders, test.ObjectSize)
require.NoError(t, err)
// copy data
@@ -97,12 +96,12 @@ func TestObjectUpload404(t *testing.T) {
deadline := time.Now().Add(testTimeout)
objectURL := ts.URL + test.ObjectPath
- object, err := objectstore.NewObject(objectURL, "", map[string]string{}, test.ObjectSize)
+ object, err := NewObject(objectURL, "", map[string]string{}, test.ObjectSize)
require.NoError(t, err)
_, err = object.Consume(ctx, strings.NewReader(test.ObjectContent), deadline)
require.Error(t, err)
- _, isStatusCodeError := err.(objectstore.StatusCodeError)
+ _, isStatusCodeError := err.(StatusCodeError)
require.True(t, isStatusCodeError, "Should fail with StatusCodeError")
require.Contains(t, err.Error(), "404")
}
@@ -140,7 +139,7 @@ func TestObjectUploadBrokenConnection(t *testing.T) {
deadline := time.Now().Add(testTimeout)
objectURL := ts.URL + test.ObjectPath
- object, err := objectstore.NewObject(objectURL, "", map[string]string{}, -1)
+ object, err := NewObject(objectURL, "", map[string]string{}, -1)
require.NoError(t, err)
_, copyErr := object.Consume(ctx, &endlessReader{}, deadline)
diff --git a/workhorse/internal/upload/destination/objectstore/s3_complete_multipart_api.go b/workhorse/internal/upload/destination/objectstore/s3_complete_multipart_api.go
index b84f5757f49..02799d0b9b0 100644
--- a/workhorse/internal/upload/destination/objectstore/s3_complete_multipart_api.go
+++ b/workhorse/internal/upload/destination/objectstore/s3_complete_multipart_api.go
@@ -2,45 +2,15 @@ package objectstore
import (
"encoding/xml"
- "fmt"
-)
-
-// CompleteMultipartUpload is the S3 CompleteMultipartUpload body
-type CompleteMultipartUpload struct {
- Part []*completeMultipartUploadPart
-}
-type completeMultipartUploadPart struct {
- PartNumber int
- ETag string
-}
-
-// CompleteMultipartUploadResult is the S3 answer to CompleteMultipartUpload request
-type CompleteMultipartUploadResult struct {
- Location string
- Bucket string
- Key string
- ETag string
-}
-
-// CompleteMultipartUploadError is the in-body error structure
-// https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html#mpUploadComplete-examples
-// the answer contains other fields we are not using
-type CompleteMultipartUploadError struct {
- XMLName xml.Name `xml:"Error"`
- Code string
- Message string
-}
-
-func (c *CompleteMultipartUploadError) Error() string {
- return fmt.Sprintf("CompleteMultipartUpload remote error %q: %s", c.Code, c.Message)
-}
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/s3api"
+)
// compoundCompleteMultipartUploadResult holds both CompleteMultipartUploadResult and CompleteMultipartUploadError
// this allow us to deserialize the response body where the root element can either be Error orCompleteMultipartUploadResult
type compoundCompleteMultipartUploadResult struct {
- *CompleteMultipartUploadResult
- *CompleteMultipartUploadError
+ *s3api.CompleteMultipartUploadResult
+ *s3api.CompleteMultipartUploadError
// XMLName this overrides CompleteMultipartUploadError.XMLName tags
XMLName xml.Name
diff --git a/workhorse/internal/upload/destination/objectstore/s3_object_test.go b/workhorse/internal/upload/destination/objectstore/s3_object_test.go
index 0ed14a2e844..c99712d18ad 100644
--- a/workhorse/internal/upload/destination/objectstore/s3_object_test.go
+++ b/workhorse/internal/upload/destination/objectstore/s3_object_test.go
@@ -1,4 +1,4 @@
-package objectstore_test
+package objectstore
import (
"context"
@@ -17,7 +17,6 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
)
@@ -50,7 +49,7 @@ func TestS3ObjectUpload(t *testing.T) {
objectName := filepath.Join(tmpDir, "s3-test-data")
ctx, cancel := context.WithCancel(context.Background())
- object, err := objectstore.NewS3Object(objectName, creds, config)
+ object, err := NewS3Object(objectName, creds, config)
require.NoError(t, err)
// copy data
@@ -107,7 +106,7 @@ func TestConcurrentS3ObjectUpload(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- object, err := objectstore.NewS3Object(objectName, creds, config)
+ object, err := NewS3Object(objectName, creds, config)
require.NoError(t, err)
// copy data
@@ -134,7 +133,7 @@ func TestS3ObjectUploadCancel(t *testing.T) {
objectName := filepath.Join(tmpDir, "s3-test-data")
- object, err := objectstore.NewS3Object(objectName, creds, config)
+ object, err := NewS3Object(objectName, creds, config)
require.NoError(t, err)
@@ -155,7 +154,7 @@ func TestS3ObjectUploadLimitReached(t *testing.T) {
tmpDir := t.TempDir()
objectName := filepath.Join(tmpDir, "s3-test-data")
- object, err := objectstore.NewS3Object(objectName, creds, config)
+ object, err := NewS3Object(objectName, creds, config)
require.NoError(t, err)
_, err = object.Consume(context.Background(), &failedReader{}, deadline)
diff --git a/workhorse/internal/upload/destination/objectstore/s3api/s3api.go b/workhorse/internal/upload/destination/objectstore/s3api/s3api.go
new file mode 100644
index 00000000000..49ab9347911
--- /dev/null
+++ b/workhorse/internal/upload/destination/objectstore/s3api/s3api.go
@@ -0,0 +1,37 @@
+package s3api
+
+import (
+ "encoding/xml"
+ "fmt"
+)
+
+// CompleteMultipartUploadError is the in-body error structure
+// https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html#mpUploadComplete-examples
+// the answer contains other fields we are not using
+type CompleteMultipartUploadError struct {
+ XMLName xml.Name `xml:"Error"`
+ Code string
+ Message string
+}
+
+func (c *CompleteMultipartUploadError) Error() string {
+ return fmt.Sprintf("CompleteMultipartUpload remote error %q: %s", c.Code, c.Message)
+}
+
+// CompleteMultipartUploadResult is the S3 answer to CompleteMultipartUpload request
+type CompleteMultipartUploadResult struct {
+ Location string
+ Bucket string
+ Key string
+ ETag string
+}
+
+// CompleteMultipartUpload is the S3 CompleteMultipartUpload body
+type CompleteMultipartUpload struct {
+ Part []*CompleteMultipartUploadPart
+}
+
+type CompleteMultipartUploadPart struct {
+ PartNumber int
+ ETag string
+}
diff --git a/workhorse/internal/upload/destination/objectstore/test/objectstore_stub.go b/workhorse/internal/upload/destination/objectstore/test/objectstore_stub.go
index 1a380bd5083..8fbb746d6ce 100644
--- a/workhorse/internal/upload/destination/objectstore/test/objectstore_stub.go
+++ b/workhorse/internal/upload/destination/objectstore/test/objectstore_stub.go
@@ -12,7 +12,7 @@ import (
"strings"
"sync"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore"
+ "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/s3api"
)
type partsEtagMap map[int]string
@@ -190,8 +190,8 @@ func (o *ObjectstoreStub) putObject(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
-func MultipartUploadInternalError() *objectstore.CompleteMultipartUploadError {
- return &objectstore.CompleteMultipartUploadError{Code: "InternalError", Message: "malformed object path"}
+func MultipartUploadInternalError() *s3api.CompleteMultipartUploadError {
+ return &s3api.CompleteMultipartUploadError{Code: "InternalError", Message: "malformed object path"}
}
func (o *ObjectstoreStub) completeMultipartUpload(w http.ResponseWriter, r *http.Request) {
@@ -212,7 +212,7 @@ func (o *ObjectstoreStub) completeMultipartUpload(w http.ResponseWriter, r *http
return
}
- var msg objectstore.CompleteMultipartUpload
+ var msg s3api.CompleteMultipartUpload
err = xml.Unmarshal(buf, &msg)
if err != nil {
http.Error(w, err.Error(), 400)
@@ -245,7 +245,7 @@ func (o *ObjectstoreStub) completeMultipartUpload(w http.ResponseWriter, r *http
bucket := split[0]
key := split[1]
- answer := objectstore.CompleteMultipartUploadResult{
+ answer := s3api.CompleteMultipartUploadResult{
Location: r.URL.String(),
Bucket: bucket,
Key: key,
diff --git a/workhorse/internal/upload/destination/upload_opts_test.go b/workhorse/internal/upload/destination/upload_opts_test.go
index fd9e56db194..a420e842e4d 100644
--- a/workhorse/internal/upload/destination/upload_opts_test.go
+++ b/workhorse/internal/upload/destination/upload_opts_test.go
@@ -1,4 +1,4 @@
-package destination_test
+package destination
import (
"testing"
@@ -8,7 +8,6 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
)
@@ -43,7 +42,7 @@ func TestUploadOptsLocalAndRemote(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- opts := destination.UploadOpts{
+ opts := UploadOpts{
LocalTempPath: test.localTempPath,
PresignedPut: test.presignedPut,
PartSize: test.partSize,
@@ -106,7 +105,7 @@ func TestGetOpts(t *testing.T) {
},
}
deadline := time.Now().Add(time.Duration(apiResponse.RemoteObject.Timeout) * time.Second)
- opts, err := destination.GetOpts(apiResponse)
+ opts, err := GetOpts(apiResponse)
require.NoError(t, err)
require.Equal(t, apiResponse.TempPath, opts.LocalTempPath)
@@ -155,22 +154,22 @@ func TestGetOptsFail(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
- _, err := destination.GetOpts(tc.in)
+ _, err := GetOpts(tc.in)
require.Error(t, err, "expect input to be rejected")
})
}
}
func TestGetOptsDefaultTimeout(t *testing.T) {
- deadline := time.Now().Add(destination.DefaultObjectStoreTimeout)
- opts, err := destination.GetOpts(&api.Response{TempPath: "/foo/bar"})
+ deadline := time.Now().Add(DefaultObjectStoreTimeout)
+ opts, err := GetOpts(&api.Response{TempPath: "/foo/bar"})
require.NoError(t, err)
require.WithinDuration(t, deadline, opts.Deadline, time.Minute)
}
func TestUseWorkhorseClientEnabled(t *testing.T) {
- cfg := destination.ObjectStorageConfig{
+ cfg := ObjectStorageConfig{
Provider: "AWS",
S3Config: config.S3Config{
Bucket: "test-bucket",
@@ -195,7 +194,7 @@ func TestUseWorkhorseClientEnabled(t *testing.T) {
name string
UseWorkhorseClient bool
remoteTempObjectID string
- objectStorageConfig destination.ObjectStorageConfig
+ objectStorageConfig ObjectStorageConfig
expected bool
}{
{
@@ -243,7 +242,7 @@ func TestUseWorkhorseClientEnabled(t *testing.T) {
name: "missing S3 bucket",
UseWorkhorseClient: true,
remoteTempObjectID: "test-object",
- objectStorageConfig: destination.ObjectStorageConfig{
+ objectStorageConfig: ObjectStorageConfig{
Provider: "AWS",
S3Config: config.S3Config{},
},
@@ -269,7 +268,7 @@ func TestUseWorkhorseClientEnabled(t *testing.T) {
},
}
deadline := time.Now().Add(time.Duration(apiResponse.RemoteObject.Timeout) * time.Second)
- opts, err := destination.GetOpts(apiResponse)
+ opts, err := GetOpts(apiResponse)
require.NoError(t, err)
opts.ObjectStorageConfig = test.objectStorageConfig
@@ -322,7 +321,7 @@ func TestGoCloudConfig(t *testing.T) {
},
}
deadline := time.Now().Add(time.Duration(apiResponse.RemoteObject.Timeout) * time.Second)
- opts, err := destination.GetOpts(apiResponse)
+ opts, err := GetOpts(apiResponse)
require.NoError(t, err)
opts.ObjectStorageConfig.URLMux = mux
diff --git a/workhorse/internal/upload/object_storage_preparer_test.go b/workhorse/internal/upload/object_storage_preparer_test.go
index 56de6bbf7d6..b983d68f1ad 100644
--- a/workhorse/internal/upload/object_storage_preparer_test.go
+++ b/workhorse/internal/upload/object_storage_preparer_test.go
@@ -1,4 +1,4 @@
-package upload_test
+package upload
import (
"testing"
@@ -7,7 +7,6 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/upload"
"github.com/stretchr/testify/require"
)
@@ -38,7 +37,7 @@ func TestPrepareWithS3Config(t *testing.T) {
},
}
- p := upload.NewObjectStoragePreparer(c)
+ p := NewObjectStoragePreparer(c)
opts, err := p.Prepare(r)
require.NoError(t, err)
@@ -51,7 +50,7 @@ func TestPrepareWithS3Config(t *testing.T) {
func TestPrepareWithNoConfig(t *testing.T) {
c := config.Config{}
r := &api.Response{RemoteObject: api.RemoteObject{ID: "id"}}
- p := upload.NewObjectStoragePreparer(c)
+ p := NewObjectStoragePreparer(c)
opts, err := p.Prepare(r)
require.NoError(t, err)
diff --git a/workhorse/internal/zipartifacts/metadata_test.go b/workhorse/internal/zipartifacts/metadata_test.go
index e4799ba4a59..6bde56ef27d 100644
--- a/workhorse/internal/zipartifacts/metadata_test.go
+++ b/workhorse/internal/zipartifacts/metadata_test.go
@@ -1,4 +1,4 @@
-package zipartifacts_test
+package zipartifacts
import (
"bytes"
@@ -11,8 +11,6 @@ import (
"github.com/stretchr/testify/require"
zip "gitlab.com/gitlab-org/golang-archive-zip"
-
- "gitlab.com/gitlab-org/gitlab/workhorse/internal/zipartifacts"
)
func generateTestArchive(w io.Writer) error {
@@ -72,10 +70,10 @@ func TestGenerateZipMetadataFromFile(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- archive, err := zipartifacts.OpenArchive(ctx, f.Name())
+ archive, err := OpenArchive(ctx, f.Name())
require.NoError(t, err, "zipartifacts: OpenArchive failed")
- err = zipartifacts.GenerateZipMetadata(&metaBuffer, archive)
+ err = GenerateZipMetadata(&metaBuffer, archive)
require.NoError(t, err, "zipartifacts: GenerateZipMetadata failed")
err = validateMetadata(&metaBuffer)
@@ -96,6 +94,6 @@ func TestErrNotAZip(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- _, err = zipartifacts.OpenArchive(ctx, f.Name())
- require.Equal(t, zipartifacts.ErrorCode[zipartifacts.CodeNotZip], err, "OpenArchive requires a zip file")
+ _, err = OpenArchive(ctx, f.Name())
+ require.Equal(t, ErrorCode[CodeNotZip], err, "OpenArchive requires a zip file")
}
diff --git a/yarn.lock b/yarn.lock
index f158fd9ec26..0184cfbdfc8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1136,10 +1136,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.14.0.tgz#b32a673f08bbd5ba6d406bcf3abb6e7276271b6c"
integrity sha512-mQYtW9eGHY7cF6elsWd76hUF7F3NznyzrJJy5eXBHjvRdYBtyHmwkVmh1Cwr3S/2Sl8fPC+qk41a+Nm6n+1mRQ==
-"@gitlab/ui@52.6.0":
- version "52.6.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-52.6.0.tgz#3a2a8a1640dd92013784281929ecde37518de433"
- integrity sha512-1s2LzOJWEGm0ik3NtjC3IE9wTA/1JlRnlTP/lpVD1HzHeQW9bgbdl1U/dNoZT5NTTcBoP4bdQusD+gdB5K7CfQ==
+"@gitlab/ui@52.6.1":
+ version "52.6.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-52.6.1.tgz#542c32802c63d071a7bcfa737d984fa66c53ea47"
+ integrity sha512-k0R7wLHiI3UoEEMpGK/CFhZNBwsvyxk6OiALYa0yZ5PAiYokEDQE96G3VshG3qc9wTzfIw6KSh6I20SU60tVsQ==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"