Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-10-04 12:06:14 +00:00
parent 1f1bdf54e1
commit 0d46bf0638
38 changed files with 431 additions and 127 deletions

View file

@ -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"]

View file

@ -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');
}

View file

@ -717,6 +717,7 @@ GitLabDropdown = (function() {
selectedObject = this.renderedData[groupName][selectedIndex];
} else {
selectedIndex = el.closest('li').index();
this.selectedIndex = selectedIndex;
selectedObject = this.renderedData[selectedIndex];
}
}

View file

@ -15,7 +15,9 @@ export default () => {
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
new IssuableTemplateSelectors();
new IssuableTemplateSelectors({
warnTemplateOverride: true,
});
initSuggestions();
};

View file

@ -16,5 +16,7 @@ export default () => {
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
new IssuableTemplateSelectors();
new IssuableTemplateSelectors({
warnTemplateOverride: true,
});
};

View file

@ -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 }}

View file

@ -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,

View file

@ -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,
});
});
}

View file

@ -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

View file

@ -0,0 +1,3 @@
.release-block {
transition: background-color 1s linear;
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -0,0 +1,5 @@
---
title: Warn before applying issue templates
merge_request: 16865
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Allow releases to be targeted by URL anchor links on the Releases page
merge_request: 17150
author:
type: added

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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 ""

View file

@ -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

View file

@ -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);
});
});
});
});

View file

@ -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();

View file

@ -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

View file

@ -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