diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-04 12:06:14 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-04 12:06:14 +0000 |
commit | 0d46bf06388d485824bc2f1e736b92b2a8a397e4 (patch) | |
tree | 626a835841722463da4def7905b95e874eb77578 | |
parent | 1f1bdf54e1974f89f3a6ba734ec2c42552e90639 (diff) | |
download | gitlab-ce-0d46bf06388d485824bc2f1e736b92b2a8a397e4.tar.gz |
Add latest changes from gitlab-org/gitlab@master
38 files changed, 430 insertions, 126 deletions
diff --git a/.gitlab/ci/notifications.gitlab-ci.yml b/.gitlab/ci/notifications.gitlab-ci.yml index 21f7312063a..8e00ba022d0 100644 --- a/.gitlab/ci/notifications.gitlab-ci.yml +++ b/.gitlab/ci/notifications.gitlab-ci.yml @@ -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"] diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 02216e4e93d..6cfe354d277 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -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'); } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 437c4941fda..4e1b4f2652c 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -717,6 +717,7 @@ GitLabDropdown = (function() { selectedObject = this.renderedData[groupName][selectedIndex]; } else { selectedIndex = el.closest('li').index(); + this.selectedIndex = selectedIndex; selectedObject = this.renderedData[selectedIndex]; } } diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 2205a7bafe3..96e47187fed 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -15,7 +15,9 @@ export default () => { new IssuableForm($('.issue-form')); new LabelsSelect(); new MilestoneSelect(); - new IssuableTemplateSelectors(); + new IssuableTemplateSelectors({ + warnTemplateOverride: true, + }); initSuggestions(); }; diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index 8f0dc8554e2..e51ab79a51d 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -16,5 +16,7 @@ export default () => { new IssuableForm($('.merge-request-form')); new LabelsSelect(); new MilestoneSelect(); - new IssuableTemplateSelectors(); + new IssuableTemplateSelectors({ + warnTemplateOverride: true, + }); }; diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 7b6bd9913a8..921ada91544 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -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 }} diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 78609ce0610..78a1c4fa8a8 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -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, diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js b/app/assets/javascripts/templates/issuable_template_selectors.js index 50e58ec5c46..443b3084113 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js +++ b/app/assets/javascripts/templates/issuable_template_selectors.js @@ -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, }); }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index bb6921225c2..1873e09c370 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -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 diff --git a/app/assets/stylesheets/components/release_block.scss b/app/assets/stylesheets/components/release_block.scss new file mode 100644 index 00000000000..7e82d0960d7 --- /dev/null +++ b/app/assets/stylesheets/components/release_block.scss @@ -0,0 +1,3 @@ +.release-block { + transition: background-color 1s linear; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 4cd6763e7d7..922051ab0e9 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -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 { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 7fa290610aa..aed95b4601b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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 diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 95de11a72bf..b510129b35d 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -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 diff --git a/app/views/shared/form_elements/_apply_template_warning.html.haml b/app/views/shared/form_elements/_apply_template_warning.html.haml new file mode 100644 index 00000000000..9027264d221 --- /dev/null +++ b/app/views/shared/form_elements/_apply_template_warning.html.haml @@ -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") diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 04a70e406ca..5e2b5f95ee3 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -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) diff --git a/changelogs/unreleased/16188-warn-before-applying-issue-templates.yml b/changelogs/unreleased/16188-warn-before-applying-issue-templates.yml new file mode 100644 index 00000000000..c6cc6cb0992 --- /dev/null +++ b/changelogs/unreleased/16188-warn-before-applying-issue-templates.yml @@ -0,0 +1,5 @@ +--- +title: Warn before applying issue templates +merge_request: 16865 +author: +type: changed diff --git a/changelogs/unreleased/nfriend-allow-release-page-anchor-scrolling.yml b/changelogs/unreleased/nfriend-allow-release-page-anchor-scrolling.yml new file mode 100644 index 00000000000..7b30852eba4 --- /dev/null +++ b/changelogs/unreleased/nfriend-allow-release-page-anchor-scrolling.yml @@ -0,0 +1,5 @@ +--- +title: Allow releases to be targeted by URL anchor links on the Releases page +merge_request: 17150 +author: +type: added diff --git a/db/migrate/20190919183411_add_index_packages_on_name_trigram_to_packages_packages.rb b/db/migrate/20190919183411_add_index_packages_on_name_trigram_to_packages_packages.rb new file mode 100644 index 00000000000..d359350a891 --- /dev/null +++ b/db/migrate/20190919183411_add_index_packages_on_name_trigram_to_packages_packages.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 4ca75ece3f9..b2caca12ce4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 3e714b446af..0702e0aa141 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -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 diff --git a/doc/development/database_review.md b/doc/development/database_review.md index 603c6290311..1ff23e2935e 100644 --- a/doc/development/database_review.md +++ b/doc/development/database_review.md @@ -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 diff --git a/doc/development/import_export.md b/doc/development/import_export.md index 470bad1777e..71f5ce18aae 100644 --- a/doc/development/import_export.md +++ b/doc/development/import_export.md @@ -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 diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index d9d3185a473..25da72b6b6a 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -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 diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 73677b019eb..7314e34666d 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -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) diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index d8c06f372b6..662701dbb56 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -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 diff --git a/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md index 3a6f3a8c20e..29a3591184b 100644 --- a/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md +++ b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md @@ -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. diff --git a/doc/user/admin_area/settings/user_and_ip_rate_limits.md b/doc/user/admin_area/settings/user_and_ip_rate_limits.md index b9d93bf3671..5d49d88d254 100644 --- a/doc/user/admin_area/settings/user_and_ip_rate_limits.md +++ b/doc/user/admin_area/settings/user_and_ip_rate_limits.md @@ -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 diff --git a/doc/user/application_security/img/dismissed_info.png b/doc/user/application_security/img/dismissed_info.png Binary files differdeleted file mode 100644 index b4470b664d2..00000000000 --- a/doc/user/application_security/img/dismissed_info.png +++ /dev/null diff --git a/doc/user/application_security/img/dismissed_info_v12_3.png b/doc/user/application_security/img/dismissed_info_v12_3.png Binary files differnew file mode 100644 index 00000000000..92037493eaa --- /dev/null +++ b/doc/user/application_security/img/dismissed_info_v12_3.png diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 28375510956..e9f5898950e 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -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 diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index f061a1916da..62cf6c86906 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -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? diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 017e536c3e7..edee4ba2486 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -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 + private - @restored_project ||= restore_project + def read_tree_hash + json = IO.read(@path) + ActiveSupport::JSON.decode(json) end - private - 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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e94cde289b0..b62b133f461 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 18eadb7c4a3..c06fee92d09 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -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 diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index 5ce85a37121..1f5da093902 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -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); + }); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index 42bf3b7df09..1949bee1406 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -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(); diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index c619a2ab237..218031784cb 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -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 diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb index 60e7e90e3f1..f9ca9660bdb 100644 --- a/spec/models/concerns/atomic_internal_id_spec.rb +++ b/spec/models/concerns/atomic_internal_id_spec.rb @@ -9,16 +9,10 @@ describe AtomicInternalId do let(:scope_attrs) { { project: milestone.project } } let(:usage) { :milestones } - describe '#ensure_project_iid!' do - subject { milestone.ensure_project_iid! } - - it 'generates a new value if non is present' do - expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid) + describe '#track_project_iid!' do + subject { milestone.track_project_iid! } - 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 + 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) @@ -27,27 +21,51 @@ describe AtomicInternalId do 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) - - milestone.iid = external_iid - milestone.iid = nil + 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 - expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i) - end + it 'does not track the value' do + expect(InternalId).not_to receive(:track_greatest) - context 'with iid_always_track disabled' do - before do - stub_feature_flags(iid_always_track: false) + milestone.ensure_project_iid! + subject + end end - it 'does not track the present value if generated by InternalId.generate_next' do - milestone.ensure_project_iid! + context 'with iid_always_track enabled' do + before do + stub_feature_flags(iid_always_track: true) + end - expect(InternalId).not_to receive(:track_greatest) + it 'does not track the value' do + expect(InternalId).to receive(:track_greatest) - subject + milestone.ensure_project_iid! + subject + end end end end + + describe '#ensure_project_iid!' do + subject { milestone.ensure_project_iid! } + + it 'generates a new value if non is present' do + expect(InternalId).to receive(:generate_next).with(milestone, scope_attrs, usage, anything).and_return(iid) + + expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i) + 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) + + milestone.iid = external_iid + milestone.iid = nil + + expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i) + end + end end |