diff options
38 files changed, 1144 insertions, 957 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 7e3515b1f4b..66cb9fd7672 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -46,7 +46,6 @@ export default class Shortcuts { $(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) { $(this).remove(); - $('.hidden-shortcut').show(); e.preventDefault(); }); } @@ -104,7 +103,6 @@ export default class Shortcuts { return results; } - $('.hidden-shortcut').show(); return $('.js-more-help-button').remove(); }); } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index c8eb96a625c..f7b327b2af1 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -6,7 +6,7 @@ import { CopyAsGFM } from '../markdown/copy_as_gfm'; import { getSelectedFragment } from '~/lib/utils/common_utils'; export default class ShortcutsIssuable extends Shortcuts { - constructor(isMergeRequest) { + constructor() { super(); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); @@ -14,12 +14,6 @@ export default class ShortcutsIssuable extends Shortcuts { Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText); Mousetrap.bind('e', ShortcutsIssuable.editIssue); - - if (isMergeRequest) { - this.enabledHelp.push('.hidden-shortcut.merge_requests'); - } else { - this.enabledHelp.push('.hidden-shortcut.issues'); - } } static replyWithSelectedText() { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index bef1553703b..b46b4132ba8 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -23,7 +23,5 @@ export default class ShortcutsNavigation extends Shortcuts { Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); - - this.enabledHelp.push('.hidden-shortcut.project'); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js index a88c280fa3b..3e791e4673a 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js @@ -11,7 +11,5 @@ export default class ShortcutsNetwork extends ShortcutsNavigation { Mousetrap.bind(['down', 'j'], graph.scrollDown); Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop); Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom); - - this.enabledHelp.push('.hidden-shortcut.network'); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js index 208c91a1f08..8b7e6a56d25 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js @@ -6,8 +6,6 @@ export default class ShortcutsWiki extends ShortcutsNavigation { constructor() { super(); Mousetrap.bind('e', ShortcutsWiki.editWiki); - - this.enabledHelp.push('.hidden-shortcut.wiki'); } static editWiki() { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index fd9a75bc5b6..9c924559135 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -2,6 +2,12 @@ max-width: 98%; } +.modal-1040 { + @include media-breakpoint-up(xl) { + max-width: 1040px; + } +} + .modal-header { background-color: $modal-body-bg; diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb index ef42f7c4074..188805c6106 100644 --- a/app/controllers/clusters/base_controller.rb +++ b/app/controllers/clusters/base_controller.rb @@ -31,6 +31,10 @@ class Clusters::BaseController < ApplicationController access_denied! unless can?(current_user, :create_cluster, clusterable) end + def authorize_read_prometheus! + access_denied! unless can?(current_user, :read_prometheus, clusterable) + end + def clusterable raise NotImplementedError end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index d5e459311f7..f10fadfdf49 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true module UserCalloutsHelper - GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze - GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze - SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'.freeze + GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration' + GCP_SIGNUP_OFFER = 'gcp_signup_offer' + SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed' def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 12ce717efd7..a2a471074a9 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -17,7 +17,7 @@ class PagesDomain < ApplicationRecord validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? } validates :key, presence: { message: 'must be present if HTTPS-only is enabled' }, if: :certificate_should_be_present? - validates :key, certificate_key: true, if: ->(domain) { domain.key.present? } + validates :key, certificate_key: true, named_ecdsa_key: true, if: ->(domain) { domain.key.present? } validates :verification_code, presence: true, allow_blank: false validate :validate_pages_domain @@ -247,7 +247,7 @@ class PagesDomain < ApplicationRecord def pkey return unless key - @pkey ||= OpenSSL::PKey::RSA.new(key) + @pkey ||= OpenSSL::PKey.read(key) rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError nil end diff --git a/app/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb index bd7ff413afe..c8e6c973bf5 100644 --- a/app/policies/clusters/instance_policy.rb +++ b/app/policies/clusters/instance_policy.rb @@ -8,6 +8,7 @@ module Clusters enable :create_cluster enable :update_cluster enable :admin_cluster + enable :read_prometheus end end end diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb index 5b2bbffc066..b9d54d9636e 100644 --- a/app/validators/certificate_key_validator.rb +++ b/app/validators/certificate_key_validator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# UrlValidator +# CertificateKeyValidator # # Custom validator for private keys. # @@ -20,7 +20,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator def valid_private_key_pem?(value) return false unless value - pkey = OpenSSL::PKey::RSA.new(value) + pkey = OpenSSL::PKey.read(value) pkey.private? rescue OpenSSL::PKey::PKeyError false diff --git a/app/validators/named_ecdsa_key_validator.rb b/app/validators/named_ecdsa_key_validator.rb new file mode 100644 index 00000000000..42ee02b6ad4 --- /dev/null +++ b/app/validators/named_ecdsa_key_validator.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# NamedEcdsaKeyValidator +# +# Custom validator for ecdsa private keys. +# Golang currently doesn't support explicit curves for ECDSA certificates +# This validator checks if curve is set by name, not by parameters +# +# class Project < ActiveRecord::Base +# validates :certificate_key, named_ecdsa_key: true +# end +# +class NamedEcdsaKeyValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if explicit_ec?(value) + record.errors.add(attribute, "ECDSA keys with explicit curves are not supported") + end + end + + private + + UNNAMED_CURVE = "UNDEF" + + def explicit_ec?(value) + return false unless value + + pkey = OpenSSL::PKey.read(value) + return false unless pkey.is_a?(OpenSSL::PKey::EC) + + pkey.group.curve_name == UNNAMED_CURVE + rescue OpenSSL::PKey::PKeyError + false + end +end diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index a996c86a256..f1ba804f920 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -1,5 +1,5 @@ #modal-shortcuts.modal{ tabindex: -1 } - .modal-dialog.modal-lg + .modal-dialog.modal-lg.modal-1040 .modal-content .modal-header %h4.modal-title @@ -11,104 +11,100 @@ .modal-body .row .col-lg-4 - %table.shortcut-mappings + %table.shortcut-mappings.text-2 %tbody %tr %th %th= _('Global Shortcuts') %tr %td.shortcut - %kbd s - %td= _('Focus Search') + %kbd ? + %td= _('Toggle this dialog') %tr %td.shortcut - %kbd f - %td= _('Focus Filter') + %kbd shift p + %td= _('Go to your projects') %tr %td.shortcut - %kbd p - %kbd b - %td= _('Toggle the Performance Bar') + %kbd shift g + %td= _('Go to your groups') %tr %td.shortcut - %kbd ? - %td= _('Show/hide this dialog') + %kbd shift a + %td= _('Go to the activity feed') %tr %td.shortcut - - if browser.platform.mac? - %kbd ⌘ shift p - - else - %kbd ctrl shift p - %td= _('Toggle Markdown preview') + %kbd shift l + %td= _('Go to the milestone list') %tr %td.shortcut - %kbd - %i.fa.fa-arrow-up - %td= _('Edit last comment (when focused on an empty textarea)') + %kbd shift s + %td= _('Go to your snippets') %tr %td.shortcut - %kbd shift t - %td - = _('Go to todos') + %kbd s + %td= _('Start search') %tr %td.shortcut - %kbd shift a - %td - = _('Go to the activity feed') + %kbd shift i + %td= _('Go to your issues') %tr %td.shortcut - %kbd shift p - %td - = _('Go to projects') + %kbd shift m + %td= _('Go to your merge requests') %tr %td.shortcut - %kbd shift i - %td - = _('Go to issues') + %kbd shift t + %td= _('Go to your To-Do list') %tr %td.shortcut - %kbd shift m - %td - = _('Go to merge requests') + %kbd p + %kbd b + %td= _('Toggle the Performance Bar') + %tbody %tr - %td.shortcut - %kbd shift g - %td - = _('Go to groups') + %th + %th= _('Web IDE') %tr %td.shortcut - %kbd shift l - %td - = _('Go to milestones') + - if browser.platform.mac? + %kbd ⌘ p + - else + %kbd ctrl p + %td= _('Go to file') %tr %td.shortcut - %kbd shift s - %td - = _('Go to snippets') + - if browser.platform.mac? + %kbd ⌘ enter + - else + %kbd ctrl enter + %td= _('Commit (when editing commit message)') %tbody %tr %th - %th= _('Finding Project File') + %th= _('Wiki pages') %tr %td.shortcut - %kbd - %i.fa.fa-arrow-up - %td= _('Move selection up') + %kbd e + %td= _('Edit wiki page') + %tbody %tr - %td.shortcut - %kbd - %i.fa.fa-arrow-down - %td= _('Move selection down') + %th + %th= _('Editing') %tr %td.shortcut - %kbd enter - %td= _('Open Selection') + - if browser.platform.mac? + %kbd ⌘ shift p + - else + %kbd ctrl shift p + %td= _('Toggle Markdown preview') %tr %td.shortcut - %kbd esc - %td= _('Go back') + %kbd + %i.fa.fa-arrow-up + %td= _('Edit your most recent comment in a thread (from an empty textarea)') .col-lg-4 - %table.shortcut-mappings + %table.shortcut-mappings.text-2 %tbody %tr %th @@ -117,105 +113,94 @@ %td.shortcut %kbd g %kbd p - %td - = _('Go to the project\'s overview page') + %td= _('Go to the project\'s overview page') %tr %td.shortcut %kbd g %kbd v - %td - = _('Go to the project\'s activity feed') + %td= _('Go to the project\'s activity feed') %tr %td.shortcut %kbd g - %kbd f - %td - = _('Go to files') + %kbd r + %td= _('Go to releases') %tr %td.shortcut %kbd g - %kbd c - %td - = _('Go to commits') + %kbd f + %td= _('Go to files') + %tr + %td.shortcut + %kbd t + %td= _('Go to find file') %tr %td.shortcut %kbd g - %kbd j - %td - = _('Go to jobs') + %kbd c + %td= _('Go to commits') %tr %td.shortcut %kbd g %kbd n - %td - = _('Go to network graph') + %td= _('Go to repository graph') %tr %td.shortcut %kbd g %kbd d - %td - = _('Go to repository charts') + %td= _('Go to repository charts') %tr %td.shortcut %kbd g %kbd i - %td - = _('Go to issues') + %td= _('Go to issues') + %tr + %td.shortcut + %kbd i + %td= _('New issue') %tr %td.shortcut %kbd g %kbd b - %td - = _('Go to issue boards') + %td= _('Go to issue boards') %tr %td.shortcut %kbd g %kbd m - %td - = _('Go to merge requests') + %td= _('Go to merge requests') %tr %td.shortcut %kbd g - %kbd e - %td - = _('Go to environments') + %kbd j + %td= _('Go to jobs') %tr %td.shortcut %kbd g %kbd l - %td - = _('Go to metrics') + %td= _('Go to metrics') + %tr + %td.shortcut + %kbd g + %kbd e + %td= _('Go to environments') %tr %td.shortcut %kbd g %kbd k - %td - = _('Go to kubernetes') + %td= _('Go to kubernetes') %tr %td.shortcut %kbd g %kbd s - %td - = _('Go to snippets') + %td= _('Go to snippets') %tr %td.shortcut %kbd g %kbd w - %td - = _('Go to wiki') - %tr - %td.shortcut - %kbd t - %td= _('Go to finding file') - %tr - %td.shortcut - %kbd i - %td= _('New issue') - + %td= _('Go to wiki') %tbody %tr %th - %th= _('Project Files browsing') + %th= _('Project Files') %tr %td.shortcut %kbd @@ -230,38 +215,87 @@ %td.shortcut %kbd enter %td= _('Open Selection') - %tbody %tr - %th - %th= _('Project File') + %td.shortcut + %kbd esc + %td= _('Go back (while searching for files') %tr %td.shortcut %kbd y - %td= _('Go to file permalink') + %td= _('Go to file permalink (while viewing a file)') + .col-lg-4 + %table.shortcut-mappings.text-2 %tbody %tr %th - %th= _('Web IDE') + %th= _('Issues / Merge Requests') + %tr + %td.shortcut + %kbd a + %td= _('Change assignee') + %tr + %td.shortcut + %kbd m + %td= _('Change milestone') + %tr + %td.shortcut + %kbd r + %td= _('Comment/Reply (quoting selected text)') + %tr + %td.shortcut + %kbd e + %td= _('Edit description') + %tr + %td.shortcut + %kbd l + %td= _('Change label') + %tr + %td.shortcut + %kbd ] + \/ + %kbd j + %td= _('Next file in diff (MRs only)') + %tr + %td.shortcut + %kbd [ + \/ + %kbd k + %td= _('Previous file in diff (MRs only)') %tr %td.shortcut - if browser.platform.mac? %kbd ⌘ p - else %kbd ctrl p - %td= _('Go to file') + %td= _('Go to file (MRs only)') %tr %td.shortcut - - if browser.platform.mac? - %kbd ⌘ enter - - else - %kbd ctrl enter - %td= _('Commit (when editing commit message)') - .col-lg-4 - %table.shortcut-mappings - %tbody.hidden-shortcut.network{ style: 'display:none' } + %kbd n + %td= _('Next unresolved discussion (MRs only)') + %tr + %td.shortcut + %kbd p + %td= _('Previous unresolved discussion (MRs only)') + %tbody %tr %th - %th= _('Network Graph') + %th= _('Epics (Ultimate / Gold license only)') + %tr + %td.shortcut + %kbd r + %td= _('Comment/Reply (quoting selected text)') + %tr + %td.shortcut + %kbd e + %td= _('Edit epic description') + %tr + %td.shortcut + %kbd l + %td= _('Change label') + %tbody + %tr + %th + %th= _('Repository Graph') %tr %td.shortcut %kbd @@ -295,92 +329,12 @@ %kbd shift %i.fa.fa-arrow-up - \/ - %kbd - shift k + \/ k %td= _('Scroll to top') %tr %td.shortcut %kbd shift %i.fa.fa-arrow-down - \/ - %kbd - shift j + \/ j %td= _('Scroll to bottom') - %tbody.hidden-shortcut.issues{ style: 'display:none' } - %tr - %th - %th= _('Issues') - %tr - %td.shortcut - %kbd a - %td= _('Change assignee') - %tr - %td.shortcut - %kbd m - %td= _('Change milestone') - %tr - %td.shortcut - %kbd r - %td= _('Reply (quoting selected text)') - %tr - %td.shortcut - %kbd e - %td= _('Edit issue') - %tr - %td.shortcut - %kbd l - %td= _('Change Label') - %tbody.hidden-shortcut.merge_requests{ style: 'display:none' } - %tr - %th - %th= _('Merge Requests') - %tr - %td.shortcut - %kbd a - %td= _('Change assignee') - %tr - %td.shortcut - %kbd m - %td= _('Change milestone') - %tr - %td.shortcut - %kbd r - %td= _('Reply (quoting selected text)') - %tr - %td.shortcut - %kbd e - %td= _('Edit merge request') - %tr - %td.shortcut - %kbd l - %td= _('Change Label') - %tr - %td.shortcut - %kbd ] - \/ - %kbd j - %td= _('Move to next file') - %tr - %td.shortcut - %kbd [ - \/ - %kbd k - %td= _('Move to previous file') - %tr - %td.shortcut - %kbd n - %td= _('Move to next unresolved discussion') - %tr - %td.shortcut - %kbd p - %td= _('Move to previous unresolved discussion') - %tbody.hidden-shortcut.wiki{ style: 'display:none' } - %tr - %th - %th= _('Wiki pages') - %tr - %td.shortcut - %kbd e - %td= _('Edit wiki page') diff --git a/changelogs/unreleased/ecdsa_pages_certificates.yml b/changelogs/unreleased/ecdsa_pages_certificates.yml new file mode 100644 index 00000000000..059cb434b62 --- /dev/null +++ b/changelogs/unreleased/ecdsa_pages_certificates.yml @@ -0,0 +1,5 @@ +--- +title: Allow ECDSA certificates for pages domains +merge_request: 32393 +author: +type: added diff --git a/changelogs/unreleased/keyboard-shortcuts-2.yml b/changelogs/unreleased/keyboard-shortcuts-2.yml new file mode 100644 index 00000000000..a6a2266b20a --- /dev/null +++ b/changelogs/unreleased/keyboard-shortcuts-2.yml @@ -0,0 +1,5 @@ +--- +title: Clean up keyboard shortcuts help modal, removing and adding as needed +merge_request: 31642 +author: +type: other diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 6dbfd5404d0..5c348702ba2 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -7,9 +7,9 @@ installations from source you'll have to configure it yourself. To enable the GitLab Prometheus metrics: 1. Log into GitLab as an administrator, and go to the Admin area. -1. Click on the gear, then click on Settings. -1. Find the `Metrics - Prometheus` section, and click `Enable Prometheus Metrics` -1. [Restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart) for the changes to take effect +1. Navigate to GitLab's **Settings > Metrics and profiling**. +1. Find the **Metrics - Prometheus** section, and click **Enable Prometheus Metrics**. +1. [Restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart) for the changes to take effect. ## Collecting the metrics diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index 5d08bf5e77d..2ec733182f8 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -1,109 +1,134 @@ +--- +type: reference +--- + # GitLab keyboard shortcuts -You can see GitLab's keyboard shortcuts by using <kbd>shift</kbd> + <kbd>?</kbd> +GitLab has many useful keyboard shortcuts to make it easier to access different features. +You can see the quick reference sheet within GitLab itself with <kbd>Shift</kbd> + <kbd>?</kbd>. -## Global Shortcuts +The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must +be in specific pages for the other shortcuts to be available, as explained in each +section below. -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>n</kbd> | Main navigation | -| <kbd>s</kbd> | Focus search | -| <kbd>f</kbd> | Focus filter | -| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar | -| <kbd>?</kbd> | Show/hide this dialog | -| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle markdown preview | -| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) | +## Global Shortcuts -## Project Files Browsing +These shortcuts are available in most areas of GitLab + +| Keyboard Shortcut | Description | +| ------------------------------- | ----------- | +| <kbd>?</kbd> | Show/hide shortcut reference sheet. | +| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to your Projects page. | +| <kbd>Shift</kbd> + <kbd>g</kbd> | Go to your Groups page. | +| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to your Activity page. | +| <kbd>Shift</kbd> + <kbd>l</kbd> | Go to your Milestones page. | +| <kbd>Shift</kbd> + <kbd>s</kbd> | Go to your Snippets page. | +| <kbd>s</kbd> | Put cursor in the issues/merge requests search. | +| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. | +| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.| +| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. | +| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. | + +Additionally, the following shortcuts are available when editing text in text fields, +for example comments, replies, or issue and merge request descriptions: + +| Keyboard Shortcut | Description | +| ---------------------------------------------------------------------- | ----------- | +| <kbd>↑</kbd> | Edit your last comment. You must be in a blank text field below a thread, and you must already have at least one comment in the thread. | +| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle Markdown preview, when editing text in a text field that has **Write** and **Preview** tabs at the top. | -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>↑</kbd> | Move selection up | -| <kbd>↓</kbd> | Move selection down | -| <kbd>enter</kbd> | Open selection | +## Project -## Finding Project File +These shortcuts are available from any page within a project. You must type them +relatively quickly to work, and they will take you to another page in the project. + +| Keyboard Shortcut | Description | +| --------------------------- | ----------- | +| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project home page (**Project > Details**). | +| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project activity feed (**Project > Activity**). | +| <kbd>g</kbd> + <kbd>r</kbd> | Go to the project releases list (**Project > Releases**). | +| <kbd>g</kbd> + <kbd>f</kbd> | Go to the [project files](#project-files) list (**Repository > Files**). | +| <kbd>t</kbd> | Go to the project file search page. (**Repository > Files**, click **Find Files**). | +| <kbd>g</kbd> + <kbd>c</kbd> | Go to the project commits list (**Repository > Commits**). | +| <kbd>g</kbd> + <kbd>n</kbd> | Go to the [repository graph](#repository-graph) page (**Repository > Graph**). | +| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts (**Repository > Charts**). | +| <kbd>g</kbd> + <kbd>i</kbd> | Go to the project issues list (**Issues > List**). | +| <kbd>i</kbd> | Go to the New Issue page (**Issues**, click **New Issue** ). | +| <kbd>g</kbd> + <kbd>b</kbd> | Go to the project issue boards list (**Issues > Boards**). | +| <kbd>g</kbd> + <kbd>m</kbd> | Go to the project merge requests list (**Merge Requests**). | +| <kbd>g</kbd> + <kbd>j</kbd> | Go to the CI/CD jobs list (**CI/CD > Jobs**). | +| <kbd>g</kbd> + <kbd>l</kbd> | Go to the project metrics (**Operations > Metrics**). | +| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project environments (**Operations > Environments**). | +| <kbd>g</kbd> + <kbd>k</kbd> | Go to the project Kubernetes cluster integration page (**Operations > Kubernetes**). Note that you must have at least [`maintainer` permissions](../user/permissions.md) to access this page. | +| <kbd>g</kbd> + <kbd>s</kbd> | Go to the project snippets list (**Snippets**). | +| <kbd>g</kbd> + <kbd>w</kbd> | Go to the project wiki (**Wiki**), if enabled. | + +### Issues and Merge Requests + +These shortcuts are available when viewing issues and merge requests. + +| Keyboard Shortcut | Description | +| ---------------------------- | ----------- | +| <kbd>e</kbd> | Edit description. | +| <kbd>a</kbd> | Change assignee. | +| <kbd>m</kbd> | Change milestone. | +| <kbd>l</kbd> | Change label. | +| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. | +| <kbd>n</kbd> | Move to next unresolved discussion (Merge requests only). | +| <kbd>p</kbd> | Move to previous unresolved discussion (Merge requests only). | +| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (Merge requests only). | +| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (Merge requests only). | + +### Project Files + +These shortcuts are available when browsing the files in a project (navigate to +**Repository** > **Files**): | Keyboard Shortcut | Description | | ----------------- | ----------- | -| <kbd>↑</kbd> | Move selection up | -| <kbd>↓</kbd> | Move selection down | -| <kbd>enter</kbd> | Open selection | -| <kbd>esc</kbd> | Go back | +| <kbd>↑</kbd> | Move selection up. | +| <kbd>↓</kbd> | Move selection down. | +| <kbd>enter</kbd> | Open selection. | +| <kbd>esc</kbd> | Go back to file list screen (only while searching for files, **Repository > Files** then click on **Find File**). | +| <kbd>y</kbd> | Go to file permalink (only while viewing a file). | -## Global Dashboard +### Web IDE -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to the activity feed | -| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to projects | -| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to issues | -| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to merge requests | -| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to todos | +These shortcuts are available when editing a file with the [Web IDE](../user/project/web_ide/index.md): -## Project +| Keyboard Shortcut | Description | +| ------------------------------------------------------- | ----------- | +| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. | +| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message). | -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | -| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed | -| <kbd>g</kbd> + <kbd>f</kbd> | Go to files | -| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | -| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs | -| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph | -| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts | -| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues | -| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards | -| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests | -| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments | -| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes | -| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets | -| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki | -| <kbd>t</kbd> | Go to finding file | -| <kbd>i</kbd> | New issue | - -## Network Graph +### Repository Graph -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left | -| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right | -| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up | -| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down | -| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top | -| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom | +These shortcuts are available when viewing the project [repository graph](../user/project/repository/index.md#repository-graph) +page (navigate to **Repository > Graph**): -## Issues and Merge Requests +| Keyboard Shortcut | Description | +| ------------------------------------------------------------------ | ----------- | +| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left. | +| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right. | +| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up. | +| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down. | +| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top. | +| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom. | -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>a</kbd> | Change assignee | -| <kbd>m</kbd> | Change milestone | -| <kbd>r</kbd> | Reply (quoting selected text) | -| <kbd>e</kbd> | Edit issue/merge request | -| <kbd>l</kbd> | Change label | -| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file | -| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file | -| <kbd>n</kbd> | Move to next unresolved discussion | -| <kbd>p</kbd> | Move to previous unresolved discussion | +### Wiki pages -## Epics **(ULTIMATE)** +This shortcut is available when viewing a [wiki page](../user/project/wiki/index.md): | Keyboard Shortcut | Description | | ----------------- | ----------- | -| <kbd>r</kbd> | Reply (quoting selected text) | -| <kbd>e</kbd> | Edit description | -| <kbd>l</kbd> | Change label | - -## Wiki pages +| <kbd>e</kbd> | Edit wiki page. | -| Keyboard Shortcut | Description | -| ----------------- | ----------- | -| <kbd>e</kbd> | Edit wiki page| +## Epics **(ULTIMATE)** -## Web IDE +These shortcuts are available when viewing [Epics](../user/group/epics/index.md): | Keyboard Shortcut | Description | | ----------------- | ----------- | -| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>p</kbd> | Go to file | -| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message) | +| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. | +| <kbd>e</kbd> | Edit description. | +| <kbd>l</kbd> | Change label. | diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb index 42cd94add79..13883ca7f3d 100644 --- a/lib/gitlab/import_export/attributes_finder.rb +++ b/lib/gitlab/import_export/attributes_finder.rb @@ -3,35 +3,19 @@ module Gitlab module ImportExport class AttributesFinder - def initialize(included_attributes:, excluded_attributes:, methods:) - @included_attributes = included_attributes || {} - @excluded_attributes = excluded_attributes || {} - @methods = methods || {} + def initialize(config:) + @tree = config[:tree] || {} + @included_attributes = config[:included_attributes] || {} + @excluded_attributes = config[:excluded_attributes] || {} + @methods = config[:methods] || {} end - def find(model_object) - parsed_hash = find_attributes_only(model_object) - parsed_hash.empty? ? model_object : { model_object => parsed_hash } + def find_root(model_key) + find(model_key, @tree[model_key]) end - def parse(model_object) - parsed_hash = find_attributes_only(model_object) - yield parsed_hash unless parsed_hash.empty? - end - - def find_included(value) - key = key_from_hash(value) - @included_attributes[key].nil? ? {} : { only: @included_attributes[key] } - end - - def find_excluded(value) - key = key_from_hash(value) - @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] } - end - - def find_method(value) - key = key_from_hash(value) - @methods[key].nil? ? {} : { methods: @methods[key] } + def find_relations_tree(model_key) + @tree[model_key] end def find_excluded_keys(klass_name) @@ -40,12 +24,24 @@ module Gitlab private - def find_attributes_only(value) - find_included(value).merge(find_excluded(value)).merge(find_method(value)) + def find(model_key, model_tree) + { + only: @included_attributes[model_key], + except: @excluded_attributes[model_key], + methods: @methods[model_key], + include: resolve_model_tree(model_tree) + }.compact + end + + def resolve_model_tree(model_tree) + return unless model_tree + + model_tree + .map(&method(:resolve_model)) end - def key_from_hash(value) - value.is_a?(Hash) ? value.first.first : value + def resolve_model(model_key, model_tree) + { model_key => find(model_key, model_tree) } end end end diff --git a/lib/gitlab/import_export/config.rb b/lib/gitlab/import_export/config.rb index f6cd4eb5e0c..6f4919ead4e 100644 --- a/lib/gitlab/import_export/config.rb +++ b/lib/gitlab/import_export/config.rb @@ -3,70 +3,49 @@ module Gitlab module ImportExport class Config + def initialize + @hash = parse_yaml + @hash.deep_symbolize_keys! + @ee_hash = @hash.delete(:ee) || {} + + @hash[:tree] = normalize_tree(@hash[:tree]) + @ee_hash[:tree] = normalize_tree(@ee_hash[:tree] || {}) + end + # Returns a Hash of the YAML file, including EE specific data if EE is # used. def to_h - hash = parse_yaml - ee_hash = hash['ee'] - - if merge? && ee_hash - ee_hash.each do |key, value| - if key == 'project_tree' - merge_project_tree(value, hash[key]) - else - merge_attributes_list(value, hash[key]) - end - end + if merge_ee? + deep_merge(@hash, @ee_hash) + else + @hash end - - # We don't want to expose this section after this point, as it is no - # longer needed. - hash.delete('ee') - - hash end - # Merges a project relationships tree into the target tree. - # - # @param [Array<Hash|Symbol>] source_values - # @param [Array<Hash|Symbol>] target_values - def merge_project_tree(source_values, target_values) - source_values.each do |value| - if value.is_a?(Hash) - # Examples: - # - # { 'project_tree' => [{ 'labels' => [...] }] } - # { 'notes' => [:author, { 'events' => [:push_event_payload] }] } - value.each do |key, val| - target = target_values - .find { |h| h.is_a?(Hash) && h[key] } + private - if target - merge_project_tree(val, target[key]) - else - target_values << { key => val.dup } - end - end - else - # Example: :priorities, :author, etc - target_values << value - end + def deep_merge(hash_a, hash_b) + hash_a.deep_merge(hash_b) do |_, this_val, other_val| + this_val.to_a + other_val.to_a end end - # Merges a Hash containing a flat list of attributes, such as the entries - # in a `excluded_attributes` section. - # - # @param [Hash] source_values - # @param [Hash] target_values - def merge_attributes_list(source_values, target_values) - source_values.each do |key, values| - target_values[key] ||= [] - target_values[key].concat(values) + def normalize_tree(item) + case item + when Array + item.reduce({}) do |hash, subitem| + hash.merge!(normalize_tree(subitem)) + end + when Hash + item.transform_values(&method(:normalize_tree)) + when Symbol + { item => {} } + else + raise ArgumentError, "#{item} needs to be Array, Hash, Symbol or NilClass" end end - def merge? + def merge_ee? Gitlab.ee? end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 40acb24d191..06c94beead8 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -3,89 +3,92 @@ # This list _must_ only contain relationships that are available to both CE and # EE. EE specific relationships must be defined in the `ee` section further # down below. -project_tree: - - labels: - - :priorities - - milestones: - - events: - - :push_event_payload - - issues: - - events: - - :push_event_payload - - :timelogs - - notes: - - :author - - events: - - :push_event_payload - - label_links: - - label: - - :priorities - - milestone: - - events: - - :push_event_payload - - resource_label_events: - - label: - - :priorities - - :issue_assignees - - snippets: - - :award_emoji - - notes: - - :author - - releases: - - :links - - project_members: - - :user - - merge_requests: - - :metrics - - notes: - - :author +tree: + project: + - labels: + - :priorities + - milestones: - events: - :push_event_payload - - :suggestions - - merge_request_diff: - - :merge_request_diff_commits - - :merge_request_diff_files - - events: - - :push_event_payload - - :timelogs - - label_links: - - label: - - :priorities - - milestone: + - issues: - events: - :push_event_payload - - resource_label_events: - - label: - - :priorities - - ci_pipelines: - - notes: - - :author + - :timelogs + - notes: + - :author + - events: + - :push_event_payload + - label_links: + - label: + - :priorities + - milestone: + - events: + - :push_event_payload + - resource_label_events: + - label: + - :priorities + - :issue_assignees + - snippets: + - :award_emoji + - notes: + - :author + - releases: + - :links + - project_members: + - :user + - merge_requests: + - :metrics + - notes: + - :author + - events: + - :push_event_payload + - :suggestions + - merge_request_diff: + - :merge_request_diff_commits + - :merge_request_diff_files - events: - :push_event_payload - - stages: - - :statuses - - :external_pull_request - - :external_pull_requests - - :auto_devops - - :triggers - - :pipeline_schedules - - :services - - protected_branches: - - :merge_access_levels - - :push_access_levels - - protected_tags: - - :create_access_levels - - :project_feature - - :custom_attributes - - :prometheus_metrics - - :project_badges - - :ci_cd_settings - - :error_tracking_setting - - :metrics_setting - - boards: - - lists: - - label: - - :priorities + - :timelogs + - label_links: + - label: + - :priorities + - milestone: + - events: + - :push_event_payload + - resource_label_events: + - label: + - :priorities + - ci_pipelines: + - notes: + - :author + - events: + - :push_event_payload + - stages: + - :statuses + - :external_pull_request + - :external_pull_requests + - :auto_devops + - :triggers + - :pipeline_schedules + - :services + - protected_branches: + - :merge_access_levels + - :push_access_levels + - protected_tags: + - :create_access_levels + - :project_feature + - :custom_attributes + - :prometheus_metrics + - :project_badges + - :ci_cd_settings + - :error_tracking_setting + - :metrics_setting + - boards: + - lists: + - label: + - :priorities + group_members: + - :user # Only include the following attributes for the models specified. included_attributes: @@ -225,12 +228,15 @@ methods: - :type lists: - :list_type + ci_pipelines: + - :notes # EE specific relationships and settings to include. All of this will be merged # into the previous structures if EE is used. ee: - project_tree: - - protected_branches: - - :unprotect_access_levels - - protected_environments: - - :deploy_access_levels + tree: + project: + protected_branches: + - :unprotect_access_levels + protected_environments: + - :deploy_access_levels diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb deleted file mode 100644 index a92e3862361..00000000000 --- a/lib/gitlab/import_export/json_hash_builder.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module ImportExport - # Generates a hash that conforms with http://apidock.com/rails/Hash/to_json - # and its peculiar options. - class JsonHashBuilder - def self.build(model_objects, attributes_finder) - new(model_objects, attributes_finder).build - end - - def initialize(model_objects, attributes_finder) - @model_objects = model_objects - @attributes_finder = attributes_finder - end - - def build - process_model_objects(@model_objects) - end - - private - - # Called when the model is actually a hash containing other relations (more models) - # Returns the config in the right format for calling +to_json+ - # - # +model_object_hash+ - A model relationship such as: - # {:merge_requests=>[:merge_request_diff, :notes]} - def process_model_objects(model_object_hash) - json_config_hash = {} - current_key = model_object_hash.first.first - - model_object_hash.values.flatten.each do |model_object| - @attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash } - handle_model_object(current_key, model_object, json_config_hash) - end - - json_config_hash - end - - # Creates or adds to an existing hash an individual model or list - # - # +current_key+ main model that will be a key in the hash - # +model_object+ model or list of models to include in the hash - # +json_config_hash+ the original hash containing the root model - def handle_model_object(current_key, model_object, json_config_hash) - model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object - - if json_config_hash[current_key] - add_model_value(current_key, model_or_sub_model, json_config_hash) - else - create_model_value(current_key, model_or_sub_model, json_config_hash) - end - end - - # Constructs a new hash that will hold the configuration for that particular object - # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ - # - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - # +json_config_hash+ the original hash containing the root model - def create_model_value(current_key, value, json_config_hash) - json_config_hash[current_key] = parse_hash(value) || { include: value } - end - - # Calls attributes finder to parse the hash and add any attributes to it - # - # +value+ existing model to be included in the hash - # +parsed_hash+ the original hash - def parse_hash(value) - return if already_contains_methods?(value) - - @attributes_finder.parse(value) do |hash| - { include: hash_or_merge(value, hash) } - end - end - - def already_contains_methods?(value) - value.is_a?(Hash) && value.values.detect { |val| val[:methods]} - end - - # Adds new model configuration to an existing hash with key +current_key+ - # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ - # - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - # +json_config_hash+ the original hash containing the root model - def add_model_value(current_key, value, json_config_hash) - @attributes_finder.parse(value) do |hash| - value = { value => hash } unless value.is_a?(Hash) - end - - add_to_array(current_key, json_config_hash, value) - end - - # Adds new model configuration to an existing hash with key +current_key+ - # it creates a new array if it was previously a single value - # - # +current_key+ main model that will be a key in the hash - # +value+ existing model to be included in the hash - # +json_config_hash+ the original hash containing the root model - def add_to_array(current_key, json_config_hash, value) - old_values = json_config_hash[current_key][:include] - - json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten - end - - # Construct a new hash or merge with an existing one a model configuration - # This is to fulfil +to_json+ requirements. - # - # +hash+ hash containing configuration generated mainly from +@attributes_finder+ - # +value+ existing model to be included in the hash - def hash_or_merge(value, hash) - value.is_a?(Hash) ? value.merge(hash) : { value => hash } - end - end - end -end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 685cf53f27f..2dd18616cd6 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -58,11 +58,13 @@ module Gitlab # the configuration yaml file too. # Finally, it updates each attribute in the newly imported project. def create_relations - default_relation_list.each do |relation| - if relation.is_a?(Hash) - create_sub_relations(relation, @tree_hash) - elsif @tree_hash[relation.to_s].present? - save_relation_hash(@tree_hash[relation.to_s], relation) + project_relations_without_project_members.each do |relation_key, relation_definition| + relation_key_s = relation_key.to_s + + if relation_definition.present? + create_sub_relations(relation_key_s, relation_definition, @tree_hash) + elsif @tree_hash[relation_key_s].present? + save_relation_hash(relation_key_s, @tree_hash[relation_key_s]) end end @@ -71,7 +73,7 @@ module Gitlab @saved end - def save_relation_hash(relation_hash_batch, relation_key) + def save_relation_hash(relation_key, relation_hash_batch) relation_hash = create_relation(relation_key, relation_hash_batch) remove_group_models(relation_hash) if relation_hash.is_a?(Array) @@ -91,10 +93,13 @@ module Gitlab end end - def default_relation_list - reader.tree.reject do |model| - model.is_a?(Hash) && model[:project_members] - end + def project_relations_without_project_members + # We remove `project_members` as they are deserialized separately + project_relations.except(:project_members) + end + + def project_relations + reader.attributes_finder.find_relations_tree(:project) end def restore_project @@ -150,8 +155,7 @@ module Gitlab # issue, finds any subrelations such as notes, creates them and assign them back to the hash # # Recursively calls this method if the sub-relation is a hash containing more sub-relations - def create_sub_relations(relation, tree_hash, save: true) - relation_key = relation.keys.first.to_s + def create_sub_relations(relation_key, relation_definition, tree_hash, save: true) return if tree_hash[relation_key].blank? tree_array = [tree_hash[relation_key]].flatten @@ -171,13 +175,13 @@ module Gitlab # But we can't have it in the upper level or GC won't get rid of the AR objects # after we save the batch. Project.transaction do - process_sub_relation(relation, relation_item) + process_sub_relation(relation_key, relation_definition, relation_item) # For every subrelation that hangs from Project, save the associated records altogether # This effectively batches all records per subrelation item, only keeping those in memory # We have to keep in mind that more batch granularity << Memory, but >> Slowness if save - save_relation_hash([relation_item], relation_key) + save_relation_hash(relation_key, [relation_item]) tree_hash[relation_key].delete(relation_item) end end @@ -186,37 +190,35 @@ module Gitlab tree_hash.delete(relation_key) if save end - def process_sub_relation(relation, relation_item) - relation.values.flatten.each do |sub_relation| + def process_sub_relation(relation_key, relation_definition, relation_item) + relation_definition.each do |sub_relation_key, sub_relation_definition| # We just use author to get the user ID, do not attempt to create an instance. - next if sub_relation == :author + next if sub_relation_key == :author - create_sub_relations(sub_relation, relation_item, save: false) if sub_relation.is_a?(Hash) + sub_relation_key_s = sub_relation_key.to_s - relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) - relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? - end - end + # create dependent relations if present + if sub_relation_definition.present? + create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false) + end - def assign_relation_hash(relation_item, sub_relation) - if sub_relation.is_a?(Hash) - relation_hash = relation_item[sub_relation.keys.first.to_s] - sub_relation = sub_relation.keys.first - else - relation_hash = relation_item[sub_relation.to_s] + # transform relation hash to actual object + sub_relation_hash = relation_item[sub_relation_key_s] + if sub_relation_hash.present? + relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash) + end end - - [relation_hash, sub_relation] end - def create_relation(relation, relation_hash_list) + def create_relation(relation_key, relation_hash_list) relation_array = [relation_hash_list].flatten.map do |relation_hash| - Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, - relation_hash: relation_hash, - members_mapper: members_mapper, - user: @user, - project: @restored_project, - excluded_keys: excluded_keys_for_relation(relation)) + Gitlab::ImportExport::RelationFactory.create( + relation_sym: relation_key.to_sym, + relation_hash: relation_hash, + members_mapper: members_mapper, + user: @user, + project: @restored_project, + excluded_keys: excluded_keys_for_relation(relation_key)) end.compact relation_hash_list.is_a?(Array) ? relation_array : relation_array.first diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 2255635acdf..f1b3db6b208 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -18,7 +18,10 @@ module Gitlab def save mkdir_p(@shared.export_path) - File.write(full_path, project_json_tree) + project_tree = serialize_project_tree + fix_project_tree(project_tree) + File.write(full_path, project_tree.to_json) + true rescue => e @shared.error(e) @@ -27,27 +30,25 @@ module Gitlab private - def project_json_tree + def fix_project_tree(project_tree) if @params[:description].present? - project_json['description'] = @params[:description] + project_tree['description'] = @params[:description] end - project_json['project_members'] += group_members_json - - RelationRenameService.add_new_associations(project_json) + project_tree['project_members'] += group_members_array - project_json.to_json + RelationRenameService.add_new_associations(project_tree) end - def project_json - @project_json ||= @project.as_json(reader.project_tree) + def serialize_project_tree + @project.as_json(reader.project_tree) end def reader @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared) end - def group_members_json + def group_members_array group_members.as_json(reader.group_members_tree).each do |group_member| group_member['source_type'] = 'Project' # Make group members project members of the future import end diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb index 8bdf6ca491d..9e81c6a3d07 100644 --- a/lib/gitlab/import_export/reader.rb +++ b/lib/gitlab/import_export/reader.rb @@ -7,42 +7,22 @@ module Gitlab def initialize(shared:) @shared = shared - config_hash = ImportExport::Config.new.to_h.deep_symbolize_keys - @tree = config_hash[:project_tree] - @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], - excluded_attributes: config_hash[:excluded_attributes], - methods: config_hash[:methods]) + + @attributes_finder = Gitlab::ImportExport::AttributesFinder.new( + config: ImportExport::Config.new.to_h) end # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html # for outputting a project in JSON format, including its relations and sub relations. def project_tree - attributes = @attributes_finder.find(:project) - project_attributes = attributes.is_a?(Hash) ? attributes[:project] : {} - - project_attributes.merge(include: build_hash(@tree)) + attributes_finder.find_root(:project) rescue => e @shared.error(e) false end def group_members_tree - @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user)) - end - - private - - # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html - # - # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file - def build_hash(model_list) - model_list.map do |model_objects| - if model_objects.is_a?(Hash) - Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder) - else - @attributes_finder.find(model_objects) - end - end + attributes_finder.find_root(:group_members) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d26ce9fa911..f2277dc3446 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2035,10 +2035,10 @@ msgstr "" msgid "Certificate (PEM)" msgstr "" -msgid "Change Label" +msgid "Change assignee" msgstr "" -msgid "Change assignee" +msgid "Change label" msgstr "" msgid "Change milestone" @@ -3019,6 +3019,9 @@ msgstr "" msgid "Comment is being updated" msgstr "" +msgid "Comment/Reply (quoting selected text)" +msgstr "" + msgid "Comments" msgstr "" @@ -4189,9 +4192,15 @@ msgstr "" msgid "Edit comment" msgstr "" +msgid "Edit description" +msgstr "" + msgid "Edit environment" msgstr "" +msgid "Edit epic description" +msgstr "" + msgid "Edit file" msgstr "" @@ -4204,25 +4213,22 @@ msgstr "" msgid "Edit identity for %{user_name}" msgstr "" -msgid "Edit issue" -msgstr "" - msgid "Edit issues" msgstr "" -msgid "Edit last comment (when focused on an empty textarea)" +msgid "Edit public deploy key" msgstr "" -msgid "Edit merge request" +msgid "Edit stage" msgstr "" -msgid "Edit public deploy key" +msgid "Edit wiki page" msgstr "" -msgid "Edit stage" +msgid "Edit your most recent comment in a thread (from an empty textarea)" msgstr "" -msgid "Edit wiki page" +msgid "Editing" msgstr "" msgid "Email" @@ -4543,6 +4549,9 @@ msgstr "" msgid "Epic" msgstr "" +msgid "Epics (Ultimate / Gold license only)" +msgstr "" + msgid "Error" msgstr "" @@ -5100,9 +5109,6 @@ msgstr "" msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file." msgstr "" -msgid "Finding Project File" -msgstr "" - msgid "Fingerprint" msgstr "" @@ -5127,12 +5133,6 @@ msgstr "" msgid "FlowdockService|Flowdock is a collaboration web app for technical teams." msgstr "" -msgid "Focus Filter" -msgstr "" - -msgid "Focus Search" -msgstr "" - msgid "FogBugz Email" msgstr "" @@ -5376,6 +5376,9 @@ msgstr "" msgid "Go back" msgstr "" +msgid "Go back (while searching for files" +msgstr "" + msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board." msgstr "" @@ -5397,16 +5400,17 @@ msgstr "" msgid "Go to file" msgstr "" -msgid "Go to file permalink" +msgid "Go to file (MRs only)" msgstr "" -msgid "Go to files" + +msgid "Go to file permalink (while viewing a file)" msgstr "" -msgid "Go to finding file" +msgid "Go to files" msgstr "" -msgid "Go to groups" +msgid "Go to find file" msgstr "" msgid "Go to issue boards" @@ -5427,45 +5431,60 @@ msgstr "" msgid "Go to metrics" msgstr "" -msgid "Go to milestones" -msgstr "" - -msgid "Go to network graph" -msgstr "" - msgid "Go to parent" msgstr "" msgid "Go to project" msgstr "" -msgid "Go to projects" +msgid "Go to releases" msgstr "" msgid "Go to repository charts" msgstr "" +msgid "Go to repository graph" +msgstr "" + msgid "Go to snippets" msgstr "" msgid "Go to the activity feed" msgstr "" +msgid "Go to the milestone list" +msgstr "" + msgid "Go to the project's activity feed" msgstr "" msgid "Go to the project's overview page" msgstr "" -msgid "Go to todos" +msgid "Go to wiki" msgstr "" -msgid "Go to wiki" +msgid "Go to your To-Do list" msgstr "" msgid "Go to your fork" msgstr "" +msgid "Go to your groups" +msgstr "" + +msgid "Go to your issues" +msgstr "" + +msgid "Go to your merge requests" +msgstr "" + +msgid "Go to your projects" +msgstr "" + +msgid "Go to your snippets" +msgstr "" + msgid "Google Code import" msgstr "" @@ -6233,6 +6252,9 @@ msgstr "" msgid "Issues" msgstr "" +msgid "Issues / Merge Requests" +msgstr "" + msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable." msgstr "" @@ -7254,18 +7276,6 @@ msgstr "" msgid "Move this issue to another project." msgstr "" -msgid "Move to next file" -msgstr "" - -msgid "Move to next unresolved discussion" -msgstr "" - -msgid "Move to previous file" -msgstr "" - -msgid "Move to previous unresolved discussion" -msgstr "" - msgid "MoveIssue|Cannot move issue due to insufficient permissions!" msgstr "" @@ -7326,9 +7336,6 @@ msgstr "" msgid "Network" msgstr "" -msgid "Network Graph" -msgstr "" - msgid "Never" msgstr "" @@ -7451,6 +7458,12 @@ msgstr "" msgid "Next" msgstr "" +msgid "Next file in diff (MRs only)" +msgstr "" + +msgid "Next unresolved discussion (MRs only)" +msgstr "" + msgid "Nickname" msgstr "" @@ -8446,6 +8459,12 @@ msgstr "" msgid "Previous Artifacts" msgstr "" +msgid "Previous file in diff (MRs only)" +msgstr "" + +msgid "Previous unresolved discussion (MRs only)" +msgstr "" + msgid "Prioritize" msgstr "" @@ -8824,10 +8843,7 @@ msgstr "" msgid "Project Badges" msgstr "" -msgid "Project File" -msgstr "" - -msgid "Project Files browsing" +msgid "Project Files" msgstr "" msgid "Project ID" @@ -9621,9 +9637,6 @@ msgstr "" msgid "Replaced all labels with %{label_references} %{label_text}." msgstr "" -msgid "Reply (quoting selected text)" -msgstr "" - msgid "Reply by email" msgstr "" @@ -9672,6 +9685,9 @@ msgstr "" msgid "Repository" msgstr "" +msgid "Repository Graph" +msgstr "" + msgid "Repository Settings" msgstr "" @@ -10499,9 +10515,6 @@ msgstr "" msgid "Show whitespace changes" msgstr "" -msgid "Show/hide this dialog" -msgstr "" - msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" @@ -10936,6 +10949,9 @@ msgstr "" msgid "Start date" msgstr "" +msgid "Start search" +msgstr "" + msgid "Start the Runner!" msgstr "" @@ -12274,6 +12290,9 @@ msgstr "" msgid "Toggle the Performance Bar" msgstr "" +msgid "Toggle this dialog" +msgstr "" + msgid "Toggle thread" msgstr "" diff --git a/package.json b/package.json index 1c3eb409d9b..4256b8bfdcc 100644 --- a/package.json +++ b/package.json @@ -197,6 +197,7 @@ "stylelint": "^10.1.0", "stylelint-config-recommended": "^2.2.0", "stylelint-scss": "^3.9.2", + "timezone-mock": "^1.0.8", "vue-jest": "^4.0.0-beta.2", "webpack-dev-server": "^3.1.14", "yarn-deduplicate": "^1.1.1" diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index b79a1ac4810..5d87dbdee8b 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -74,11 +74,6 @@ describe RegistrationsController do end context 'when reCAPTCHA is enabled' do - def fail_recaptcha - # Without this, `verify_recaptcha` arbitrarily returns true in test env - Recaptcha.configuration.skip_verify_env.delete('test') - end - before do stub_application_setting(recaptcha_enabled: true) end @@ -91,7 +86,7 @@ describe RegistrationsController do end it 'displays an error when the reCAPTCHA is not solved' do - fail_recaptcha + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) post(:create, params: user_params) @@ -107,7 +102,6 @@ describe RegistrationsController do it 'does not require reCAPTCHA if disabled by feature flag' do stub_feature_flags(registrations_recaptcha: false) - fail_recaptcha post(:create, params: user_params) diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index ee5be82cd19..ae3988bdd69 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -271,5 +271,88 @@ ZDXgrA== auto_ssl_enabled { true } certificate_source { :gitlab_provided } end + + trait :explicit_ecdsa do + certificate '-----BEGIN CERTIFICATE----- +MIID1zCCAzkCCQDatOIwBlktwjAKBggqhkjOPQQDAjBPMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCTlkxCzAJBgNVBAcMAk5ZMQswCQYDVQQLDAJJVDEZMBcGA1UEAwwQ +dGVzdC1jZXJ0aWZpY2F0ZTAeFw0xOTA4MjkxMTE1NDBaFw0yMTA4MjgxMTE1NDBa +ME8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTELMAkGA1UEBwwCTlkxCzAJBgNV +BAsMAklUMRkwFwYDVQQDDBB0ZXN0LWNlcnRpZmljYXRlMIICXDCCAc8GByqGSM49 +AgEwggHCAgEBME0GByqGSM49AQECQgH///////////////////////////////// +/////////////////////////////////////////////////////zCBngRCAf// +//////////////////////////////////////////////////////////////// +///////////////////8BEFRlT65YY4cmh+SmiGgtoVA7qLacluZsxXzuLSJkY7x +CeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/AAMVANCeiAApHLhTlsxn +FzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0QpxkgTkFP7Uh+CivYGtNPbqh +S1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEYOSlqeJo7wARcil+0LH0b +2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdhNTxwhqJywkCIvpR2n9Fm +UAJCAf//////////////////////////////////////////+lGGh4O/L5Zrf8wB +SPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBVG/4c/hgl36toHj+eGL4pqv7 +l7M+ZKQJ4vz0Y9E6xIx+gvfVaZ58krmbBAP53ikwneQbFdcvw3L/ACPEib/qWjkB +ogykguy3OwHtKLYNnDWIsfiLumEjElhcBMZVXiXhb5txf11uXAWn5n6Qhey5YKPM +NjLLqDqaG19efCLCd21A0TcwCgYIKoZIzj0EAwIDgYsAMIGHAkEm68kYFVnN1c2N +OjSJpIDdFWGVYJHyMDI5WgQyhm4hAioXJ0T22Zab8Wmq+hBYRJNcHoaV894blfqR +V3ZJgam8EQJCAcnPpJQ0IqoT1pAQkaL3+Ka8ZaaCd6/8RnoDtGvWljisuyH65SRu +kmYv87bZe1KqOZDoaDBdfVsoxcGbik19lBPV +-----END CERTIFICATE-----' + + key '-----BEGIN EC PARAMETERS----- +MIIBwgIBATBNBgcqhkjOPQEBAkIB//////////////////////////////////// +//////////////////////////////////////////////////8wgZ4EQgH///// +//////////////////////////////////////////////////////////////// +/////////////////ARBUZU+uWGOHJofkpohoLaFQO6i2nJbmbMV87i0iZGO8Qnh +Vhk5Uex+k3sWUsC9O7G/BzVz34g9LDTx70Uf1GtQPwADFQDQnogAKRy4U5bMZxc5 +MoSqoNpkugSBhQQAxoWOBrcEBOnNnj7LZiOVtEKcZIE5BT+1Ifgor2BrTT26oUte +d+/nWSj+HcEnov+o3jNIs8GFakKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY +9URJV5tEaBevvRcnPmYsl+5ymV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlAC +QgH///////////////////////////////////////////pRhoeDvy+Wa3/MAUj3 +CaXQO7XJuImcR667b7cekThkCQIBAQ== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIICnQIBAQRCAZZRG4FJO+OK29ygycrNzjxQDB+dp+QPo1Pk6RAl5PcraohyhFnI +MGUL4ba1efZUxCbAWxjVRSi7QEUNYCCdUPAtoIIBxjCCAcICAQEwTQYHKoZIzj0B +AQJCAf////////////////////////////////////////////////////////// +////////////////////////////MIGeBEIB//////////////////////////// +//////////////////////////////////////////////////////////wEQVGV +PrlhjhyaH5KaIaC2hUDuotpyW5mzFfO4tImRjvEJ4VYZOVHsfpN7FlLAvTuxvwc1 +c9+IPSw08e9FH9RrUD8AAxUA0J6IACkcuFOWzGcXOTKEqqDaZLoEgYUEAMaFjga3 +BATpzZ4+y2YjlbRCnGSBOQU/tSH4KK9ga009uqFLXnfv51ko/h3BJ6L/qN4zSLPB +hWpCm/l+fjHC5b1mARg5KWp4mjvABFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfu +cple9CZAxVC5AT+tB2E1PHCGonLCQIi+lHaf0WZQAkIB//////////////////// +///////////////////////6UYaHg78vlmt/zAFI9wml0Du1ybiJnEeuu2+3HpE4 +ZAkCAQGhgYkDgYYABAFUb/hz+GCXfq2geP54Yvimq/uXsz5kpAni/PRj0TrEjH6C +99VpnnySuZsEA/neKTCd5BsV1y/Dcv8AI8SJv+paOQGiDKSC7Lc7Ae0otg2cNYix ++Iu6YSMSWFwExlVeJeFvm3F/XW5cBafmfpCF7Llgo8w2MsuoOpobX158IsJ3bUDR +Nw== +-----END EC PRIVATE KEY-----' + end + + trait :ecdsa do + certificate '-----BEGIN CERTIFICATE----- +MIIB8zCCAVUCCQCGKuPQ6SBxUTAKBggqhkjOPQQDAjA+MQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw +HhcNMTkwOTAyMDkyMDUxWhcNMjEwOTAxMDkyMDUxWjA+MQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw +gZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAH9Jd7ZWnTasgltZRbIMreihycOh/G4 +TXpkp8tTtEsuD+sh8au3Jywsi89RSZ6vgVoCY7//DQ2vamYnyBZqbL+cTQBsQ7wD +UEaSyP0R3P4b6Ox347pYzXwSdSOra9Cm4TMQe+prVMesxulqIm7G7CTI+9J8LHlJ +z0wUDQz/o+tUSYwv6zAKBggqhkjOPQQDAgOBiwAwgYcCQUOlTnn2QP/uYSh1dUSl +R9WYUg5+PQMg7kS+4K/5+5gonWCvaMcP+2P7hltUcvq41l3uMKKCZRU/x60/FMHc +1ZXdAkIBuVtm9RJXziNOKS4TcpH9os/FuREW8YQlpec58LDZdlivcHnikHZ4LCri +T7zu3VY6Rq+V/IKpsQwQjmoTJ0IpCM8= +-----END CERTIFICATE-----' + + key '-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEFa72+eREW25IHbke0TiWFdW1R1ad9Nyqaz7CDtv5Kqdgd6Kcl8V2az +Lr6z1PS+JSERWzRP+fps7kdFRrtqy/ECpKAHBgUrgQQAI6GBiQOBhgAEAf0l3tla +dNqyCW1lFsgyt6KHJw6H8bhNemSny1O0Sy4P6yHxq7cnLCyLz1FJnq+BWgJjv/8N +Da9qZifIFmpsv5xNAGxDvANQRpLI/RHc/hvo7HfjuljNfBJ1I6tr0KbhMxB76mtU +x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r +-----END EC PRIVATE KEY-----' + end end end diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb index fef84c87509..cc8ca1d87e3 100644 --- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb @@ -12,7 +12,7 @@ describe 'Import/Export attribute configuration' do let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys } let(:relation_names) do - names = names_from_tree(config_hash['project_tree']) + names = names_from_tree(config_hash.dig('tree', 'project')) # Remove duplicated or add missing models # - project is not part of the tree, so it has to be added manually. diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb new file mode 100644 index 00000000000..208b60844e3 --- /dev/null +++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::ImportExport::AttributesFinder do + describe '#find_root' do + subject { described_class.new(config: config).find_root(model_key) } + + let(:test_config) { 'spec/support/import_export/import_export.yml' } + let(:config) { Gitlab::ImportExport::Config.new.to_h } + let(:model_key) { :project } + + let(:project_tree_hash) do + { + except: [:id, :created_at], + include: [ + { issues: { include: [] } }, + { labels: { include: [] } }, + { merge_requests: { + except: [:iid], + include: [ + { merge_request_diff: { + include: [] + } }, + { merge_request_test: { include: [] } } + ], + only: [:id] + } }, + { commit_statuses: { + include: [{ commit: { include: [] } }] + } }, + { project_members: { + include: [{ user: { include: [], + only: [:email] } }] + } } + ] + } + end + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config) + end + + it 'generates hash from project tree config' do + is_expected.to match(project_tree_hash) + end + + context 'individual scenarios' do + it 'generates the correct hash for a single project relation' do + setup_yaml(tree: { project: [:issues] }) + + is_expected.to match( + include: [{ issues: { include: [] } }] + ) + end + + it 'generates the correct hash for a single project feature relation' do + setup_yaml(tree: { project: [:project_feature] }) + + is_expected.to match( + include: [{ project_feature: { include: [] } }] + ) + end + + it 'generates the correct hash for a multiple project relation' do + setup_yaml(tree: { project: [:issues, :snippets] }) + + is_expected.to match( + include: [{ issues: { include: [] } }, + { snippets: { include: [] } }] + ) + end + + it 'generates the correct hash for a single sub-relation' do + setup_yaml(tree: { project: [issues: [:notes]] }) + + is_expected.to match( + include: [{ issues: { include: [{ notes: { include: [] } }] } }] + ) + end + + it 'generates the correct hash for a multiple sub-relation' do + setup_yaml(tree: { project: [merge_requests: [:notes, :merge_request_diff]] }) + + is_expected.to match( + include: [{ merge_requests: + { include: [{ notes: { include: [] } }, + { merge_request_diff: { include: [] } }] } }] + ) + end + + it 'generates the correct hash for a sub-relation with another sub-relation' do + setup_yaml(tree: { project: [merge_requests: [notes: [:author]]] }) + + is_expected.to match( + include: [{ merge_requests: { + include: [{ notes: { include: [{ author: { include: [] } }] } }] + } }] + ) + end + + it 'generates the correct hash for a relation with included attributes' do + setup_yaml(tree: { project: [:issues] }, + included_attributes: { issues: [:name, :description] }) + + is_expected.to match( + include: [{ issues: { include: [], + only: [:name, :description] } }] + ) + end + + it 'generates the correct hash for a relation with excluded attributes' do + setup_yaml(tree: { project: [:issues] }, + excluded_attributes: { issues: [:name] }) + + is_expected.to match( + include: [{ issues: { except: [:name], + include: [] } }] + ) + end + + it 'generates the correct hash for a relation with both excluded and included attributes' do + setup_yaml(tree: { project: [:issues] }, + excluded_attributes: { issues: [:name] }, + included_attributes: { issues: [:description] }) + + is_expected.to match( + include: [{ issues: { except: [:name], + include: [], + only: [:description] } }] + ) + end + + it 'generates the correct hash for a relation with custom methods' do + setup_yaml(tree: { project: [:issues] }, + methods: { issues: [:name] }) + + is_expected.to match( + include: [{ issues: { include: [], + methods: [:name] } }] + ) + end + + def setup_yaml(hash) + allow(YAML).to receive(:load_file).with(test_config).and_return(hash) + end + end + end + + describe '#find_relations_tree' do + subject { described_class.new(config: config).find_relations_tree(model_key) } + + let(:tree) { { project: { issues: {} } } } + let(:model_key) { :project } + + context 'when initialized with config including tree' do + let(:config) { { tree: tree } } + + context 'when relation is in top-level keys of the tree' do + it { is_expected.to eq({ issues: {} }) } + end + + context 'when the relation is not in top-level keys' do + let(:model_key) { :issues } + + it { is_expected.to be_nil } + end + end + + context 'when tree is not present in config' do + let(:config) { {} } + + it { is_expected.to be_nil } + end + end + + describe '#find_excluded_keys' do + subject { described_class.new(config: config).find_excluded_keys(klass_name) } + + let(:klass_name) { 'project' } + + context 'when initialized with excluded_attributes' do + let(:config) { { excluded_attributes: excluded_attributes } } + let(:excluded_attributes) { { project: [:name, :path], issues: [:milestone_id] } } + + it { is_expected.to eq(%w[name path]) } + end + + context 'when excluded_attributes are not present in config' do + let(:config) { {} } + + it { is_expected.to eq([]) } + end + end +end diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb index cf396dba382..e53db37def4 100644 --- a/spec/lib/gitlab/import_export/config_spec.rb +++ b/spec/lib/gitlab/import_export/config_spec.rb @@ -1,163 +1,159 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' describe Gitlab::ImportExport::Config do let(:yaml_file) { described_class.new } describe '#to_h' do - context 'when using CE' do - before do - allow(yaml_file) - .to receive(:merge?) - .and_return(false) + subject { yaml_file.to_h } + + context 'when using default config' do + using RSpec::Parameterized::TableSyntax + + where(:ee) do + [true, false] end - it 'just returns the parsed Hash without the EE section' do - expected = YAML.load_file(Gitlab::ImportExport.config_file) - expected.delete('ee') + with_them do + before do + allow(Gitlab).to receive(:ee?) { ee } + end - expect(yaml_file.to_h).to eq(expected) + it 'parses default config' do + expect { subject }.not_to raise_error + expect(subject).to be_a(Hash) + expect(subject.keys).to contain_exactly( + :tree, :excluded_attributes, :included_attributes, :methods) + end end end - context 'when using EE' do - before do - allow(yaml_file) - .to receive(:merge?) - .and_return(true) - end + context 'when using custom config' do + let(:config) do + <<-EOF.strip_heredoc + tree: + project: + - labels: + - :priorities + - milestones: + - events: + - :push_event_payload - it 'merges the EE project tree into the CE project tree' do - allow(yaml_file) - .to receive(:parse_yaml) - .and_return({ - 'project_tree' => [ - { - 'issues' => [ - :id, - :title, - { 'notes' => [:id, :note, { 'author' => [:name] }] } - ] - } - ], - 'ee' => { - 'project_tree' => [ - { - 'issues' => [ - :description, - { 'notes' => [:date, { 'author' => [:email] }] } - ] - }, - { 'foo' => [{ 'bar' => %i[baz] }] } - ] - } - }) + included_attributes: + user: + - :id - expect(yaml_file.to_h).to eq({ - 'project_tree' => [ - { - 'issues' => [ - :id, - :title, - { - 'notes' => [ - :id, - :note, - { 'author' => [:name, :email] }, - :date - ] - }, - :description - ] - }, - { 'foo' => [{ 'bar' => %i[baz] }] } - ] - }) + excluded_attributes: + project: + - :name + + methods: + labels: + - :type + events: + - :action + + ee: + tree: + project: + protected_branches: + - :unprotect_access_levels + included_attributes: + user: + - :name_ee + excluded_attributes: + project: + - :name_without_ee + methods: + labels: + - :type_ee + events_ee: + - :action_ee + EOF end - it 'merges the excluded attributes list' do - allow(yaml_file) - .to receive(:parse_yaml) - .and_return({ - 'project_tree' => [], - 'excluded_attributes' => { - 'project' => %i[id title], - 'notes' => %i[id] - }, - 'ee' => { - 'project_tree' => [], - 'excluded_attributes' => { - 'project' => %i[date], - 'foo' => %i[bar baz] - } - } - }) - - expect(yaml_file.to_h).to eq({ - 'project_tree' => [], - 'excluded_attributes' => { - 'project' => %i[id title date], - 'notes' => %i[id], - 'foo' => %i[bar baz] - } - }) + let(:config_hash) { YAML.safe_load(config, [Symbol]) } + + before do + allow_any_instance_of(described_class).to receive(:parse_yaml) do + config_hash.deep_dup + end end - it 'merges the included attributes list' do - allow(yaml_file) - .to receive(:parse_yaml) - .and_return({ - 'project_tree' => [], - 'included_attributes' => { - 'project' => %i[id title], - 'notes' => %i[id] - }, - 'ee' => { - 'project_tree' => [], - 'included_attributes' => { - 'project' => %i[date], - 'foo' => %i[bar baz] + context 'when using CE' do + before do + allow(Gitlab).to receive(:ee?) { false } + end + + it 'just returns the normalized Hash' do + is_expected.to eq( + { + tree: { + project: { + labels: { + priorities: {} + }, + milestones: { + events: { + push_event_payload: {} + } + } + } + }, + included_attributes: { + user: [:id] + }, + excluded_attributes: { + project: [:name] + }, + methods: { + labels: [:type], + events: [:action] } } - }) - - expect(yaml_file.to_h).to eq({ - 'project_tree' => [], - 'included_attributes' => { - 'project' => %i[id title date], - 'notes' => %i[id], - 'foo' => %i[bar baz] - } - }) + ) + end end - it 'merges the methods list' do - allow(yaml_file) - .to receive(:parse_yaml) - .and_return({ - 'project_tree' => [], - 'methods' => { - 'project' => %i[id title], - 'notes' => %i[id] - }, - 'ee' => { - 'project_tree' => [], - 'methods' => { - 'project' => %i[date], - 'foo' => %i[bar baz] + context 'when using EE' do + before do + allow(Gitlab).to receive(:ee?) { true } + end + + it 'just returns the normalized Hash' do + is_expected.to eq( + { + tree: { + project: { + labels: { + priorities: {} + }, + milestones: { + events: { + push_event_payload: {} + } + }, + protected_branches: { + unprotect_access_levels: {} + } + } + }, + included_attributes: { + user: [:id, :name_ee] + }, + excluded_attributes: { + project: [:name, :name_without_ee] + }, + methods: { + labels: [:type, :type_ee], + events: [:action], + events_ee: [:action_ee] } } - }) - - expect(yaml_file.to_h).to eq({ - 'project_tree' => [], - 'methods' => { - 'project' => %i[id title date], - 'notes' => %i[id], - 'foo' => %i[bar baz] - } - }) + ) + end end end end diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb index 5ed9fef1597..3442e22c11f 100644 --- a/spec/lib/gitlab/import_export/model_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb @@ -8,7 +8,7 @@ describe 'Import/Export model configuration' do let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys } let(:model_names) do - names = names_from_tree(config_hash['project_tree']) + names = names_from_tree(config_hash.dig('tree', 'project')) # Remove duplicated or add missing models # - project is not part of the tree, so it has to be added manually. diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index f93ff074770..87f665bd995 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -2,96 +2,45 @@ require 'spec_helper' describe Gitlab::ImportExport::Reader do let(:shared) { Gitlab::ImportExport::Shared.new(nil) } - let(:test_config) { 'spec/support/import_export/import_export.yml' } - let(:project_tree_hash) do - { - except: [:id, :created_at], - include: [:issues, :labels, - { merge_requests: { - only: [:id], - except: [:iid], - include: [:merge_request_diff, :merge_request_test] - } }, - { commit_statuses: { include: :commit } }, - { project_members: { include: { user: { only: [:email] } } } }] - } - end - - before do - allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config) - end - - it 'generates hash from project tree config' do - expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash) - end - - context 'individual scenarios' do - it 'generates the correct hash for a single project relation' do - setup_yaml(project_tree: [:issues]) - - expect(described_class.new(shared: shared).project_tree).to match(include: [:issues]) - end - - it 'generates the correct hash for a single project feature relation' do - setup_yaml(project_tree: [:project_feature]) - expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature]) - end + describe '#project_tree' do + subject { described_class.new(shared: shared).project_tree } - it 'generates the correct hash for a multiple project relation' do - setup_yaml(project_tree: [:issues, :snippets]) + it 'delegates to AttributesFinder#find_root' do + expect_any_instance_of(Gitlab::ImportExport::AttributesFinder) + .to receive(:find_root) + .with(:project) - expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets]) + subject end - it 'generates the correct hash for a single sub-relation' do - setup_yaml(project_tree: [issues: [:notes]]) + context 'when exception raised' do + before do + expect_any_instance_of(Gitlab::ImportExport::AttributesFinder) + .to receive(:find_root) + .with(:project) + .and_raise(StandardError) + end - expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }]) - end - - it 'generates the correct hash for a multiple sub-relation' do - setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]]) - - expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }]) - end + it { is_expected.to be false } - it 'generates the correct hash for a sub-relation with another sub-relation' do - setup_yaml(project_tree: [merge_requests: [notes: :author]]) + it 'logs the error' do + expect(shared).to receive(:error).with(instance_of(StandardError)) - expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }]) + subject + end end + end - it 'generates the correct hash for a relation with included attributes' do - setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] }) - - expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }]) - end - - it 'generates the correct hash for a relation with excluded attributes' do - setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }) - - expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }]) - end - - it 'generates the correct hash for a relation with both excluded and included attributes' do - setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] }) - - expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }]) - end - - it 'generates the correct hash for a relation with custom methods' do - setup_yaml(project_tree: [:issues], methods: { issues: [:name] }) - - expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }]) - end + describe '#group_members_tree' do + subject { described_class.new(shared: shared).group_members_tree } - it 'generates the correct hash for group members' do - expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } }) - end + it 'delegates to AttributesFinder#find_root' do + expect_any_instance_of(Gitlab::ImportExport::AttributesFinder) + .to receive(:find_root) + .with(:group_members) - def setup_yaml(hash) - allow(YAML).to receive(:load_file).with(test_config).and_return(hash) + subject end end end diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb index 15748407f0c..17bb5bcc155 100644 --- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb +++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::ImportExport::RelationRenameService do let(:user) { create(:admin) } let(:group) { create(:group, :nested) } - let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let!(:project) { create(:project, :builds_disabled, :issues_disabled, group: group, name: 'project', path: 'project') } let(:shared) { project.import_export_shared } before do @@ -24,7 +24,6 @@ describe Gitlab::ImportExport::RelationRenameService do let(:import_path) { 'spec/lib/gitlab/import_export' } let(:file_content) { IO.read("#{import_path}/project.json") } let!(:json_file) { ActiveSupport::JSON.decode(file_content) } - let(:tree_hash) { project_tree_restorer.instance_variable_get(:@tree_hash) } before do allow(shared).to receive(:export_path).and_return(import_path) @@ -92,21 +91,25 @@ describe Gitlab::ImportExport::RelationRenameService do end context 'when exporting' do - let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: user, shared: shared) } - let(:project_tree) { project_tree_saver.send(:project_json) } + let(:export_content_path) { project_tree_saver.full_path } + let(:export_content_hash) { ActiveSupport::JSON.decode(File.read(export_content_path)) } + let(:injected_hash) { renames.values.product([{}]).to_h } - it 'adds old relationships to the exported file' do - project_tree.merge!(renames.values.map { |new_name| [new_name, []] }.to_h) + let(:project_tree_saver) do + Gitlab::ImportExport::ProjectTreeSaver.new( + project: project, current_user: user, shared: shared) + end - allow(project_tree_saver).to receive(:save) do |arg| - project_tree_saver.send(:project_json_tree) + it 'adds old relationships to the exported file' do + # we inject relations with new names that should be rewritten + expect(project_tree_saver).to receive(:serialize_project_tree).and_wrap_original do |method, *args| + method.call(*args).merge(injected_hash) end - result = project_tree_saver.save - - saved_data = ActiveSupport::JSON.decode(result) + expect(project_tree_saver.save).to eq(true) - expect(saved_data.keys).to include(*(renames.keys + renames.values)) + expect(export_content_hash.keys).to include(*renames.keys) + expect(export_content_hash.keys).to include(*renames.values) end end end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 519c519fbcf..5168064bb84 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -151,6 +151,24 @@ describe PagesDomain do end end end + + context 'with ecdsa certificate' do + it "is valid" do + domain = build(:pages_domain, :ecdsa) + + expect(domain).to be_valid + end + + context 'when curve is set explicitly by parameters' do + it 'adds errors to private key' do + domain = build(:pages_domain, :explicit_ecdsa) + + expect(domain).to be_invalid + + expect(domain.errors[:key]).not_to be_empty + end + end + end end describe 'validations' do diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml index 734d6838f4d..ed2a3243f0d 100644 --- a/spec/support/import_export/import_export.yml +++ b/spec/support/import_export/import_export.yml @@ -1,13 +1,16 @@ # Class relationships to be included in the project import/export -project_tree: - - :issues - - :labels - - merge_requests: - - :merge_request_diff - - :merge_request_test - - commit_statuses: - - :commit - - project_members: +tree: + project: + - :issues + - :labels + - merge_requests: + - :merge_request_diff + - :merge_request_test + - commit_statuses: + - :commit + - project_members: + - :user + group_members: - :user included_attributes: diff --git a/spec/validators/named_ecdsa_key_validator_spec.rb b/spec/validators/named_ecdsa_key_validator_spec.rb new file mode 100644 index 00000000000..044c5b84a56 --- /dev/null +++ b/spec/validators/named_ecdsa_key_validator_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamedEcdsaKeyValidator do + let(:validator) { described_class.new(attributes: [:key]) } + let!(:domain) { build(:pages_domain) } + + subject { validator.validate_each(domain, :key, value) } + + context 'with empty value' do + let(:value) { nil } + + it 'does not add any error if value is empty' do + subject + + expect(domain.errors).to be_empty + end + end + + shared_examples 'does not add any error' do + it 'does not add any error' do + expect(value).to be_present + + subject + + expect(domain.errors).to be_empty + end + end + + context 'when key is not EC' do + let(:value) { attributes_for(:pages_domain)[:key] } + + include_examples 'does not add any error' + end + + context 'with ECDSA certificate with named curve' do + let(:value) { attributes_for(:pages_domain, :ecdsa)[:key] } + + include_examples 'does not add any error' + end + + context 'with ECDSA certificate with explicit curve params' do + let(:value) { attributes_for(:pages_domain, :explicit_ecdsa)[:key] } + + it 'adds errors' do + expect(value).to be_present + + subject + + expect(domain.errors[:key]).not_to be_empty + end + end +end diff --git a/yarn.lock b/yarn.lock index 88f5b82b283..c64c3a6acaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11815,6 +11815,11 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +timezone-mock@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.0.8.tgz#1b9f7af13f2bf84b7aa3d3d6e24aa17255b6037d" + integrity sha512-7dgx34HJPY8O/c5dbqG+I9S3TVDjrfssXmS8BNqiy8sdYvYDfM7shHpNA6VTDQWcDGyv43bE3El6YuFDQf1X3g== + tiny-emitter@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" |