Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1f1bdf54e1
commit
0d46bf0638
38 changed files with 431 additions and 127 deletions
|
@ -10,8 +10,8 @@ schedule:package-and-qa:notify-success:
|
|||
extends:
|
||||
- .only-canonical-schedules
|
||||
- .notify
|
||||
before_script:
|
||||
- export COMMIT_NOTES_URL="https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
|
||||
variables:
|
||||
COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
|
||||
script:
|
||||
- 'scripts/notify-slack qa-master ":tada: Scheduled QA against master passed! :tada: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_passing'
|
||||
needs: ["schedule:package-and-qa"]
|
||||
|
@ -21,8 +21,8 @@ schedule:package-and-qa:notify-failure:
|
|||
extends:
|
||||
- .only-canonical-schedules
|
||||
- .notify
|
||||
before_script:
|
||||
- export COMMIT_NOTES_URL="https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
|
||||
variables:
|
||||
COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
|
||||
script:
|
||||
- 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_failing'
|
||||
needs: ["schedule:package-and-qa"]
|
||||
|
|
|
@ -27,11 +27,16 @@ export default class TemplateSelector {
|
|||
search: {
|
||||
fields: ['name'],
|
||||
},
|
||||
clicked: options => this.fetchFileTemplate(options),
|
||||
clicked: options => this.onDropdownClicked(options),
|
||||
text: item => item.name,
|
||||
});
|
||||
}
|
||||
|
||||
// Subclasses can override this method to conditionally prevent fetching file templates
|
||||
onDropdownClicked(options) {
|
||||
this.fetchFileTemplate(options);
|
||||
}
|
||||
|
||||
initAutosizeUpdateEvent() {
|
||||
this.autosizeUpdateEvent = document.createEvent('Event');
|
||||
this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
|
||||
|
@ -81,6 +86,10 @@ export default class TemplateSelector {
|
|||
}
|
||||
}
|
||||
|
||||
getEditorContent() {
|
||||
return this.editor.getValue();
|
||||
}
|
||||
|
||||
startLoadingSpinner() {
|
||||
this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
|
||||
}
|
||||
|
|
|
@ -717,6 +717,7 @@ GitLabDropdown = (function() {
|
|||
selectedObject = this.renderedData[groupName][selectedIndex];
|
||||
} else {
|
||||
selectedIndex = el.closest('li').index();
|
||||
this.selectedIndex = selectedIndex;
|
||||
selectedObject = this.renderedData[selectedIndex];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ export default () => {
|
|||
new IssuableForm($('.issue-form'));
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssuableTemplateSelectors();
|
||||
new IssuableTemplateSelectors({
|
||||
warnTemplateOverride: true,
|
||||
});
|
||||
|
||||
initSuggestions();
|
||||
};
|
||||
|
|
|
@ -16,5 +16,7 @@ export default () => {
|
|||
new IssuableForm($('.merge-request-form'));
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssuableTemplateSelectors();
|
||||
new IssuableTemplateSelectors({
|
||||
warnTemplateOverride: true,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,6 +6,9 @@ import Icon from '~/vue_shared/components/icon.vue';
|
|||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { __, n__, sprintf } from '../../locale';
|
||||
import { slugify } from '~/lib/utils/text_utility';
|
||||
import { getLocationHash } from '~/lib/utils/url_utility';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
name: 'ReleaseBlock',
|
||||
|
@ -26,7 +29,15 @@ export default {
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHighlighted: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return slugify(this.release.tag_name);
|
||||
},
|
||||
releasedTimeAgo() {
|
||||
return sprintf(__('released %{time}'), {
|
||||
time: this.timeFormated(this.release.released_at),
|
||||
|
@ -62,10 +73,21 @@ export default {
|
|||
return n__('Milestone', 'Milestones', this.release.milestones.length);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const hash = getLocationHash();
|
||||
if (hash && slugify(hash) === this.id) {
|
||||
this.isHighlighted = true;
|
||||
setTimeout(() => {
|
||||
this.isHighlighted = false;
|
||||
}, 2000);
|
||||
|
||||
scrollToElement(this.$el);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :id="release.tag_name" class="card">
|
||||
<div :id="id" :class="{ 'bg-line-target-blue': isHighlighted }" class="card release-block">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mt-0">
|
||||
{{ release.name }}
|
||||
|
|
|
@ -8,10 +8,13 @@ import { __ } from '~/locale';
|
|||
export default class IssuableTemplateSelector extends TemplateSelector {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.projectPath = this.dropdown.data('projectPath');
|
||||
this.namespacePath = this.dropdown.data('namespacePath');
|
||||
this.issuableType = this.$dropdownContainer.data('issuableType');
|
||||
this.titleInput = $(`#${this.issuableType}_title`);
|
||||
this.templateWarningEl = $('.js-issuable-template-warning');
|
||||
this.warnTemplateOverride = args[0].warnTemplateOverride;
|
||||
|
||||
const initialQuery = {
|
||||
name: this.dropdown.data('selected'),
|
||||
|
@ -24,14 +27,61 @@ export default class IssuableTemplateSelector extends TemplateSelector {
|
|||
});
|
||||
|
||||
$('.no-template', this.dropdown.parent()).on('click', () => {
|
||||
this.currentTemplate.content = '';
|
||||
this.setInputValueToTemplateContent();
|
||||
$('.dropdown-toggle-text', this.dropdown).text(__('Choose a template'));
|
||||
this.reset();
|
||||
});
|
||||
|
||||
this.templateWarningEl.find('.js-close-btn').on('click', () => {
|
||||
if (this.previousSelectedIndex) {
|
||||
this.dropdown.data('glDropdown').selectRowAtIndex(this.previousSelectedIndex);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this.templateWarningEl.addClass('hidden');
|
||||
});
|
||||
|
||||
this.templateWarningEl.find('.js-override-template').on('click', () => {
|
||||
this.requestFile(this.overridingTemplate);
|
||||
this.setSelectedIndex();
|
||||
|
||||
this.templateWarningEl.addClass('hidden');
|
||||
this.overridingTemplate = null;
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.currentTemplate) {
|
||||
this.currentTemplate.content = '';
|
||||
}
|
||||
|
||||
this.setInputValueToTemplateContent();
|
||||
$('.dropdown-toggle-text', this.dropdown).text(__('Choose a template'));
|
||||
this.previousSelectedIndex = null;
|
||||
}
|
||||
|
||||
setSelectedIndex() {
|
||||
this.previousSelectedIndex = this.dropdown.data('glDropdown').selectedIndex;
|
||||
}
|
||||
|
||||
onDropdownClicked(query) {
|
||||
const content = this.getEditorContent();
|
||||
const isContentUnchanged =
|
||||
content === '' || (this.currentTemplate && content === this.currentTemplate.content);
|
||||
|
||||
if (!this.warnTemplateOverride || isContentUnchanged) {
|
||||
super.onDropdownClicked(query);
|
||||
this.setSelectedIndex();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.overridingTemplate = query.selectedObj;
|
||||
this.templateWarningEl.removeClass('hidden');
|
||||
}
|
||||
|
||||
requestFile(query) {
|
||||
this.startLoadingSpinner();
|
||||
|
||||
Api.issueTemplate(
|
||||
this.namespacePath,
|
||||
this.projectPath,
|
||||
|
|
|
@ -4,7 +4,7 @@ import $ from 'jquery';
|
|||
import IssuableTemplateSelector from './issuable_template_selector';
|
||||
|
||||
export default class IssuableTemplateSelectors {
|
||||
constructor({ $dropdowns, editor } = {}) {
|
||||
constructor({ $dropdowns, editor, warnTemplateOverride } = {}) {
|
||||
this.$dropdowns = $dropdowns || $('.js-issuable-selector');
|
||||
this.editor = editor || this.initEditor();
|
||||
|
||||
|
@ -16,6 +16,7 @@ export default class IssuableTemplateSelectors {
|
|||
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
|
||||
dropdown: $dropdown,
|
||||
editor: this.editor,
|
||||
warnTemplateOverride,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ export default {
|
|||
<template v-else>
|
||||
<review-app-link
|
||||
:link="deploymentExternalUrl"
|
||||
css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline"
|
||||
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
|
||||
/>
|
||||
</template>
|
||||
<visual-review-app-link
|
||||
|
|
3
app/assets/stylesheets/components/release_block.scss
Normal file
3
app/assets/stylesheets/components/release_block.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.release-block {
|
||||
transition: background-color 1s linear;
|
||||
}
|
|
@ -55,6 +55,10 @@
|
|||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.bg-line-target-blue {
|
||||
background: $line-target-blue;
|
||||
}
|
||||
|
||||
.text-break-word {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
@ -210,18 +214,26 @@ li.note {
|
|||
@mixin message($background-color, $border-color, $text-color) {
|
||||
border-left: 4px solid $border-color;
|
||||
color: $text-color;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: $background-color;
|
||||
padding-left: 20px;
|
||||
padding: $gl-padding $gl-padding-24;
|
||||
margin-bottom: $gl-padding-12;
|
||||
background-color: $background-color;
|
||||
|
||||
&.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close {
|
||||
svg {
|
||||
width: $gl-font-size-large;
|
||||
height: $gl-font-size-large;
|
||||
}
|
||||
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.warning_message {
|
||||
@include message($orange-100, $orange-200, $orange-700);
|
||||
@include message($orange-100, $orange-200, $orange-800);
|
||||
}
|
||||
|
||||
.danger_message {
|
||||
|
|
|
@ -25,7 +25,7 @@ module Ci
|
|||
belongs_to :merge_request, class_name: 'MergeRequest'
|
||||
belongs_to :external_pull_request
|
||||
|
||||
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
|
||||
has_internal_id :iid, scope: :project, presence: false, ensure_if: -> { !importing? }, init: ->(s) do
|
||||
s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
|
||||
end
|
||||
|
||||
|
|
|
@ -27,53 +27,73 @@ module AtomicInternalId
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName
|
||||
def has_internal_id(column, scope:, init:, ensure_if: nil, presence: true) # rubocop:disable Naming/PredicateName
|
||||
# We require init here to retain the ability to recalculate in the absence of a
|
||||
# InternaLId record (we may delete records in `internal_ids` for example).
|
||||
raise "has_internal_id requires a init block, none given." unless init
|
||||
raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope)
|
||||
|
||||
before_validation :"ensure_#{scope}_#{column}!", on: :create
|
||||
before_validation :"track_#{scope}_#{column}!", on: :create
|
||||
before_validation :"ensure_#{scope}_#{column}!", on: :create, if: ensure_if
|
||||
validates column, presence: presence
|
||||
|
||||
define_method("ensure_#{scope}_#{column}!") do
|
||||
scope_value = association(scope).reader
|
||||
scope_value = internal_id_read_scope(scope)
|
||||
value = read_attribute(column)
|
||||
|
||||
return value unless scope_value
|
||||
|
||||
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
|
||||
usage = self.class.table_name.to_sym
|
||||
|
||||
if value.present? && (@iid_needs_tracking || Feature.enabled?(:iid_always_track, default_enabled: true))
|
||||
# The value was set externally, e.g. by the user
|
||||
# We update the InternalId record to keep track of the greatest value.
|
||||
InternalId.track_greatest(self, scope_attrs, usage, value, init)
|
||||
|
||||
@iid_needs_tracking = false
|
||||
elsif !value.present?
|
||||
if value.nil?
|
||||
# We don't have a value yet and use a InternalId record to generate
|
||||
# the next value.
|
||||
value = InternalId.generate_next(self, scope_attrs, usage, init)
|
||||
value = InternalId.generate_next(
|
||||
self,
|
||||
internal_id_scope_attrs(scope),
|
||||
internal_id_scope_usage,
|
||||
init)
|
||||
write_attribute(column, value)
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
define_method("track_#{scope}_#{column}!") do
|
||||
iid_always_track = Feature.enabled?(:iid_always_track, default_enabled: true)
|
||||
return unless @internal_id_needs_tracking || iid_always_track
|
||||
|
||||
@internal_id_needs_tracking = false
|
||||
|
||||
scope_value = internal_id_read_scope(scope)
|
||||
value = read_attribute(column)
|
||||
return unless scope_value
|
||||
|
||||
if value.present?
|
||||
# The value was set externally, e.g. by the user
|
||||
# We update the InternalId record to keep track of the greatest value.
|
||||
InternalId.track_greatest(
|
||||
self,
|
||||
internal_id_scope_attrs(scope),
|
||||
internal_id_scope_usage,
|
||||
value,
|
||||
init)
|
||||
end
|
||||
end
|
||||
|
||||
define_method("#{column}=") do |value|
|
||||
super(value).tap do |v|
|
||||
# Indicate the iid was set from externally
|
||||
@iid_needs_tracking = true
|
||||
@internal_id_needs_tracking = true
|
||||
end
|
||||
end
|
||||
|
||||
define_method("reset_#{scope}_#{column}") do
|
||||
if value = read_attribute(column)
|
||||
scope_value = association(scope).reader
|
||||
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
|
||||
usage = self.class.table_name.to_sym
|
||||
did_reset = InternalId.reset(
|
||||
self,
|
||||
internal_id_scope_attrs(scope),
|
||||
internal_id_scope_usage,
|
||||
value)
|
||||
|
||||
if InternalId.reset(self, scope_attrs, usage, value)
|
||||
if did_reset
|
||||
write_attribute(column, nil)
|
||||
end
|
||||
end
|
||||
|
@ -82,4 +102,18 @@ module AtomicInternalId
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def internal_id_scope_attrs(scope)
|
||||
scope_value = internal_id_read_scope(scope)
|
||||
|
||||
{ scope_value.class.table_name.singularize.to_sym => scope_value } if scope_value
|
||||
end
|
||||
|
||||
def internal_id_scope_usage
|
||||
self.class.table_name.to_sym
|
||||
end
|
||||
|
||||
def internal_id_read_scope(scope)
|
||||
association(scope).reader
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
.form-group.row.js-template-warning.mb-0.hidden.js-issuable-template-warning
|
||||
.offset-sm-2.col-sm-10
|
||||
|
||||
.warning_message.mb-0{ role: 'alert' }
|
||||
%btn.js-close-btn.close{ type: "button", "aria-hidden": true, "aria-label": _("Close") }
|
||||
= sprite_icon("close")
|
||||
|
||||
%p
|
||||
= _("Applying a template will replace the existing issue description. Any changes you have made will be lost.")
|
||||
|
||||
%button.js-override-template.btn.btn-warning.mr-2{ type: 'button' }
|
||||
= _("Apply template")
|
||||
%button.js-cancel-btn.btn.btn-inverted{ type: 'button' }
|
||||
= _("Cancel")
|
|
@ -19,6 +19,7 @@
|
|||
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
|
||||
#js-suggestions{ data: { project_path: @project.full_path } }
|
||||
|
||||
= render 'shared/form_elements/apply_template_warning'
|
||||
= render 'shared/form_elements/description', model: issuable, form: form, project: project
|
||||
|
||||
- if issuable.respond_to?(:confidential)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Warn before applying issue templates
|
||||
merge_request: 16865
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow releases to be targeted by URL anchor links on the Releases page
|
||||
merge_request: 17150
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexPackagesOnNameTrigramToPackagesPackages < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_packages_packages_on_name_trigram'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :packages_packages, :name, name: INDEX_NAME, using: :gin, opclass: { name: :gin_trgm_ops }
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:packages_packages, INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -2556,6 +2556,7 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do
|
|||
t.string "name", null: false
|
||||
t.string "version"
|
||||
t.integer "package_type", limit: 2, null: false
|
||||
t.index ["name"], name: "index_packages_packages_on_name_trigram", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["project_id"], name: "index_packages_packages_on_project_id"
|
||||
end
|
||||
|
||||
|
|
|
@ -48,10 +48,9 @@ as appropriate.
|
|||
## Set a global Git hook for all repositories
|
||||
|
||||
To create a Git hook that applies to all of your repositories in
|
||||
your instance, set a global Git hook. Since all the repositories' `hooks`
|
||||
directories are symlinked to GitLab Shell's `hooks` directory, adding any hook
|
||||
to the GitLab Shell `hooks` directory will also apply it to all repositories. Follow
|
||||
the steps below to properly set up a custom hook for all repositories:
|
||||
your instance, set a global Git hook. Since GitLab will look inside the GitLab Shell
|
||||
`hooks` directory for global hooks, adding any hook there will apply it to all repositories.
|
||||
Follow the steps below to properly set up a custom hook for all repositories:
|
||||
|
||||
1. On the GitLab server, navigate to the configured custom hook directory. The
|
||||
default is in the GitLab Shell directory. The GitLab Shell `hook` directory
|
||||
|
|
|
@ -78,7 +78,8 @@ and details for a database reviewer:
|
|||
- Format any queries with a SQL query formatter, for example with [sqlformat.darold.net](http://sqlformat.darold.net).
|
||||
- Consider providing query plans via a link to [explain.depesz.com](https://explain.depesz.com) or another tool instead of textual form.
|
||||
- For query changes, it is best to provide the SQL query along with a plan *before* and *after* the change. This helps to spot differences quickly.
|
||||
- When providing query plans, make sure to use good parameter values, so that the query executed is a good example and also hits enough data. Usually, the `gitlab-org` namespace (`namespace_id = 9970`) and the `gitlab-org/gitlab-foss` project (`project_id = 13083`) provides enough data to serve as a good example.
|
||||
- When providing query plans, make sure to use good parameter values, so that the query executed is a good example and also hits enough data.
|
||||
- Usually, the `gitlab-org` namespace (`namespace_id = 9970`) and the `gitlab-org/gitlab-foss` (`project_id = 13083`) or the `gitlab-org/gitlab` (`project_id = 278964`) projects provide enough data to serve as a good example.
|
||||
|
||||
### How to review for database
|
||||
|
||||
|
@ -121,7 +122,7 @@ and details for a database reviewer:
|
|||
pipeline](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd)
|
||||
in order to establish a proper testing environment.
|
||||
|
||||
### Timing guidelines for migrations
|
||||
### Timing guidelines for migrations
|
||||
|
||||
In general, migrations for a single deploy shouldn't take longer than
|
||||
1 hour for GitLab.com. The following guidelines are not hard rules, they were
|
||||
|
|
|
@ -312,7 +312,7 @@ module Gitlab
|
|||
class Importer
|
||||
def execute
|
||||
if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
|
||||
project_tree.restored_project
|
||||
project
|
||||
else
|
||||
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
|
||||
end
|
||||
|
|
|
@ -60,6 +60,10 @@ Everything you should know about how to test Rake tasks.
|
|||
Everything you should know about how to run end-to-end tests using
|
||||
[GitLab QA][gitlab-qa] testing framework.
|
||||
|
||||
## [Migrations tests](testing_migrations_guide.md)
|
||||
|
||||
Everything you should know about how to test migrations.
|
||||
|
||||
[Return to Development documentation](../README.md)
|
||||
|
||||
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
|
||||
|
|
|
@ -87,7 +87,7 @@ cd /home/git/gitlab-shell
|
|||
|
||||
sudo -u git -H git fetch --all --tags
|
||||
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) -b v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
|
||||
sudo -u git -H sh -c 'if [ -x bin/compile ]; then bin/compile; fi'
|
||||
sudo -u git -H make build
|
||||
```
|
||||
|
||||
### 7. Update GitLab Pages to the corresponding version (skip if not using pages)
|
||||
|
|
|
@ -193,7 +193,7 @@ cd /home/git/gitlab-shell
|
|||
|
||||
sudo -u git -H git fetch --all --tags --prune
|
||||
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
|
||||
sudo -u git -H bin/compile
|
||||
sudo -u git -H make build
|
||||
```
|
||||
|
||||
### 9. Update GitLab Workhorse
|
||||
|
|
|
@ -7,7 +7,7 @@ type: reference
|
|||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/30829) in GitLab 12.2.
|
||||
|
||||
This setting allows you to rate limit the requests to raw endpoints, defaults to `300` requests per minute.
|
||||
It can be modified in **Admin Area > Network > Performance Optimization**.
|
||||
It can be modified in **Admin Area > Settings > Network > Performance Optimization**.
|
||||
|
||||
For example, requests over `300` per minute to `https://gitlab.com/gitlab-org/gitlab-foss/raw/master/app/controllers/application_controller.rb` will be blocked. Access to the raw file will be released after 1 minute.
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ Rate limiting is a common technique used to improve the security and durability
|
|||
of a web application. For more details, see
|
||||
[Rate limits](../../../security/rate_limits.md).
|
||||
|
||||
The following limits can be enforced in **Admin Area > Network > User and
|
||||
The following limits can be enforced in **Admin Area > Settings > Network > User and
|
||||
IP rate limits**:
|
||||
|
||||
- Unauthenticated requests
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 7.7 KiB |
BIN
doc/user/application_security/img/dismissed_info_v12_3.png
Normal file
BIN
doc/user/application_security/img/dismissed_info_v12_3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
|
@ -87,10 +87,12 @@ If you wish to undo this dismissal, you can click the **Undo dismiss** button.
|
|||
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
|
||||
|
||||
When dismissing a vulnerability, it's often helpful to provide a reason for doing so.
|
||||
If you press the comment button next to **Dismiss vulnerability** in the modal, a text box will appear, allowing you to add a comment with your dismissal.
|
||||
This comment can not currently be edited or removed, but [future versions](https://gitlab.com/gitlab-org/gitlab/issues/11721) will add this functionality.
|
||||
If you press the comment button next to **Dismiss vulnerability** in the modal,
|
||||
a text box will appear, allowing you to add a comment with your dismissal.
|
||||
Once added, you can edit it or delete it. This allows you to add and update
|
||||
context for a vulnerability as you learn more over time.
|
||||
|
||||
![Dismissed vulnerability comment](img/dismissed_info.png)
|
||||
![Dismissed vulnerability comment](img/dismissed_info_v12_3.png)
|
||||
|
||||
### Creating an issue for a vulnerability
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
|
||||
def execute
|
||||
if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
|
||||
project_tree.restored_project
|
||||
project
|
||||
else
|
||||
raise Projects::ImportService::Error.new(shared.errors.to_sentence)
|
||||
end
|
||||
|
@ -55,32 +55,32 @@ module Gitlab
|
|||
end
|
||||
|
||||
def avatar_restorer
|
||||
Gitlab::ImportExport::AvatarRestorer.new(project: project_tree.restored_project, shared: shared)
|
||||
Gitlab::ImportExport::AvatarRestorer.new(project: project, shared: shared)
|
||||
end
|
||||
|
||||
def repo_restorer
|
||||
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
|
||||
shared: shared,
|
||||
project: project_tree.restored_project)
|
||||
project: project)
|
||||
end
|
||||
|
||||
def wiki_restorer
|
||||
Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
|
||||
shared: shared,
|
||||
project: ProjectWiki.new(project_tree.restored_project),
|
||||
project: ProjectWiki.new(project),
|
||||
wiki_enabled: project.wiki_enabled?)
|
||||
end
|
||||
|
||||
def uploads_restorer
|
||||
Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: shared)
|
||||
Gitlab::ImportExport::UploadsRestorer.new(project: project, shared: shared)
|
||||
end
|
||||
|
||||
def lfs_restorer
|
||||
Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: shared)
|
||||
Gitlab::ImportExport::LfsRestorer.new(project: project, shared: shared)
|
||||
end
|
||||
|
||||
def statistics_restorer
|
||||
Gitlab::ImportExport::StatisticsRestorer.new(project: project_tree.restored_project, shared: shared)
|
||||
Gitlab::ImportExport::StatisticsRestorer.new(project: project, shared: shared)
|
||||
end
|
||||
|
||||
def path_with_namespace
|
||||
|
@ -105,8 +105,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def overwrite_project
|
||||
project = project_tree.restored_project
|
||||
|
||||
return unless can?(current_user, :admin_namespace, project.namespace)
|
||||
|
||||
if overwrite_project?
|
||||
|
|
|
@ -6,19 +6,21 @@ module Gitlab
|
|||
# Relations which cannot be saved at project level (and have a group assigned)
|
||||
GROUP_MODELS = [GroupLabel, Milestone].freeze
|
||||
|
||||
attr_reader :user
|
||||
attr_reader :shared
|
||||
attr_reader :project
|
||||
|
||||
def initialize(user:, shared:, project:)
|
||||
@path = File.join(shared.export_path, 'project.json')
|
||||
@user = user
|
||||
@shared = shared
|
||||
@project = project
|
||||
@project_id = project.id
|
||||
@saved = true
|
||||
end
|
||||
|
||||
def restore
|
||||
begin
|
||||
json = IO.read(@path)
|
||||
@tree_hash = ActiveSupport::JSON.decode(json)
|
||||
@tree_hash = read_tree_hash
|
||||
rescue => e
|
||||
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
|
||||
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
|
||||
|
@ -30,26 +32,31 @@ module Gitlab
|
|||
|
||||
ActiveRecord::Base.uncached do
|
||||
ActiveRecord::Base.no_touching do
|
||||
update_project_params
|
||||
create_relations
|
||||
end
|
||||
end
|
||||
|
||||
# ensure that we have latest version of the restore
|
||||
@project.reload # rubocop:disable Cop/ActiveRecordAssociationReload
|
||||
|
||||
true
|
||||
rescue => e
|
||||
@shared.error(e)
|
||||
false
|
||||
end
|
||||
|
||||
def restored_project
|
||||
return @project unless @tree_hash
|
||||
|
||||
@restored_project ||= restore_project
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_tree_hash
|
||||
json = IO.read(@path)
|
||||
ActiveSupport::JSON.decode(json)
|
||||
end
|
||||
|
||||
def members_mapper
|
||||
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
|
||||
user: @user,
|
||||
project: restored_project)
|
||||
project: @project)
|
||||
end
|
||||
|
||||
# A Hash of the imported merge request ID -> imported ID.
|
||||
|
@ -83,12 +90,11 @@ module Gitlab
|
|||
|
||||
remove_group_models(relation_hash) if relation_hash.is_a?(Array)
|
||||
|
||||
@saved = false unless restored_project.append_or_update_attribute(relation_key, relation_hash)
|
||||
@saved = false unless @project.append_or_update_attribute(relation_key, relation_hash)
|
||||
|
||||
save_id_mappings(relation_key, relation_hash_batch, relation_hash)
|
||||
|
||||
# Restore the project again, extra query that skips holding the AR objects in memory
|
||||
@restored_project = Project.find(@project_id)
|
||||
@project.reset
|
||||
end
|
||||
|
||||
# Older, serialized CI pipeline exports may only have a
|
||||
|
@ -127,12 +133,10 @@ module Gitlab
|
|||
reader.attributes_finder.find_relations_tree(:project)
|
||||
end
|
||||
|
||||
def restore_project
|
||||
def update_project_params
|
||||
Gitlab::Timeless.timeless(@project) do
|
||||
@project.update(project_params)
|
||||
end
|
||||
|
||||
@project
|
||||
end
|
||||
|
||||
def project_params
|
||||
|
@ -184,18 +188,9 @@ module Gitlab
|
|||
return if tree_hash[relation_key].blank?
|
||||
|
||||
tree_array = [tree_hash[relation_key]].flatten
|
||||
null_iid_pipelines = []
|
||||
|
||||
# Avoid keeping a possible heavy object in memory once we are done with it
|
||||
while relation_item = (tree_array.shift || null_iid_pipelines.shift)
|
||||
if nil_iid_pipeline?(relation_key, relation_item) && tree_array.any?
|
||||
# Move pipelines with NULL IIDs to the end
|
||||
# so they don't clash with existing IIDs.
|
||||
null_iid_pipelines << relation_item
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
while relation_item = tree_array.shift
|
||||
remove_feature_dependent_sub_relations(relation_item)
|
||||
|
||||
# The transaction at this level is less speedy than one single transaction
|
||||
|
@ -245,7 +240,7 @@ module Gitlab
|
|||
members_mapper: members_mapper,
|
||||
merge_requests_mapping: merge_requests_mapping,
|
||||
user: @user,
|
||||
project: @restored_project,
|
||||
project: @project,
|
||||
excluded_keys: excluded_keys_for_relation(relation_key))
|
||||
end.compact
|
||||
|
||||
|
@ -259,10 +254,6 @@ module Gitlab
|
|||
def excluded_keys_for_relation(relation)
|
||||
reader.attributes_finder.find_excluded_keys(relation)
|
||||
end
|
||||
|
||||
def nil_iid_pipeline?(relation_key, relation_item)
|
||||
relation_key == 'ci_pipelines' && relation_item['iid'].nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1692,6 +1692,12 @@ msgstr ""
|
|||
msgid "Apply suggestion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply template"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applying a template will replace the existing issue description. Any changes you have made will be lost."
|
||||
msgstr ""
|
||||
|
||||
msgid "Applying command"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -92,6 +92,9 @@ describe 'issuable templates', :js do
|
|||
|
||||
context 'user creates a merge request using templates' do
|
||||
let(:template_content) { 'this is a test "feature-proposal" template' }
|
||||
let(:bug_template_content) { 'this is merge request bug template' }
|
||||
let(:template_override_warning) { 'Applying a template will replace the existing issue description.' }
|
||||
let(:updated_description) { 'updated merge request description' }
|
||||
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
|
||||
|
||||
before do
|
||||
|
@ -101,6 +104,12 @@ describe 'issuable templates', :js do
|
|||
template_content,
|
||||
message: 'added merge request template',
|
||||
branch_name: 'master')
|
||||
project.repository.create_file(
|
||||
user,
|
||||
'.gitlab/merge_request_templates/bug.md',
|
||||
bug_template_content,
|
||||
message: 'added merge request bug template',
|
||||
branch_name: 'master')
|
||||
visit edit_project_merge_request_path project, merge_request
|
||||
fill_in :'merge_request[title]', with: 'test merge request title'
|
||||
end
|
||||
|
@ -111,6 +120,27 @@ describe 'issuable templates', :js do
|
|||
assert_template
|
||||
save_changes
|
||||
end
|
||||
|
||||
context 'changes template' do
|
||||
before do
|
||||
select_template 'bug'
|
||||
wait_for_requests
|
||||
fill_in :'merge_request[description]', with: updated_description
|
||||
select_template 'feature-proposal'
|
||||
expect(page).to have_content template_override_warning
|
||||
end
|
||||
|
||||
it 'user selects "bug" template, then updates description, then selects "feature-proposal" template, then cancels template change' do
|
||||
page.find('.js-template-warning .js-cancel-btn').click
|
||||
expect(find('textarea')['value']).to eq(updated_description)
|
||||
end
|
||||
|
||||
it 'user selects "bug" template, then updates description, then selects "feature-proposal" template, then applies template change' do
|
||||
page.find('.js-template-warning .js-override-template').click
|
||||
wait_for_requests
|
||||
assert_template
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user creates a merge request from a forked project using templates' do
|
||||
|
|
|
@ -4,6 +4,18 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
|
|||
import { first } from 'underscore';
|
||||
import { release } from '../mock_data';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
|
||||
let mockLocationHash;
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
__esModule: true,
|
||||
getLocationHash: jest.fn().mockImplementation(() => mockLocationHash),
|
||||
}));
|
||||
|
||||
jest.mock('~/lib/utils/common_utils', () => ({
|
||||
__esModule: true,
|
||||
scrollToElement: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Release block', () => {
|
||||
let wrapper;
|
||||
|
@ -159,4 +171,61 @@ describe('Release block', () => {
|
|||
|
||||
expect(wrapper.text()).toContain('Upcoming Release');
|
||||
});
|
||||
|
||||
it('slugifies the tag_name before setting it as the elements ID', () => {
|
||||
const releaseClone = JSON.parse(JSON.stringify(release));
|
||||
releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
|
||||
|
||||
factory(releaseClone);
|
||||
|
||||
expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
|
||||
});
|
||||
|
||||
describe('anchor scrolling', () => {
|
||||
beforeEach(() => {
|
||||
scrollToElement.mockClear();
|
||||
});
|
||||
|
||||
const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue');
|
||||
|
||||
it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
|
||||
mockLocationHash = '';
|
||||
factory(release);
|
||||
|
||||
expect(scrollToElement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
|
||||
mockLocationHash = 'v0.4';
|
||||
factory(release);
|
||||
|
||||
expect(scrollToElement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
|
||||
mockLocationHash = release.tag_name;
|
||||
factory(release);
|
||||
|
||||
expect(scrollToElement).toHaveBeenCalledTimes(1);
|
||||
expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
|
||||
});
|
||||
|
||||
it('renders with a light blue background if it is the target of the anchor', () => {
|
||||
mockLocationHash = release.tag_name;
|
||||
factory(release);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(hasTargetBlueBackground()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render with a light blue background if it is not the target of the anchor', () => {
|
||||
mockLocationHash = '';
|
||||
factory(release);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(hasTargetBlueBackground()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -207,7 +207,7 @@ describe('Deployment component', () => {
|
|||
|
||||
it('renders the link to the review app without dropdown', () => {
|
||||
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
|
||||
expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
|
||||
expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -223,12 +223,12 @@ describe('Deployment component', () => {
|
|||
|
||||
it('renders the link to the review app without dropdown', () => {
|
||||
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
|
||||
expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
|
||||
expect(vm.$el.querySelector('.js-deploy-url')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the link to the review app linked to to the first change', () => {
|
||||
const expectedUrl = deploymentMockData.changes[0].external_url;
|
||||
const deployUrl = vm.$el.querySelector('.js-deploy-url-feature-flag');
|
||||
const deployUrl = vm.$el.querySelector('.js-deploy-url');
|
||||
|
||||
expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
|
||||
expect(deployUrl).not.toBeNull();
|
||||
|
|
|
@ -520,20 +520,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#restored_project' do
|
||||
context 'Minimal JSON' do
|
||||
let(:project) { create(:project) }
|
||||
let(:tree_hash) { { 'visibility_level' => visibility } }
|
||||
let(:restorer) { described_class.new(user: nil, shared: shared, project: project) }
|
||||
|
||||
before do
|
||||
restorer.instance_variable_set(:@tree_hash, tree_hash)
|
||||
expect(restorer).to receive(:read_tree_hash) { tree_hash }
|
||||
end
|
||||
|
||||
context 'no group visibility' do
|
||||
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
|
||||
|
||||
it 'uses the project visibility' do
|
||||
expect(restorer.restored_project.visibility_level).to eq(visibility)
|
||||
expect(restorer.restore).to eq(true)
|
||||
expect(restorer.project.visibility_level).to eq(visibility)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -544,7 +545,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
it 'uses private visibility' do
|
||||
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||
|
||||
expect(restorer.restored_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
expect(restorer.restore).to eq(true)
|
||||
expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -561,7 +563,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
|
||||
|
||||
it 'uses the group visibility' do
|
||||
expect(restorer.restored_project.visibility_level).to eq(group_visibility)
|
||||
expect(restorer.restore).to eq(true)
|
||||
expect(restorer.project.visibility_level).to eq(group_visibility)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -570,7 +573,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
|
||||
|
||||
it 'uses the project visibility' do
|
||||
expect(restorer.restored_project.visibility_level).to eq(visibility)
|
||||
expect(restorer.restore).to eq(true)
|
||||
expect(restorer.project.visibility_level).to eq(visibility)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -579,14 +583,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
|
|||
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
|
||||
|
||||
it 'uses the group visibility' do
|
||||
expect(restorer.restored_project.visibility_level).to eq(group_visibility)
|
||||
expect(restorer.restore).to eq(true)
|
||||
expect(restorer.project.visibility_level).to eq(group_visibility)
|
||||
end
|
||||
|
||||
context 'with restricted internal visibility' do
|
||||
it 'sets private visibility' do
|
||||
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||
|
||||
expect(restorer.restored_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
expect(restorer.restore).to eq(true)
|
||||
expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,47 @@ describe AtomicInternalId do
|
|||
let(:scope_attrs) { { project: milestone.project } }
|
||||
let(:usage) { :milestones }
|
||||
|
||||
describe '#track_project_iid!' do
|
||||
subject { milestone.track_project_iid! }
|
||||
|
||||
it 'tracks the present value' do
|
||||
milestone.iid = external_iid
|
||||
|
||||
expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
|
||||
expect(InternalId).not_to receive(:generate_next)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'when value is set by ensure_project_iid!' do
|
||||
context 'with iid_always_track true' do
|
||||
before do
|
||||
stub_feature_flags(iid_always_track: false)
|
||||
end
|
||||
|
||||
it 'does not track the value' do
|
||||
expect(InternalId).not_to receive(:track_greatest)
|
||||
|
||||
milestone.ensure_project_iid!
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with iid_always_track enabled' do
|
||||
before do
|
||||
stub_feature_flags(iid_always_track: true)
|
||||
end
|
||||
|
||||
it 'does not track the value' do
|
||||
expect(InternalId).to receive(:track_greatest)
|
||||
|
||||
milestone.ensure_project_iid!
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ensure_project_iid!' do
|
||||
subject { milestone.ensure_project_iid! }
|
||||
|
||||
|
@ -18,15 +59,6 @@ describe AtomicInternalId do
|
|||
expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
|
||||
end
|
||||
|
||||
it 'tracks the present value if not generated by InternalId.generate_next' do
|
||||
milestone.iid = external_iid
|
||||
|
||||
expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
|
||||
expect(InternalId).not_to receive(:generate_next)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'generates a new value if first set with iid= but later set to nil' do
|
||||
expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid)
|
||||
|
||||
|
@ -35,19 +67,5 @@ describe AtomicInternalId do
|
|||
|
||||
expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
|
||||
end
|
||||
|
||||
context 'with iid_always_track disabled' do
|
||||
before do
|
||||
stub_feature_flags(iid_always_track: false)
|
||||
end
|
||||
|
||||
it 'does not track the present value if generated by InternalId.generate_next' do
|
||||
milestone.ensure_project_iid!
|
||||
|
||||
expect(InternalId).not_to receive(:track_greatest)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue