summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-21 09:09:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-21 09:09:01 +0000
commita53d2c37c4934f564caa94543dd4cf5af1703e2d (patch)
treea028dc39771a4612a9845ab700a73af2d6f3f51b
parent18b8435318887d3fc6e9f9d305967a953cdd7d3f (diff)
downloadgitlab-ce-a53d2c37c4934f564caa94543dd4cf5af1703e2d.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml1
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue9
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue19
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue4
-rw-r--r--app/assets/javascripts/right_sidebar.js34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue19
-rw-r--r--changelogs/unreleased/13810-cluster-environments-table.yml5
-rw-r--r--changelogs/unreleased/195969-multi-select-on-issue-boards-inconsistent-erratic.yml5
-rw-r--r--changelogs/unreleased/205435.yml5
-rw-r--r--changelogs/unreleased/28560_cleanup_optimistic_locking_db.yml5
-rw-r--r--db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb44
-rw-r--r--db/schema.rb3
-rw-r--r--doc/.linting/vale/styles/gitlab/OxfordComma.yml2
-rw-r--r--doc/.linting/vale/styles/gitlab/SentenceSpacing.yml36
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/git_protocol.md26
-rw-r--r--doc/administration/job_artifacts.md10
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/ci/parent_child_pipelines.md46
-rw-r--r--doc/user/application_security/container_scanning/index.md6
-rw-r--r--doc/user/group/issues_analytics/index.md5
-rw-r--r--lib/gitlab/background_migration/cleanup_optimistic_locking_nulls.rb32
-rw-r--r--lib/gitlab/database/migration_helpers.rb5
-rw-r--r--spec/features/projects/navbar_spec.rb203
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js103
-rw-r--r--spec/frontend/frequent_items/mock_data.js9
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js94
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb9
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_spec.rb53
29 files changed, 505 insertions, 291 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 964aa8e523f..688cfb5033e 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -168,6 +168,7 @@ dast:
# DAST_USERNAME: "root"
# DAST_USERNAME_FIELD: "user[login]"
# DAST_PASSWORD_FIELD: "user[passowrd]"
+ DAST_VERSION: 1
allow_failure: true
script:
- 'export DAST_WEBSITE="${DAST_WEBSITE:-$(cat environment_url.txt)}"'
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 12d68256598..0fc60528eb6 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -65,11 +65,12 @@ export default {
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
- if (this.showDetail) {
- this.showDetail = false;
- // If CMD or CTRL is clicked
- const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
+ // If CMD or CTRL is clicked
+ const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
+
+ if (this.showDetail || isMultiSelect) {
+ this.showDetail = false;
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue', isMultiSelect);
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 3276d85f1cd..c0dadedbc51 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,6 +1,5 @@
<script>
/* eslint-disable vue/require-default-prop */
-import { isEmpty, isString } from 'lodash';
import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -38,9 +37,6 @@ export default {
},
},
computed: {
- hasAvatar() {
- return isString(this.avatarUrl) && !isEmpty(this.avatarUrl);
- },
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
@@ -54,8 +50,11 @@ export default {
<template>
<li class="frequent-items-list-item-container">
<a :href="webUrl" class="clearfix">
- <div class="frequent-items-item-avatar-container avatar-container rect-avatar s32">
- <img v-if="hasAvatar" :src="avatarUrl" class="avatar s32" />
+ <div
+ ref="frequentItemsItemAvatarContainer"
+ class="frequent-items-item-avatar-container avatar-container rect-avatar s32"
+ >
+ <img v-if="avatarUrl" ref="frequentItemsItemAvatar" :src="avatarUrl" class="avatar s32" />
<identicon
v-else
:entity-id="itemId"
@@ -64,16 +63,18 @@ export default {
class="rect-avatar"
/>
</div>
- <div class="frequent-items-item-metadata-container">
+ <div ref="frequentItemsItemMetadataContainer" class="frequent-items-item-metadata-container">
<div
+ ref="frequentItemsItemTitle"
:title="itemName"
- class="frequent-items-item-title js-frequent-items-item-title"
+ class="frequent-items-item-title"
v-html="highlightedItemName"
></div>
<div
v-if="namespace"
+ ref="frequentItemsItemNamespace"
:title="namespace"
- class="frequent-items-item-namespace js-frequent-items-item-namespace"
+ class="frequent-items-item-namespace"
>
{{ truncatedNamespace }}
</div>
diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
index 4cd320d5d66..8dc22620eca 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
@@ -31,8 +31,8 @@ export default {
<template>
<dropdown-button>
- <span class="row">
- <span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }">
+ <span class="row flex-nowrap">
+ <span class="col-auto flex-fill text-truncate">
<icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span>
<span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index fa5649679d7..550ec3cb0d1 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, consistent-return, no-else-return, no-param-reassign */
+/* eslint-disable func-names, consistent-return, no-param-reassign */
import $ from 'jquery';
import _ from 'underscore';
@@ -34,8 +34,6 @@ Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
- $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
- $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
return $(document)
@@ -133,36 +131,6 @@ Sidebar.prototype.todoUpdateDone = function(data) {
});
};
-Sidebar.prototype.sidebarDropdownLoading = function() {
- const $sidebarCollapsedIcon = $(this)
- .closest('.block')
- .find('.sidebar-collapsed-icon');
- const img = $sidebarCollapsedIcon.find('img');
- const i = $sidebarCollapsedIcon.find('i');
- const $loading = $('<i class="fa fa-spinner fa-spin"></i>');
- if (img.length) {
- img.before($loading);
- return img.hide();
- } else if (i.length) {
- i.before($loading);
- return i.hide();
- }
-};
-
-Sidebar.prototype.sidebarDropdownLoaded = function() {
- const $sidebarCollapsedIcon = $(this)
- .closest('.block')
- .find('.sidebar-collapsed-icon');
- const img = $sidebarCollapsedIcon.find('img');
- $sidebarCollapsedIcon.find('i.fa-spin').remove();
- const i = $sidebarCollapsedIcon.find('i');
- if (img.length) {
- return img.show();
- } else {
- return i.show();
- }
-};
-
Sidebar.prototype.sidebarCollapseClicked = function(e) {
if ($(e.currentTarget).hasClass('dont-change-state')) {
return;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 18d4073ecd4..e1a1b1022e3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLink } from '@gitlab/ui';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
import ReviewAppLink from '../review_app_link.vue';
@@ -6,6 +7,7 @@ export default {
name: 'DeploymentViewButton',
components: {
FilteredSearchDropdown,
+ GlLink,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
@@ -67,7 +69,7 @@ export default {
</template>
<template slot="result" slot-scope="slotProps">
- <a
+ <gl-link
:href="slotProps.result.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
@@ -80,16 +82,15 @@ export default {
<p class="text-secondary str-truncated-100 append-bottom-0 d-block">
{{ slotProps.result.external_url }}
</p>
- </a>
+ </gl-link>
</template>
</filtered-search-dropdown>
- <template v-else>
- <review-app-link
- :display="appButtonText"
- :link="deploymentExternalUrl"
- css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
- />
- </template>
+ <review-app-link
+ v-else
+ :display="appButtonText"
+ :link="deploymentExternalUrl"
+ css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
+ />
<visual-review-app-link
v-if="showVisualReviewApp"
:link="deploymentExternalUrl"
diff --git a/changelogs/unreleased/13810-cluster-environments-table.yml b/changelogs/unreleased/13810-cluster-environments-table.yml
new file mode 100644
index 00000000000..bbcebec9106
--- /dev/null
+++ b/changelogs/unreleased/13810-cluster-environments-table.yml
@@ -0,0 +1,5 @@
+---
+title: Add responsivity to cluster environments table
+merge_request: 25501
+author:
+type: fixed
diff --git a/changelogs/unreleased/195969-multi-select-on-issue-boards-inconsistent-erratic.yml b/changelogs/unreleased/195969-multi-select-on-issue-boards-inconsistent-erratic.yml
new file mode 100644
index 00000000000..614379d7ad8
--- /dev/null
+++ b/changelogs/unreleased/195969-multi-select-on-issue-boards-inconsistent-erratic.yml
@@ -0,0 +1,5 @@
+---
+title: Improved selection of multiple cards
+merge_request:
+author: Gwen_
+type: fixed
diff --git a/changelogs/unreleased/205435.yml b/changelogs/unreleased/205435.yml
new file mode 100644
index 00000000000..c004bdf35c3
--- /dev/null
+++ b/changelogs/unreleased/205435.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up conditional `col-` classes in `nav_dropdown_button.vue`
+merge_request: 25312
+author:
+type: other
diff --git a/changelogs/unreleased/28560_cleanup_optimistic_locking_db.yml b/changelogs/unreleased/28560_cleanup_optimistic_locking_db.yml
new file mode 100644
index 00000000000..30d1b6ce94d
--- /dev/null
+++ b/changelogs/unreleased/28560_cleanup_optimistic_locking_db.yml
@@ -0,0 +1,5 @@
+---
+title: Set all NULL `lock_version` values to 0 for issuables
+merge_request: 18418
+author:
+type: fixed
diff --git a/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb b/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb
new file mode 100644
index 00000000000..8bc037c7333
--- /dev/null
+++ b/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupOptimisticLockingNulls < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TABLES = %w(epics merge_requests issues).freeze
+ BATCH_SIZE = 10_000
+
+ def declare_class(table)
+ Class.new(ActiveRecord::Base) do
+ include EachBatch
+
+ self.table_name = table
+ self.inheritance_column = :_type_disabled # Disable STI
+ end
+ end
+
+ def up
+ TABLES.each do |table|
+ add_concurrent_index table.to_sym, :lock_version, where: "lock_version IS NULL"
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ declare_class(table).where(lock_version: nil),
+ 'CleanupOptimisticLockingNulls',
+ 2.minutes,
+ batch_size: BATCH_SIZE,
+ other_arguments: [table]
+ )
+ end
+ end
+
+ def down
+ TABLES.each do |table|
+ remove_concurrent_index table.to_sym, :lock_version, where: "lock_version IS NULL"
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 36f15e000f7..0b3eab9797e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1574,6 +1574,7 @@ ActiveRecord::Schema.define(version: 2020_02_20_180944) do
t.index ["end_date"], name: "index_epics_on_end_date"
t.index ["group_id"], name: "index_epics_on_group_id"
t.index ["iid"], name: "index_epics_on_iid"
+ t.index ["lock_version"], name: "index_epics_on_lock_version", where: "(lock_version IS NULL)"
t.index ["parent_id"], name: "index_epics_on_parent_id"
t.index ["start_date"], name: "index_epics_on_start_date"
t.index ["start_date_sourcing_epic_id"], name: "index_epics_on_start_date_sourcing_epic_id", where: "(start_date_sourcing_epic_id IS NOT NULL)"
@@ -2193,6 +2194,7 @@ ActiveRecord::Schema.define(version: 2020_02_20_180944) do
t.index ["confidential"], name: "index_issues_on_confidential"
t.index ["description"], name: "index_issues_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["duplicated_to_id"], name: "index_issues_on_duplicated_to_id", where: "(duplicated_to_id IS NOT NULL)"
+ t.index ["lock_version"], name: "index_issues_on_lock_version", where: "(lock_version IS NULL)"
t.index ["milestone_id"], name: "index_issues_on_milestone_id"
t.index ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)"
t.index ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state"
@@ -2611,6 +2613,7 @@ ActiveRecord::Schema.define(version: 2020_02_20_180944) do
t.index ["id", "merge_jid"], name: "idx_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND (state_id = 4))"
t.index ["id", "merge_jid"], name: "index_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND ((state)::text = 'locked'::text))"
t.index ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id"
+ t.index ["lock_version"], name: "index_merge_requests_on_lock_version", where: "(lock_version IS NULL)"
t.index ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)"
t.index ["milestone_id"], name: "index_merge_requests_on_milestone_id"
t.index ["source_branch"], name: "index_merge_requests_on_source_branch"
diff --git a/doc/.linting/vale/styles/gitlab/OxfordComma.yml b/doc/.linting/vale/styles/gitlab/OxfordComma.yml
index c9f4d2895d1..4b37ba8c2b9 100644
--- a/doc/.linting/vale/styles/gitlab/OxfordComma.yml
+++ b/doc/.linting/vale/styles/gitlab/OxfordComma.yml
@@ -8,4 +8,4 @@ message: Use a comma before the last "and" or "or" in a list of four or more ite
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#punctuation
level: warning
raw:
- - '(?:[\w-_` ]+,){2,}(?:[\w-_` ]+) (and|or)'
+ - '(?:[\w-_` ]+,){2,}(?:[\w-_` ]+) (and |or )'
diff --git a/doc/.linting/vale/styles/gitlab/SentenceSpacing.yml b/doc/.linting/vale/styles/gitlab/SentenceSpacing.yml
index 5efc6ceeef5..0fc8bfedadc 100644
--- a/doc/.linting/vale/styles/gitlab/SentenceSpacing.yml
+++ b/doc/.linting/vale/styles/gitlab/SentenceSpacing.yml
@@ -1,32 +1,12 @@
---
-# `extends` indicates the Vale extension point being used.
-# Full list of styles: https://errata-ai.github.io/vale/styles/
+# Checks the presence of more than one space between sentences or clauses.
+#
+# For a list of all options, see https://errata-ai.github.io/vale/styles/
extends: existence
-
-# Existence rules can display the matched strings in the user message.
-message: "'%s' should have one space between sentences."
-
-# Should a result be flagged as a suggestion, warning, or error?
-# Results that fall below the MinAlertLevel set in
-# https://gitlab.com/gitlab-org/gitlab/blob/master/.vale.ini won't be shown.
-level: suggestion
-
-# Should a match be case-insensitive or case-sensitive?
-# Acceptable values are 'true' or 'false'
-# This value is irrelevant when testing non-alphabetical characters
-#ignorecase: true
-
-# Should this rule be limited to a specific scope? If yes, uncomment the line.
-# Possible scopes: https://errata-ai.github.io/vale/formats/#available-scopes
-# scope: heading
-
-# Should this rule ignore normal word boundaries, such as \b ?
-# Acceptable values are 'true' or 'false'
-nonword: true
-
-# What is the source for this rule?
+message: "'%s' should have one space between sentences or clauses."
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#punctuation
-
+level: warning
+nonword: true
tokens:
- - '[a-z][.?!][A-Z]'
- - '[.?!] {2,}[A-Z]'
+ - '[a-z][.?!,][A-Z]'
+ - '[.?!,] {2,}[\w]'
diff --git a/doc/README.md b/doc/README.md
index 132351f5353..3efba168f48 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -112,7 +112,7 @@ The following documentation relates to the DevOps **Plan** stage:
| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable threads in issues, commits, and merge requests. |
| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. |
| [Epics](user/group/epics/index.md) **(ULTIMATE)** | Tracking groups of issues that share a theme. |
-| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/managing_issues.md#moving-issues) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. |
+| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/managing_issues.md#moving-issues) | Project issues and restricting access to issues as well as creating templates for submitting new issues and merge requests. Also, moving issues between projects. |
| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. |
| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. |
| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md
index 2e5c362a3ab..b15c3160459 100644
--- a/doc/administration/git_protocol.md
+++ b/doc/administration/git_protocol.md
@@ -4,17 +4,9 @@ description: "Set and configure Git protocol v2"
# Configuring Git Protocol v2
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/46555) in GitLab 11.4.
-> Temporarily disabled (see [confidential issue](../user/project/issues/confidential_issues.md)
-> `https://gitlab.com/gitlab-org/gitlab-foss/issues/55769`) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+.
-
-NOTE: **Note:**
-Git protocol v2 support has been temporarily disabled
-because a feature used to hide certain internal references does not function when it
-is enabled, and this has a security impact. Once this problem has been resolved,
-protocol v2 support will be re-enabled. For more information, see the
-[confidential issue](../user/project/issues/confidential_issues.md)
-`https://gitlab.com/gitlab-org/gitlab-foss/issues/55769`.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/46555) in GitLab 11.4.
+> - [Temporarily disabled](https://gitlab.com/gitlab-org/gitlab-foss/issues/55769) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+.
+> - [Re-enabled](https://gitlab.com/gitlab-org/gitlab/issues/27828) in GitLab 12.8.
Git protocol v2 improves the v1 wire protocol in several ways and is
enabled by default in GitLab for HTTP requests. In order to enable SSH,
@@ -110,3 +102,15 @@ debug1: Sending env GIT_PROTOCOL = version=2
For the server side, you can use the [same examples from HTTP](#http-connections), changing the
URL to use SSH.
+
+### Observe Git protocol version of connections
+
+To observe what Git protocol versions are being used in a
+production environment, you can use the following Prometheus query:
+
+```prometheus
+sum(rate(gitaly_git_protocol_requests_total[1m])) by (grpc_method,git_protocol,grpc_service)
+```
+
+You can view what Git protocol versions are being used on GitLab.com at
+<https://dashboards.gitlab.com/d/pqlQq0xik/git-protocol-versions>.
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 441ad2186f6..0c0be1da3c0 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -160,6 +160,11 @@ _The artifacts are stored by default in
gitlab-rake gitlab:artifacts:migrate
```
+CAUTION: **CAUTION:**
+JUnit test report artifact (`junit.xml.gz`) migration
+[is not supported](https://gitlab.com/gitlab-org/gitlab/issues/27698)
+by the `gitlab:artifacts:migrate` script.
+
**In installations from source:**
_The artifacts are stored by default in
@@ -188,6 +193,11 @@ _The artifacts are stored by default in
sudo -u git -H bundle exec rake gitlab:artifacts:migrate RAILS_ENV=production
```
+CAUTION: **CAUTION:**
+JUnit test report artifact (`junit.xml.gz`) migration
+[is not supported](https://gitlab.com/gitlab-org/gitlab/issues/27698)
+by the `gitlab:artifacts:migrate` script.
+
### Migrating from object storage to local storage
In order to migrate back to local storage:
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 946626859f8..14e04b9dd17 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -498,7 +498,7 @@ Parameters:
## Transfer project to group
-Transfer a project to the Group namespace. Available only to instance administrators. Transferring projects may fail when tagged packages exist in the project's repository.
+Transfer a project to the Group namespace. Available only to instance administrators. Transferring projects may fail when tagged packages exist in the project's repository.
```
POST /groups/:id/projects/:project_id
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
index 387f2e69606..f8d100b83b8 100644
--- a/doc/ci/parent_child_pipelines.md
+++ b/doc/ci/parent_child_pipelines.md
@@ -80,6 +80,52 @@ microservice_a:
strategy: depend
```
+## Merge Request child pipelines
+
+To trigger a child pipeline as a [Merge Request Pipeline](merge_request_pipelines/index.md) we need to:
+
+- Set the trigger job to run on merge requests:
+
+```yaml
+# parent .gitlab-ci.yml
+microservice_a:
+ trigger:
+ include: path/to/microservice_a.yml
+ rules:
+ - if: $CI_MERGE_REQUEST_ID
+```
+
+- Configure the child pipeline by either:
+
+ - Setting all jobs in the child pipeline to evaluate in the context of a merge request:
+
+ ```yaml
+ # child path/to/microservice_a.yml
+ workflow:
+ rules:
+ - if: $CI_MERGE_REQUEST_ID
+
+ job1:
+ script: ...
+
+ job2:
+ script: ...
+ ```
+
+ - Alternatively, setting the rule per job. For example, to create only `job1` in
+ the context of merge request pipelines:
+
+ ```yaml
+ # child path/to/microservice_a.yml
+ job1:
+ script: ...
+ rules:
+ - if: $CI_MERGE_REQUEST_ID
+
+ job2:
+ script: ...
+ ```
+
## Limitations
A parent pipeline can trigger many child pipelines, but a child pipeline cannot trigger
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 8fc3ea4ee98..861c0e53103 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -176,7 +176,7 @@ using environment variables.
| `CI_APPLICATION_TAG` | Docker respository tag for the image to be scanned. | `$CI_COMMIT_SHA` |
| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [Postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise air-gapped installation. | `arminc/clair-db:latest` |
| `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [Postgres server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` |
-| `DOCKERFILE_PATH` | The path to the `Dockerfile` to be used for generating remediations. By default, the scanner will look for a file named `Dockerfile` in the root directory of the project, so this variable should only be configured if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | `Dockerfile` |
+| `DOCKERFILE_PATH` | The path to the `Dockerfile` to be used for generating remediations. By default, the scanner will look for a file named `Dockerfile` in the root directory of the project, so this variable should only be configured if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | `Dockerfile` |
## Security Dashboard
@@ -352,7 +352,7 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
-| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Container Scanning etc.). For Container Scanning, it will always be `container_scanning`. |
+| `vulnerabilities[].category` | Where this vulnerability belongs (for example, SAST or Container Scanning). For Container Scanning, it will always be `container_scanning`. |
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
@@ -388,7 +388,7 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
### docker: Error response from daemon: failed to copy xattrs
When the GitLab Runner uses the Docker executor and NFS is used
-(e.g., `/var/lib/docker` is on an NFS mount), Container Scanning might fail with
+(for example, `/var/lib/docker` is on an NFS mount), Container Scanning might fail with
an error like the following:
```text
diff --git a/doc/user/group/issues_analytics/index.md b/doc/user/group/issues_analytics/index.md
index 6c710885b98..4477b9bb1e6 100644
--- a/doc/user/group/issues_analytics/index.md
+++ b/doc/user/group/issues_analytics/index.md
@@ -4,13 +4,14 @@ type: reference
# Issues Analytics **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/196561) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9 at the project level.
Issues Analytics is a bar graph which illustrates the number of issues created each month.
The default timespan is 13 months, which includes the current month, and the 12 months
prior.
-To access the chart, navigate to a group's sidebar and select **Analytics > Issues Analytics**.
+To access the chart, navigate to your group or project sidebar and select **{chart}** **Analytics > Issues Analytics**.
Hover over each bar to see the total number of issues.
diff --git a/lib/gitlab/background_migration/cleanup_optimistic_locking_nulls.rb b/lib/gitlab/background_migration/cleanup_optimistic_locking_nulls.rb
new file mode 100644
index 00000000000..3aa1ebb49f9
--- /dev/null
+++ b/lib/gitlab/background_migration/cleanup_optimistic_locking_nulls.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class CleanupOptimisticLockingNulls
+ QUERY_ITEM_SIZE = 1_000
+
+ # table - The name of the table the migration is performed for.
+ # start_id - The ID of the object to start at
+ # stop_id - The ID of the object to end at
+ def perform(start_id, stop_id, table)
+ model = define_model_for(table)
+
+ # After analysis done, a batch size of 1,000 items per query was found to be
+ # the most optimal. Discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18418#note_282285336
+ (start_id..stop_id).each_slice(QUERY_ITEM_SIZE).each do |range|
+ model
+ .where(lock_version: nil)
+ .where(id: range)
+ .update_all(lock_version: 0)
+ end
+ end
+
+ def define_model_for(table)
+ Class.new(ActiveRecord::Base) do
+ self.table_name = table
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 6317e034cfb..95a562ca1f3 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1042,6 +1042,7 @@ into similar problems in the future (e.g. when new tables are created).
# job_class_name - The background migration job class as a string
# delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
# batch_size - The maximum number of rows per job
+ # other_arguments - Other arguments to send to the job
#
# Example:
#
@@ -1059,7 +1060,7 @@ into similar problems in the future (e.g. when new tables are created).
# # do something
# end
# end
- def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_arguments: [])
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
# To not overload the worker too much we enforce a minimum interval both
@@ -1074,7 +1075,7 @@ into similar problems in the future (e.g. when new tables are created).
# `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
# spread the work over time.
- migrate_in(delay_interval * index, job_class_name, [start_id, end_id])
+ migrate_in(delay_interval * index, job_class_name, [start_id, end_id] + other_arguments)
end
end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index bcb05e1c718..4ab6b0ce506 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -3,102 +3,123 @@
require 'spec_helper'
describe 'Project navbar' do
- it_behaves_like 'verified navigation bar' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
- let(:structure) do
- [
- {
- nav_item: _('Project overview'),
- nav_sub_items: [
- _('Details'),
- _('Activity'),
- _('Releases')
- ]
- },
- {
- nav_item: _('Repository'),
- nav_sub_items: [
- _('Files'),
- _('Commits'),
- _('Branches'),
- _('Tags'),
- _('Contributors'),
- _('Graph'),
- _('Compare'),
- (_('Locked Files') if Gitlab.ee?)
- ]
- },
- {
- nav_item: _('Issues'),
- nav_sub_items: [
- _('List'),
- _('Boards'),
- _('Labels'),
- _('Milestones')
- ]
- },
- {
- nav_item: _('Merge Requests'),
- nav_sub_items: []
- },
- {
- nav_item: _('CI / CD'),
- nav_sub_items: [
- _('Pipelines'),
- _('Jobs'),
- _('Artifacts'),
- _('Schedules')
- ]
- },
- {
- nav_item: _('Operations'),
- nav_sub_items: [
- _('Metrics'),
- _('Environments'),
- _('Error Tracking'),
- _('Serverless'),
- _('Kubernetes')
- ]
- },
- {
- nav_item: _('Analytics'),
- nav_sub_items: [
- _('CI / CD Analytics'),
- (_('Code Review') if Gitlab.ee?),
- _('Repository Analytics'),
- _('Value Stream Analytics')
- ]
- },
- {
- nav_item: _('Wiki'),
- nav_sub_items: []
- },
- {
- nav_item: _('Snippets'),
- nav_sub_items: []
- },
- {
- nav_item: _('Settings'),
- nav_sub_items: [
- _('General'),
- _('Members'),
- _('Integrations'),
- _('Repository'),
- _('CI / CD'),
- _('Operations'),
- (_('Audit Events') if Gitlab.ee?)
- ].compact
- }
+ let(:analytics_nav_item) do
+ {
+ nav_item: _('Analytics'),
+ nav_sub_items: [
+ _('CI / CD Analytics'),
+ (_('Code Review') if Gitlab.ee?),
+ _('Repository Analytics'),
+ _('Value Stream Analytics')
]
- end
+ }
+ end
- before do
- project.add_maintainer(user)
- sign_in(user)
+ let(:structure) do
+ [
+ {
+ nav_item: _('Project overview'),
+ nav_sub_items: [
+ _('Details'),
+ _('Activity'),
+ _('Releases')
+ ]
+ },
+ {
+ nav_item: _('Repository'),
+ nav_sub_items: [
+ _('Files'),
+ _('Commits'),
+ _('Branches'),
+ _('Tags'),
+ _('Contributors'),
+ _('Graph'),
+ _('Compare'),
+ (_('Locked Files') if Gitlab.ee?)
+ ]
+ },
+ {
+ nav_item: _('Issues'),
+ nav_sub_items: [
+ _('List'),
+ _('Boards'),
+ _('Labels'),
+ _('Milestones')
+ ]
+ },
+ {
+ nav_item: _('Merge Requests'),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _('CI / CD'),
+ nav_sub_items: [
+ _('Pipelines'),
+ _('Jobs'),
+ _('Artifacts'),
+ _('Schedules')
+ ]
+ },
+ {
+ nav_item: _('Operations'),
+ nav_sub_items: [
+ _('Metrics'),
+ _('Environments'),
+ _('Error Tracking'),
+ _('Serverless'),
+ _('Kubernetes')
+ ]
+ },
+ analytics_nav_item,
+ {
+ nav_item: _('Wiki'),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _('Snippets'),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _('Settings'),
+ nav_sub_items: [
+ _('General'),
+ _('Members'),
+ _('Integrations'),
+ _('Repository'),
+ _('CI / CD'),
+ _('Operations'),
+ (_('Audit Events') if Gitlab.ee?)
+ ].compact
+ }
+ ]
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+ it_behaves_like 'verified navigation bar' do
+ before do
visit project_path(project)
end
end
+
+ if Gitlab.ee?
+ context 'when issues analytics is available' do
+ before do
+ stub_licensed_features(issues_analytics: true)
+
+ analytics_nav_item[:nav_sub_items] << _('Issues Analytics')
+ analytics_nav_item[:nav_sub_items].sort!
+
+ visit project_path(project)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
+ end
end
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
new file mode 100644
index 00000000000..925699f5623
--- /dev/null
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -0,0 +1,103 @@
+import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
+import mockData from '../mock_data'; // can also use 'mockGroup', but not useful to test here
+
+const mockProject = mockData();
+
+describe('FrequentItemsListItemComponent', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
+ const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' });
+ const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
+ const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
+ const findAllAnchors = () => wrapper.findAll('a');
+ const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
+ const findAvatarContainer = () => wrapper.findAll({ ref: 'frequentItemsItemAvatarContainer' });
+ const findAllMetadataContainers = () =>
+ wrapper.findAll({ ref: 'frequentItemsItemMetadataContainer' });
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(frequentItemsListItemComponent, {
+ propsData: {
+ itemId: mockProject.id,
+ itemName: mockProject.name,
+ namespace: mockProject.namespace,
+ webUrl: mockProject.webUrl,
+ avatarUrl: mockProject.avatarUrl,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('computed', () => {
+ describe('highlightedItemName', () => {
+ it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
+ createComponent({ matcher: 'lab' });
+
+ expect(findTitle().element.innerHTML).toContain('<b>L</b><b>a</b><b>b</b>');
+ });
+
+ it('should return project name as it is if `matcher` is not available', () => {
+ createComponent({ matcher: null });
+
+ expect(trimText(findTitle().text())).toBe(mockProject.name);
+ });
+ });
+
+ describe('truncatedNamespace', () => {
+ it('should truncate project name from namespace string', () => {
+ createComponent({ namespace: 'platform / nokia-3310' });
+
+ expect(trimText(findNamespace().text())).toBe('platform');
+ });
+
+ it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
+ createComponent({
+ namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310',
+ });
+
+ expect(trimText(findNamespace().text())).toBe('platform / ... / Mobile Chipset');
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render avatar if avatarUrl is present', () => {
+ wrapper.setProps({ avatarUrl: 'path/to/avatar.png' });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findAvatar().exists()).toBe(true);
+ });
+ });
+
+ it('should not render avatar if avatarUrl is not present', () => {
+ expect(findAvatar().exists()).toBe(false);
+ });
+
+ it('renders root element with the right classes', () => {
+ expect(wrapper.classes('frequent-items-list-item-container')).toBe(true);
+ });
+
+ it.each`
+ name | selector | expected
+ ${'anchor'} | ${findAllAnchors} | ${1}
+ ${'avatar container'} | ${findAvatarContainer} | ${1}
+ ${'metadata container'} | ${findAllMetadataContainers} | ${1}
+ ${'title'} | ${findAllTitles} | ${1}
+ ${'namespace'} | ${findAllNamespace} | ${1}
+ `('should render $expected $name', ({ selector, expected }) => {
+ expect(selector()).toHaveLength(expected);
+ });
+ });
+});
diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js
new file mode 100644
index 00000000000..81f34053543
--- /dev/null
+++ b/spec/frontend/frequent_items/mock_data.js
@@ -0,0 +1,9 @@
+import { TEST_HOST } from 'helpers/test_constants';
+
+export default () => ({
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
+ avatarUrl: null,
+});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
deleted file mode 100644
index e3f05e89a2d..00000000000
--- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { trimText } from 'spec/helpers/text_helper';
-import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
-import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
-
-const localVue = createLocalVue();
-
-describe('FrequentItemsListItemComponent', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(localVue.extend(frequentItemsListItemComponent), {
- propsData: {
- itemId: mockProject.id,
- itemName: mockProject.name,
- namespace: mockProject.namespace,
- webUrl: mockProject.webUrl,
- avatarUrl: mockProject.avatarUrl,
- ...props,
- },
- localVue,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('computed', () => {
- describe('hasAvatar', () => {
- it('should return `true` or `false` if whether avatar is present or not', () => {
- createComponent({ avatarUrl: 'path/to/avatar.png' });
-
- expect(wrapper.vm.hasAvatar).toBe(true);
- });
-
- it('should return `false` if avatar is not present', () => {
- createComponent({ avatarUrl: null });
-
- expect(wrapper.vm.hasAvatar).toBe(false);
- });
- });
-
- describe('highlightedItemName', () => {
- it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
- createComponent({ matcher: 'lab' });
-
- expect(wrapper.find('.js-frequent-items-item-title').html()).toContain(
- '<b>L</b><b>a</b><b>b</b>',
- );
- });
-
- it('should return project name as it is if `matcher` is not available', () => {
- createComponent({ matcher: null });
-
- expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe(
- mockProject.name,
- );
- });
- });
-
- describe('truncatedNamespace', () => {
- it('should truncate project name from namespace string', () => {
- createComponent({ namespace: 'platform / nokia-3310' });
-
- expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform');
- });
-
- it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
- createComponent({
- namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310',
- });
-
- expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe(
- 'platform / ... / Mobile Chipset',
- );
- });
- });
- });
-
- describe('template', () => {
- it('should render component element', () => {
- createComponent();
-
- expect(wrapper.classes()).toContain('frequent-items-list-item-container');
- expect(wrapper.findAll('a').length).toBe(1);
- expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1);
- expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1);
- expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1);
- expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1);
- });
- });
-});
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index bb5475130cf..ce6e8c731e2 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1332,6 +1332,15 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ context 'with other_arguments option' do
+ it 'queues jobs correctly' do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_arguments: [1, 2])
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ end
+ end
end
context "when the model doesn't have an ID column" do
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
new file mode 100644
index 00000000000..bec8435b2f0
--- /dev/null
+++ b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200128210353_cleanup_optimistic_locking_nulls')
+
+describe CleanupOptimisticLockingNulls, :migration do
+ TABLES = %w(epics merge_requests issues).freeze
+ TABLES.each do |table|
+ let(table.to_sym) { table(table.to_sym) }
+ end
+ let(:tables) { TABLES.map { |t| method(t.to_sym).call } }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users)}
+
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ users.create!(id: 123, username: 'author', projects_limit: 1000)
+
+ # Create necessary rows
+ epics.create!(iid: 123, group_id: 123, author_id: 123, title: 'a', title_html: 'a')
+ merge_requests.create!(iid: 123, target_project_id: 123, source_project_id: 123, target_branch: 'master', source_branch: 'hmm', title: 'a', title_html: 'a')
+ issues.create!(iid: 123, project_id: 123, title: 'a', title_html: 'a')
+
+ # Nullify `lock_version` column for all rows
+ # Needs to be done with a SQL fragment, otherwise Rails will coerce it to 0
+ tables.each do |table|
+ table.update_all('lock_version = NULL')
+ end
+ end
+
+ it 'correctly migrates nullified lock_version column', :sidekiq_inline do
+ tables.each do |table|
+ expect(table.where(lock_version: nil).count).to eq(1)
+ end
+
+ tables.each do |table|
+ expect(table.where(lock_version: 0).count).to eq(0)
+ end
+
+ migrate!
+
+ tables.each do |table|
+ expect(table.where(lock_version: nil).count).to eq(0)
+ end
+
+ tables.each do |table|
+ expect(table.where(lock_version: 0).count).to eq(1)
+ end
+ end
+end