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: extends:
- .only-canonical-schedules - .only-canonical-schedules
- .notify - .notify
before_script: variables:
- export COMMIT_NOTES_URL="https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list" COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
script: 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' - '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"] needs: ["schedule:package-and-qa"]
@ -21,8 +21,8 @@ schedule:package-and-qa:notify-failure:
extends: extends:
- .only-canonical-schedules - .only-canonical-schedules
- .notify - .notify
before_script: variables:
- export COMMIT_NOTES_URL="https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list" COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
script: 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' - '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"] needs: ["schedule:package-and-qa"]

View File

@ -27,11 +27,16 @@ export default class TemplateSelector {
search: { search: {
fields: ['name'], fields: ['name'],
}, },
clicked: options => this.fetchFileTemplate(options), clicked: options => this.onDropdownClicked(options),
text: item => item.name, text: item => item.name,
}); });
} }
// Subclasses can override this method to conditionally prevent fetching file templates
onDropdownClicked(options) {
this.fetchFileTemplate(options);
}
initAutosizeUpdateEvent() { initAutosizeUpdateEvent() {
this.autosizeUpdateEvent = document.createEvent('Event'); this.autosizeUpdateEvent = document.createEvent('Event');
this.autosizeUpdateEvent.initEvent('autosize:update', true, false); this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
@ -81,6 +86,10 @@ export default class TemplateSelector {
} }
} }
getEditorContent() {
return this.editor.getValue();
}
startLoadingSpinner() { startLoadingSpinner() {
this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down'); this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down');
} }

View File

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

View File

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

View File

