Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-07 21:11:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-07 21:11:28 +0300
commit5a71c032e8e0dcb5593df5c257c88487e332e5e5 (patch)
treef18718e7e11658efd66709ee5947e6d054470e4c
parentc3524d16b2ef6a13fd6b398aaeae9af5462f1e33 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/dast.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml36
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml92
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue2
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue2
-rw-r--r--app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue73
-rw-r--r--app/assets/javascripts/pages/projects/new/event_hub.js3
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js37
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue40
-rw-r--r--app/assets/javascripts/repository/components/blob_edit.vue16
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql5
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue10
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss2
-rw-r--r--app/assets/stylesheets/framework/job_log.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_mixins.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss14
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss4
-rw-r--r--app/models/concerns/bulk_insert_safe.rb8
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb4
-rw-r--r--app/views/projects/_new_project_fields.html.haml7
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml6
-rw-r--r--app/views/shared/builds/_build_output.html.haml2
-rw-r--r--data/deprecations/14-3-repository-push-audit-events.yml2
-rw-r--r--doc/development/packages.md8
-rw-r--r--doc/development/sidekiq_style_guide.md66
-rw-r--r--doc/update/deprecations.md2
-rw-r--r--doc/user/group/import/img/import_panel_v14_1.pngbin42789 -> 17447 bytes
-rw-r--r--doc/user/group/import/img/new_group_navigation_v13_8.pngbin39500 -> 26219 bytes
-rw-r--r--doc/user/group/import/index.md3
-rw-r--r--qa/qa/page/project/import/repo_by_url.rb16
-rw-r--r--qa/qa/page/project/new.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb3
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js4
-rw-r--r--spec/frontend/jobs/components/job_container_item_spec.js2
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js2
-rw-r--r--spec/frontend/pages/projects/new/components/new_project_url_select_spec.js86
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js3
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js4
-rw-r--r--spec/frontend/releases/components/app_show_spec.js6
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js4
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js8
-rw-r--r--spec/frontend/releases/util_spec.js14
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js80
-rw-r--r--spec/frontend/repository/components/blob_edit_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js22
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb17
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb9
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb15
61 files changed, 568 insertions, 256 deletions
diff --git a/.gitlab/ci/dast.gitlab-ci.yml b/.gitlab/ci/dast.gitlab-ci.yml
index f4e30830185..512c850b7da 100644
--- a/.gitlab/ci/dast.gitlab-ci.yml
+++ b/.gitlab/ci/dast.gitlab-ci.yml
@@ -37,6 +37,7 @@
reports:
dast: gl-dast-report.json
expire_in: 1 week # GitLab-specific
+ allow_failure: true
# DAST scan with a subset of Release scan rules.
# ZAP rule details can be found at https://www.zaproxy.org/docs/alerts/
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index a5403073e1b..abda6fad9aa 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -1,7 +1,7 @@
include:
- template: Jobs/Code-Quality.gitlab-ci.yml
- - template: Security/SAST.gitlab-ci.yml
- - template: Security/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/SAST.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/License-Scanning.gitlab-ci.yml
@@ -13,6 +13,7 @@ code_quality:
paths:
- gl-code-quality-report.json # GitLab-specific
rules: !reference [".reports:rules:code_quality", rules]
+ allow_failure: true
.sast-analyzer:
# We need to re-`extends` from `sast` as the `extends` here overrides the one from the template.
@@ -30,13 +31,16 @@ code_quality:
SAST_EXCLUDED_ANALYZERS: bandit, flawfinder, phpcs-security-audit, pmd-apex, security-code-scan, spotbugs, eslint
brakeman-sast:
- rules: !reference [".reports:rules:sast", rules]
+ rules: !reference [".reports:rules:brakeman-sast", rules]
+ allow_failure: true
nodejs-scan-sast:
- rules: !reference [".reports:rules:sast", rules]
+ rules: !reference [".reports:rules:nodejs-scan-sast", rules]
+ allow_failure: true
semgrep-sast:
- rules: !reference [".reports:rules:sast", rules]
+ rules: !reference [".reports:rules:semgrep-sast", rules]
+ allow_failure: true
gosec-sast:
variables:
@@ -52,7 +56,8 @@ gosec-sast:
cache:
paths:
- vendor/go
- rules: !reference [".reports:rules:sast", rules]
+ rules: !reference [".reports:rules:gosec-sast", rules]
+ allow_failure: true
.secret-analyzer:
extends: .default-retry
@@ -64,6 +69,7 @@ gosec-sast:
secret_detection:
rules: !reference [".reports:rules:secret_detection", rules]
+ allow_failure: true
.ds-analyzer:
# We need to re-`extends` from `dependency_scanning` as the `extends` here overrides the one from the template.
@@ -88,21 +94,24 @@ gemnasium-dependency_scanning:
# Lower execa severity based on https://gitlab.com/gitlab-org/gitlab/-/issues/223859#note_452922390
- jq '(.vulnerabilities[] | select (.cve == "yarn.lock:execa:gemnasium:05cfa2e8-2d0c-42c1-8894-638e2f12ff3d")).severity = "Medium"' gl-dependency-scanning-report.json > temp.json && mv temp.json gl-dependency-scanning-report.json
rules: !reference [".reports:rules:gemnasium-dependency_scanning", rules]
+ allow_failure: true
bundler-audit-dependency_scanning:
rules: !reference [".reports:rules:bundler-audit-dependency_scanning", rules]
+ allow_failure: true
retire-js-dependency_scanning:
rules: !reference [".reports:rules:retire-js-dependency_scanning", rules]
+ allow_failure: true
gemnasium-python-dependency_scanning:
rules: !reference [".reports:rules:gemnasium-python-dependency_scanning", rules]
+ allow_failure: true
# Analyze dependencies for malicious behavior
# See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter
.package_hunter-base:
- extends:
- - .default-retry
+ extends: .default-retry
stage: test
image:
name: registry.gitlab.com/gitlab-com/gl-security/security-research/package-hunter-cli:1.1.0
@@ -116,6 +125,8 @@ gemnasium-python-dependency_scanning:
before_script:
- rm -r spec locale .git app/assets/images doc/
- cd .. && tar -I "gzip --best" -cf gitlab.tgz gitlab/
+ script:
+ - node /usr/src/app/cli.js analyze --format gitlab --manager ${PACKAGE_MANAGER} gitlab.tgz | tee ${CI_PROJECT_DIR}/gl-dependency-scanning-report.json
artifacts:
paths:
- gl-dependency-scanning-report.json
@@ -127,15 +138,15 @@ package_hunter-yarn:
extends:
- .package_hunter-base
- .reports:rules:package_hunter-yarn
- script:
- - node /usr/src/app/cli.js analyze --format gitlab --manager yarn gitlab.tgz | tee $CI_PROJECT_DIR/gl-dependency-scanning-report.json
+ variables:
+ PACKAGE_MANAGER: yarn
package_hunter-bundler:
extends:
- .package_hunter-base
- .reports:rules:package_hunter-bundler
- script:
- - node /usr/src/app/cli.js analyze --format gitlab --manager bundler gitlab.tgz | tee $CI_PROJECT_DIR/gl-dependency-scanning-report.json
+ variables:
+ PACKAGE_MANAGER: bundler
license_scanning:
extends: .default-retry
@@ -143,3 +154,4 @@ license_scanning:
artifacts:
expire_in: 1 week # GitLab-specific
rules: !reference [".reports:rules:license_scanning", rules]
+ allow_failure: true
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 9d017ad0bd0..9b6ddb61956 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1254,75 +1254,85 @@
when: never
- <<: *if-default-refs
changes: *code-backstage-patterns
- allow_failure: true
-.reports:rules:sast:
+.reports:rules:brakeman-sast:
rules:
- - if: '$SAST_DISABLED || $GITLAB_FEATURES !~ /\bsast\b/'
+ - if: $SAST_DISABLED
when: never
- - <<: *if-default-refs
- changes: *code-backstage-qa-patterns
- allow_failure: true
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /brakeman/
+ when: never
+ - changes:
+ - '**/*.rb'
+ - '**/Gemfile'
+
+.reports:rules:nodejs-scan-sast:
+ rules:
+ - if: $SAST_DISABLED
+ when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /nodejs-scan/
+ when: never
+ - changes:
+ - '**/package.json'
+
+.reports:rules:gosec-sast:
+ rules:
+ - if: $SAST_DISABLED
+ when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /gosec/
+ when: never
+ - changes:
+ - '**/*.go'
+
+.reports:rules:semgrep-sast:
+ rules:
+ - if: $SAST_DISABLED
+ when: never
+ - if: $SAST_EXCLUDED_ANALYZERS =~ /semgrep/
+ when: never
+ - changes:
+ - '**/*.py'
+ - '**/*.js'
+ - '**/*.jsx'
+ - '**/*.ts'
+ - '**/*.tsx'
+ - '**/*.c'
+ - '**/*.go'
.reports:rules:secret_detection:
rules:
- if: '$SECRET_DETECTION_DISABLED'
when: never
- - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' # The Secret-Detection template already has a `secret_detection_default_branch` job
- when: never
- changes: *code-backstage-qa-patterns
- allow_failure: true
.reports:rules:gemnasium-dependency_scanning:
rules:
- - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /gemnasium([^-]|$)/'
+ - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /gemnasium([^-]|$)/ || $DS_DEFAULT_ANALYZERS !~ /gemnasium([^-]|$)/'
when: never
- - <<: *if-default-refs
- changes: *dependency-patterns
- allow_failure: true
+ - changes: *dependency-patterns
.reports:rules:bundler-audit-dependency_scanning:
rules:
- - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /bundler-audit/'
+ - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /bundler-audit/ || $DS_DEFAULT_ANALYZERS !~ /bundler-audit/'
when: never
- - <<: *if-default-refs
- changes: *bundler-patterns
- allow_failure: true
+ - changes: *bundler-patterns
.reports:rules:retire-js-dependency_scanning:
rules:
- - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /retire.js/'
+ - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /retire.js/ || $DS_DEFAULT_ANALYZERS !~ /retire.js/'
when: never
- - <<: *if-default-refs
- changes: *nodejs-patterns
- allow_failure: true
+ - changes: *nodejs-patterns
.reports:rules:gemnasium-python-dependency_scanning:
rules:
- - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /gemnasium-python/'
+ - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /gemnasium-python/ || $DS_DEFAULT_ANALYZERS !~ /gemnasium-python/'
when: never
- - <<: *if-default-refs
- changes: *python-patterns
- allow_failure: true
-
-.reports:rules:dast:
- rules:
- - if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/'
- when: never
- - <<: *if-dot-com-gitlab-org-merge-request
- changes: *frontend-patterns
- allow_failure: true
- - <<: *if-dot-com-gitlab-org-merge-request
- changes: *code-qa-patterns
- when: manual
- allow_failure: true
+ - changes: *python-patterns
.reports:rules:schedule-dast:
rules:
- if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/'
when: never
- <<: *if-dot-com-ee-nightly-schedule
- allow_failure: true
.reports:rules:package_hunter-yarn:
rules:
@@ -1342,11 +1352,9 @@
.reports:rules:license_scanning:
rules:
- - if: '$LICENSE_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\blicense_scanning\b/'
+ - if: '$LICENSE_MANAGEMENT_DISABLED || $GITLAB_FEATURES !~ /\blicense_scanning\b/'
when: never
- - <<: *if-default-refs
- changes: *code-backstage-qa-patterns
- allow_failure: true
+ - changes: *code-backstage-qa-patterns
################
# Review rules #
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index cc54dd52561..96cb4f3d495 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -97,7 +97,7 @@ export default {
<scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" />
</div>
</div>
- <pre ref="buildJobLog" class="build-trace mb-0 h-100 mr-3" @scroll="scrollBuildLog">
+ <pre ref="buildJobLog" class="build-log mb-0 h-100 mr-3" @scroll="scrollBuildLog">
<code
v-show="!detailJob.isLoading"
class="bash"
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 2914e4918df..fe4158a1bd1 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -279,7 +279,7 @@ export default {
<!-- job log -->
<div
v-if="hasJobLog"
- class="build-trace-container gl-relative"
+ class="build-log-container gl-relative"
:class="{ 'gl-mt-3': !job.archived }"
>
<log-top-bar
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 3db9fa01629..2a60825a427 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -214,7 +214,7 @@ export default {
<template #items>
<pre
ref="logTrace"
- class="build-trace"
+ class="build-log"
><code class="bash js-build-output"><div v-if="showLoader" class="build-loader-animation js-build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
diff --git a/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
index c30d3ec5db4..e2572183082 100644
--- a/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
@@ -14,6 +14,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import eventHub from '../event_hub';
export default {
components: {
@@ -41,15 +42,28 @@ export default {
debounce: DEBOUNCE_DELAY,
},
},
- inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel'],
+ inject: [
+ 'namespaceFullPath',
+ 'namespaceId',
+ 'rootUrl',
+ 'trackLabel',
+ 'userNamespaceFullPath',
+ 'userNamespaceId',
+ ],
data() {
return {
currentUser: {},
+ groupToFilterBy: undefined,
search: '',
- selectedNamespace: {
- id: this.namespaceId,
- fullPath: this.namespaceFullPath,
- },
+ selectedNamespace: this.namespaceId
+ ? {
+ id: this.namespaceId,
+ fullPath: this.namespaceFullPath,
+ }
+ : {
+ id: this.userNamespaceId,
+ fullPath: this.userNamespaceFullPath,
+ },
};
},
computed: {
@@ -59,21 +73,43 @@ export default {
userNamespace() {
return this.currentUser.namespace || {};
},
+ filteredGroups() {
+ return this.groupToFilterBy
+ ? this.userGroups.filter((group) =>
+ group.fullPath.startsWith(this.groupToFilterBy.fullPath),
+ )
+ : this.userGroups;
+ },
hasGroupMatches() {
- return this.userGroups.length;
+ return this.filteredGroups.length;
},
hasNamespaceMatches() {
- return this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase());
+ return (
+ this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) &&
+ !this.groupToFilterBy
+ );
},
hasNoMatches() {
return !this.hasGroupMatches && !this.hasNamespaceMatches;
},
},
+ created() {
+ eventHub.$on('select-template', this.handleSelectTemplate);
+ },
+ beforeDestroy() {
+ eventHub.$off('select-template', this.handleSelectTemplate);
+ },
methods: {
focusInput() {
this.$refs.search.focusInput();
},
- handleClick({ id, fullPath }) {
+ handleSelectTemplate(groupId) {
+ this.groupToFilterBy = this.userGroups.find(
+ (group) => getIdFromGraphQLId(group.id) === groupId,
+ );
+ this.setNamespace(this.groupToFilterBy);
+ },
+ setNamespace({ id, fullPath }) {
this.selectedNamespace = {
id: getIdFromGraphQLId(id),
fullPath,
@@ -84,28 +120,35 @@ export default {
</script>
<template>
- <gl-button-group class="gl-w-full">
- <gl-button label>{{ rootUrl }}</gl-button>
+ <gl-button-group class="input-lg">
+ <gl-button class="gl-text-truncate" label :title="rootUrl">{{ rootUrl }}</gl-button>
<gl-dropdown
- class="gl-w-full"
:text="selectedNamespace.fullPath"
- toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!"
+ toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@shown="focusInput"
>
- <gl-search-box-by-type ref="search" v-model.trim="search" />
+ <gl-search-box-by-type
+ ref="search"
+ v-model.trim="search"
+ data-qa-selector="select_namespace_dropdown_search_field"
+ />
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else>
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
- <gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
+ <gl-dropdown-item
+ v-for="group of filteredGroups"
+ :key="group.id"
+ @click="setNamespace(group)"
+ >
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="handleClick(userNamespace)">
+ <gl-dropdown-item @click="setNamespace(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/pages/projects/new/event_hub.js b/app/assets/javascripts/pages/projects/new/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/new/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index ed816e3be95..e43847caa5f 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -39,27 +39,32 @@ function initNewProjectCreation() {
}
function initNewProjectUrlSelect() {
- const el = document.querySelector('.js-vue-new-project-url-select');
+ const elements = document.querySelectorAll('.js-vue-new-project-url-select');
- if (!el) {
- return undefined;
+ if (!elements.length) {
+ return;
}
Vue.use(VueApollo);
- return new Vue({
- el,
- apolloProvider: new VueApollo({
- defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
- }),
- provide: {
- namespaceFullPath: el.dataset.namespaceFullPath,
- namespaceId: el.dataset.namespaceId,
- rootUrl: el.dataset.rootUrl,
- trackLabel: el.dataset.trackLabel,
- },
- render: (createElement) => createElement(NewProjectUrlSelect),
- });
+ elements.forEach(
+ (el) =>
+ new Vue({
+ el,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ }),
+ provide: {
+ namespaceFullPath: el.dataset.namespaceFullPath,
+ namespaceId: el.dataset.namespaceId,
+ rootUrl: el.dataset.rootUrl,
+ trackLabel: el.dataset.trackLabel,
+ userNamespaceFullPath: el.dataset.userNamespaceFullPath,
+ userNamespaceId: el.dataset.userNamespaceId,
+ },
+ render: (createElement) => createElement(NewProjectUrlSelect),
+ }),
+ );
}
initProjectVisibilitySelector();
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 8891cdc40c0..7ad9fb56972 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -8,10 +8,12 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue';
+import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer, viewerProps } from './blob_viewers';
export default {
@@ -21,6 +23,7 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
+ ForkSuggestion,
},
mixins: [getRefMixin],
inject: {
@@ -65,6 +68,7 @@ export default {
},
data() {
return {
+ forkTarget: null,
legacyRichViewer: null,
legacySimpleViewer: null,
isBinary: false,
@@ -74,6 +78,8 @@ export default {
userPermissions: {
pushCode: false,
downloadCode: false,
+ createMergeRequestIn: false,
+ forkProject: false,
},
pathLocks: {
nodes: [],
@@ -92,12 +98,14 @@ export default {
path: '',
editBlobPath: '',
ideEditPath: '',
+ forkAndEditPath: '',
+ ideForkAndEditPath: '',
storedExternally: false,
+ canModifyBlob: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
deletePath: '',
- forkPath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
@@ -149,6 +157,17 @@ export default {
isLocked() {
return this.project.pathLocks.nodes.some((node) => node.path === this.path);
},
+ showForkSuggestion() {
+ const { createMergeRequestIn, forkProject } = this.project.userPermissions;
+ const { canModifyBlob } = this.blobInfo;
+
+ return this.isLoggedIn && !canModifyBlob && createMergeRequestIn && forkProject;
+ },
+ forkPath() {
+ return this.forkTarget === 'ide'
+ ? this.blobInfo.ideForkAndEditPath
+ : this.blobInfo.forkAndEditPath;
+ },
},
methods: {
loadLegacyViewer(type) {
@@ -187,6 +206,18 @@ export default {
this.loadLegacyViewer(this.activeViewerType);
}
},
+ editBlob(target) {
+ if (this.showForkSuggestion) {
+ this.setForkTarget(target);
+ return;
+ }
+
+ const { ideEditPath, editBlobPath } = this.blobInfo;
+ redirectTo(target === 'ide' ? ideEditPath : editBlobPath);
+ },
+ setForkTarget(target) {
+ this.forkTarget = target;
+ },
},
};
</script>
@@ -208,6 +239,8 @@ export default {
:show-edit-button="!isBinaryFileType"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
+ :needs-to-fork="showForkSuggestion"
+ @edit="editBlob"
/>
<blob-button-group
v-if="isLoggedIn"
@@ -223,6 +256,11 @@ export default {
/>
</template>
</blob-header>
+ <fork-suggestion
+ v-if="forkTarget && showForkSuggestion"
+ :fork-path="forkPath"
+ @cancel="setForkTarget(null)"
+ />
<blob-content
v-if="!blobViewer"
:rich-viewer="legacyRichViewer"
diff --git a/app/assets/javascripts/repository/components/blob_edit.vue b/app/assets/javascripts/repository/components/blob_edit.vue
index 30ed4cd57f1..fd377ba1b81 100644
--- a/app/assets/javascripts/repository/components/blob_edit.vue
+++ b/app/assets/javascripts/repository/components/blob_edit.vue
@@ -27,6 +27,16 @@ export default {
type: String,
required: true,
},
+ needsToFork: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ onEdit(target) {
+ this.$emit('edit', target);
+ },
},
};
</script>
@@ -38,7 +48,9 @@ export default {
class="gl-mr-3"
:edit-url="editPath"
:web-ide-url="webIdePath"
+ :needs-to-fork="needsToFork"
:is-blob="true"
+ @edit="onEdit"
/>
<div v-else>
<gl-button
@@ -46,8 +58,8 @@ export default {
class="gl-mr-2"
category="primary"
variant="confirm"
- :href="editPath"
data-testid="edit"
+ @click="onEdit('simple')"
>
{{ $options.i18n.edit }}
</gl-button>
@@ -56,8 +68,8 @@ export default {
class="gl-mr-3"
category="primary"
variant="confirm"
- :href="webIdePath"
data-testid="web-ide"
+ @click="onEdit('ide')"
>
{{ $options.i18n.webIde }}
</gl-button>
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 45f07f7dc58..8e0b5e21ca3 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -4,6 +4,8 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
userPermissions {
pushCode
downloadCode
+ createMergeRequestIn
+ forkProject
}
pathLocks {
nodes {
@@ -23,6 +25,9 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
path
editBlobPath
ideEditPath
+ forkAndEditPath
+ ideForkAndEditPath
+ canModifyBlob
storedExternally
rawPath
replacePath
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index df0981aea7a..6da2d39a95a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -92,7 +92,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-edit',
- handle: () => this.showModal('#modal-confirm-fork-edit'),
+ handle: () => {
+ this.$emit('edit', 'simple');
+ this.showModal('#modal-confirm-fork-edit');
+ },
}
: { href: this.editUrl };
@@ -128,7 +131,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-webide',
- handle: () => this.showModal('#modal-confirm-fork-webide'),
+ handle: () => {
+ this.$emit('edit', 'ide');
+ this.showModal('#modal-confirm-fork-webide');
+ },
}
: { href: this.webIdeUrl };
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index c4f292dd05d..27ddff181c5 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -112,7 +112,7 @@ code {
border-radius: $border-radius-default;
.code > &,
- .build-trace & {
+ .build-log & {
background-color: inherit;
padding: unset;
}
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
index 0a493b3a977..f77f64f1d76 100644
--- a/app/assets/stylesheets/framework/job_log.scss
+++ b/app/assets/stylesheets/framework/job_log.scss
@@ -5,10 +5,10 @@
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
- color: color-yiq($builds-trace-bg);
+ color: color-yiq($builds-log-bg);
border-radius: $border-radius-small;
min-height: 42px;
- background-color: $builds-trace-bg;
+ background-color: $builds-log-bg;
}
.log-line {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index fcf86680bb3..33f7aa4dba1 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -239,7 +239,7 @@
/*
* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
*/
-@mixin build-trace($background: $black) {
+@mixin build-log($background: $black) {
background: $background;
color: $gray-darkest;
white-space: pre;
@@ -253,13 +253,13 @@
display: block;
}
- &.build-trace-rounded {
+ &.build-log-rounded {
border-radius: $gl-border-radius-base;
}
}
// Used in EE for Web Terminal
-@mixin build-trace-bar($height) {
+@mixin build-log-bar($height) {
height: $height;
min-height: $height;
background: var(--gray-50, $gray-50);
@@ -268,8 +268,8 @@
padding: $grid-size;
}
-@mixin build-trace-top-bar($height) {
- @include build-trace-bar($height);
+@mixin build-log-top-bar($height) {
+ @include build-log-bar($height);
position: -webkit-sticky;
position: sticky;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index c0e297d8554..026aeeb1e8e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -697,7 +697,7 @@ $blame-blue: #254e77;
/*
* Builds
*/
-$builds-trace-bg: #111;
+$builds-log-bg: #111;
$job-log-highlight-height: 18px;
$job-log-line-padding: 55px;
$job-line-number-width: 50px;
diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
index 48b8a7230b1..bbc47c5cd5d 100644
--- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
@@ -4,7 +4,7 @@
height: 100%;
.top-bar {
- @include build-trace-bar(35px);
+ @include build-log-bar(35px);
top: 0;
font-size: 12px;
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index ec41909beec..ed62e055427 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -1,8 +1,8 @@
@import 'mixins_and_variables_and_functions';
.build-page {
- .build-trace {
- @include build-trace();
+ .build-log {
+ @include build-log();
}
.archived-job {
@@ -18,7 +18,7 @@
}
.top-bar {
- @include build-trace-top-bar(50px);
+ @include build-log-top-bar(50px);
&.has-archived-block {
top: $header-height + 28px;
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 206c2eb09d0..c8b1b6cf9aa 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -44,14 +44,14 @@
line-height: initial;
}
- .build-trace-row td {
+ .build-log-row td {
border-top: 0;
border-bottom-width: 1px;
border-bottom-style: solid;
padding-top: 0;
}
- .build-trace {
+ .build-log {
width: 100%;
text-align: left;
margin-top: $gl-padding;
@@ -93,7 +93,7 @@
}
.build-state,
- .build-trace-row {
+ .build-log-row {
> td:last-child {
padding-right: 0;
}
@@ -108,12 +108,12 @@
margin-top: 2 * $gl-padding;
}
- .build-trace-container {
+ .build-log-container {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
}
- .build-trace {
+ .build-log {
margin-bottom: 0;
margin-top: 0;
}
@@ -221,8 +221,8 @@
}
.test-reports-table {
- .build-trace {
- @include build-trace();
+ .build-log {
+ @include build-log();
}
}
diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss
index 03993e5321d..f8f40076142 100644
--- a/app/assets/stylesheets/pages/environment_logs.scss
+++ b/app/assets/stylesheets/pages/environment_logs.scss
@@ -40,8 +40,8 @@
height: 100%;
}
- .build-trace {
- @include build-trace($black);
+ .build-log {
+ @include build-log($black);
}
.gl-infinite-scroll-legend {
diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb
index 908f0b6a7e2..0d92689e4a3 100644
--- a/app/models/concerns/bulk_insert_safe.rb
+++ b/app/models/concerns/bulk_insert_safe.rb
@@ -51,6 +51,12 @@ module BulkInsertSafe
PrimaryKeySetError = Class.new(StandardError)
class_methods do
+ def insert_all_proxy_class
+ @insert_all_proxy_class ||= Class.new(self) do
+ attr_readonly :created_at
+ end
+ end
+
def set_callback(name, *args)
unless _bulk_insert_callback_allowed?(name, args)
raise MethodNotAllowedError,
@@ -153,7 +159,7 @@ module BulkInsertSafe
item_batch, validate, &handle_attributes)
ActiveRecord::InsertAll
- .new(self, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by)
+ .new(insert_all_proxy_class, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by)
.execute
.pluck(primary_key)
end
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
index c3498c5ce97..3e294aeaa07 100644
--- a/app/services/merge_requests/mergeability_check_service.rb
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -157,9 +157,7 @@ module MergeRequests
def merge_to_ref
params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) }
- result = MergeRequests::MergeToRefService
- .new(project: project, current_user: merge_request.author, params: params)
- .execute(merge_request, true)
+ result = MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author, params: params).execute(merge_request)
result[:status] == :success
end
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index fb7a7ef8985..b7859e27b31 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -16,7 +16,12 @@
- if current_user.can_select_namespace?
- namespace_id = namespace_id_from(params)
- if Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
- .js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path, namespace_id: namespace_id, root_url: root_url, track_label: track_label } }
+ .js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path,
+ namespace_id: namespace_id,
+ root_url: root_url,
+ track_label: track_label,
+ user_namespace_full_path: current_user.namespace.full_path,
+ user_namespace_id: current_user.namespace.id } }
- else
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 23606e24563..93afddce779 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -70,10 +70,10 @@
= link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'gl-button btn btn-default btn-icon' do
= sprite_icon('repeat', css_class: 'gl-icon')
- if can?(current_user, :read_build, job)
- %tr.build-trace-row.responsive-table-border-end
+ %tr.build-log-row.responsive-table-border-end
%td
- %td.responsive-table-cell.build-trace-container{ colspan: 4 }
- %pre.build-trace.build-trace-rounded
+ %td.responsive-table-cell.build-log-container{ colspan: 4 }
+ %pre.build-log.build-log-rounded
%code.bash.js-build-output
= build_summary(build)
diff --git a/app/views/shared/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml
index 380fac4d0e4..a3b7d4926f8 100644
--- a/app/views/shared/builds/_build_output.html.haml
+++ b/app/views/shared/builds/_build_output.html.haml
@@ -1,4 +1,4 @@
-%pre.build-trace#build-trace
+%pre.build-log
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
.dot
diff --git a/data/deprecations/14-3-repository-push-audit-events.yml b/data/deprecations/14-3-repository-push-audit-events.yml
index 3a39c1f4304..4bca8751db4 100644
--- a/data/deprecations/14-3-repository-push-audit-events.yml
+++ b/data/deprecations/14-3-repository-push-audit-events.yml
@@ -11,4 +11,4 @@
tiers: Premium
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337993
documentation_url: https://docs.gitlab.com/ee/administration/audit_events.html#repository-push
- announcement_date: "2021-09-02" # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69024
+ announcement_date: "2021-09-22" # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69024
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 869a1755d8f..38c1b941eaf 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -30,9 +30,9 @@ The existing database model requires the following:
### API endpoints
-Package systems work with GitLab via API. For example `lib/api/npm_packages.rb`
+Package systems work with GitLab via API. For example `lib/api/npm_project_packages.rb`
implements API endpoints to work with npm clients. So, the first thing to do is to
-add a new `lib/api/your_name_packages.rb` file with API endpoints that are
+add a new `lib/api/your_name_project_packages.rb` file with API endpoints that are
necessary to make the package system client to work. Usually that means having
endpoints like:
@@ -48,7 +48,7 @@ GET https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/
PUT https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/
```
-Group-level and instance-level endpoints are good to have but are optional.
+Group-level and instance-level endpoints should only be considered after the project-level endpoint is available in production.
#### Remote hierarchy
@@ -168,7 +168,7 @@ The implementation of the different Merge Requests varies between different pack
The MVC must support [Personal Access Tokens](../user/profile/personal_access_tokens.md) right from the start. We currently support two options for these tokens: OAuth and Basic Access.
-OAuth authentication is already supported. You can see an example in the [npm API](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/npm_packages.rb).
+OAuth authentication is already supported. You can see an example in the [npm API](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/npm_project_packages.rb).
[Basic Access authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)
support is done by overriding a specific function in the API helpers, like
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index 04b7e2f5c45..a156d776597 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -154,12 +154,6 @@ A good example of that would be a cache expiration worker.
A job scheduled for an idempotent worker is [deduplicated](#deduplication) when
an unstarted job with the same arguments is already in the queue.
-WARNING:
-For [data consistency jobs](#job-data-consistency-strategies), the deduplication is not compatible with the
-`data_consistency` attribute set to `:sticky` or `:delayed`.
-The reason for this is that deduplication always takes into account the latest binary replication pointer into account, not the first one.
-There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/325291) to improve this.
-
### Ensuring a worker is idempotent
Make sure the worker tests pass using the following shared example:
@@ -285,6 +279,55 @@ module AuthorizedProjectUpdate
end
```
+### Deduplication with load balancing
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6763) in GitLab 14.4.
+
+Jobs that declare either `:sticky` or `:delayed` data consistency
+are eligible for database load-balancing.
+In both cases, jobs are [scheduled in the future](#scheduling-jobs-in-the-future) with a short delay (1 second).
+This minimizes the chance of replication lag after a write.
+
+If you really want to deduplicate jobs eligible for load balancing,
+specify `including_scheduled: true` argument when defining deduplication strategy:
+
+```ruby
+class DelayedIdempotentWorker
+ include ApplicationWorker
+ data_consistency :delayed
+
+ deduplicate :until_executing, including_scheduled: true
+ idempotent!
+
+ # ...
+end
+```
+
+#### Preserve the latest WAL location for idempotent jobs
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69372) in GitLab 14.3.
+> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/338350) in GitLab 14.4.
+
+The deduplication always take into account the latest binary replication pointer, not the first one.
+This happens because we drop the same job scheduled for the second time and the Write-Ahead Log (WAL) is lost.
+This could lead to comparing the old WAL location and reading from a stale replica.
+
+To support both deduplication and maintaining data consistency with load balancing,
+we are preserving the latest WAL location for idempotent jobs in Redis.
+This way we are always comparing the latest binary replication pointer,
+making sure that we read from the replica that is fully caught up.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available.
+To make it available,
+ask an administrator to [enable the preserve_latest_wal_locations_for_idempotent_jobs flag](../administration/feature_flags.md).
+FLAG:
+On self-managed GitLab, by default this feature is not available.
+To make it available,
+ask an administrator to [enable the `preserve_latest_wal_locations_for_idempotent_jobs` flag](../administration/feature_flags.md).
+This feature flag is related to GitLab development and is not intended to be used by GitLab administrators, though.
+On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
+
## Limited capacity worker
It is possible to limit the number of concurrent running jobs for a worker class
@@ -553,11 +596,6 @@ class DelayedWorker
end
```
-For [idempotent jobs](#idempotent-jobs), the deduplication is not compatible with the
-`data_consistency` attribute set to `:sticky` or `:delayed`.
-The reason for this is that deduplication always takes into account the latest binary replication pointer into account, not the first one.
-There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/325291) to improve this.
-
### `feature_flag` property
The `feature_flag` property allows you to toggle a job's `data_consistency`,
@@ -583,6 +621,12 @@ class DelayedWorker
end
```
+### Data consistency with idempotent jobs
+
+For [idempotent jobs](#idempotent-jobs) that declare either `:sticky` or `:delayed` data consistency, we are
+[preserving the latest WAL location](#preserve-the-latest-wal-location-for-idempotent-jobs) while deduplicating,
+ensuring that we read from the replica that is fully caught up.
+
## Jobs with External Dependencies
Most background jobs in the GitLab application communicate with other GitLab
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index c0be12d5dab..6087562dcf7 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -54,7 +54,7 @@ These events have always been disabled by default and had to be manually enabled
feature flag. Enabling them can cause too many events to be generated which can
dramatically slow down GitLab instances. For this reason, they are being removed.
-Announced: 2021-09-02
+Announced: 2021-09-22
### OmniAuth Kerberos gem
diff --git a/doc/user/group/import/img/import_panel_v14_1.png b/doc/user/group/import/img/import_panel_v14_1.png
index 28417383b6c..52791a82c3c 100644
--- a/doc/user/group/import/img/import_panel_v14_1.png
+++ b/doc/user/group/import/img/import_panel_v14_1.png
Binary files differ
diff --git a/doc/user/group/import/img/new_group_navigation_v13_8.png b/doc/user/group/import/img/new_group_navigation_v13_8.png
index 307175727c7..4953207ea5b 100644
--- a/doc/user/group/import/img/new_group_navigation_v13_8.png
+++ b/doc/user/group/import/img/new_group_navigation_v13_8.png
Binary files differ
diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md
index 5718d57319e..bcff31c65d1 100644
--- a/doc/user/group/import/index.md
+++ b/doc/user/group/import/index.md
@@ -5,7 +5,7 @@ group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Import groups from another instance of GitLab **(FREE)**
+# Migrate groups from another instance of GitLab **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/249160) in GitLab 13.7.
> - [Deployed behind a feature flag](../../feature_flags.md), disabled by default.
@@ -103,6 +103,7 @@ This might involve reconfiguring your firewall to prevent blocking connection on
### Connect to the remote GitLab instance
1. Go to the New Group page:
+
- On the top bar, select `+` and then **New group**.
- Or, on an existing group's page, in the top right, select **New subgroup**.
diff --git a/qa/qa/page/project/import/repo_by_url.rb b/qa/qa/page/project/import/repo_by_url.rb
index 0e7524a181a..ed56bb1adcd 100644
--- a/qa/qa/page/project/import/repo_by_url.rb
+++ b/qa/qa/page/project/import/repo_by_url.rb
@@ -5,10 +5,9 @@ module QA
module Project
module Import
class RepoByURL < Page::Base
- include Page::Component::Select2
-
- view 'app/views/projects/_new_project_fields.html.haml' do
+ view 'app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue' do
element :select_namespace_dropdown
+ element :select_namespace_dropdown_search_field
end
def import!(gitlab_repo_path, name)
@@ -33,8 +32,15 @@ module QA
end
def choose_test_namespace
- find('.js-select-namespace').click
- search_and_select(Runtime::Namespace.path)
+ choose_namespace(Runtime::Namespace.path)
+ end
+
+ def choose_namespace(namespace)
+ retry_on_exception do
+ click_element :select_namespace_dropdown
+ fill_element :select_namespace_dropdown_search_field, namespace
+ click_button namespace
+ end
end
def click_create_button
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 06e476f009a..85fe5e54aea 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -5,7 +5,6 @@ module QA
module Project
class New < Page::Base
include Page::Component::Project::Templates
- include Page::Component::Select2
include Page::Component::VisibilitySetting
include Layout::Flash
@@ -14,7 +13,6 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
- element :project_namespace_select
element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
@@ -28,6 +26,11 @@ module QA
element :template_option_row
end
+ view 'app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue' do
+ element :select_namespace_dropdown
+ element :select_namespace_dropdown_search_field
+ end
+
view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do
element :panel_link
end
@@ -46,8 +49,9 @@ module QA
def choose_namespace(namespace)
retry_on_exception do
- click_element :project_namespace_select unless dropdown_open?
- search_and_select(namespace)
+ click_element :select_namespace_dropdown
+ fill_element :select_namespace_dropdown_search_field, namespace
+ click_button namespace
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index e41d87612de..af4e7126c29 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', :smoke do
- describe 'Project' do
+ describe 'Project', :requires_admin do
shared_examples 'successful project creation' do
it 'creates a new project' do
Page::Project::Show.perform do |project|
@@ -17,6 +17,7 @@ module QA
end
before do
+ Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
Flow::Login.sign_in
project
end
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index e904d0da92d..07e6ee46c41 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -2,7 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import { TEST_HOST } from 'helpers/test_constants';
import EmptyState from '~/jobs/components/empty_state.vue';
import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
@@ -19,8 +19,6 @@ describe('Job App', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
-
let store;
let wrapper;
let mock;
diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js
index 36038b69e64..6b488821bc1 100644
--- a/spec/frontend/jobs/components/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job_container_item_spec.js
@@ -1,12 +1,12 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import JobContainerItem from '~/jobs/components/job_container_item.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import job from '../mock_data';
describe('JobContainerItem', () => {
let wrapper;
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const findCiIconComponent = () => wrapper.findComponent(CiIcon);
const findGlIconComponent = () => wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
index 838323df755..63dcd72f967 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
+import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
describe('DelayedJobMixin', () => {
let wrapper;
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const dummyComponent = {
props: {
job: {
diff --git a/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js b/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
index 4902b830d0e..4b33bafa342 100644
--- a/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
@@ -10,6 +10,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import eventHub from '~/pages/projects/new/event_hub';
import NewProjectUrlSelect from '~/pages/projects/new/components/new_project_url_select.vue';
import searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
@@ -28,6 +29,10 @@ describe('NewProjectUrlSelect component', () => {
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
},
+ {
+ id: 'gid://gitlab/Group/30',
+ fullPath: 'h5bp/subgroup',
+ },
],
},
namespace: {
@@ -40,14 +45,21 @@ describe('NewProjectUrlSelect component', () => {
const localVue = createLocalVue();
localVue.use(VueApollo);
- const provide = {
+ const defaultProvide = {
namespaceFullPath: 'h5bp',
namespaceId: '28',
rootUrl: 'https://gitlab.com/',
trackLabel: 'blank_project',
+ userNamespaceFullPath: 'root',
+ userNamespaceId: '1',
};
- const mountComponent = ({ search = '', queryResponse = data, mountFn = shallowMount } = {}) => {
+ const mountComponent = ({
+ search = '',
+ queryResponse = data,
+ provide = defaultProvide,
+ mountFn = shallowMount,
+ } = {}) => {
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]];
const apolloProvider = createMockApollo(requestHandlers);
@@ -75,20 +87,42 @@ describe('NewProjectUrlSelect component', () => {
it('renders the root url as a label', () => {
wrapper = mountComponent();
- expect(findButtonLabel().text()).toBe(provide.rootUrl);
+ expect(findButtonLabel().text()).toBe(defaultProvide.rootUrl);
expect(findButtonLabel().props('label')).toBe(true);
});
- it('renders a dropdown with the initial namespace full path as the text', () => {
- wrapper = mountComponent();
+ describe('when namespaceId is provided', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
- expect(findDropdown().props('text')).toBe(provide.namespaceFullPath);
+ it('renders a dropdown with the given namespace full path as the text', () => {
+ expect(findDropdown().props('text')).toBe(defaultProvide.namespaceFullPath);
+ });
+
+ it('renders a dropdown with the given namespace id in the hidden input', () => {
+ expect(findHiddenInput().attributes('value')).toBe(defaultProvide.namespaceId);
+ });
});
- it('renders a dropdown with the initial namespace id in the hidden input', () => {
- wrapper = mountComponent();
+ describe('when namespaceId is not provided', () => {
+ const provide = {
+ ...defaultProvide,
+ namespaceFullPath: undefined,
+ namespaceId: undefined,
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent({ provide });
+ });
+
+ it("renders a dropdown with the user's namespace full path as the text", () => {
+ expect(findDropdown().props('text')).toBe(defaultProvide.userNamespaceFullPath);
+ });
- expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId);
+ it("renders a dropdown with the user's namespace id in the hidden input", () => {
+ expect(findHiddenInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
+ });
});
it('focuses on the input when the dropdown is opened', async () => {
@@ -112,11 +146,39 @@ describe('NewProjectUrlSelect component', () => {
const listItems = wrapper.findAll('li');
+ expect(listItems).toHaveLength(6);
expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath);
expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
- expect(listItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
- expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath);
+ expect(listItems.at(3).text()).toBe(data.currentUser.groups.nodes[2].fullPath);
+ expect(listItems.at(4).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
+ expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
+ });
+
+ describe('when selecting from a group template', () => {
+ const groupId = getIdFromGraphQLId(data.currentUser.groups.nodes[1].id);
+
+ beforeEach(async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ eventHub.$emit('select-template', groupId);
+ });
+
+ it('filters the dropdown items to the selected group and children', async () => {
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems).toHaveLength(3);
+ expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
+ expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
+ expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[2].fullPath);
+ });
+
+ it('sets the selection to the group', async () => {
+ expect(findDropdown().props('text')).toBe(data.currentUser.groups.nodes[1].fullPath);
+ });
});
it('renders `No matches found` when there are no matching dropdown items', async () => {
@@ -164,7 +226,7 @@ describe('NewProjectUrlSelect component', () => {
findDropdown().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
- label: provide.trackLabel,
+ label: defaultProvide.trackLabel,
property: 'project_path',
});
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 1db6fa21d6b..029d720f7b9 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -3,7 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as commonUtils from '~/lib/utils/common_utils';
@@ -11,7 +11,6 @@ import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
-const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
index 096d319c82f..32bbfd386f5 100644
--- a/spec/frontend/releases/components/app_index_apollo_client_spec.js
+++ b/spec/frontend/releases/components/app_index_apollo_client_spec.js
@@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
@@ -32,9 +33,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
describe('app_index_apollo_client.vue', () => {
- const originalAllReleasesQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
- );
const projectPath = 'project/path';
const newReleasePath = 'path/to/new/release/page';
const before = 'beforeCursor';
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index 7ea7a6ffe94..72ebaaaf76c 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { getJSONFixture } from 'helpers/fixtures';
+import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import createFlash from '~/flash';
import ReleaseShowApp from '~/releases/components/app_show.vue';
@@ -11,10 +11,6 @@ import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphq
jest.mock('~/flash');
-const oneReleaseQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release.query.graphql.json',
-);
-
Vue.use(VueApollo);
const EXPECTED_ERROR_MESSAGE = 'Something went wrong while getting the release details.';
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 460007e48ef..839d127e00f 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -1,6 +1,6 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import * as commonUtils from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
@@ -9,8 +9,6 @@ import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release edit component', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 50b6d1c4707..973428257b7 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -1,13 +1,11 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Evidence Block', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 3b9b16fa890..c63689e11ac 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -1,13 +1,11 @@
import { GlCollapse } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import { assets } from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
-const { assets } = getJSONFixture('api/releases/release.json');
-
describe('Release block assets', () => {
let wrapper;
let defaultProps;
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index e9fa22b4ec7..f645dc309d7 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,13 +1,11 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31;
const mockFutureDate = new Date(new Date().getTime() + MONTHS_IN_MS).toISOString();
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index 47fd6377fcf..167ae4f32a2 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -1,14 +1,12 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release block header', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index a2bf45c7861..146b2cc7490 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -1,12 +1,12 @@
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
-const { milestones: originalMilestones } = getJSONFixture('api/releases/release.json');
+const { milestones: originalMilestones } = originalRelease;
describe('Release block milestone info', () => {
let wrapper;
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 1ca441f7a5a..a847c32b8f1 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import $ from 'jquery';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
@@ -9,8 +9,6 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release block', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 6504a09df2f..d8329fb82b1 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -27,10 +27,6 @@ jest.mock('~/releases/util', () => ({
},
}));
-const originalOneReleaseForEditingQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json',
-);
-
describe('Release edit/new actions', () => {
let state;
let releaseResponse;
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 20ae332e500..24dcedb3580 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -1,12 +1,10 @@
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
import * as types from '~/releases/stores/modules/edit_new/mutation_types';
import mutations from '~/releases/stores/modules/edit_new/mutations';
import createState from '~/releases/stores/modules/edit_new/state';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release edit/new mutations', () => {
let state;
let release;
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
index af520c2eb20..91406f7e2f4 100644
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/list/actions_spec.js
@@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalGraphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import { PAGE_SIZE } from '~/releases/constants';
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
@@ -12,10 +12,6 @@ import * as types from '~/releases/stores/modules/index/mutation_types';
import createState from '~/releases/stores/modules/index/state';
import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
-const originalGraphqlReleasesResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
-);
-
describe('Releases State actions', () => {
let mockedState;
let graphqlReleasesResponse;
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
index 08d803b3c2c..49e324c28a5 100644
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js
@@ -1,17 +1,13 @@
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
+import graphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from '~/releases/stores/modules/index/mutation_types';
import mutations from '~/releases/stores/modules/index/mutations';
import createState from '~/releases/stores/modules/index/state';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
-const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
-const graphqlReleasesResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
-);
-
describe('Releases Store Mutations', () => {
let stateCopy;
let pageInfo;
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
index 36e7be369d3..3c1060cb0e8 100644
--- a/spec/frontend/releases/util_spec.js
+++ b/spec/frontend/releases/util_spec.js
@@ -1,21 +1,13 @@
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
+import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
+import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import {
convertGraphQLRelease,
convertAllReleasesGraphQLResponse,
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
-const originalAllReleasesQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
-);
-const originalOneReleaseQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release.query.graphql.json',
-);
-const originalOneReleaseForEditingQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json',
-);
-
describe('releases/util.js', () => {
describe('convertGraphQLRelease', () => {
let releaseFromResponse;
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 5059508fe2e..59db537282b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -11,13 +11,18 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
+import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { isLoggedIn } from '~/lib/utils/common_utils';
jest.mock('~/repository/components/blob_viewers');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
@@ -34,12 +39,14 @@ const simpleMockData = {
webPath: 'some_file.js',
editBlobPath: 'some_file.js/edit',
ideEditPath: 'some_file.js/ide/edit',
+ forkAndEditPath: 'some_file.js/fork/edit',
+ ideForkAndEditPath: 'some_file.js/fork/ide',
+ canModifyBlob: true,
storedExternally: false,
rawPath: 'some_file.js',
externalStorageUrl: 'some_file.js',
replacePath: 'some_file.js/replace',
deletePath: 'some_file.js/delete',
- forkPath: 'some_file.js/fork',
simpleViewer: {
fileType: 'text',
tooLarge: false,
@@ -62,6 +69,8 @@ const projectMockData = {
userPermissions: {
pushCode: true,
downloadCode: true,
+ createMergeRequestIn: true,
+ forkProject: true,
},
repository: {
empty: false,
@@ -82,6 +91,8 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
emptyRepo = defaultEmptyRepo,
canPushCode = defaultPushCode,
canDownloadCode = defaultDownloadCode,
+ createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn,
+ forkProject = projectMockData.userPermissions.forkProject,
pathLocks = [],
} = mockData;
@@ -89,7 +100,12 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
data: {
project: {
id: '1234',
- userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode },
+ userPermissions: {
+ pushCode: canPushCode,
+ downloadCode: canDownloadCode,
+ createMergeRequestIn,
+ forkProject,
+ },
pathLocks: {
nodes: pathLocks,
},
@@ -158,9 +174,11 @@ describe('Blob content viewer component', () => {
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
+ const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
beforeEach(() => {
gon.features = { refactorTextViewer: true };
+ isLoggedIn.mockReturnValue(true);
});
afterEach(() => {
@@ -469,7 +487,7 @@ describe('Blob content viewer component', () => {
});
it('does not render if not logged in', async () => {
- window.gon.current_user_id = null;
+ isLoggedIn.mockReturnValueOnce(false);
fullFactory({
mockData: { blobInfo: simpleMockData },
@@ -513,4 +531,60 @@ describe('Blob content viewer component', () => {
);
});
});
+
+ describe('edit blob', () => {
+ beforeEach(() => {
+ fullFactory({
+ mockData: { blobInfo: simpleMockData },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+ });
+
+ it('simple edit redirects to the simple editor', () => {
+ findBlobEdit().vm.$emit('edit', 'simple');
+ expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath);
+ });
+
+ it('IDE edit redirects to the IDE editor', () => {
+ findBlobEdit().vm.$emit('edit', 'ide');
+ expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath);
+ });
+
+ it.each`
+ loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion
+ ${true} | ${false} | ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true} | ${true} | ${false}
+ ${true} | ${true} | ${false} | ${true} | ${false}
+ ${true} | ${true} | ${true} | ${false} | ${false}
+ `(
+ 'shows/hides a fork suggestion according to a set of conditions',
+ async ({
+ loggedIn,
+ canModifyBlob,
+ createMergeRequestIn,
+ forkProject,
+ showForkSuggestion,
+ }) => {
+ isLoggedIn.mockReturnValueOnce(loggedIn);
+ fullFactory({
+ mockData: {
+ blobInfo: { ...simpleMockData, canModifyBlob },
+ project: { userPermissions: { createMergeRequestIn, forkProject } },
+ },
+ stubs: {
+ BlobContent: true,
+ BlobButtonGroup: true,
+ },
+ });
+
+ findBlobEdit().vm.$emit('edit', 'simple');
+ await nextTick();
+
+ expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
+ },
+ );
+ });
});
diff --git a/spec/frontend/repository/components/blob_edit_spec.js b/spec/frontend/repository/components/blob_edit_spec.js
index 11739674bc9..e2de7bc2957 100644
--- a/spec/frontend/repository/components/blob_edit_spec.js
+++ b/spec/frontend/repository/components/blob_edit_spec.js
@@ -7,6 +7,7 @@ const DEFAULT_PROPS = {
editPath: 'some_file.js/edit',
webIdePath: 'some_file.js/ide/edit',
showEditButton: true,
+ needsToFork: false,
};
describe('BlobEdit component', () => {
@@ -56,7 +57,6 @@ describe('BlobEdit component', () => {
it('renders the Edit button', () => {
createComponent();
- expect(findEditButton().attributes('href')).toBe(DEFAULT_PROPS.editPath);
expect(findEditButton().text()).toBe('Edit');
expect(findEditButton()).not.toBeDisabled();
});
@@ -64,7 +64,6 @@ describe('BlobEdit component', () => {
it('renders the Web IDE button', () => {
createComponent();
- expect(findWebIdeButton().attributes('href')).toBe(DEFAULT_PROPS.webIdePath);
expect(findWebIdeButton().text()).toBe('Web IDE');
expect(findWebIdeButton()).not.toBeDisabled();
});
@@ -72,13 +71,14 @@ describe('BlobEdit component', () => {
it('renders WebIdeLink component', () => {
createComponent(true);
- const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS;
+ const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS;
expect(findWebIdeLink().props()).toMatchObject({
editUrl,
webIdeUrl,
isBlob: true,
showEditButton: true,
+ needsToFork,
});
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 5fe4eeb6061..92938b2717f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -160,4 +160,26 @@ describe('Web IDE link component', () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
});
});
+
+ describe('edit actions', () => {
+ it.each([
+ {
+ props: { showWebIdeButton: true, showEditButton: false },
+ expectedEventPayload: 'ide',
+ },
+ {
+ props: { showWebIdeButton: false, showEditButton: true },
+ expectedEventPayload: 'simple',
+ },
+ ])(
+ 'emits the correct event when an action handler is called',
+ async ({ props, expectedEventPayload }) => {
+ createComponent({ ...props, needsToFork: true });
+
+ findActionsButton().props('actions')[0].handle();
+
+ expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
+ },
+ );
+ });
});
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 209ee1264d5..012f80210f2 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe BulkInsertSafe do
t.binary :sha_value, null: false, limit: 20
t.jsonb :jsonb_value, null: false
t.belongs_to :bulk_insert_parent_item, foreign_key: true, null: true
+ t.timestamps null: true
t.index :name, unique: true
end
@@ -228,10 +229,20 @@ RSpec.describe BulkInsertSafe do
end
describe '.bulk_upsert!' do
+ subject(:bulk_upsert) { bulk_insert_item_class.bulk_upsert!([new_object], unique_by: %w[name]) }
+
it 'updates existing object' do
- bulk_insert_item_class.bulk_upsert!([new_object], unique_by: %w[name])
+ expect { bulk_upsert }.to change { existing_object.reload.secret_value }.to('new value')
+ end
- expect(existing_object.reload.secret_value).to eq('new value')
+ context 'when the `created_at` attribute is provided' do
+ before do
+ new_object.created_at = 10.days.from_now
+ end
+
+ it 'does not change the existing `created_at` value' do
+ expect { bulk_upsert }.not_to change { existing_object.reload.created_at }
+ end
end
end
end
@@ -250,7 +261,7 @@ RSpec.describe BulkInsertSafe do
it 'successfully inserts an item' do
expect(ActiveRecord::InsertAll).to receive(:new)
.with(
- bulk_insert_items_with_composite_pk_class, [new_object.as_json], on_duplicate: :raise, returning: false, unique_by: %w[id name]
+ bulk_insert_items_with_composite_pk_class.insert_all_proxy_class, [new_object.as_json], on_duplicate: :raise, returning: false, unique_by: %w[id name]
).and_call_original
expect { bulk_insert_items_with_composite_pk_class.bulk_insert!([new_object]) }.to(
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
index 4f7be0f5965..65599b7e046 100644
--- a/spec/services/merge_requests/mergeability_check_service_spec.rb
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -132,15 +132,6 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
it_behaves_like 'mergeable merge request'
- it 'calls MergeToRefService with cache parameter' do
- service = instance_double(MergeRequests::MergeToRefService)
-
- expect(MergeRequests::MergeToRefService).to receive(:new).once { service }
- expect(service).to receive(:execute).once.with(merge_request, true).and_return(success: true)
-
- described_class.new(merge_request).execute(recheck: true)
- end
-
context 'when concurrent calls' do
it 'waits first lock and returns "cached" result in subsequent calls' do
threads = execute_within_threads(amount: 3)
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 42ff3251023..bb4767c9c11 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -236,28 +236,25 @@ RSpec.describe 'Every Sidekiq worker' do
'FlushCounterIncrementsWorker' => 3,
'Geo::Batch::ProjectRegistrySchedulerWorker' => 3,
'Geo::Batch::ProjectRegistryWorker' => 3,
- 'Geo::ContainerRepositorySyncWorker' => 3,
+ 'Geo::ContainerRepositorySyncWorker' => 1,
'Geo::DesignRepositoryShardSyncWorker' => false,
- 'Geo::DesignRepositorySyncWorker' => 3,
+ 'Geo::DesignRepositorySyncWorker' => 1,
'Geo::DestroyWorker' => 3,
'Geo::EventWorker' => 3,
'Geo::FileDownloadWorker' => 3,
'Geo::FileRegistryRemovalWorker' => 3,
'Geo::FileRemovalWorker' => 3,
- 'Geo::HashedStorageAttachmentsMigrationWorker' => 3,
- 'Geo::HashedStorageMigrationWorker' => 3,
- 'Geo::ProjectSyncWorker' => 3,
+ 'Geo::ProjectSyncWorker' => 1,
'Geo::RenameRepositoryWorker' => 3,
- 'Geo::RepositoriesCleanUpWorker' => 3,
'Geo::RepositoryCleanupWorker' => 3,
'Geo::RepositoryShardSyncWorker' => false,
'Geo::RepositoryVerification::Primary::ShardWorker' => false,
'Geo::RepositoryVerification::Primary::SingleWorker' => false,
'Geo::RepositoryVerification::Secondary::SingleWorker' => false,
'Geo::ReverificationBatchWorker' => 0,
- 'Geo::Scheduler::Primary::SchedulerWorker' => 3,
- 'Geo::Scheduler::SchedulerWorker' => 3,
- 'Geo::Scheduler::Secondary::SchedulerWorker' => 3,
+ 'Geo::Scheduler::Primary::SchedulerWorker' => false,
+ 'Geo::Scheduler::SchedulerWorker' => false,
+ 'Geo::Scheduler::Secondary::SchedulerWorker' => false,
'Geo::VerificationBatchWorker' => 0,
'Geo::VerificationStateBackfillWorker' => false,
'Geo::VerificationTimeoutWorker' => false,