@ -16,5 +16,7 @@ export default () => {
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); 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 UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, n__, sprintf } from '../../locale'; 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 { export default {
name: 'ReleaseBlock', name: 'ReleaseBlock',
@ -26,7 +29,15 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
data() {
return {
isHighlighted: false,
};
},
computed: { computed: {
id() {
return slugify(this.release.tag_name);
},
releasedTimeAgo() { releasedTimeAgo() {
return sprintf(__('released %{time}'), { return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.released_at), time: this.timeFormated(this.release.released_at),
@ -62,10 +73,21 @@ export default {
return n__('Milestone', 'Milestones', this.release.milestones.length); 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> </script>
<template> <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"> <div class="card-body">
<h2 class="card-title mt-0"> <h2 class="card-title mt-0">
{{ release.name }} {{ release.name }}

View File

@ -8,10 +8,13 @@ import { __ } from '~/locale';
export default class IssuableTemplateSelector extends TemplateSelector { export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.projectPath = this.dropdown.data('projectPath'); this.projectPath = this.dropdown.data('projectPath');
this.namespacePath = this.dropdown.data('namespacePath'); this.namespacePath = this.dropdown.data('namespacePath');
this.issuableType = this.$dropdownContainer.data('issuableType'); this.issuableType = this.$dropdownContainer.data('issuableType');
this.titleInput = $(`#${this.issuableType}_title`); this.titleInput = $(`#${this.issuableType}_title`);
this.templateWarningEl = $('.js-issuable-template-warning');
this.warnTemplateOverride = args[0].warnTemplateOverride;
const initialQuery = { const initialQuery = {
name: this.dropdown.data('selected'), name: this.dropdown.data('selected'),
@ -24,14 +27,61 @@ export default class IssuableTemplateSelector extends TemplateSelector {
}); });
$('.no-template', this.dropdown.parent()).on('click', () => { $('.no-template', this.dropdown.parent()).on('click', () => {
this.currentTemplate.content = ''; this.reset();
this.setInputValueToTemplateContent();
$('.dropdown-toggle-text', this.dropdown).text(__('Choose a template'));
}); });
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) { requestFile(query) {
this.startLoadingSpinner(); this.startLoadingSpinner();
Api.issueTemplate( Api.issueTemplate(
this.namespacePath, this.namespacePath,
this.projectPath, this.projectPath,

View File

@ -4,7 +4,7 @@ import $ from 'jquery';
import IssuableTemplateSelector from './issuable_template_selector'; import IssuableTemplateSelector from './issuable_template_selector';
export default class IssuableTemplateSelectors { export default class IssuableTemplateSelectors {
constructor({ $dropdowns, editor } = {}) { constructor({ $dropdowns, editor, warnTemplateOverride } = {}) {
this.$dropdowns = $dropdowns || $('.js-issuable-selector'); this.$dropdowns = $dropdowns || $('.js-issuable-selector');
this.editor = editor || this.initEditor(); this.editor = editor || this.initEditor();
@ -16,6 +16,7 @@ export default class IssuableTemplateSelectors {
wrapper: $dropdown.closest('.js-issuable-selector-wrap'), wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
dropdown: $dropdown, dropdown: $dropdown,
editor: this.editor, editor: this.editor,
warnTemplateOverride,
}); });
}); });
} }

View File

@ -211,7 +211,7 @@ export default {
<template v-else> <template v-else>
<review-app-link <review-app-link
:link="deploymentExternalUrl" :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> </template>
<visual-review-app-link <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; background-color: $gray-light;
} }
.bg-line-target-blue {
background: $line-target-blue;
}
.text-break-word { .text-break-word {
word-break: break-all; word-break: break-all;
} }
@ -210,18 +214,26 @@ li.note {
@mixin message($background-color, $border-color, $text-color) { @mixin message($background-color, $border-color, $text-color) {
border-left: 4px solid $border-color; border-left: 4px solid $border-color;
color: $text-color; color: $text-color;
padding: 10px; padding: $gl-padding $gl-padding-24;
margin-bottom: 10px; margin-bottom: $gl-padding-12;
background: $background-color; background-color: $background-color;
padding-left: 20px;
&.centered { &.centered {
text-align: center; text-align: center;
} }
.close {
svg {
width: $gl-font-size-large;
height: $gl-font-size-large;
}
color: inherit;
}
} }
.warning_message { .warning_message {
@include message($orange-100, $orange-200, $orange-700); @include message($orange-100, $orange-200, $orange-800);
} }
.danger_message { .danger_message {

View File

@ -25,7 +25,7 @@ module Ci
belongs_to :merge_request, class_name: 'MergeRequest' belongs_to :merge_request, class_name: 'MergeRequest'
belongs_to :external_pull_request 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 s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
end end

View File

@ -27,53 +27,73 @@ module AtomicInternalId
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do 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 # 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). # 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 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 validates column, presence: presence
define_method("ensure_#{scope}_#{column}!") do define_method("ensure_#{scope}_#{column}!") do
scope_value = association(scope).reader scope_value = internal_id_read_scope(scope)
value = read_attribute(column) value = read_attribute(column)
return value unless scope_value return value unless scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } if value.nil?
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?
# We don't have a value yet and use a InternalId record to generate # We don't have a value yet and use a InternalId record to generate
# the next value. # 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) write_attribute(column, value)
end end
value value
end 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| define_method("#{column}=") do |value|
super(value).tap do |v| super(value).tap do |v|
# Indicate the iid was set from externally # Indicate the iid was set from externally
@iid_needs_tracking = true @internal_id_needs_tracking = true
end end
end end
define_method("reset_#{scope}_#{column}") do define_method("reset_#{scope}_#{column}") do
if value = read_attribute(column) if value = read_attribute(column)
scope_value = association(scope).reader did_reset = InternalId.reset(
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } self,
usage = self.class.table_name.to_sym 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) write_attribute(column, nil)
end end
end end
@ -82,4 +102,18 @@ module AtomicInternalId
end end
end 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 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?) = 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 } } #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 = render 'shared/form_elements/description', model: issuable, form: form, project: project
- if issuable.respond_to?(:confidential) - 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 "name", null: false
t.string "version" t.string "version"
t.integer "package_type", limit: 2, null: false 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" t.index ["project_id"], name: "index_packages_packages_on_project_id"
end end

View File

@ -48,10 +48,9 @@ as appropriate.
## Set a global Git hook for all repositories ## Set a global Git hook for all repositories
To create a Git hook that applies to all of your repositories in 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` your instance, set a global Git hook. Since GitLab will look inside the GitLab Shell
directories are symlinked to GitLab Shell's `hooks` directory, adding any hook `hooks` directory for global hooks, adding any hook there will apply it to all repositories.
to the GitLab Shell `hooks` directory will also apply it to all repositories. Follow Follow the steps below to properly set up a custom hook for all repositories:
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 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 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). - 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. - 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. - 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 ### 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) pipeline](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd)
in order to establish a proper testing environment. 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 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 1 hour for GitLab.com. The following guidelines are not hard rules, they were

View File

@ -312,7 +312,7 @@ module Gitlab
class Importer class Importer
def execute def execute
if import_file && check_version! && restorers.all?(&:restore) && overwrite_project if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
project_tree.restored_project project
else else
raise Projects::ImportService::Error.new(@shared.errors.join(', ')) raise Projects::ImportService::Error.new(@shared.errors.join(', '))
end 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 Everything you should know about how to run end-to-end tests using
[GitLab QA][gitlab-qa] testing framework. [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) [Return to Development documentation](../README.md)
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs [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 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 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) ### 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 fetch --all --tags --prune
sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) 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 ### 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. > [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. 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. 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 of a web application. For more details, see
[Rate limits](../../../security/rate_limits.md). [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**: IP rate limits**:
- Unauthenticated requests - 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. > 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. 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. If you press the comment button next to **Dismiss vulnerability** in the modal,
This comment can not currently be edited or removed, but [future versions](https://gitlab.com/gitlab-org/gitlab/issues/11721) will add this functionality. 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 ### Creating an issue for a vulnerability

View File

@ -19,7 +19,7 @@ module Gitlab
def execute def execute
if import_file && check_version! && restorers.all?(&:restore) && overwrite_project if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
project_tree.restored_project project
else else
raise Projects::ImportService::Error.new(shared.errors.to_sentence) raise Projects::ImportService::Error.new(shared.errors.to_sentence)
end end
@ -55,32 +55,32 @@ module Gitlab
end end
def avatar_restorer def avatar_restorer
Gitlab::ImportExport::AvatarRestorer.new(project: project_tree.restored_project, shared: shared) Gitlab::ImportExport::AvatarRestorer.new(project: project, shared: shared)
end end
def repo_restorer def repo_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path, Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
shared: shared, shared: shared,
project: project_tree.restored_project) project: project)
end end
def wiki_restorer def wiki_restorer
Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path, Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
shared: shared, shared: shared,
project: ProjectWiki.new(project_tree.restored_project), project: ProjectWiki.new(project),
wiki_enabled: project.wiki_enabled?) wiki_enabled: project.wiki_enabled?)
end end
def uploads_restorer def uploads_restorer
Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: shared) Gitlab::ImportExport::UploadsRestorer.new(project: project, shared: shared)
end end
def lfs_restorer def lfs_restorer
Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: shared) Gitlab::ImportExport::LfsRestorer.new(project: project, shared: shared)
end end
def statistics_restorer def statistics_restorer
Gitlab::ImportExport::StatisticsRestorer.new(project: project_tree.restored_project, shared: shared) Gitlab::ImportExport::StatisticsRestorer.new(project: project, shared: shared)
end end
def path_with_namespace def path_with_namespace
@ -105,8 +105,6 @@ module Gitlab
end end
def overwrite_project def overwrite_project
project = project_tree.restored_project
return unless can?(current_user, :admin_namespace, project.namespace) return unless can?(current_user, :admin_namespace, project.namespace)
if overwrite_project? if overwrite_project?

View File

@ -6,19 +6,21 @@ module Gitlab
# Relations which cannot be saved at project level (and have a group assigned) # Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone].freeze GROUP_MODELS = [GroupLabel, Milestone].freeze
attr_reader :user
attr_reader :shared
attr_reader :project
def initialize(user:, shared:, project:) def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json') @path = File.join(shared.export_path, 'project.json')
@user = user @user = user
@shared = shared @shared = shared
@project = project @project = project
@project_id = project.id
@saved = true @saved = true
end end
def restore def restore
begin begin
json = IO.read(@path) @tree_hash = read_tree_hash
@tree_hash = ActiveSupport::JSON.decode(json)
rescue => e rescue => e
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
raise Gitlab::ImportExport::Error.new('Incorrect JSON format') raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
@ -30,26 +32,31 @@ module Gitlab
ActiveRecord::Base.uncached do ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
update_project_params
create_relations create_relations
end end
end end
# ensure that we have latest version of the restore
@project.reload # rubocop:disable Cop/ActiveRecordAssociationReload
true
rescue => e rescue => e
@shared.error(e) @shared.error(e)
false false
end end
def restored_project
return @project unless @tree_hash
@restored_project ||= restore_project
end
private private
def read_tree_hash
json = IO.read(@path)
ActiveSupport::JSON.decode(json)
end
def members_mapper def members_mapper
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members, @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
user: @user, user: @user,
project: restored_project) project: @project)
end end
# A Hash of the imported merge request ID -> imported ID. # 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) 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) save_id_mappings(relation_key, relation_hash_batch, relation_hash)
# Restore the project again, extra query that skips holding the AR objects in memory @project.reset
@restored_project = Project.find(@project_id)
end end
# Older, serialized CI pipeline exports may only have a # Older, serialized CI pipeline exports may only have a
@ -127,12 +133,10 @@ module Gitlab
reader.attributes_finder.find_relations_tree(:project) reader.attributes_finder.find_relations_tree(:project)
end end
def restore_project def update_project_params
Gitlab::Timeless.timeless(@project) do Gitlab::Timeless.timeless(@project) do
@project.update(project_params) @project.update(project_params)
end end
@project
end end
def project_params def project_params
@ -184,18 +188,9 @@ module Gitlab
return if tree_hash[relation_key].blank? return if tree_hash[relation_key].blank?
tree_array = [tree_hash[relation_key]].flatten tree_array = [tree_hash[relation_key]].flatten
null_iid_pipelines = []
# Avoid keeping a possible heavy object in memory once we are done with it # Avoid keeping a possible heavy object in memory once we are done with it
while relation_item = (tree_array.shift || null_iid_pipelines.shift) while relation_item = tree_array.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
remove_feature_dependent_sub_relations(relation_item) remove_feature_dependent_sub_relations(relation_item)
# The transaction at this level is less speedy than one single transaction # The transaction at this level is less speedy than one single transaction
@ -245,7 +240,7 @@ module Gitlab
members_mapper: members_mapper, members_mapper: members_mapper,
merge_requests_mapping: merge_requests_mapping, merge_requests_mapping: merge_requests_mapping,
user: @user, user: @user,
project: @restored_project, project: @project,
excluded_keys: excluded_keys_for_relation(relation_key)) excluded_keys: excluded_keys_for_relation(relation_key))
end.compact end.compact
@ -259,10 +254,6 @@ module Gitlab
def excluded_keys_for_relation(relation) def excluded_keys_for_relation(relation)
reader.attributes_finder.find_excluded_keys(relation) reader.attributes_finder.find_excluded_keys(relation)
end end
def nil_iid_pipeline?(relation_key, relation_item)
relation_key == 'ci_pipelines' && relation_item['iid'].nil?
end
end end
end end
end end

View File

@ -1692,6 +1692,12 @@ msgstr ""
msgid "Apply suggestion" msgid "Apply suggestion"
msgstr "" 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" msgid "Applying command"
msgstr "" msgstr ""

View File

@ -92,6 +92,9 @@ describe 'issuable templates', :js do
context 'user creates a merge request using templates' do context 'user creates a merge request using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' } 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) } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
before do before do
@ -101,6 +104,12 @@ describe 'issuable templates', :js do
template_content, template_content,
message: 'added merge request template', message: 'added merge request template',
branch_name: 'master') 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 visit edit_project_merge_request_path project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title' fill_in :'merge_request[title]', with: 'test merge request title'
end end
@ -111,6 +120,27 @@ describe 'issuable templates', :js do
assert_template assert_template
save_changes save_changes
end 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 end
context 'user creates a merge request from a forked project using templates' do 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 { first } from 'underscore';
import { release } from '../mock_data'; import { release } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue'; 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', () => { describe('Release block', () => {
let wrapper; let wrapper;
@ -159,4 +171,61 @@ describe('Release block', () => {
expect(wrapper.text()).toContain('Upcoming Release'); 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', () => { 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-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', () => { 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-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', () => { it('renders the link to the review app linked to to the first change', () => {
const expectedUrl = deploymentMockData.changes[0].external_url; 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(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
expect(deployUrl).not.toBeNull(); expect(deployUrl).not.toBeNull();

View File

@ -520,20 +520,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
end end
describe '#restored_project' do context 'Minimal JSON' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:tree_hash) { { 'visibility_level' => visibility } } let(:tree_hash) { { 'visibility_level' => visibility } }
let(:restorer) { described_class.new(user: nil, shared: shared, project: project) } let(:restorer) { described_class.new(user: nil, shared: shared, project: project) }
before do before do
restorer.instance_variable_set(:@tree_hash, tree_hash) expect(restorer).to receive(:read_tree_hash) { tree_hash }
end end
context 'no group visibility' do context 'no group visibility' do
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE } let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
it 'uses the project visibility' do 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
end end
@ -544,7 +545,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'uses private visibility' do it 'uses private visibility' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) 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 end
end end
@ -561,7 +563,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC } let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
it 'uses the group visibility' do 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
end end
@ -570,7 +573,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE } let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
it 'uses the project visibility' do 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
end end
@ -579,14 +583,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC } let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
it 'uses the group visibility' do 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
context 'with restricted internal visibility' do context 'with restricted internal visibility' do
it 'sets private visibility' do it 'sets private visibility' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) 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 end
end end

View File

@ -9,6 +9,47 @@ describe AtomicInternalId do
let(:scope_attrs) { { project: milestone.project } } let(:scope_attrs) { { project: milestone.project } }
let(:usage) { :milestones } 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 describe '#ensure_project_iid!' do
subject { milestone.ensure_project_iid! } subject { milestone.ensure_project_iid! }
@ -18,15 +59,6 @@ describe AtomicInternalId do
expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i) expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
end 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 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) 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) expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
end 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
end end