diff options
195 files changed, 4210 insertions, 1793 deletions
diff --git a/.gitignore b/.gitignore index 3ffe4263c4f..fcbb4c352a9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,11 +61,9 @@ eslint-report.html /shared/artifacts/ /rails_best_practices_output.html /tags -/tmp/* /vendor/bundle/* /vendor/gitaly-ruby /builds* -/shared/* /.gitlab_workhorse_secret /webpack-report/ /knapsack/ @@ -73,7 +71,6 @@ eslint-report.html /locale/**/LC_MESSAGES /locale/**/*.time_stamp /.rspec -/plugins/* /.gitlab_pages_secret /.gitlab_smime_key /.gitlab_smime_cert diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index ba9624aeeab..e502614b5ca 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -15,6 +15,7 @@ ## Author's checklist - [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). +- [ ] If applicable, update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html). - [ ] Link docs to and from the higher-level index page, plus other related docs where helpful. - [ ] Apply the ~Documentation label. diff --git a/Dangerfile b/Dangerfile index 094d55e8652..228190cd530 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,23 +1,12 @@ # frozen_string_literal: true + +require_relative 'lib/gitlab_danger' + danger.import_plugin('danger/plugins/helper.rb') danger.import_plugin('danger/plugins/roulette.rb') unless helper.release_automation? - danger.import_dangerfile(path: 'danger/metadata') - danger.import_dangerfile(path: 'danger/changes_size') - danger.import_dangerfile(path: 'danger/changelog') - danger.import_dangerfile(path: 'danger/specs') - danger.import_dangerfile(path: 'danger/gemfile') - danger.import_dangerfile(path: 'danger/database') - danger.import_dangerfile(path: 'danger/documentation') - danger.import_dangerfile(path: 'danger/frozen_string') - danger.import_dangerfile(path: 'danger/commit_messages') - danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies') - danger.import_dangerfile(path: 'danger/prettier') - danger.import_dangerfile(path: 'danger/eslint') - danger.import_dangerfile(path: 'danger/roulette') - danger.import_dangerfile(path: 'danger/single_codebase') - danger.import_dangerfile(path: 'danger/gitlab_ui_wg') - danger.import_dangerfile(path: 'danger/ce_ee_vue_templates') - danger.import_dangerfile(path: 'danger/only_documentation') + GitlabDanger.new(helper.gitlab_helper).rule_names.each do |file| + danger.import_dangerfile(path: File.join('danger', file)) + end end @@ -106,7 +106,7 @@ gem 'fog-aws', '~> 3.5' # Locked until fog-google resolves https://github.com/fog/fog-google/issues/421. # Also see config/initializers/fog_core_patch.rb. gem 'fog-core', '= 2.1.0' -gem 'fog-google', '~> 1.8' +gem 'fog-google', '~> 1.9' gem 'fog-local', '~> 0.6' gem 'fog-openstack', '~> 1.0' gem 'fog-rackspace', '~> 0.1.1' @@ -135,7 +135,7 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '0.0.9' -gem 'rouge', '~> 3.7' +gem 'rouge', '~> 3.10' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.4' @@ -318,6 +318,7 @@ end group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false + gem 'danger', '~> 6.0', require: false gem 'letter_opener_web', '~> 1.3.4' gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6add217bc32..011a365110c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,9 +140,15 @@ GEM numerizer (~> 0.1.1) chunky_png (1.3.5) citrus (3.0.2) + claide (1.0.3) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) + colored2 (3.1.2) commonmarker (0.17.13) ruby-enum (~> 0.5) concord (0.1.5) @@ -151,6 +157,8 @@ GEM concurrent-ruby (1.1.5) connection_pool (2.2.2) contracts (0.11.0) + cork (0.3.0) + colored2 (~> 3.1) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) @@ -158,6 +166,19 @@ GEM css_parser (1.5.0) addressable daemons (1.2.6) + danger (6.0.9) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (~> 0.9) + faraday-http-cache (~> 2.0) + git (~> 1.5) + kramdown (~> 2.0) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (~> 4.7) + terminal-table (~> 1) database_cleaner (1.7.0) debug_inspector (0.0.3) debugger-ruby_core_source (1.3.8) @@ -227,6 +248,8 @@ GEM railties (>= 3.0.0) faraday (0.12.2) multipart-post (>= 1.2, < 3) + faraday-http-cache (2.0.0) + faraday (~> 0.8) faraday_middleware (0.12.2) faraday (>= 0.7.4, < 1.0) faraday_middleware-multi_json (0.0.6) @@ -261,7 +284,7 @@ GEM excon (~> 0.58) formatador (~> 0.2) mime-types - fog-google (1.8.2) + fog-google (1.9.1) fog-core (<= 2.1.0) fog-json (~> 1.2) fog-xml (~> 0.1.0) @@ -308,6 +331,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) + git (1.5.0) gitaly (1.58.0) grpc (~> 1.0) github-markup (1.7.0) @@ -478,6 +502,9 @@ GEM kgio (2.11.2) knapsack (1.17.0) rake + kramdown (2.1.0) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) kubeclient (4.2.2) http (~> 3.0) recursive-open-struct (~> 1.0, >= 1.0.4) @@ -535,10 +562,12 @@ GEM mustermann-grape (1.0.0) mustermann (~> 1.0.0) nakayoshi_fork (0.0.4) + nap (1.1.0) net-ldap (0.16.0) net-ssh (5.2.0) netrc (0.11.0) nio4r (2.3.1) + no_proxy_fix (0.1.2) nokogiri (1.10.4) mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) @@ -615,6 +644,7 @@ GEM addressable (~> 2.5) omniauth (~> 1.3) openid_connect (~> 1.1) + open4 (1.3.4) openid_connect (1.1.6) activemodel attr_required (>= 1.0.0) @@ -769,7 +799,7 @@ GEM retriable (3.1.2) rinku (2.0.0) rotp (2.1.2) - rouge (3.7.0) + rouge (3.10.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -926,6 +956,8 @@ GEM ffi sysexits (1.2.0) temple (0.8.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) test-prof (0.2.5) text (1.3.1) thin (1.7.2) @@ -1055,6 +1087,7 @@ DEPENDENCIES concurrent-ruby (~> 1.1) connection_pool (~> 2.0) creole (~> 0.5.0) + danger (~> 6.0) database_cleaner (~> 1.7.0) deckar01-task_list (= 2.2.0) default_value_for (~> 3.2.0) @@ -1081,7 +1114,7 @@ DEPENDENCIES fog-aliyun (~> 0.3) fog-aws (~> 3.5) fog-core (= 2.1.0) - fog-google (~> 1.8) + fog-google (~> 1.9) fog-local (~> 0.6) fog-openstack (~> 1.0) fog-rackspace (~> 0.1.1) @@ -1196,7 +1229,7 @@ DEPENDENCIES redis-rails (~> 5.0.2) request_store (~> 1.3) responders (~> 2.0) - rouge (~> 3.7) + rouge (~> 3.10) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 3.8.0) 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/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index f9284266b72..f9a08f151c5 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -2,9 +2,9 @@ import $ from 'jquery'; import { GlButton } from '@gitlab/ui'; import { getMilestone } from 'ee_else_ce/boards/boards_util'; +import ListIssue from 'ee_else_ce/boards/models/issue'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; -import ListIssue from '../models/issue'; import boardsStore from '../stores/boards_store'; export default { @@ -54,6 +54,9 @@ export default { const assignees = this.list.assignee ? [this.list.assignee] : []; const milestone = getMilestone(this.list); + const { weightFeatureAvailable } = boardsStore; + const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {}; + const issue = new ListIssue({ title: this.title, labels, @@ -61,6 +64,7 @@ export default { assignees, milestone, project_id: this.selectedProject.id, + weight, }); eventHub.$emit(`scroll-board-list-${this.list.id}`); diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 7296426549a..ebb2f5b23e4 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -245,6 +245,7 @@ export default { <div v-if="!loading" ref="content" + data-qa-selector="boards_dropdown_content" class="dropdown-content flex-fill" @scroll.passive="throttledSetScrollFade" > diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index b6da572b201..27959898fb7 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -458,7 +458,6 @@ export default { </div> </application-row> <application-row - v-if="isProjectCluster" id="knative" :logo-url="knativeLogo" :title="applications.knative.title" diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 43ae54133af..b1d568532a6 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -38,6 +38,10 @@ export default { type: String, required: true, }, + userCanEnableErrorTracking: { + type: Boolean, + required: true, + }, }, computed: { ...mapState(['errors', 'externalUrl', 'loading']), @@ -111,14 +115,26 @@ export default { </gl-table> </div> </div> - <div v-else> + <div v-else-if="userCanEnableErrorTracking"> <gl-empty-state :title="__('Get started with error tracking')" - :description="__('Monitor your errors by integrating with Sentry')" + :description="__('Monitor your errors by integrating with Sentry.')" :primary-button-text="__('Enable error tracking')" :primary-button-link="enableErrorTrackingLink" :svg-path="illustrationPath" /> </div> + <div v-else> + <gl-empty-state :title="__('Get started with error tracking')" :svg-path="illustrationPath"> + <template #description> + <div> + <span>{{ __('Monitor your errors by integrating with Sentry.') }}</span> + <a href="/help/user/project/operations/error_tracking.html"> + {{ __('More information') }} + </a> + </div> + </template> + </gl-empty-state> + </div> </div> </template> diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js index 3d609448efe..073e2c8f1c7 100644 --- a/app/assets/javascripts/error_tracking/index.js +++ b/app/assets/javascripts/error_tracking/index.js @@ -14,9 +14,10 @@ export default () => { render(createElement) { const domEl = document.querySelector(this.$options.el); const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset; - let { errorTrackingEnabled } = domEl.dataset; + let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset; errorTrackingEnabled = parseBoolean(errorTrackingEnabled); + userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking); return createElement('error-tracking-list', { props: { @@ -24,6 +25,7 @@ export default () => { enableErrorTrackingLink, errorTrackingEnabled, illustrationPath, + userCanEnableErrorTracking, }, }); }, diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index ba0dea626dc..27c1b639889 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -20,6 +20,9 @@ export default { <stage-column-component v-for="(stage, index) in graph" :key="stage.name" + :class="{ + 'append-right-48': shouldAddRightMargin(index), + }" :title="capitalizeStageName(stage.name)" :groups="stage.groups" :stage-connector-class="stageConnectorClass(index, stage)" diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js index 66e9476dadf..f383a4b3368 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js @@ -40,5 +40,15 @@ export default { refreshPipelineGraph() { this.$emit('refreshPipelineGraph'); }, + /** + * CSS class is applied: + * - if pipeline graph contains only one stage column component + * + * @param {number} index + * @returns {boolean} + */ + shouldAddRightMargin(index) { + return !(index === this.graph.length - 1); + }, }, }; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index e9218dcec67..b95978b6966 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -395,6 +395,7 @@ img.emoji { .prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left: 20px; } .prepend-left-32 { margin-left: 32px; } +.prepend-left-64 { margin-left: 64px; } .append-right-4 { margin-right: 4px; } .append-right-5 { margin-right: 5px; } .append-right-8 { margin-right: 8px; } @@ -402,6 +403,8 @@ img.emoji { .append-right-15 { margin-right: 15px; } .append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right: 20px; } +.append-right-32 { margin-right: 32px; } +.append-right-48 { margin-right: 48px; } .prepend-right-32 { margin-right: 32px; } .append-bottom-0 { margin-bottom: 0; } .append-bottom-4 { margin-bottom: $gl-padding-4; } 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/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 5f4db37c317..d4bd5b1b7dc 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -476,10 +476,6 @@ display: inline-block; vertical-align: top; - &:not(:last-child) { - margin-right: 44px; - } - &.left-margin { &:not(:first-child) { margin-left: 44px; 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/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 5cfb0ac307d..ec89bb89edc 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -3,7 +3,7 @@ module Projects module Settings class OperationsController < Projects::ApplicationController - before_action :authorize_update_environment! + before_action :authorize_admin_operations! helper_method :error_tracking_setting diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index e773ec09924..fb631f09f10 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -20,6 +20,7 @@ class RegistrationsController < Devise::RegistrationsController super do |new_user| persist_accepted_terms_if_required(new_user) + yield new_user if block_given? end rescue Gitlab::Access::AccessDeniedError redirect_to(new_user_session_path) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b735f9ff3b8..8ed6ff56e2b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -193,15 +193,30 @@ class IssuableFinder projects = if current_user && params[:authorized_only].presence && !current_user_related? current_user.authorized_projects(min_access_level) - elsif group - find_group_projects else - Project.public_or_visible_to_user(current_user, min_access_level) + projects_public_or_visible_to_user end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord end + def projects_public_or_visible_to_user + projects = + if group + if params[:projects] + find_group_projects.id_in(params[:projects]) + else + find_group_projects + end + elsif params[:projects] + Project.id_in(params[:projects]) + else + Project + end + + projects.public_or_visible_to_user(current_user, min_access_level) + end + def find_group_projects return Project.none unless group @@ -209,7 +224,7 @@ class IssuableFinder Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord else group.projects - end.public_or_visible_to_user(current_user, min_access_level) + end end def search diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index e9a4ea9157b..993d33c4fc2 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -16,7 +16,7 @@ module Types :create_deployment, :push_to_delete_protected_branch, :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, - :create_pages, :destroy_pages, :read_pages_content + :create_pages, :destroy_pages, :read_pages_content, :admin_operations end end end diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb index 6daf2e21ca2..fd1222a1dfb 100644 --- a/app/helpers/projects/error_tracking_helper.rb +++ b/app/helpers/projects/error_tracking_helper.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true module Projects::ErrorTrackingHelper - def error_tracking_data(project) + def error_tracking_data(current_user, project) error_tracking_enabled = !!project.error_tracking_setting&.enabled? { 'index-path' => project_error_tracking_index_path(project, format: :json), + 'user-can-enable-error-tracking' => can?(current_user, :admin_operations, project).to_s, 'enable-error-tracking-link' => project_settings_operations_path(project), 'error-tracking-enabled' => error_tracking_enabled.to_s, 'illustration-path' => image_path('illustrations/cluster_popover.svg') 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/ci/pipeline.rb b/app/models/ci/pipeline.rb index d620959b538..d2271c1335c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -23,6 +23,7 @@ module Ci belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :merge_request, class_name: 'MergeRequest' + belongs_to :external_pull_request has_internal_id :iid, scope: :project, presence: false, init: ->(s) do s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count @@ -64,6 +65,11 @@ module Ci validates :merge_request, presence: { if: :merge_request_event? } validates :merge_request, absence: { unless: :merge_request_event? } validates :tag, inclusion: { in: [false], if: :merge_request_event? } + + validates :external_pull_request, presence: { if: :external_pull_request_event? } + validates :external_pull_request, absence: { unless: :external_pull_request_event? } + validates :tag, inclusion: { in: [false], if: :external_pull_request_event? } + validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create @@ -683,6 +689,10 @@ module Ci variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s) variables.concat(merge_request.predefined_variables) end + + if external_pull_request_event? && external_pull_request + variables.concat(external_pull_request.predefined_variables) + end end end diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index 571c4271475..0c2bd0aa8eb 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -23,7 +23,8 @@ module Ci api: 5, external: 6, chat: 8, - merge_request_event: 10 + merge_request_event: 10, + external_pull_request_event: 11 } end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index ef1af1fc8bc..a976093ac0c 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -10,7 +10,6 @@ module Clusters self.table_name = 'clusters' PROJECT_ONLY_APPLICATIONS = { - Applications::Knative.application_name => Applications::Knative }.freeze APPLICATIONS = { Applications::Helm.application_name => Applications::Helm, @@ -18,7 +17,8 @@ module Clusters Applications::CertManager.application_name => Applications::CertManager, Applications::Prometheus.application_name => Applications::Prometheus, Applications::Runner.application_name => Applications::Runner, - Applications::Jupyter.application_name => Applications::Jupyter + Applications::Jupyter.application_name => Applications::Jupyter, + Applications::Knative.application_name => Applications::Knative }.merge(PROJECT_ONLY_APPLICATIONS).freeze DEFAULT_ENVIRONMENT = '*' KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN' diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb new file mode 100644 index 00000000000..65ae8d95500 --- /dev/null +++ b/app/models/external_pull_request.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# This model stores pull requests coming from external providers, such as +# GitHub, when GitLab project is set as CI/CD only and remote mirror. +# +# When setting up a remote mirror with GitHub we subscribe to push and +# pull_request webhook events. When a pull request is opened on GitHub, +# a webhook is sent out, we create or update the status of the pull +# request locally. +# +# When the mirror is updated and changes are pushed to branches we check +# if there are open pull requests for the source and target branch. +# If so, we create pipelines for external pull requests. +class ExternalPullRequest < ApplicationRecord + include Gitlab::Utils::StrongMemoize + include ShaAttribute + + belongs_to :project + + sha_attribute :source_sha + sha_attribute :target_sha + + validates :source_branch, presence: true + validates :target_branch, presence: true + validates :source_sha, presence: true + validates :target_sha, presence: true + validates :source_repository, presence: true + validates :target_repository, presence: true + validates :status, presence: true + + enum status: { + open: 1, + closed: 2 + } + + # We currently don't support pull requests from fork, so + # we are going to return an error to the webhook + validate :not_from_fork + + scope :by_source_branch, ->(branch) { where(source_branch: branch) } + scope :by_source_repository, -> (repository) { where(source_repository: repository) } + + def self.create_or_update_from_params(params) + find_params = params.slice(:project_id, :source_branch, :target_branch) + + safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request| + yield(pull_request) if block_given? + end + end + + def actual_branch_head? + actual_source_branch_sha == source_sha + end + + def from_fork? + source_repository != target_repository + end + + def source_ref + Gitlab::Git::BRANCH_REF_PREFIX + source_branch + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch) + end + end + + private + + def actual_source_branch_sha + project.commit(source_ref)&.sha + end + + def not_from_fork + if from_fork? + errors.add(:base, 'Pull requests from fork are not supported') + end + end + + def self.safe_find_or_initialize_and_update(find:, update:) + safe_ensure_unique(retries: 1) do + model = find_or_initialize_by(find) + + if model.update(update) + yield(model) if block_given? + end + + model + end + end +end 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/models/project.rb b/app/models/project.rb index 17b52d0578e..d948410e397 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -291,6 +291,8 @@ class Project < ApplicationRecord has_many :remote_mirrors, inverse_of: :project has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' + has_many :external_pull_requests, inverse_of: :project + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :import_data 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/policies/project_policy.rb b/app/policies/project_policy.rb index e2634692dc7..5c36b59f07b 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -294,6 +294,7 @@ class ProjectPolicy < BasePolicy enable :destroy_release enable :destroy_artifacts enable :daily_statistics + enable :admin_operations end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb index e22be6880bb..9504fdd8eac 100644 --- a/app/serializers/merge_request_noteable_entity.rb +++ b/app/serializers/merge_request_noteable_entity.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class MergeRequestNoteableEntity < Grape::Entity +class MergeRequestNoteableEntity < IssuableEntity include RequestAwareEntity # Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 8f8582afb43..4a7f62de9e1 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -18,7 +18,8 @@ module Ci Gitlab::Ci::Pipeline::Chain::Limit::Activity, Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze - def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block) + # rubocop: disable Metrics/ParameterLists + def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block) @pipeline = Ci::Pipeline.new command = Gitlab::Ci::Pipeline::Chain::Command.new( @@ -32,6 +33,7 @@ module Ci trigger_request: trigger_request, schedule: schedule, merge_request: merge_request, + external_pull_request: external_pull_request, ignore_skip_ci: ignore_skip_ci, save_incompleted: save_on_errors, seeds_block: block, @@ -62,6 +64,7 @@ module Ci pipeline end + # rubocop: enable Metrics/ParameterLists def execute!(*args, &block) execute(*args, &block).tap do |pipeline| diff --git a/app/services/external_pull_requests/create_pipeline_service.rb b/app/services/external_pull_requests/create_pipeline_service.rb new file mode 100644 index 00000000000..36411465ff1 --- /dev/null +++ b/app/services/external_pull_requests/create_pipeline_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# This service is responsible for creating a pipeline for a given +# ExternalPullRequest coming from other providers such as GitHub. + +module ExternalPullRequests + class CreatePipelineService < BaseService + def execute(pull_request) + return unless pull_request.open? && pull_request.actual_branch_head? + + create_pipeline_for(pull_request) + end + + private + + def create_pipeline_for(pull_request) + Ci::CreatePipelineService.new(project, current_user, create_params(pull_request)) + .execute(:external_pull_request_event, external_pull_request: pull_request) + end + + def create_params(pull_request) + { + ref: pull_request.source_ref, + source_sha: pull_request.source_sha, + target_sha: pull_request.target_sha + } + end + end +end diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index 47c308c8280..35a4d2015fa 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -57,7 +57,9 @@ module Git Ci::CreatePipelineService .new(project, current_user, pipeline_params) - .execute(:push, pipeline_options) + .execute!(:push, pipeline_options) + rescue Ci::CreatePipelineService::CreateError => ex + log_pipeline_errors(ex) end def execute_project_hooks @@ -125,5 +127,29 @@ module Git project.mark_stuck_remote_mirrors_as_failed! project.update_remote_mirrors end + + def log_pipeline_errors(exception) + data = { + class: self.class.name, + correlation_id: Labkit::Correlation::CorrelationId.current_id.to_s, + project_id: project.id, + project_path: project.full_path, + message: "Error creating pipeline", + errors: exception.to_s, + pipeline_params: pipeline_params + } + + logger.warn(data) + end + + def logger + if Sidekiq.server? + Sidekiq.logger + else + # This service runs in Sidekiq, so this shouldn't ever be + # called, but this is included just in case. + Gitlab::ProjectServiceLogger + end + end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 3555864f834..900e5063621 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -76,13 +76,16 @@ class IssuableBaseService < BaseService end def filter_labels - params[:add_label_ids] = labels_service.filter_labels_ids_in_param(:add_label_ids) if params[:add_label_ids] - params[:remove_label_ids] = labels_service.filter_labels_ids_in_param(:remove_label_ids) if params[:remove_label_ids] + label_ids_to_filter(:add_label_ids, :add_labels, false) + label_ids_to_filter(:remove_label_ids, :remove_labels, true) + label_ids_to_filter(:label_ids, :labels, false) + end - if params[:label_ids] - params[:label_ids] = labels_service.filter_labels_ids_in_param(:label_ids) - elsif params[:labels] - params[:label_ids] = labels_service.find_or_create_by_titles.map(&:id) + def label_ids_to_filter(label_id_key, label_key, find_only) + if params[label_id_key] + params[label_id_key] = labels_service.filter_labels_ids_in_param(label_id_key) + elsif params[label_key] + params[label_id_key] = labels_service.find_or_create_by_titles(label_key, find_only: find_only).map(&:id) end end diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb index fe477d96970..8886e58d6ef 100644 --- a/app/services/labels/available_labels_service.rb +++ b/app/services/labels/available_labels_service.rb @@ -9,8 +9,8 @@ module Labels @params = params end - def find_or_create_by_titles - labels = params.delete(:labels) + def find_or_create_by_titles(key = :labels, find_only: false) + labels = params.delete(key) return [] unless labels @@ -23,7 +23,7 @@ module Labels include_ancestor_groups: true, title: label_name.strip, available_labels: available_labels - ).execute + ).execute(find_only: find_only) label end.compact diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 628873519d7..a47dd42aea0 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -9,9 +9,9 @@ module Labels @params = params.dup.with_indifferent_access end - def execute(skip_authorization: false) + def execute(skip_authorization: false, find_only: false) @skip_authorization = skip_authorization - find_or_create_label + find_or_create_label(find_only: find_only) end private @@ -30,9 +30,11 @@ module Labels # Only creates the label if current_user can do so, if the label does not exist # and the user can not create the label, nil is returned # rubocop: disable CodeReuse/ActiveRecord - def find_or_create_label + def find_or_create_label(find_only: false) new_label = available_labels.find_by(title: title) + return new_label if find_only + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent)) create_params = params.except(:include_ancestor_groups) new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index b28f80939ae..308a3a10d1a 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -18,6 +18,18 @@ module MergeRequests merge_request.target_project = find_target_project filter_params(merge_request) + + # merge_request.assign_attributes(...) below is a Rails + # method that only work if all the params it is passed have + # corresponding fields in the database. As there are no fields + # in the database for :add_label_ids and :remove_label_ids, we + # need to remove them from the params before the call to + # merge_request.assign_attributes(...) + # + # IssuableBaseService#process_label_ids takes care + # of the removal. + params[:label_ids] = process_label_ids(params, extra_label_ids: merge_request.label_ids.to_a) + merge_request.assign_attributes(params.to_h.compact) merge_request.compare_commits = [] diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb index b210004e6e1..0168b31005e 100644 --- a/app/services/merge_requests/push_options_handler_service.rb +++ b/app/services/merge_requests/push_options_handler_service.rb @@ -100,7 +100,8 @@ module MergeRequests merge_request = ::MergeRequests::CreateService.new( project, current_user, - merge_request.attributes.merge(assignees: merge_request.assignees) + merge_request.attributes.merge(assignees: merge_request.assignees, + label_ids: merge_request.label_ids) ).execute end @@ -122,7 +123,9 @@ module MergeRequests title: push_options[:title], description: push_options[:description], target_branch: push_options[:target], - force_remove_source_branch: push_options[:remove_source_branch] + force_remove_source_branch: push_options[:remove_source_branch], + label: push_options[:label], + unlabel: push_options[:unlabel] } params.compact! @@ -134,6 +137,9 @@ module MergeRequests ) end + params[:add_labels] = params.delete(:label).keys if params.has_key?(:label) + params[:remove_labels] = params.delete(:unlabel).keys if params.has_key?(:unlabel) + params 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/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index c1f4b3adfec..7cc7d1783c4 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -48,14 +48,14 @@ - if group_sidebar_link?(:issues) = nav_link(path: group_issues_sub_menu_items) do - = link_to issues_group_path(@group) do + = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' } do .nav-icon-container = sprite_icon('issues') %span.nav-item-name = _('Issues') %span.badge.badge-pill.count= number_with_delimiter(issues_count) - %ul.sidebar-sub-level-items + %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} } = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do = link_to issues_group_path(@group) do %strong.fly-out-top-item-name @@ -70,7 +70,7 @@ - if group_sidebar_link?(:boards) = nav_link(path: ['boards#index', 'boards#show']) do - = link_to group_boards_path(@group), title: boards_link_text do + = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do %span = boards_link_text diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml index bc02c5f0e5a..96f61584a99 100644 --- a/app/views/projects/error_tracking/index.html.haml +++ b/app/views/projects/error_tracking/index.html.haml @@ -1,3 +1,3 @@ - page_title _('Errors') -#js-error_tracking{ data: error_tracking_data(@project) } +#js-error_tracking{ data: error_tracking_data(@current_user, @project) } diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 15499c89ffb..928b54ea28f 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -13,12 +13,13 @@ %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve - git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch} - git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD + -# All repo/branch refs have been quoted to allow support for special characters (such as #my-branch) + git fetch "#{h default_url_to_repo(@merge_request.source_project)}" "#{h @merge_request.source_branch}" + git checkout -b "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}" FETCH_HEAD - else :preserve git fetch origin - git checkout -b #{h @merge_request.source_branch} origin/#{h @merge_request.source_branch} + git checkout -b "#{h @merge_request.source_branch}" "origin/#{h @merge_request.source_branch}" %p %strong Step 2. Review the changes locally @@ -31,20 +32,20 @@ - if @merge_request.for_fork? :preserve git fetch origin - git checkout origin/#{h @merge_request.target_branch} - git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} + git checkout "origin/#{h @merge_request.target_branch}" + git merge --no-ff "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}" - else :preserve git fetch origin - git checkout origin/#{h @merge_request.target_branch} - git merge --no-ff #{h @merge_request.source_branch} + git checkout "origin/#{h @merge_request.target_branch}" + git merge --no-ff "#{h @merge_request.source_branch}" %p %strong Step 4. Push the result of the merge to GitLab = clipboard_button(target: "pre#merge-info-4", title: "Copy commands to clipboard") %pre.dark#merge-info-4 :preserve - git push origin #{h @merge_request.target_branch} + git push origin "#{h @merge_request.target_branch}" - unless @merge_request.can_be_merged_by?(current_user) %p Note that pushing to GitLab requires write access to this repository. diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml index 1a163cc4a54..7ff6c0a2019 100644 --- a/app/views/projects/mirrors/_instructions.html.haml +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -3,6 +3,7 @@ %li = _('The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>.').html_safe + %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe %li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe %li - minutes = Gitlab.config.gitlab_shell.git_timeout / 60 diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index d0f9374e832..b2ea45d6f1a 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -27,6 +27,7 @@ %ul %li = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe + %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe %li = _('If your HTTP repository is not publicly accessible, add your credentials.') %li diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml index feca109dade..5f3d49adff7 100644 --- a/app/views/shared/members/_sort_dropdown.html.haml +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -1,4 +1,4 @@ -= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold pr-2' += label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold px-2' .dropdown.inline.qa-user-sort-dropdown = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }) %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 991a177018e..a33afd436b0 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -160,6 +160,7 @@ - repository_import - repository_remove_remote - system_hook_push +- update_external_pull_requests - update_merge_requests - update_project_statistics - upload_checksum diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb new file mode 100644 index 00000000000..c5acfa82ada --- /dev/null +++ b/app/workers/update_external_pull_requests_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class UpdateExternalPullRequestsWorker + include ApplicationWorker + + def perform(project_id, user_id, ref) + project = Project.find_by_id(project_id) + return unless project + + user = User.find_by_id(user_id) + return unless user + + branch = Gitlab::Git.branch_name(ref) + return unless branch + + external_pull_requests = project.external_pull_requests + .by_source_repository(project.import_source) + .by_source_branch(branch) + + external_pull_requests.find_each do |pull_request| + ExternalPullRequests::CreatePipelineService.new(project, user) + .execute(pull_request) + end + end +end diff --git a/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml b/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml new file mode 100644 index 00000000000..9f34a912dc5 --- /dev/null +++ b/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml @@ -0,0 +1,5 @@ +--- +title: Only show /copy_metadata quick action when usable +merge_request: 31735 +author: Lee Tickett +type: fixed diff --git a/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml b/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml new file mode 100644 index 00000000000..1962a662179 --- /dev/null +++ b/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml @@ -0,0 +1,5 @@ +--- +title: Fix parsing of months in time tracking commands +merge_request: 32165 +author: +type: fixed diff --git a/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml b/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml new file mode 100644 index 00000000000..b152943942f --- /dev/null +++ b/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml @@ -0,0 +1,5 @@ +--- +title: Display `more information` docs link on error tracking page when users do not have permissions to enable that feature +merge_request: 32365 +author: Romain Maneschi +type: fixed diff --git a/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml b/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml new file mode 100644 index 00000000000..54bec43815c --- /dev/null +++ b/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml @@ -0,0 +1,5 @@ +--- +title: Update merge train documentation +merge_request: 32218 +author: +type: changed diff --git a/changelogs/unreleased/ac-accelerate-wiki-attachments.yml b/changelogs/unreleased/ac-accelerate-wiki-attachments.yml new file mode 100644 index 00000000000..347a570488e --- /dev/null +++ b/changelogs/unreleased/ac-accelerate-wiki-attachments.yml @@ -0,0 +1,5 @@ +--- +title: Preprocess wiki attachments with GitLab-Workhorse +merge_request: 32663 +author: +type: performance diff --git a/changelogs/unreleased/add-label-push-opts.yml b/changelogs/unreleased/add-label-push-opts.yml new file mode 100644 index 00000000000..1289020e4e5 --- /dev/null +++ b/changelogs/unreleased/add-label-push-opts.yml @@ -0,0 +1,5 @@ +--- +title: Support adding and removing labels w/ push opts +merge_request: 31831 +author: +type: added 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/id-autosave-for-new-mr.yml b/changelogs/unreleased/id-autosave-for-new-mr.yml new file mode 100644 index 00000000000..8f269094715 --- /dev/null +++ b/changelogs/unreleased/id-autosave-for-new-mr.yml @@ -0,0 +1,5 @@ +--- +title: Fix sharing localStorage with all MRs +merge_request: 32699 +author: +type: fixed diff --git a/changelogs/unreleased/instance-group-level-knative.yml b/changelogs/unreleased/instance-group-level-knative.yml new file mode 100644 index 00000000000..5108334a3ea --- /dev/null +++ b/changelogs/unreleased/instance-group-level-knative.yml @@ -0,0 +1,5 @@ +--- +title: Allow Knative to be installed on group and instance level clusters +merge_request: 32128 +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/changelogs/unreleased/quote-branch-names-in-instructions.yml b/changelogs/unreleased/quote-branch-names-in-instructions.yml new file mode 100644 index 00000000000..fe5964c8918 --- /dev/null +++ b/changelogs/unreleased/quote-branch-names-in-instructions.yml @@ -0,0 +1,5 @@ +--- +title: Quote branch names in how to merge instructions +merge_request: 32639 +author: Lee Tickett +type: fixed diff --git a/changelogs/unreleased/sh-add-margin-member-list.yml b/changelogs/unreleased/sh-add-margin-member-list.yml new file mode 100644 index 00000000000..35d35aad8bf --- /dev/null +++ b/changelogs/unreleased/sh-add-margin-member-list.yml @@ -0,0 +1,5 @@ +--- +title: Add padding to left of "Sort by" in members dropdown +merge_request: 32602 +author: +type: other diff --git a/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml b/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml new file mode 100644 index 00000000000..b334355cab6 --- /dev/null +++ b/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml @@ -0,0 +1,5 @@ +--- +title: Log errors for failed pipeline creation in PostReceive +merge_request: 32633 +author: +type: other diff --git a/changelogs/unreleased/update-rouge.yml b/changelogs/unreleased/update-rouge.yml new file mode 100644 index 00000000000..6f44de02d76 --- /dev/null +++ b/changelogs/unreleased/update-rouge.yml @@ -0,0 +1,5 @@ +--- +title: Update rouge to v3.10.0 +merge_request: 32745 +author: +type: other diff --git a/changelogs/unreleased/use_default_external_auth_label_empty.yml b/changelogs/unreleased/use_default_external_auth_label_empty.yml new file mode 100644 index 00000000000..9c1039b0875 --- /dev/null +++ b/changelogs/unreleased/use_default_external_auth_label_empty.yml @@ -0,0 +1,6 @@ +--- +title: Prevent empty external authorization classification labels from overriding + the default label +merge_request: 32517 +author: Will Chandler +type: fixed diff --git a/config/initializers/chronic_duration.rb b/config/initializers/chronic_duration.rb index b65b06c813a..aa43eef434c 100644 --- a/config/initializers/chronic_duration.rb +++ b/config/initializers/chronic_duration.rb @@ -1 +1,5 @@ +# frozen_string_literal: true + ChronicDuration.raise_exceptions = true + +ChronicDuration.prepend Gitlab::Patch::ChronicDuration diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 7edec576f9a..e89e9657314 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -115,3 +115,4 @@ - [export_csv, 1] - [incident_management, 2] - [jira_connect, 1] + - [update_external_pull_requests, 3] diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile index 3550cb7eabf..5cdad09db6e 100644 --- a/danger/database/Dangerfile +++ b/danger/database/Dangerfile @@ -1,7 +1,13 @@ # frozen_string_literal: true -SCHEMA_NOT_UPDATED_MESSAGE = <<~MSG -**New %<migrations>s added but %<schema>s wasn't updated.** +gitlab_danger = GitlabDanger.new(helper.gitlab_helper) + +SCHEMA_NOT_UPDATED_MESSAGE_SHORT = <<~MSG +New %<migrations>s added but %<schema>s wasn't updated. +MSG + +SCHEMA_NOT_UPDATED_MESSAGE_FULL = <<~MSG +**#{SCHEMA_NOT_UPDATED_MESSAGE_SHORT}** Usually, when adding new %<migrations>s, %<schema>s should be updated too (unless the migration isn't changing the DB schema @@ -29,14 +35,18 @@ geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb}).emp non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty? geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty? +format_str = gitlab_danger.ci? ? SCHEMA_NOT_UPDATED_MESSAGE_FULL : SCHEMA_NOT_UPDATED_MESSAGE_SHORT + if non_geo_migration_created && !non_geo_db_schema_updated - warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'migrations', schema: gitlab.html_link("db/schema.rb")) + warn format(format_str, migrations: 'migrations', schema: gitlab_danger.html_link("db/schema.rb")) end if geo_migration_created && !geo_db_schema_updated - warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'Geo migrations', schema: gitlab.html_link("ee/db/geo/schema.rb")) + warn format(format_str, migrations: 'Geo migrations', schema: gitlab_danger.html_link("ee/db/geo/schema.rb")) end +return unless gitlab_danger.ci? + db_paths_to_review = helper.changes_by_category[:database] if gitlab.mr_labels.include?('database') || db_paths_to_review.any? diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile index 96c0d9b7478..ad64c3a6c60 100644 --- a/danger/documentation/Dangerfile +++ b/danger/documentation/Dangerfile @@ -6,20 +6,22 @@ unless docs_paths_to_review.empty? message 'This merge request adds or changes files that require a review ' \ 'from the Technical Writing team.' - markdown(<<~MARKDOWN) -## Documentation review + if GitlabDanger.new(helper.gitlab_helper).ci? + markdown(<<~MARKDOWN) + ## Documentation review -The following files require a review from a technical writer: + The following files require a review from a technical writer: -* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")} + * #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")} -The review does not need to block merging this merge request. See the: + The review does not need to block merging this merge request. See the: -- [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review. -- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review. - MARKDOWN + - [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review. + - [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review. + MARKDOWN - unless gitlab.mr_labels.include?('Documentation') - warn 'This merge request is missing the ~Documentation label.' + unless gitlab.mr_labels.include?('Documentation') + warn 'This merge request is missing the ~Documentation label.' + end end end diff --git a/danger/duplicate_yarn_dependencies/Dangerfile b/danger/duplicate_yarn_dependencies/Dangerfile index 25f81ec86a4..492b888d00e 100644 --- a/danger/duplicate_yarn_dependencies/Dangerfile +++ b/danger/duplicate_yarn_dependencies/Dangerfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -return unless helper.all_changed_files.include? 'yarn.lock' +return unless helper.all_changed_files.include?('yarn.lock') duplicate = `node_modules/.bin/yarn-deduplicate --list --strategy fewer yarn.lock` .split(/$/) @@ -11,17 +11,19 @@ return if duplicate.empty? warn 'This merge request has introduced duplicated yarn dependencies.' -markdown(<<~MARKDOWN) - ## Duplicate yarn dependencies +if GitlabDanger.new(helper.gitlab_helper).ci? + markdown(<<~MARKDOWN) + ## Duplicate yarn dependencies - The following dependencies should be de-duplicated: + The following dependencies should be de-duplicated: - * #{duplicate.map { |path| "`#{path}`" }.join("\n* ")} + * #{duplicate.map { |path| "`#{path}`" }.join("\n* ")} - Please run the following command and commit the changes to `yarn.lock`: + Please run the following command and commit the changes to `yarn.lock`: - ``` - node_modules/.bin/yarn-deduplicate --strategy fewer yarn.lock \\ - && yarn install - ``` -MARKDOWN + ``` + node_modules/.bin/yarn-deduplicate --strategy fewer yarn.lock \\ + && yarn install + ``` + MARKDOWN +end diff --git a/danger/eslint/Dangerfile b/danger/eslint/Dangerfile index 4916cacfd7e..92830bd7706 100644 --- a/danger/eslint/Dangerfile +++ b/danger/eslint/Dangerfile @@ -13,17 +13,19 @@ return if eslint_candidates.empty? warn 'This merge request changed files with disabled eslint rules. Please consider fixing them.' -markdown(<<~MARKDOWN) - ## Disabled eslint rules +if GitlabDanger.new(helper.gitlab_helper).ci? + markdown(<<~MARKDOWN) + ## Disabled eslint rules - The following files have disabled `eslint` rules. Please consider fixing them: + The following files have disabled `eslint` rules. Please consider fixing them: - * #{eslint_candidates.map { |path| "`#{path}`" }.join("\n* ")} + * #{eslint_candidates.map { |path| "`#{path}`" }.join("\n* ")} - Run the following command for more details + Run the following command for more details - ``` - node_modules/.bin/eslint --report-unused-disable-directives --no-inline-config \\ - #{eslint_candidates.map { |path| " '#{path}'" }.join(" \\\n")} - ``` -MARKDOWN + ``` + node_modules/.bin/eslint --report-unused-disable-directives --no-inline-config \\ + #{eslint_candidates.map { |path| " '#{path}'" }.join(" \\\n")} + ``` + MARKDOWN +end diff --git a/danger/frozen_string/Dangerfile b/danger/frozen_string/Dangerfile index b9687ef6b83..8d3ac3dee68 100644 --- a/danger/frozen_string/Dangerfile +++ b/danger/frozen_string/Dangerfile @@ -16,11 +16,13 @@ if files_to_fix.any? warn 'This merge request adds files that do not enforce frozen string literal. ' \ 'See https://gitlab.com/gitlab-org/gitlab-ce/issues/47424 for more information.' - markdown(<<~MARKDOWN) - ## Enable Frozen String Literal + if GitlabDanger.new(helper.gitlab_helper).ci? + markdown(<<~MARKDOWN) + ## Enable Frozen String Literal - The following files should have `#{MAGIC_COMMENT}` on the first line: + The following files should have `#{MAGIC_COMMENT}` on the first line: - * #{files_to_fix.map { |path| "`#{path}`" }.join("\n* ")} - MARKDOWN + * #{files_to_fix.map { |path| "`#{path}`" }.join("\n* ")} + MARKDOWN + end end diff --git a/danger/gemfile/Dangerfile b/danger/gemfile/Dangerfile index dfe64f79d7b..07c4c07cfe8 100644 --- a/danger/gemfile/Dangerfile +++ b/danger/gemfile/Dangerfile @@ -1,5 +1,9 @@ -GEMFILE_LOCK_NOT_UPDATED_MESSAGE = <<~MSG.freeze -**%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.** +GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT = <<~MSG.freeze +%<gemfile>s was updated but %<gemfile_lock>s wasn't updated. +MSG + +GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL = <<~MSG.freeze +**#{GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT}** Usually, when %<gemfile>s is updated, you should run ``` @@ -19,5 +23,14 @@ gemfile_modified = git.modified_files.include?("Gemfile") gemfile_lock_modified = git.modified_files.include?("Gemfile.lock") if gemfile_modified && !gemfile_lock_modified - warn format(GEMFILE_LOCK_NOT_UPDATED_MESSAGE, gemfile: gitlab.html_link("Gemfile"), gemfile_lock: gitlab.html_link("Gemfile.lock")) + gitlab_danger = GitlabDanger.new(helper.gitlab_helper) + + format_str = gitlab_danger.ci? ? GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL : GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT + + message = format(format_str, + gemfile: gitlab_danger.html_link("Gemfile"), + gemfile_lock: gitlab_danger.html_link("Gemfile.lock") + ) + + warn(message) end diff --git a/danger/prettier/Dangerfile b/danger/prettier/Dangerfile index 37c4b78a213..0be75db8baa 100644 --- a/danger/prettier/Dangerfile +++ b/danger/prettier/Dangerfile @@ -19,21 +19,23 @@ return if unpretty.empty? warn 'This merge request changed frontend files without pretty printing them.' -markdown(<<~MARKDOWN) - ## Pretty print Frontend files +if GitlabDanger.new(helper.gitlab_helper).ci? + markdown(<<~MARKDOWN) + ## Pretty print Frontend files - The following files should have been pretty printed with `prettier`: + The following files should have been pretty printed with `prettier`: - * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")} + * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")} - Please run + Please run - ``` - node_modules/.bin/prettier --write \\ - #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")} - ``` + ``` + node_modules/.bin/prettier --write \\ + #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")} + ``` - Also consider auto-formatting [on-save]. + Also consider auto-formatting [on-save]. - [on-save]: https://docs.gitlab.com/ee/development/new_fe_guide/style/prettier.html -MARKDOWN + [on-save]: https://docs.gitlab.com/ee/development/new_fe_guide/style/prettier.html + MARKDOWN +end diff --git a/db/migrate/20190829131130_create_external_pull_requests.rb b/db/migrate/20190829131130_create_external_pull_requests.rb new file mode 100644 index 00000000000..0c3168807ec --- /dev/null +++ b/db/migrate/20190829131130_create_external_pull_requests.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class CreateExternalPullRequests < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX = 'index_external_pull_requests_on_project_and_branches' + + def change + create_table :external_pull_requests do |t| + t.timestamps_with_timezone null: false + t.references :project, null: false, foreign_key: { on_delete: :cascade }, index: false + t.integer :pull_request_iid, null: false + t.integer :status, null: false, limit: 2 + t.string :source_branch, null: false, limit: 255 + t.string :target_branch, null: false, limit: 255 + t.string :source_repository, null: false, limit: 255 + t.string :target_repository, null: false, limit: 255 + t.binary :source_sha, null: false + t.binary :target_sha, null: false + + t.index [:project_id, :source_branch, :target_branch], unique: true, name: INDEX + end + end +end diff --git a/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb b/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb new file mode 100644 index 00000000000..5abf56742b1 --- /dev/null +++ b/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddExternalPullRequestIdToCiPipelines < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :ci_pipelines, :external_pull_request_id, :bigint + end + + def down + remove_column :ci_pipelines, :external_pull_request_id + end +end diff --git a/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb b/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb new file mode 100644 index 00000000000..d2f5ad7a420 --- /dev/null +++ b/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_pipelines, :external_pull_request_id, where: 'external_pull_request_id IS NOT NULL' + end + + def down + remove_concurrent_index :ci_pipelines, :external_pull_request_id + end +end diff --git a/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb b/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb new file mode 100644 index 00000000000..b38fda83047 --- /dev/null +++ b/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddForeignKeyToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :ci_pipelines, :external_pull_requests, column: :external_pull_request_id, on_delete: :nullify + end + + def down + remove_foreign_key :ci_pipelines, :external_pull_requests + end +end diff --git a/db/schema.rb b/db/schema.rb index 61f7787f192..6ddfb8bcb39 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -754,7 +754,9 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do t.integer "merge_request_id" t.binary "source_sha" t.binary "target_sha" + t.bigint "external_pull_request_id" t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id" + t.index ["external_pull_request_id"], name: "index_ci_pipelines_on_external_pull_request_id", where: "(external_pull_request_id IS NOT NULL)" t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)" t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id" t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)" @@ -1323,6 +1325,21 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id" end + create_table "external_pull_requests", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.bigint "project_id", null: false + t.integer "pull_request_iid", null: false + t.integer "status", limit: 2, null: false + t.string "source_branch", limit: 255, null: false + t.string "target_branch", limit: 255, null: false + t.string "source_repository", limit: 255, null: false + t.string "target_repository", limit: 255, null: false + t.binary "source_sha", null: false + t.binary "target_sha", null: false + t.index ["project_id", "source_branch", "target_branch"], name: "index_external_pull_requests_on_project_and_branches", unique: true + end + create_table "feature_gates", id: :serial, force: :cascade do |t| t.string "feature_key", null: false t.string "key", null: false @@ -3785,6 +3802,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify + add_foreign_key "ci_pipelines", "external_pull_requests", name: "fk_190998ef09", on_delete: :nullify add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade @@ -3849,6 +3867,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do add_foreign_key "events", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade + add_foreign_key "external_pull_requests", "projects", on_delete: :cascade add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify add_foreign_key "fork_network_members", "projects", on_delete: :cascade diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md index 64eca0b00f6..f643d853d10 100644 --- a/doc/administration/database_load_balancing.md +++ b/doc/administration/database_load_balancing.md @@ -146,7 +146,11 @@ The following options can be set: | `use_tcp` | Lookup DNS resources using TCP instead of UDP | false | If `record_type` is set to `SRV`, GitLab will continue to use a round-robin algorithm -and will ignore the `weight` and `priority` in the record. +and will ignore the `weight` and `priority` in the record. Since SRV records usually +return hostnames instead of IPs, GitLab will look for the IPs of returned hostnames +in the additional section of the SRV response. If no IP is found for a hostname, Gitlab +will query the configured `nameserver` for ANY record for each such hostname looking for A or AAAA records, eventually +dropping this hostname from rotation if it can't resolve its IP. The `interval` value specifies the _minimum_ time between checks. If the A record has a TTL greater than this value, then service discovery will honor said @@ -212,28 +216,25 @@ without it immediately leading to errors being presented to the users. ## Logging -The load balancer logs various messages, such as: +The load balancer logs various events in +[`database_load_balancing.log`](logs.md#database_load_balancinglog-premium-only), such as - When a host is marked as offline - When a host comes back online - When all secondaries are offline +- When a read is retried on a different host due to a query conflict -Each log message contains the tag `[DB-LB]` to make searching/filtering of such -log entries easier. For example: +The log is structured with each entry a JSON object containing at least: -``` -[DB-LB] Host 10.123.2.5 came back online -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Host 10.123.2.6 came back online -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Marking host 10.123.2.7 as offline -[DB-LB] Host 10.123.2.7 came back online -[DB-LB] Host 10.123.2.7 came back online +- An `event` field useful for filtering. +- A human-readable `message` field. +- Some event-specific metadata. For example, `db_host` +- Contextual information that is always logged. For example, `severity` and `time`. + +For example: + +```json +{"severity":"INFO","time":"2019-09-02T12:12:01.728Z","correlation_id":"abcdefg","event":"host_online","message":"Host came back online","db_host":"111.222.333.444","db_port":null,"tag":"rails.database_load_balancing","environment":"production","hostname":"web-example-1","fqdn":"gitlab.example.com","path":null,"params":null} ``` ## Handling Stale Reads diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index df6c554decb..318711fd281 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -54,6 +54,21 @@ http://localhost:8080/plantuml you can change these defaults by editing the `/etc/tomcat7/server.xml` file. +### Making local PlantUML accessible using custom GitLab setup + +The PlantUML server runs locally on your server, so it is not accessible +externally. As such, it is necessary to catch external PlantUML calls and +redirect them to the local server. + +The idea is to redirect each call to `https://gitlab.example.com/-/plantuml/` +to the local PlantUML server `http://localhost:8080/plantuml`. + +To enable the redirection, add the following line in `/etc/gitlab/gitlab.rb`: + +```ruby +nginx['custom_gitlab_server_config'] = "location /-/plantuml { \n proxy_cache off; \n proxy_pass http://127.0.0.1:8080; \n}\n" +``` + ## GitLab You need to enable PlantUML integration from Settings under Admin Area. To do @@ -62,7 +77,7 @@ that, login with an Admin account and do following: - In GitLab, go to **Admin Area > Settings > Integrations**. - Expand the **PlantUML** section. - Check **Enable PlantUML** checkbox. -- Set the PlantUML instance as **PlantUML URL**. +- Set the PlantUML instance as `https://gitlab.example.com/-/plantuml/`. ## Creating Diagrams diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 4c43a434817..c51b53c596e 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -339,11 +339,11 @@ installations from source. [Rack Attack]: ../security/rack_attack.md [Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md -## `database_load_balancing.log` +## `database_load_balancing.log` **(PREMIUM ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15442) in GitLab 12.3. -This log is used for observability of [Database Load Balancing](database_load_balancing.md). +Contains details of GitLab's [Database Load Balancing](database_load_balancing.md). It is stored at: - `/var/log/gitlab/gitlab-rails/database_load_balancing.log` for Omnibus GitLab packages. 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/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index b73fe38f53e..d19f11ba1d4 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -525,6 +525,77 @@ PUT /projects/:id/merge_requests/:merge_request_iid/approvers } ``` +### Get the approval state of merge requests + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/13712) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3. + +You can request information about a merge request's approval state by using the following endpoint: + +``` +GET /projects/:id/merge_requests/:merge_request_iid/approval_state +``` + +The `approval_rules_overwritten` will be `true` if the merge request level rules +are created for the merge request. If there's none, it'll be `false`. + +This includes additional information about the users who have already approved +(`approved_by`) and whether a rule is already approved (`approved`). + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The IID of MR | + +```json +{ + "approval_rules_overwritten": true, + "rules": [ + { + "id": 1, + "name": "Ruby", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 4, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "approvals_required": 2, + "users": [ + { + "id": 4, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [], + "contains_hidden_groups": false, + "approved_by": [ + { + "id": 4, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "source_rule": null, + "approved": true + } + ] +} +``` + ### Get merge request level rules >**Note:** This API endpoint is only available on 12.3 Starter and above. diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 5f875528a6c..b6059c71b27 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -23,8 +23,8 @@ Below are the changes made between V3 and V4. - Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093) - Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328) - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853) - - `/licences` - - `/licences/:key` + - `/licenses` + - `/licenses/:key` - `/gitignores` - `/gitlab_ci_ymls` - `/dockerfiles` diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 00995f881da..f9612d0c53d 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -22,7 +22,7 @@ The following table lists examples with step-by-step tutorials that are containe | Use case | Resource | |:----------------------------|:---------------------------------------------------------------------------------------------------------------------------| -| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). | +| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](../../user/project/merge_requests/browser_performance_testing.md). | | Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). | | Deployment with Dpl | [Using `dpl` as deployment tool](deployment/README.md). | | Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). | diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md index 3266e5dc62e..4a73fe2e62c 100644 --- a/doc/ci/examples/browser_performance.md +++ b/doc/ci/examples/browser_performance.md @@ -1,158 +1,5 @@ --- -type: howto +redirect_to: '../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing' --- -# Browser Performance Testing with the sitespeed.io container - -NOTE: **Note:** -The job definition shown below is supported on GitLab 11.5 and later versions. -It also requires the GitLab Runner 11.5 or later. -For earlier versions, use the [previous job definitions](#previous-job-definitions). - -This example shows how to run the -[sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on -your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io) -using Docker-in-Docker. - -First, you need GitLab Runner with -[docker-in-docker build](../docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor). - -Once you set up the Runner, add a new job to `.gitlab-ci.yml` that -generates the expected report: - -```yaml -performance: - stage: performance - image: docker:git - variables: - URL: https://example.com - services: - - docker:stable-dind - script: - - mkdir gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL - - mv sitespeed-results/data/performance.json performance.json - artifacts: - paths: - - sitespeed-results/ - reports: - performance: performance.json -``` - -The above example will create a `performance` job in your CI/CD pipeline and will run -sitespeed.io against the webpage you defined in `URL` to gather key metrics. -The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for -sitespeed.io is downloaded in order to save the report as a -[Performance report artifact](../yaml/README.md#artifactsreportsperformance-premium) -that you can later download and analyze. -Due to implementation limitations we always take the latest Performance artifact available. - -The full HTML sitespeed.io report will also be saved as an artifact, and if you have -[GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed -directly in your browser. - -For further customization options for sitespeed.io, including the ability to -provide a list of URLs to test, please see the -[Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) documentation. - -TIP: **Tip:** -For [GitLab Premium](https://about.gitlab.com/pricing/) users, key metrics are automatically -extracted and shown right in the merge request widget. -[Learn more on Browser Performance Testing in merge requests](../../user/project/merge_requests/browser_performance_testing.md). - -## Performance testing on Review Apps - -The above CI YML is great for testing against static environments, and it can -be extended for dynamic environments. There are a few extra steps to take to -set this up: - -1. The `performance` job should run after the dynamic environment has started. -1. In the `review` job, persist the hostname and upload it as an artifact so - it's available to the `performance` job (the same can be done for static - environments like staging and production to unify the code path). Saving it - as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt` - in your job's `script`. -1. In the `performance` job, read the previous artifact into an environment - variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test - URLs. -1. You can now run the sitespeed.io container against the desired hostname and - paths. - -Your `.gitlab-ci.yml` file would look like: - -```yaml -stages: - - deploy - - performance - -review: - stage: deploy - environment: - name: review/$CI_COMMIT_REF_SLUG - url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN - script: - - run_deploy_script - - echo $CI_ENVIRONMENT_URL > environment_url.txt - artifacts: - paths: - - environment_url.txt - only: - - branches - except: - - master - -performance: - stage: performance - image: docker:git - services: - - docker:stable-dind - dependencies: - - review - script: - - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) - - mkdir gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" - - mv sitespeed-results/data/performance.json performance.json - artifacts: - paths: - - sitespeed-results/ - reports: - performance: performance.json -``` - -A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml). - -## Previous job definitions - -CAUTION: **Caution:** -Before GitLab 11.5, Performance job and artifact had to be named specifically -to automatically extract report data and show it in the merge request widget. -While these old job definitions are still maintained they have been deprecated -and may be removed in next major release, GitLab 12.0. -You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change. - -For GitLab 11.4 and earlier, the job should look like: - -```yaml -performance: - stage: performance - image: docker:git - variables: - URL: https://example.com - services: - - docker:stable-dind - script: - - mkdir gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL - - mv sitespeed-results/data/performance.json performance.json - artifacts: - paths: - - performance.json - - sitespeed-results/ -``` +This document was moved to [another location](../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing). diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md index 126e12e460f..d68fba82f4b 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md @@ -61,6 +61,20 @@ CAUTION: **Warning:** Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](../index.md#configuring-pipelines-for-merge-requests), otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state. +## Automatic pipeline cancelation + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12996) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3. + +GitLab CI can detect the presence of redundant pipelines, +and will cancel them automatically in order to conserve CI resources. + +When a user merges a merge request immediately within an ongoing merge +train, the train will be reconstructed, as it will recreate the expected +post-merge commit and pipeline. In this case, the merge train may already +have pipelines running against the previous expected post-merge commit. +These pipelines are considered redundant and will be automatically +canceled. + ## Troubleshooting ### Pipelines for merged results not created even with new change pushed to merge request diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md index 377ae9717b2..9e18d1d883c 100644 --- a/doc/ci/multi_project_pipelines.md +++ b/doc/ci/multi_project_pipelines.md @@ -178,6 +178,8 @@ the ones defined in the upstream project will take precedence. ### Mirroring status from triggered pipeline +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3. + You can mirror the pipeline status from the triggered pipeline to the source bridge job by using `strategy: depend`. For example: diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index 7569ccc04c1..c3165dc2e21 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -191,6 +191,51 @@ end policies at once. The fields for these will all have be non-nullable booleans with a default description. +## Enums + +GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the +following rules apply: + +- Values must be uppercase. +- Class names must end with the string `Enum`. +- The `graphql_name` must not contain the string `Enum`. + +For example: + +```ruby +module Types + class TrafficLightStateEnum < BaseEnum + graphql_name 'TrafficLightState' + description 'State of a traffic light' + + value 'RED', description: 'Drivers must stop' + value 'YELLOW', description: 'Drivers must stop when it is safe to' + value 'GREEN', description: 'Drivers can start or keep driving' + end +end +``` + +If the enum will be used for a class property in Ruby that is not an uppercase string, +you can provide a `value:` option that will adapt the uppercase value. + +In the following example: + +- GraphQL inputs of `OPENED` will be converted to `'opened'`. +- Ruby values of `'opened'` will be converted to `"OPENED"` in GraphQL responses. + +```ruby +module Types + class EpicStateEnum < BaseEnum + graphql_name 'EpicState' + description 'State of a GitLab epic' + + value 'OPENED', value: 'opened', description: 'An open Epic' + value 'CLOSED', value: 'closed', description: 'An closed Epic' + end +end + +``` + ## Authorization Authorizations can be applied to both types and fields using the same diff --git a/doc/development/architecture.md b/doc/development/architecture.md index ee5fc553e27..147bd21e6c7 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -61,8 +61,8 @@ graph TB Unicorn --> PgBouncer[PgBouncer] Unicorn --> Redis Unicorn --> Gitaly - Redis --> Sidekiq - Sidekiq["Sidekiq (GitLab Rails, ES Indexer)"] --> PgBouncer + Sidekiq --> Redis + Sidekiq --> PgBouncer GitLabWorkhorse[GitLab Workhorse] --> Unicorn GitLabWorkhorse --> Redis GitLabWorkhorse --> Gitaly diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md index a456bbc781f..606ee431c3e 100644 --- a/doc/development/background_migrations.md +++ b/doc/development/background_migrations.md @@ -302,18 +302,18 @@ for more details. ## Best practices -1. Make sure to know how much data you're dealing with +1. Make sure to know how much data you're dealing with. 1. Make sure that background migration jobs are idempotent. 1. Make sure that tests you write are not false positives. 1. Make sure that if the data being migrated is critical and cannot be lost, the clean-up migration also checks the final state of the data before completing. -1. Make sure to know how much time it'll take to run all scheduled migrations +1. Make sure to know how much time it'll take to run all scheduled migrations. 1. When migrating many columns, make sure it won't generate too many dead tuples in the process (you may need to directly query the number of dead tuples - and adjust the scheduling according to this piece of data) + and adjust the scheduling according to this piece of data). 1. Make sure to discuss the numbers with a database specialist, the migration may add more pressure on DB than you expect (measure on staging, - or ask someone to measure on production) + or ask someone to measure on production). [migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md [issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351 diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md index ae0e2361840..9224a2548ab 100644 --- a/doc/development/fe_guide/development_process.md +++ b/doc/development/fe_guide/development_process.md @@ -58,6 +58,24 @@ Please use your best judgement when to use it and please contribute new points t - [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations. ``` +### Merge Request Review + +With the purpose of being [respectful of others' time](https://about.gitlab.com/handbook/values/#be-respectful-of-others-time) please follow these guidelines when asking for a review: + +- Make sure your Merge Request: + - milestone is set + - at least the labels suggested by danger-bot are set + - has a clear description + - includes before/after screenshots if there is a UI change + - pipeline is green + - includes tests + - includes a changelog entry (when necessary) +- Before assigning to a maintainer, assign to a reviewer. +- If you assigned a merge request, or pinged someone directly, keep in mind that we work in different timezones and asynchronously, so be patient. Unless the merge request is urgent (like fixing a broken master), please don't DM or reassign the merge request before waiting for a 24-hour window. +- If you have a question regarding your merge request/issue, make it on the merge request/issue. When we DM each other, we no longer have a SSOT and [no one else is able to contribute](https://about.gitlab.com/handbook/values/#public-by-default). +- When you have a big WIP merge request with many changes, you're adivsed to get the review started before adding/removing significant code. Make sure it is assigned well before the release cut-off, as the reviewer(s)/maintainer(s) would always prioritize reviewing finished MRs before WIP ones. +- Make sure to remove the WIP title before the last round of review. + ### Share your work early 1. Before writing code, ensure your vision of the architecture is aligned with diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md index 850ea6b60ac..8820b54fa87 100644 --- a/doc/development/testing_guide/end_to_end/page_objects.md +++ b/doc/development/testing_guide/end_to_end/page_objects.md @@ -167,6 +167,18 @@ There are two supported methods of defining elements within a view. Any existing `.qa-selector` class should be considered deprecated and we should prefer the `data-qa-selector` method of definition. +### Exceptions + +In some cases it might not be possible or worthwhile to add a selector. + +Some UI components use external libraries, including some maintained by third parties. +Even if a library is maintained by GitLab, the selector sanity test only runs +on code within the GitLab project, so it's not possible to specify the path for +the view for code in a library. + +In such rare cases it's reasonable to use CSS selectors in page object methods, +with a comment explaining why an `element` can't be added. + ## Running the test locally During development, you can run the `qa:selectors` test by running diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index ef319f7f0ce..f1456146032 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -298,3 +298,33 @@ gitlab_rails['omniauth_allow_bypass_two_factor'] = ['twitter', 'google_oauth2'] omniauth: allow_bypass_two_factor: ['twitter', 'google_oauth2'] ``` + +## Automatically sign in with provider + +You can add the `auto_sign_in_with_provider` setting to your +GitLab configuration to automatically redirect login requests +to your OmniAuth provider for authentication, thus removing the need to click a button +before actually signing in. + +For example, when using the Azure integration, you would set the following +to enable auto sign in. + +For Omnibus package: + +```ruby +gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'azure_oauth2' +``` + +For installations from source: + +```yaml +omniauth: + auto_sign_in_with_provider: azure_oauth2 +``` + +Please keep in mind that every sign in attempt will be redirected to the OmniAuth provider, +so you will not be able to sign in using local credentials. Make sure that at least one +of the OmniAuth users has admin permissions. + +You may also bypass the auto signin feature by browsing to +`https://gitlab.example.com/users/sign_in?auto_sign_in=false`. diff --git a/doc/security/reset_root_password.md b/doc/security/reset_root_password.md index ec360e2d338..00c9dc1407d 100644 --- a/doc/security/reset_root_password.md +++ b/doc/security/reset_root_password.md @@ -9,7 +9,7 @@ To reset your root password, first log into your server with root privileges. Start a Ruby on Rails console with this command: ```bash -gitlab-rails console production +gitlab-rails console -e production ``` Wait until the console has loaded. diff --git a/doc/user/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md index 9fe4b50a991..5117b5f476f 100644 --- a/doc/user/admin_area/diff_limits.md +++ b/doc/user/admin_area/diff_limits.md @@ -6,6 +6,8 @@ type: reference You can set a maximum size for display of diff files (patches). +For details about diff files, [View changes between files](../project/merge_requests/index.md#view-changes-between-file-versions). + ## Maximum diff patch size Diff files which exceed this value will be presented as 'too large' and won't diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index fbc130689e0..15a21bb82e0 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -205,6 +205,7 @@ Some analyzers can be customized with environment variables. | `GRADLE_PATH` | spotbugs | Path to the `gradle` executable. | | `JAVA_OPTS` | spotbugs | Additional arguments for the `java` executable. | | `JAVA_PATH` | spotbugs | Path to the `java` executable. | +| `SAST_JAVA_VERSION` | spotbugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. | | `MAVEN_CLI_OPTS` | spotbugs | Additional arguments for the `mvn` or `mvnw` executable. | | `MAVEN_PATH` | spotbugs | Path to the `mvn` executable. | | `MAVEN_REPO_PATH` | spotbugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). | diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 7169dfdf1e1..65b654d1553 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -82,10 +82,12 @@ certificates are valid and up-to-date. NOTE: **Note:** The -[stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) +[jetstack/cert-manager](https://github.com/jetstack/cert-manager) chart is used to install this application with a [`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/cert_manager/values.yaml) -file. +file. Prior to GitLab 12.3, +the [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) +chart was used. ### GitLab Runner @@ -188,7 +190,8 @@ You can clone repositories from the files tab in Jupyter: ### Knative -> Available for project-level clusters since GitLab 11.5. +> - Available for project-level clusters since GitLab 11.5. +> - Available for group-level and instance-level clusters since GitLab 12.3. [Knative](https://cloud.google.com/knative) provides a platform to create, deploy, and manage serverless workloads from a Kubernetes diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index f53dc056010..a79f368499c 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -87,7 +87,7 @@ pre-filled with the text you entered in the template(s). We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples. > **Tip:** -It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions. +It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions to perform the relevant actions. Here is an example for a Bug report template: diff --git a/doc/user/project/import/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png Binary files differindex c453c7e558a..90bcff5d31b 100644 --- a/doc/user/project/import/img/import_projects_from_repo_url.png +++ b/doc/user/project/import/img/import_projects_from_repo_url.png diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md index 49b9826a52a..2339cfa0db8 100644 --- a/doc/user/project/merge_requests/browser_performance_testing.md +++ b/doc/user/project/merge_requests/browser_performance_testing.md @@ -4,8 +4,7 @@ type: reference, howto # Browser Performance Testing **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507) -in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3. If your application offers a web interface and you are using [GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance @@ -25,18 +24,20 @@ for [additional metrics](https://gitlab.com/gitlab-org/gitlab-ee/issues/4370) in a future release. Going a step further, GitLab can show the Performance report right -in the merge request widget area: +in the merge request widget area (see below). ## Use cases For instance, consider the following workflow: -1. A member of the marketing team is attempting to track engagement by adding a new tool -1. With browser performance metrics, they see how their changes are impacting the usability of the page for end users -1. The metrics show that after their changes the performance score of the page has gone down -1. When looking at the detailed report, they see that the new Javascript library was included in `<head>` which affects loading page speed -1. They ask a front end developer to help them, who sets the library to load asynchronously -1. The frontend developer approves the merge request and authorizes its deployment to production +1. A member of the marketing team is attempting to track engagement by adding a new tool. +1. With browser performance metrics, they see how their changes are impacting the usability + of the page for end users. +1. The metrics show that after their changes the performance score of the page has gone down. +1. When looking at the detailed report, they see that the new JavaScript library was + included in `<head>` which affects loading page speed. +1. They ask a front end developer to help them, who sets the library to load asynchronously. +1. The frontend developer approves the merge request and authorizes its deployment to production. ## How it works @@ -48,15 +49,165 @@ example on [Testing Browser Performance](../../../ci/examples/browser_performanc GitLab then checks this report, compares key performance metrics for each page between the source and target branches, and shows the information right on the merge request. ->**Note:** +NOTE: **Note:** If the Performance report doesn't have anything to compare to, no information will be displayed in the merge request area. That is the case when you add the Performance job in your `.gitlab-ci.yml` for the very first time. -Consecutive merge requests will have something to compare to and the Performance +Consecutive merge requests will have something to compare to, and the Performance report will be shown properly. ![Performance Widget](img/browser_performance_testing.png) +## Configuring Browser Performance Testing + +NOTE: **Note:** +The job definition shown below is supported in GitLab 11.5 and later versions. +It also requires GitLab Runner 11.5 or later. For earlier versions, use the +[previous job definitions](#previous-job-definitions). + +This example shows how to run the [sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) +on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io) +using Docker-in-Docker. + +First, you need GitLab Runner with +[docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor). + +Once you set up the Runner, add a new job to `.gitlab-ci.yml` that generates the +expected report: + +```yaml +performance: + stage: performance + image: docker:git + variables: + URL: https://example.com + services: + - docker:stable-dind + script: + - mkdir gitlab-exporter + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js + - mkdir sitespeed-results + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - sitespeed-results/ + reports: + performance: performance.json +``` + +The above example will create a `performance` job in your CI/CD pipeline and will run +sitespeed.io against the webpage you defined in `URL` to gather key metrics. +The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance) +is downloaded in order to save the report as a [Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium) +that you can later download and analyze. Due to implementation limitations we always +take the latest Performance artifact available. + +The full HTML sitespeed.io report will also be saved as an artifact, and if you have +[GitLab Pages](../pages/index.md) enabled, it can be viewed directly in your browser. + +For further customization options for sitespeed.io, including the ability to provide a +list of URLs to test, please see the [Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) +documentation. + +TIP: **Tip:** +Key metrics are automatically extracted and shown in the merge request widget. + +### Performance testing on Review Apps + +The above CI YML is great for testing against static environments, and it can +be extended for dynamic environments. There are a few extra steps to take to +set this up: + +1. The `performance` job should run after the dynamic environment has started. +1. In the `review` job, persist the hostname and upload it as an artifact so + it's available to the `performance` job (the same can be done for static + environments like staging and production to unify the code path). Saving it + as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt` + in your job's `script`. +1. In the `performance` job, read the previous artifact into an environment + variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test + URLs. +1. You can now run the sitespeed.io container against the desired hostname and + paths. + +Your `.gitlab-ci.yml` file would look like: + +```yaml +stages: + - deploy + - performance + +review: + stage: deploy + environment: + name: review/$CI_COMMIT_REF_SLUG + url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN + script: + - run_deploy_script + - echo $CI_ENVIRONMENT_URL > environment_url.txt + artifacts: + paths: + - environment_url.txt + only: + - branches + except: + - master + +performance: + stage: performance + image: docker:git + services: + - docker:stable-dind + dependencies: + - review + script: + - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) + - mkdir gitlab-exporter + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js + - mkdir sitespeed-results + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - sitespeed-results/ + reports: + performance: performance.json +``` + +A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml). + +### Previous job definitions + +CAUTION: **Caution:** +Before GitLab 11.5, Performance job and artifact had to be named specifically +to automatically extract report data and show it in the merge request widget. +While these old job definitions are still maintained they have been deprecated +and may be removed in next major release, GitLab 12.0. +You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change. + +For GitLab 11.4 and earlier, the job should look like: + +```yaml +performance: + stage: performance + image: docker:git + variables: + URL: https://example.com + services: + - docker:stable-dind + script: + - mkdir gitlab-exporter + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js + - mkdir sitespeed-results + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - performance.json + - sitespeed-results/ +``` + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png Binary files differnew file mode 100644 index 00000000000..e56fbb9750f --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index d6da8cb99c7..aa58e971cc3 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -164,6 +164,26 @@ you hide threads that are no longer relevant. [Read more about resolving threads in merge requests reviews.](../../discussions/index.md) +## View changes between file versions + +The **Changes** tab of a merge request shows the changes to files between branches or +commits. This view of changes to a file is also known as a **diff**. By default, the diff view +compares the file in the merge request branch and the file in the target branch. + +The diff view includes the following: + +- The file's name and path. +- The number of lines added and deleted. +- Buttons for the following options: + - Toggle comments for this file; useful for inline reviews. + - Edit the file in the merge request's branch. + - Show full file, in case you want to look at the changes in context with the rest of the file. + - View file at the current commit. + - Preview the changes with [Review Apps](../../../ci/review_apps/index.md). +- The changed lines, with the specific changes highlighted. + +![Example screenshot of a source code diff](img/merge_request_diff_v12_2.png) + ## Commenting on any file line in merge requests > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13950) in GitLab 11.5. @@ -289,6 +309,7 @@ as pushing changes: - Set the merge request to remove the source branch when it's merged. - Set the title of the merge request to a particular title. - Set the description of the merge request to a particular description. +- Add or remove labels from the merge request. ### Create a new merge request using git push options @@ -375,6 +396,35 @@ git push -o merge_request.description="The description I want" You can also use this push option in addition to the `merge_request.create` push option. +### Add or remove labels using git push options + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31831) in GitLab 12.3. + +You can add or remove labels from merge requests using push options. + +For example, to add two labels to an existing merge request, use the +`merge_request.label` push option: + +```sh +git push -o merge_request.label="label1" -o merge_request.label="label2" +``` + +To remove two labels from an existing merge request, use +the `merge_request.unlabel` push option: + +```sh +git push -o merge_request.unlabel="label1" -o merge_request.unlabel="label2" +``` + +You can also use these push options in addition to the +`merge_request.create` push option. + +To create a merge request and add two labels to it, use: + +```sh +git push -o merge_request.create -o merge_request.label="label1" -o merge_request.label="label2" +``` + ## Find the merge request that introduced a change > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5. diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 9bf400e7dff..ecd8f74194e 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -194,7 +194,7 @@ terminal: Once the terminal has started, the console will be displayed and we could access the project repository files. -**Important**. The terminal job is branch dependant. This means that the +**Important**. The terminal job is branch dependent. This means that the configuration file used to trigger and configure the terminal will be the one in the selected branch of the Web IDE. diff --git a/doc/workflow/img/repository_mirroring_push_settings.png b/doc/workflow/img/repository_mirroring_push_settings.png Binary files differindex 21a6aca4526..3c0eacaa2df 100644 --- a/doc/workflow/img/repository_mirroring_push_settings.png +++ b/doc/workflow/img/repository_mirroring_push_settings.png diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index f5593927cc2..36db4f73885 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -62,9 +62,8 @@ git commit -am "Added Debian iso" # commit the file meta data git push origin master # sync the git repo and large file to the GitLab server ``` -NOTE: **Note:** **Make sure** that `.gitattributes` is tracked by Git. Otherwise Git -LFS will not be working properly for people cloning the project. +LFS will not be working properly for people cloning the project: ```bash git add .gitattributes diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index 753518d0424..5bf1f484106 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -37,7 +37,8 @@ The following are some possible use cases for repository mirroring: ## Pushing to a remote repository **(CORE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. +> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. For an existing project, you can set up push mirroring as follows: @@ -66,7 +67,8 @@ section. ### Push only protected branches **(CORE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. +> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. You can choose to only push your protected branches from GitLab to your remote repository. @@ -96,7 +98,8 @@ The repository will push soon. To force a push, click the appropriate button. ## Pulling from a remote repository **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. +> - [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11. NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab-ee/issues/10361) to GitLab.com users until September 22nd, 2019. @@ -154,7 +157,8 @@ Repository mirrors are updated as Sidekiq becomes available to process them. If ### SSH authentication -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6 +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6 SSH authentication is mutual: 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/api/validations/types/workhorse_file.rb b/lib/api/validations/types/workhorse_file.rb new file mode 100644 index 00000000000..18d111f6556 --- /dev/null +++ b/lib/api/validations/types/workhorse_file.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Validations + module Types + class WorkhorseFile < Virtus::Attribute + def coerce(input) + # Processing of multipart file objects + # is already taken care of by Gitlab::Middleware::Multipart. + # Nothing to do here. + input + end + + def value_coerced?(value) + value.is_a?(::UploadedFile) + end + end + end + end +end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 5724adb2c40..c5a5488950d 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -4,11 +4,21 @@ module API class Wikis < Grape::API helpers do def commit_params(attrs) - { - file_name: attrs[:file][:filename], - file_content: attrs[:file][:tempfile].read, - branch_name: attrs[:branch] - } + # In order to avoid service disruption this can work with an old workhorse without the acceleration + # the first branch of this if must be removed when we drop support for non accelerated uploads + if attrs[:file].is_a?(Hash) + { + file_name: attrs[:file][:filename], + file_content: attrs[:file][:tempfile].read, + branch_name: attrs[:branch] + } + else + { + file_name: attrs[:file].original_filename, + file_content: attrs[:file].read, + branch_name: attrs[:branch] + } + end end params :common_wiki_page_params do @@ -106,7 +116,7 @@ module API success Entities::WikiAttachment end params do - requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded' + requires :file, types: [::API::Validations::Types::SafeFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end post ":id/wikis/attachments" do diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index 164a4634d84..899df81ea5c 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -19,6 +19,7 @@ module Gitlab user: @command.current_user, pipeline_schedule: @command.schedule, merge_request: @command.merge_request, + external_pull_request: @command.external_pull_request, variables_attributes: Array(@command.variables_attributes) ) diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index afad391e8e0..58f89a6be5e 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -7,7 +7,7 @@ module Gitlab Command = Struct.new( :source, :project, :current_user, :origin_ref, :checkout_sha, :after_sha, :before_sha, :source_sha, :target_sha, - :trigger_request, :schedule, :merge_request, + :trigger_request, :schedule, :merge_request, :external_pull_request, :ignore_skip_ci, :save_incompleted, :seeds_block, :variables_attributes, :push_options, :chat_data, :allow_mirror_update diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index 17ad07bfc0c..e2911b4e6c8 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -38,8 +38,17 @@ module Gitlab ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md') end + def gitlab_helper + # Unfortunately the following does not work: + # - respond_to?(:gitlab) + # - respond_to?(:gitlab, true) + gitlab + rescue NoMethodError + nil + end + def release_automation? - gitlab.mr_author == RELEASE_TOOLS_BOT + gitlab_helper&.mr_author == RELEASE_TOOLS_BOT end def project_name 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 bd0f3e70749..06c94beead8 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -3,87 +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 - - :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: @@ -223,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 91fe4e5d074..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 @@ -107,7 +112,7 @@ module Gitlab def project_params @project_params ||= begin - attrs = json_params.merge(override_params).merge(visibility_level) + attrs = json_params.merge(override_params).merge(visibility_level, external_label) # Cleaning all imported and overridden params Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs, @@ -135,6 +140,13 @@ module Gitlab { 'visibility_level' => level } end + def external_label + label = override_params['external_authorization_classification_label'].presence || + json_params['external_authorization_classification_label'].presence + + { 'external_authorization_classification_label' => label } + end + # Given a relation hash containing one or more models and its relationships, # loops through each model and each object from a model type and # and assigns its correspondent attributes hash from +tree_hash+ @@ -143,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 @@ -164,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 @@ -179,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/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index 86b28e4e20a..0ee9563c227 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -32,7 +32,7 @@ module Gitlab class Handler def initialize(env, message) - @request = ActionDispatch::Request.new(env) + @request = Rack::Request.new(env) @rewritten_fields = message['rewritten_fields'] @open_files = [] end @@ -50,7 +50,7 @@ module Gitlab value = decorate_params_value(value, @request.params[key]) end - @request.update_param(key, value) + update_param(key, value) end yield @@ -92,6 +92,20 @@ module Gitlab ::UploadedFile.from_params(params, key, allowed_paths) end + + # update_params ensures that both rails controllers and rack middleware can find + # workhorse accelerate files in the request + def update_param(key, value) + # we make sure we have key in POST otherwise update_params will add it in GET + @request.POST[key] ||= value + + # this will force Rack::Request to properly update env keys + @request.update_param(key, value) + + # ActionDispatch::Request is based on Rack::Request but it caches params + # inside other env keys, here we ensure everything is updated correctly + ActionDispatch::Request.new(@request.env).update_param(key, value) + end end def initialize(app) diff --git a/lib/gitlab/patch/chronic_duration.rb b/lib/gitlab/patch/chronic_duration.rb new file mode 100644 index 00000000000..ab3cba3657f --- /dev/null +++ b/lib/gitlab/patch/chronic_duration.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Fixes a bug where parsing months doesn't take into account +# the ChronicDuration.days_per_week setting +# +# We can remove this when we do a refactor and push upstream in +# https://gitlab.com/gitlab-org/gitlab-ce/issues/66637 + +module Gitlab + module Patch + module ChronicDuration + extend ActiveSupport::Concern + + class_methods do + def duration_units_seconds_multiplier(unit) + return 0 unless duration_units_list.include?(unit) + + case unit + when 'months' + 3600 * ::ChronicDuration.hours_per_day * ::ChronicDuration.days_per_month + else + super + end + end + + # ChronicDuration#output uses 1mo = 4w as the conversion so we do the same here. + # We do need to add a special case for the default days_per_week value because + # we want to retain existing behavior for the default case + def days_per_month + ::ChronicDuration.days_per_week == 7 ? 30 : ::ChronicDuration.days_per_week * 4 + end + end + end + end +end diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb index 682edfc4259..a2296d265cd 100644 --- a/lib/gitlab/push_options.rb +++ b/lib/gitlab/push_options.rb @@ -7,10 +7,12 @@ module Gitlab keys: [ :create, :description, + :label, :merge_when_pipeline_succeeds, :remove_source_branch, :target, - :title + :title, + :unlabel ] }, ci: { @@ -18,6 +20,11 @@ module Gitlab } }).freeze + MULTI_VALUE_OPTIONS = [ + %w[merge_request label], + %w[merge_request unlabel] + ].freeze + NAMESPACE_ALIASES = HashWithIndifferentAccess.new({ mr: :merge_request }).freeze @@ -50,12 +57,22 @@ module Gitlab next if [namespace, key].any?(&:nil?) options[namespace] ||= HashWithIndifferentAccess.new - options[namespace][key] = value + + if option_multi_value?(namespace, key) + options[namespace][key] ||= HashWithIndifferentAccess.new(0) + options[namespace][key][value] += 1 + else + options[namespace][key] = value + end end options end + def option_multi_value?(namespace, key) + MULTI_VALUE_OPTIONS.any? { |arr| arr == [namespace, key] } + end + def parse_option(option) parts = OPTION_MATCHER.match(option) return unless parts diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb index 533c74ba9b4..183191f31a6 100644 --- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb +++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb @@ -122,7 +122,7 @@ module Gitlab params '#issue | !merge_request' types Issue, MergeRequest condition do - current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target) + current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target) end parse_params do |issuable_param| extract_references(issuable_param, :issue).first || diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb new file mode 100644 index 00000000000..b4768a9546d --- /dev/null +++ b/lib/gitlab_danger.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class GitlabDanger + LOCAL_RULES ||= %w[ + changes_size + gemfile + documentation + frozen_string + duplicate_yarn_dependencies + prettier + eslint + database + ].freeze + + CI_ONLY_RULES ||= %w[ + metadata + changelog + specs + commit_messages + roulette + single_codebase + gitlab_ui_wg + ce_ee_vue_templates + only_documentation + ].freeze + + MESSAGE_PREFIX = '==>'.freeze + + attr_reader :gitlab_danger_helper + + def initialize(gitlab_danger_helper) + @gitlab_danger_helper = gitlab_danger_helper + end + + def self.local_warning_message + "#{MESSAGE_PREFIX} Only the following Danger rules can be run locally: #{LOCAL_RULES.join(', ')}" + end + + def self.success_message + "#{MESSAGE_PREFIX} No Danger rule violations!" + end + + def rule_names + ci? ? LOCAL_RULES | CI_ONLY_RULES : LOCAL_RULES + end + + def html_link(str) + self.ci? ? gitlab_danger_helper.html_link(str) : str + end + + def ci? + !gitlab_danger_helper.nil? + end +end diff --git a/lib/tasks/gitlab_danger.rake b/lib/tasks/gitlab_danger.rake new file mode 100644 index 00000000000..e75539f048c --- /dev/null +++ b/lib/tasks/gitlab_danger.rake @@ -0,0 +1,17 @@ +desc 'Run local Danger rules' +task :danger_local do + require 'gitlab_danger' + require 'gitlab/popen' + + puts("#{GitlabDanger.local_warning_message}\n") + + # _status will _always_ be 0, regardless of failure or success :( + output, _status = Gitlab::Popen.popen(%w{danger dry_run}) + + if output.empty? + puts(GitlabDanger.success_message) + else + puts(output) + exit(1) + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e15000b5184..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 "" @@ -7209,7 +7231,7 @@ msgstr "" msgid "Monday" msgstr "" -msgid "Monitor your errors by integrating with Sentry" +msgid "Monitor your errors by integrating with Sentry." msgstr "" msgid "Monitoring" @@ -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 "" @@ -13023,6 +13042,9 @@ msgid_plural "When these merge requests are accepted" msgstr[0] "" msgstr[1] "" +msgid "When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed." +msgstr "" + msgid "When:" msgstr "" diff --git a/package.json b/package.json index 23e611ae6cc..4256b8bfdcc 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.5.5", - "@gitlab/svgs": "^1.71.0", - "@gitlab/ui": "5.21.0", + "@gitlab/svgs": "^1.72.0", + "@gitlab/ui": "5.21.1", "@gitlab/visual-review-tools": "1.0.1", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", @@ -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/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 00000000000..e4ccdc9e2ec --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1,5 @@ +* +!*/ +!.gitignore +!.gitkeep +!examples/* diff --git a/qa/README.md b/qa/README.md index dede3cd2473..332e5c8170f 100644 --- a/qa/README.md +++ b/qa/README.md @@ -36,7 +36,7 @@ using `package-and-qa-manual` manual action, to test if everything works fine. You can use GitLab QA to exercise tests on any live instance! If you don't have an instance available you can follow the instructions below to use -the [GitLab Development Kit (GDK)][GDK]. +the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit). This is the recommended option if you would like to contribute to the tests. Note: GitLab QA uses [Selenium WebDriver](https://www.seleniumhq.org/) via @@ -146,8 +146,6 @@ directory** (one level up from this directory): docker build -t gitlab/gitlab-ce-qa:nightly --file ./qa/Dockerfile ./ ``` -[GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/ - ### Quarantined tests Tests can be put in quarantine by assigning `:quarantine` metadata. This means diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index e3039149ab4..4676dc8d077 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -61,6 +61,10 @@ module QA end end + def sign_out_if_signed_in + sign_out if has_personal_area?(wait: 0) + end + def click_settings_link retry_until(reload: false) do within_user_menu do diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index 67459208c8d..f877ba76b38 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -64,3 +64,5 @@ module QA end end end + +QA::Page::MergeRequest::New.prepend_if_ee('QA::EE::Page::MergeRequest::New') diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index 44d9dc8f296..b5beba64c61 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -10,6 +10,7 @@ module QA end attribute :id + attribute :name def initialize @path = Runtime::Namespace.name @@ -47,6 +48,11 @@ module QA super end + def add_member(user, access_level = '30') + # 30 = developer access + post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level } + end + def api_get_path "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 45ab2396a04..53126c67ba3 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -5,7 +5,8 @@ require 'securerandom' module QA module Resource class MergeRequest < Base - attr_accessor :id, + attr_accessor :approval_rules, + :id, :title, :description, :source_branch, @@ -46,6 +47,7 @@ module QA end def initialize + @approval_rules = nil @title = 'QA test - merge request' @description = 'This is a test merge request' @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" @@ -63,16 +65,17 @@ module QA project.visit! Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.choose_milestone(@milestone) if @milestone - page.assign_to_me if @assignee == 'me' + Page::MergeRequest::New.perform do |new| + new.fill_title(@title) + new.fill_description(@description) + new.choose_milestone(@milestone) if @milestone + new.assign_to_me if @assignee == 'me' labels.each do |label| - page.select_label(label) + new.select_label(label) end + new.add_approval_rules(approval_rules) if approval_rules - page.create_merge_request + new.create_merge_request end end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 4a29a14c5c2..157064dfe37 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -75,6 +75,11 @@ module QA super end + def add_member(user, access_level = '30') + # 30 = developer access + post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level } + end + def api_get_path "/projects/#{CGI.escape(path_with_namespace)}" end @@ -83,6 +88,10 @@ module QA "#{api_get_path}/repository/archive.#{type}" end + def api_members_path + "#{api_get_path}/members" + end + def api_post_path '/projects' end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index eec46f46d99..911d2b2f506 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -9,6 +9,7 @@ module QA attr_writer :username, :password attr_accessor :provider, :extern_uid + attribute :id attribute :name attribute :email diff --git a/shared/.gitignore b/shared/.gitignore new file mode 100644 index 00000000000..5c0276adafd --- /dev/null +++ b/shared/.gitignore @@ -0,0 +1,4 @@ +* +!*/ +!.gitignore +!.gitkeep diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 35487682462..5d87dbdee8b 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -74,17 +74,19 @@ 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 + after do + # Avoid test ordering issue and ensure `verify_recaptcha` returns true + unless Recaptcha.configuration.skip_verify_env.include?('test') + Recaptcha.configuration.skip_verify_env << 'test' + end + 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) @@ -93,11 +95,6 @@ describe RegistrationsController do end it 'redirects to the dashboard when the recaptcha is solved' do - # Avoid test ordering issue and ensure `verify_recaptcha` returns true - unless Recaptcha.configuration.skip_verify_env.include?('test') - Recaptcha.configuration.skip_verify_env << 'test' - end - post(:create, params: user_params) expect(flash[:notice]).to include 'Welcome! You have signed up successfully.' @@ -105,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/external_pull_requests.rb b/spec/factories/external_pull_requests.rb new file mode 100644 index 00000000000..08d0fa4d419 --- /dev/null +++ b/spec/factories/external_pull_requests.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :external_pull_request do + sequence(:pull_request_iid) + project + source_branch 'feature' + source_repository 'the-repository' + source_sha '97de212e80737a608d939f648d959671fb0a0142' + target_branch 'master' + target_repository 'the-repository' + target_sha 'a09386439ca39abe575675ffd4b89ae824fec22f' + status :open + + trait(:closed) { status 'closed' } + end +end 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/features/admin/clusters/applications_spec.rb b/spec/features/admin/clusters/applications_spec.rb new file mode 100644 index 00000000000..8310811b43d --- /dev/null +++ b/spec/features/admin/clusters/applications_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../../../../spec/features/clusters/installing_applications_shared_examples' + +describe 'Instance-level Cluster Applications', :js do + include GoogleApi::CloudPlatformHelpers + + let(:user) { create(:admin) } + + before do + sign_in(user) + end + + describe 'Installing applications' do + include_examples "installing applications on a cluster" do + let(:cluster_path) { admin_cluster_path(cluster) } + let(:cluster_factory_args) { [:instance] } + end + end +end diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb new file mode 100644 index 00000000000..cb8fd8c607c --- /dev/null +++ b/spec/features/clusters/installing_applications_shared_examples.rb @@ -0,0 +1,228 @@ +# frozen_string_literal: true + +shared_examples "installing applications on a cluster" do + before do + visit cluster_path + end + + context 'when cluster is being created' do + let(:cluster) { create(:cluster, :providing_by_gcp, *cluster_factory_args) } + + it 'user is unable to install applications' do + expect(page).not_to have_text('Helm') + expect(page).not_to have_text('Install') + end + end + + context 'when cluster is created' do + let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) } + + it 'user can install applications' do + wait_for_requests + + page.within('.js-cluster-application-row-helm') do + expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') + end + end + + context 'when user installs Helm' do + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + + page.within('.js-cluster-application-row-helm') do + page.find(:css, '.js-cluster-application-install-button').click + end + + wait_for_requests + end + + it 'shows the status transition' do + page.within('.js-cluster-application-row-helm') do + # FE sends request and gets the response, then the buttons is "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_helm.make_installing! + + # FE starts polling and update the buttons to "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_helm.make_installed! + + expect(page).not_to have_css('button', exact_text: 'Install', visible: :all) + expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all) + expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') + end + + expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') + end + end + + context 'when user installs Knative' do + before do + create(:clusters_applications_helm, :installed, cluster: cluster) + end + + context 'on an abac cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, *cluster_factory_args) } + + it 'shows info block and not be installable' do + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.rbac-notice') + expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') + end + end + end + + context 'on an rbac cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) } + + it 'does not show callout block and be installable' do + page.within('.js-cluster-application-row-knative') do + expect(page).not_to have_css('p', text: 'You must have an RBAC-enabled cluster', visible: :all) + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + end + end + + describe 'when user clicks install button' do + def domainname_form_value + page.find('.js-knative-domainname').value + end + + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + + page.find('.js-knative-domainname').set("domain.example.org") + + click_button 'Install' + + wait_for_requests + + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_knative.make_installing! + Clusters::Cluster.last.application_knative.make_installed! + Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1') + end + end + + it 'shows status transition' do + page.within('.js-cluster-application-row-knative') do + expect(domainname_form_value).to eq('domain.example.org') + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') + end + + expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster') + expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes' + end + + it 'can then update the domain' do + page.within('.js-cluster-application-row-knative') do + expect(ClusterPatchAppWorker).to receive(:perform_async) + + expect(domainname_form_value).to eq('domain.example.org') + + page.find('.js-knative-domainname').set("new.domain.example.org") + + click_button 'Save changes' + + wait_for_requests + + expect(domainname_form_value).to eq('new.domain.example.org') + end + end + end + end + end + + context 'when user installs Cert Manager' do + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + + create(:clusters_applications_helm, :installed, cluster: cluster) + + page.within('.js-cluster-application-row-cert_manager') do + click_button 'Install' + end + end + + it 'shows status transition' do + def email_form_value + page.find('.js-email').value + end + + page.within('.js-cluster-application-row-cert_manager') do + expect(email_form_value).to eq(cluster.user.email) + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + page.find('.js-email').set("new_email@example.org") + Clusters::Cluster.last.application_cert_manager.make_installing! + + expect(email_form_value).to eq('new_email@example.org') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_cert_manager.make_installed! + + expect(email_form_value).to eq('new_email@example.org') + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') + end + + expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster') + end + end + + context 'when user installs Ingress' do + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + + create(:clusters_applications_helm, :installed, cluster: cluster) + + page.within('.js-cluster-application-row-ingress') do + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + page.find(:css, '.js-cluster-application-install-button').click + + wait_for_requests + end + end + + it 'shows the status transition' do + page.within('.js-cluster-application-row-ingress') do + # FE sends request and gets the response, then the buttons is "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_ingress.make_installing! + + # FE starts polling and update the buttons to "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + # The application becomes installed but we keep waiting for external IP address + Clusters::Cluster.last.application_ingress.make_installed! + + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed') + expect(page).to have_selector('.js-no-endpoint-message') + expect(page).to have_selector('.js-ingress-ip-loading-icon') + + # We receive the external IP address and display + Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100') + + expect(page).not_to have_css('button', exact_text: 'Install', visible: :all) + expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all) + expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') + expect(page).not_to have_css('p', text: 'The endpoint is in the process of being assigned', visible: :all) + expect(page.find('.js-endpoint').value).to eq('192.168.1.100') + end + + expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') + end + end + end +end diff --git a/spec/features/groups/clusters/applications_spec.rb b/spec/features/groups/clusters/applications_spec.rb new file mode 100644 index 00000000000..5d48df234eb --- /dev/null +++ b/spec/features/groups/clusters/applications_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../../../../spec/features/clusters/installing_applications_shared_examples' + +describe 'Group-level Cluster Applications', :js do + include GoogleApi::CloudPlatformHelpers + + let(:group) { create(:group) } + let(:user) { create(:user) } + + before do + group.add_maintainer(user) + sign_in(user) + end + + describe 'Installing applications' do + include_examples "installing applications on a cluster" do + let(:cluster_path) { group_cluster_path(group, cluster) } + let(:cluster_factory_args) { [:group, groups: [group]] } + end + end +end diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index 435b3cd2555..7d89b8e97a6 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' describe 'Merge request > User posts notes', :js do include NoteInteractionHelpers - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } + let(:user) { project.creator } let(:merge_request) do create(:merge_request, source_project: project, target_project: project) @@ -33,17 +34,21 @@ describe 'Merge request > User posts notes', :js do end describe 'with text' do + let(:text) { 'This is awesome' } + before do page.within('.js-main-target-form') do - fill_in 'note[note]', with: 'This is awesome' + fill_in 'note[note]', with: text end end - it 'has enable submit button and preview button' do + it 'has enable submit button, preview button and saves content to local storage' do page.within('.js-main-target-form') do expect(page).not_to have_css('.js-comment-button[disabled]') expect(page).to have_css('.js-md-preview-button', visible: true) end + + expect(page.evaluate_script("localStorage['autosave/Note/MergeRequest/#{merge_request.id}']")).to eq(text) end end end diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index 6262f1ce055..c42eb8560a4 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -64,7 +64,7 @@ describe 'Merge request > User selects branches for new MR', :js do click_button "Check out branch" - expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' + expect(page).to have_content 'git checkout -b "orphaned-branch" "origin/orphaned-branch"' end it 'allows filtering multiple dropdowns' do diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index 3d15095e2da..ce971b158a3 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true require 'spec_helper' +require_relative '../../../../spec/features/clusters/installing_applications_shared_examples' -describe 'Clusters Applications', :js do +describe 'Project-level Cluster Applications', :js do include GoogleApi::CloudPlatformHelpers let(:project) { create(:project) } @@ -14,229 +15,9 @@ describe 'Clusters Applications', :js do end describe 'Installing applications' do - before do - visit project_cluster_path(project, cluster) - end - - context 'when cluster is being created' do - let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } - - it 'user is unable to install applications' do - expect(page).not_to have_css('.js-cluster-application-row-helm') - expect(page).not_to have_css('.js-cluster-application-install-button') - end - end - - context 'when cluster is created' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - - it 'user can install applications' do - wait_for_requests - - page.within('.js-cluster-application-row-helm') do - expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') - end - end - - context 'when user installs Helm' do - before do - allow(ClusterInstallAppWorker).to receive(:perform_async) - - page.within('.js-cluster-application-row-helm') do - page.find(:css, '.js-cluster-application-install-button').click - end - - wait_for_requests - end - - it 'they see status transition' do - page.within('.js-cluster-application-row-helm') do - # FE sends request and gets the response, then the buttons is "Installing" - expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') - - Clusters::Cluster.last.application_helm.make_installing! - - # FE starts polling and update the buttons to "Installing" - expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') - - Clusters::Cluster.last.application_helm.make_installed! - - expect(page).not_to have_css('.js-cluster-application-install-button') - expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') - end - - expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') - end - end - - context 'when user installs Knative' do - before do - create(:clusters_applications_helm, :installed, cluster: cluster) - end - - context 'on an abac cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) } - - it 'shows info block and not be installable' do - page.within('.js-cluster-application-row-knative') do - expect(page).to have_css('.rbac-notice') - expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - end - end - end - - context 'on an rbac cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - - it 'does not show callout block and be installable' do - page.within('.js-cluster-application-row-knative') do - expect(page).not_to have_css('.rbac-notice') - expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') - end - end - - describe 'when user clicks install button' do - def domainname_form_value - page.find('.js-knative-domainname').value - end - - before do - allow(ClusterInstallAppWorker).to receive(:perform_async) - allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) - allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) - - page.within('.js-cluster-application-row-knative') do - expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') - - page.find('.js-knative-domainname').set("domain.example.org") - - click_button 'Install' - - wait_for_requests - - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') - - Clusters::Cluster.last.application_knative.make_installing! - Clusters::Cluster.last.application_knative.make_installed! - Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1') - end - end - - it 'shows status transition' do - page.within('.js-cluster-application-row-knative') do - expect(domainname_form_value).to eq('domain.example.org') - expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') - end - - expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster') - expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes' - end - - it 'can then update the domain' do - page.within('.js-cluster-application-row-knative') do - expect(ClusterPatchAppWorker).to receive(:perform_async) - - expect(domainname_form_value).to eq('domain.example.org') - - page.find('.js-knative-domainname').set("new.domain.example.org") - - click_button 'Save changes' - - wait_for_requests - - expect(domainname_form_value).to eq('new.domain.example.org') - end - end - end - end - end - - context 'when user installs Cert Manager' do - before do - allow(ClusterInstallAppWorker).to receive(:perform_async) - allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) - allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) - - create(:clusters_applications_helm, :installed, cluster: cluster) - - page.within('.js-cluster-application-row-cert_manager') do - click_button 'Install' - end - end - - it 'shows status transition' do - def email_form_value - page.find('.js-email').value - end - - page.within('.js-cluster-application-row-cert_manager') do - expect(email_form_value).to eq(cluster.user.email) - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') - - page.find('.js-email').set("new_email@example.org") - Clusters::Cluster.last.application_cert_manager.make_installing! - - expect(email_form_value).to eq('new_email@example.org') - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') - - Clusters::Cluster.last.application_cert_manager.make_installed! - - expect(email_form_value).to eq('new_email@example.org') - expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') - end - - expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster') - end - end - - context 'when user installs Ingress' do - context 'when user installs application: Ingress' do - before do - allow(ClusterInstallAppWorker).to receive(:perform_async) - allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) - allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) - - create(:clusters_applications_helm, :installed, cluster: cluster) - - page.within('.js-cluster-application-row-ingress') do - expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') - page.find(:css, '.js-cluster-application-install-button').click - - wait_for_requests - end - end - - it 'they see status transition' do - page.within('.js-cluster-application-row-ingress') do - # FE sends request and gets the response, then the buttons is "Installing" - expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') - - Clusters::Cluster.last.application_ingress.make_installing! - - # FE starts polling and update the buttons to "Installing" - expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') - - # The application becomes installed but we keep waiting for external IP address - Clusters::Cluster.last.application_ingress.make_installed! - - expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed') - expect(page).to have_selector('.js-no-endpoint-message') - expect(page).to have_selector('.js-ingress-ip-loading-icon') - - # We receive the external IP address and display - Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100') - - expect(page).not_to have_css('.js-cluster-application-install-button') - expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') - expect(page).not_to have_selector('.js-no-endpoint-message') - expect(page.find('.js-endpoint').value).to eq('192.168.1.100') - end - - expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') - end - end - end + include_examples "installing applications on a cluster" do + let(:cluster_path) { project_cluster_path(project, cluster) } + let(:cluster_factory_args) { [projects: [project]] } end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 879ff01f294..ef8749be0be 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -42,6 +42,24 @@ describe IssuesFinder do end end + context 'filtering by projects' do + context 'when projects are passed in a list of ids' do + let(:params) { { projects: [project1.id] } } + + it 'returns the issue belonging to the projects' do + expect(issues).to contain_exactly(issue1) + end + end + + context 'when projects are passed in a subquery' do + let(:params) { { projects: Project.id_in(project1.id) } } + + it 'returns the issue belonging to the projects' do + expect(issues).to contain_exactly(issue1) + end + end + end + context 'filtering by group_id' do let(:params) { { group_id: group.id } } @@ -49,6 +67,30 @@ describe IssuesFinder do it 'returns all group issues' do expect(issues).to contain_exactly(issue1) end + + context 'when projects outside the group are passed' do + let(:params) { { group_id: group.id, projects: [project2.id] } } + + it 'returns no issues' do + expect(issues).to be_empty + end + end + + context 'when projects of the group are passed' do + let(:params) { { group_id: group.id, projects: [project1.id] } } + + it 'returns the issue within the group and projects' do + expect(issues).to contain_exactly(issue1) + end + end + + context 'when projects of the group are passed as a subquery' do + let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } } + + it 'returns the issue within the group and projects' do + expect(issues).to contain_exactly(issue1) + end + end end context 'when include_subgroup param is true' do @@ -59,6 +101,14 @@ describe IssuesFinder do it 'returns all group and subgroup issues' do expect(issues).to contain_exactly(issue1, issue4) end + + context 'when mixed projects are passed' do + let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } } + + it 'returns the issue within the group and projects' do + expect(issues).to contain_exactly(issue4) + end + end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 78224f0b9da..6c0bbeff4f4 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -13,7 +13,7 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5) end - it 'filters by project' do + it 'filters by project_id' do params = { project_id: project1.id, scope: 'authored', state: 'opened' } merge_requests = described_class.new(user, params).execute @@ -21,6 +21,14 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request1) end + it 'filters by projects' do + params = { projects: [project2.id, project3.id] } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request3, merge_request4) + end + it 'filters by commit sha' do merge_requests = described_class.new( user, @@ -49,6 +57,16 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request5) end + + it 'filters by group projects including subgroups' do + # project3 is not in the group, so it should not return merge_request4 + projects = [project3.id, project4.id] + params = { group_id: group.id, include_subgroups: true, projects: projects } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request5) + end end it 'filters by non_archived' do diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json index 88b0fecc24c..d37f5b864d7 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json +++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json @@ -1,6 +1,10 @@ { "type": "object", "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": "string" }, "merge_params": { "type": ["object", "null"] }, "state": { "type": "string" }, "source_branch": { "type": "string" }, diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js index 1d8984cea0a..fbcab078993 100644 --- a/spec/frontend/clusters/components/applications_spec.js +++ b/spec/frontend/clusters/components/applications_spec.js @@ -89,7 +89,7 @@ describe('Applications', () => { }); it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull(); + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull(); }); }); @@ -126,7 +126,7 @@ describe('Applications', () => { }); it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull(); + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull(); }); }); diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 67e5dc399ac..ce8b8908026 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -11,19 +11,24 @@ describe('ErrorTrackingList', () => { let wrapper; let actions; - function mountComponent({ errorTrackingEnabled = true } = {}) { + function mountComponent({ + errorTrackingEnabled = true, + userCanEnableErrorTracking = true, + stubs = { + 'gl-link': GlLink, + }, + } = {}) { wrapper = shallowMount(ErrorTrackingList, { localVue, store, propsData: { indexPath: '/path', enableErrorTrackingLink: '/link', + userCanEnableErrorTracking, errorTrackingEnabled, illustrationPath: 'illustration/path', }, - stubs: { - 'gl-link': GlLink, - }, + stubs, }); } @@ -115,4 +120,23 @@ describe('ErrorTrackingList', () => { expect(wrapper.find(GlButton).exists()).toBeFalsy(); }); }); + + describe('When error tracking is disabled and user is not allowed to enable it', () => { + beforeEach(() => { + mountComponent({ + errorTrackingEnabled: false, + userCanEnableErrorTracking: false, + stubs: { + 'gl-link': GlLink, + 'gl-empty-state': GlEmptyState, + }, + }); + }); + + it('shows empty state', () => { + expect(wrapper.find('a').attributes('href')).toBe( + '/help/user/project/operations/error_tracking.html', + ); + }); + }); }); diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb index 7516a636c93..064b3ad21cb 100644 --- a/spec/helpers/projects/error_tracking_helper_spec.rb +++ b/spec/helpers/projects/error_tracking_helper_spec.rb @@ -6,21 +6,31 @@ describe Projects::ErrorTrackingHelper do include Gitlab::Routing.url_helpers set(:project) { create(:project) } + set(:current_user) { create(:user) } describe '#error_tracking_data' do + let(:can_enable_error_tracking) { true } let(:setting_path) { project_settings_operations_path(project) } let(:index_path) do project_error_tracking_index_path(project, format: :json) end + before do + allow(helper) + .to receive(:can?) + .with(current_user, :admin_operations, project) + .and_return(can_enable_error_tracking) + end + context 'without error_tracking_setting' do it 'returns frontend configuration' do - expect(error_tracking_data(project)).to eq( + expect(helper.error_tracking_data(current_user, project)).to match( 'index-path' => index_path, + 'user-can-enable-error-tracking' => 'true', 'enable-error-tracking-link' => setting_path, 'error-tracking-enabled' => 'false', - "illustration-path" => "/images/illustrations/cluster_popover.svg" + 'illustration-path' => match_asset_path('/assets/illustrations/cluster_popover.svg') ) end end @@ -36,7 +46,7 @@ describe Projects::ErrorTrackingHelper do end it 'show error tracking enabled' do - expect(error_tracking_data(project)).to include( + expect(helper.error_tracking_data(current_user, project)).to include( 'error-tracking-enabled' => 'true' ) end @@ -48,11 +58,21 @@ describe Projects::ErrorTrackingHelper do end it 'show error tracking not enabled' do - expect(error_tracking_data(project)).to include( + expect(helper.error_tracking_data(current_user, project)).to include( 'error-tracking-enabled' => 'false' ) end end end + + context 'when user is not maintainer' do + let(:can_enable_error_tracking) { false } + + it 'shows error tracking enablement as disabled' do + expect(helper.error_tracking_data(current_user, project)).to include( + 'user-can-enable-error-tracking' => 'false' + ) + end + end end end diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 721d0b8172d..76675a78db2 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -171,6 +171,32 @@ describe('Issue boards new issue form', () => { .then(done) .catch(done.fail); }); + + it('sets detail weight after submit', done => { + boardsStore.weightFeatureAvailable = true; + vm.title = 'submit issue'; + + Vue.nextTick() + .then(submitIssue) + .then(() => { + expect(boardsStore.detail.list.weight).toBe(list.weight); + }) + .then(done) + .catch(done.fail); + }); + + it('does not set detail weight after submit', done => { + boardsStore.weightFeatureAvailable = false; + vm.title = 'submit issue'; + + Vue.nextTick() + .then(submitIssue) + .then(() => { + expect(boardsStore.detail.list.weight).toBe(list.weight); + }) + .then(done) + .catch(done.fail); + }); }); describe('submit error', () => { diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index ea22ae5c4e7..50ad1442873 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -12,6 +12,7 @@ export const listObj = { position: 0, title: 'Test', list_type: 'label', + weight: 3, label: { id: 5000, title: 'Testing', @@ -26,6 +27,7 @@ export const listObjDuplicate = { position: 1, title: 'Test', list_type: 'label', + weight: 3, label: { id: listObj.label.id, title: 'Testing', diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb index 43c5d3ec980..8fc1e0a4e88 100644 --- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb @@ -84,6 +84,20 @@ describe Gitlab::Ci::Build::Policy::Refs do .not_to be_satisfied_by(pipeline) end end + + context 'when source is external_pull_request_event' do + let(:pipeline) { build_stubbed(:ci_pipeline, source: :external_pull_request_event) } + + it 'is satisfied with only: external_pull_request' do + expect(described_class.new(%w[external_pull_requests])) + .to be_satisfied_by(pipeline) + end + + it 'is not satisfied with only: external_pull_request_event' do + expect(described_class.new(%w[external_pull_request_events])) + .not_to be_satisfied_by(pipeline) + end + end end context 'when matching a ref by a regular expression' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index bf9ff922c05..ba4f841cf43 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -128,4 +128,38 @@ describe Gitlab::Ci::Pipeline::Chain::Build do expect(pipeline.target_sha).to eq(merge_request.target_branch_sha) end end + + context 'when pipeline is running for an external pull request' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + source: :external_pull_request_event, + origin_ref: 'feature', + checkout_sha: project.commit.id, + after_sha: nil, + before_sha: nil, + source_sha: external_pull_request.source_sha, + target_sha: external_pull_request.target_sha, + trigger_request: nil, + schedule: nil, + external_pull_request: external_pull_request, + project: project, + current_user: user) + end + + let(:external_pull_request) { build(:external_pull_request, project: project) } + + before do + step.perform! + end + + it 'correctly indicated that this is an external pull request pipeline' do + expect(pipeline).to be_external_pull_request_event + expect(pipeline.external_pull_request).to eq(external_pull_request) + end + + it 'correctly sets source sha and target sha to pipeline' do + expect(pipeline.source_sha).to eq(external_pull_request.source_sha) + expect(pipeline.target_sha).to eq(external_pull_request.target_sha) + end + end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 710564b7540..1b4d366ce7b 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -11,16 +11,62 @@ describe Gitlab::Danger::Helper do class FakeDanger include Gitlab::Danger::Helper - attr_reader :git + attr_reader :git, :gitlab - def initialize(git:) + def initialize(git:, gitlab:) @git = git + @gitlab = gitlab end end let(:fake_git) { double('fake-git') } - subject(:helper) { FakeDanger.new(git: fake_git) } + let(:mr_author) { nil } + let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) } + + subject(:helper) { FakeDanger.new(git: fake_git, gitlab: fake_gitlab) } + + describe '#gitlab_helper' do + context 'when gitlab helper is not available' do + let(:fake_gitlab) { nil } + + it 'returns nil' do + expect(helper.gitlab_helper).to be_nil + end + end + + context 'when gitlab helper is available' do + it 'returns the gitlab helper' do + expect(helper.gitlab_helper).to eq(fake_gitlab) + end + end + end + + describe '#release_automation?' do + context 'when gitlab helper is not available' do + it 'returns false' do + expect(helper.release_automation?).to be_falsey + end + end + + context 'when gitlab helper is available' do + context "but the MR author isn't the RELEASE_TOOLS_BOT" do + let(:mr_author) { 'johnmarston' } + + it 'returns false' do + expect(helper.release_automation?).to be_falsey + end + end + + context 'and the MR author is the RELEASE_TOOLS_BOT' do + let(:mr_author) { described_class::RELEASE_TOOLS_BOT } + + it 'returns true' do + expect(helper.release_automation?).to be_truthy + end + end + end + end describe '#all_changed_files' do subject { helper.all_changed_files } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 47ba7eff8ed..dafa4243145 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -23,6 +23,7 @@ issues: - epic_issue - epic - designs +- design_versions events: - author - project @@ -126,6 +127,8 @@ merge_requests: - blocks_as_blockee - blocking_merge_requests - blocked_merge_requests +external_pull_requests: +- project merge_request_diff: - merge_request - merge_request_diff_commits @@ -155,6 +158,7 @@ ci_pipelines: - pipeline_schedule - merge_requests_as_head_pipeline - merge_request +- external_pull_request - deployments - environments - chat_data @@ -402,6 +406,7 @@ project: - merge_trains - designs - project_aliases +- external_pull_requests award_emoji: - awardable - user 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/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 0aef4887c75..87be7857e67 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -512,6 +512,24 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2) end end + + context 'with external authorization classification labels' do + it 'converts empty external classification authorization labels to nil' do + project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } }) + + restored_project_json + + expect(project.external_authorization_classification_label).to be_nil + end + + it 'preserves valid external classification authorization labels' do + project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } }) + + restored_project_json + + expect(project.external_authorization_classification_label).to eq("foobar") + end + end end describe '#restored_project' do 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/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d34c6d2421b..e9750d23c53 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -270,6 +270,7 @@ Ci::Pipeline: - protected - iid - merge_request_id +- external_pull_request_id Ci::Stage: - id - name @@ -715,3 +716,16 @@ List: - updated_at - milestone_id - user_id +ExternalPullRequest: +- id +- created_at +- updated_at +- project_id +- pull_request_iid +- status +- source_branch +- target_branch +- source_repository +- target_repository +- source_sha +- target_sha diff --git a/spec/lib/gitlab/patch/chronic_duration_spec.rb b/spec/lib/gitlab/patch/chronic_duration_spec.rb new file mode 100644 index 00000000000..541037ec1a2 --- /dev/null +++ b/spec/lib/gitlab/patch/chronic_duration_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Patch::ChronicDuration do + subject { ChronicDuration.parse('1mo') } + + it 'uses default conversions' do + expect(subject).to eq(2_592_000) + end + + context 'with custom conversions' do + before do + ChronicDuration.hours_per_day = 8 + ChronicDuration.days_per_week = 5 + end + + after do + ChronicDuration.hours_per_day = 24 + ChronicDuration.days_per_week = 7 + end + + it 'uses custom conversions' do + expect(subject).to eq(576_000) + end + end +end diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb index a85d418777f..cfc804c13a7 100644 --- a/spec/lib/gitlab/time_tracking_formatter_spec.rb +++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb @@ -17,6 +17,14 @@ describe Gitlab::TimeTrackingFormatter do it { expect(subject).to eq(-12_000) } end + + context 'durations with months' do + let(:duration_string) { '1mo' } + + it 'uses our custom conversions' do + expect(subject).to eq(576_000) + end + end end describe '#output' do diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb new file mode 100644 index 00000000000..623ac20fa7c --- /dev/null +++ b/spec/lib/gitlab_danger_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabDanger do + let(:gitlab_danger_helper) { nil } + + subject { described_class.new(gitlab_danger_helper) } + + describe '.local_warning_message' do + it 'returns an informational message with rules that can run' do + expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database') + end + end + + describe '.success_message' do + it 'returns an informational success message' do + expect(described_class.success_message).to eq('==> No Danger rule violations!') + end + end + + describe '#rule_names' do + context 'when running locally' do + it 'returns local only rules' do + expect(subject.rule_names).to eq(described_class::LOCAL_RULES) + end + end + + context 'when running under CI' do + let(:gitlab_danger_helper) { double('danger_gitlab_helper') } + + it 'returns all rules' do + expect(subject.rule_names).to eq(described_class::LOCAL_RULES | described_class::CI_ONLY_RULES) + end + end + end + + describe '#html_link' do + context 'when running locally' do + it 'returns the same string' do + str = 'something' + + expect(subject.html_link(str)).to eq(str) + end + end + + context 'when running under CI' do + let(:gitlab_danger_helper) { double('danger_gitlab_helper') } + + it 'returns a HTML link formatted version of the string' do + str = 'something' + html_formatted_str = %Q{<a href="#{str}">#{str}</a>} + + expect(gitlab_danger_helper).to receive(:html_link).with(str).and_return(html_formatted_str) + + expect(subject.html_link(str)).to eq(html_formatted_str) + end + end + end + + describe '#ci?' do + context 'when gitlab_danger_helper is not available' do + it 'returns false' do + expect(subject.ci?).to be_falsey + end + end + + context 'when gitlab_danger_helper is available' do + let(:gitlab_danger_helper) { double('danger_gitlab_helper') } + + it 'returns true' do + expect(subject.ci?).to be_truthy + end + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 63ca383ac4b..146e479adef 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -20,6 +20,7 @@ describe Ci::Pipeline, :mailer do it { is_expected.to belong_to(:auto_canceled_by) } it { is_expected.to belong_to(:pipeline_schedule) } it { is_expected.to belong_to(:merge_request) } + it { is_expected.to belong_to(:external_pull_request) } it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:trigger_requests) } @@ -885,6 +886,25 @@ describe Ci::Pipeline, :mailer do end end end + + context 'when source is external pull request' do + let(:pipeline) do + create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request) + end + + let(:pull_request) { create(:external_pull_request, project: project) } + + it 'exposes external pull request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s, + 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha, + 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha, + 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch, + 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch + ) + end + end end describe '#protected_ref?' do diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/external_pull_request_spec.rb new file mode 100644 index 00000000000..e85d5b2f6c7 --- /dev/null +++ b/spec/models/external_pull_request_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ExternalPullRequest do + let(:project) { create(:project) } + let(:source_branch) { 'the-branch' } + let(:status) { :open } + + it { is_expected.to belong_to(:project) } + + shared_examples 'has errors on' do |attribute| + it "has errors for #{attribute}" do + expect(subject).not_to be_valid + expect(subject.errors[attribute]).not_to be_empty + end + end + + describe 'validations' do + context 'when source branch not present' do + subject { build(:external_pull_request, source_branch: nil) } + + it_behaves_like 'has errors on', :source_branch + end + + context 'when status not present' do + subject { build(:external_pull_request, status: nil) } + + it_behaves_like 'has errors on', :status + end + + context 'when pull request is from a fork' do + subject { build(:external_pull_request, source_repository: 'the-fork', target_repository: 'the-target') } + + it_behaves_like 'has errors on', :base + end + end + + describe 'create_or_update_from_params' do + subject { described_class.create_or_update_from_params(params) } + + context 'when pull request does not exist' do + context 'when params are correct' do + let(:params) do + { + project_id: project.id, + pull_request_iid: 123, + source_branch: 'feature', + target_branch: 'master', + source_repository: 'the-repository', + target_repository: 'the-repository', + source_sha: '97de212e80737a608d939f648d959671fb0a0142', + target_sha: 'a09386439ca39abe575675ffd4b89ae824fec22f', + status: :open + } + end + + it 'saves the model successfully and returns it' do + expect(subject).to be_persisted + expect(subject).to be_valid + end + + it 'yields the model' do + yielded_value = nil + + result = described_class.create_or_update_from_params(params) do |pull_request| + yielded_value = pull_request + end + + expect(result).to eq(yielded_value) + end + end + + context 'when params are not correct' do + let(:params) do + { + pull_request_iid: 123, + source_branch: 'feature', + target_branch: 'master', + source_repository: 'the-repository', + target_repository: 'the-repository', + source_sha: nil, + target_sha: nil, + status: :open + } + end + + it 'returns an invalid model' do + expect(subject).not_to be_persisted + expect(subject).not_to be_valid + end + end + end + + context 'when pull request exists' do + let!(:pull_request) do + create(:external_pull_request, + project: project, + source_sha: '97de212e80737a608d939f648d959671fb0a0142') + end + + context 'when params are correct' do + let(:params) do + { + pull_request_iid: pull_request.pull_request_iid, + source_branch: pull_request.source_branch, + target_branch: pull_request.target_branch, + source_repository: 'the-repository', + target_repository: 'the-repository', + source_sha: 'ce84140e8b878ce6e7c4d298c7202ff38170e3ac', + target_sha: pull_request.target_sha, + status: :open + } + end + + it 'updates the model successfully and returns it' do + expect(subject).to be_valid + expect(subject.source_sha).to eq(params[:source_sha]) + expect(pull_request.reload.source_sha).to eq(params[:source_sha]) + end + end + + context 'when params are not correct' do + let(:params) do + { + pull_request_iid: pull_request.pull_request_iid, + source_branch: pull_request.source_branch, + target_branch: pull_request.target_branch, + source_repository: 'the-repository', + target_repository: 'the-repository', + source_sha: nil, + target_sha: nil, + status: :open + } + end + + it 'returns an invalid model' do + expect(subject).not_to be_valid + expect(pull_request.reload.source_sha).not_to be_nil + expect(pull_request.target_sha).not_to be_nil + end + end + end + end + + describe '#open?' do + it 'returns true if status is open' do + pull_request = create(:external_pull_request, status: :open) + + expect(pull_request).to be_open + end + + it 'returns false if status is not open' do + pull_request = create(:external_pull_request, status: :closed) + + expect(pull_request).not_to be_open + end + end + + describe '#closed?' do + it 'returns true if status is closed' do + pull_request = build(:external_pull_request, status: :closed) + + expect(pull_request).to be_closed + end + + it 'returns false if status is not closed' do + pull_request = build(:external_pull_request, status: :open) + + expect(pull_request).not_to be_closed + end + end + + describe '#actual_branch_head?' do + let(:project) { create(:project, :repository) } + let(:branch) { project.repository.branches.first } + let(:source_branch) { branch.name } + + let(:pull_request) do + create(:external_pull_request, + project: project, + source_branch: source_branch, + source_sha: source_sha) + end + + context 'when source sha matches the head of the branch' do + let(:source_sha) { branch.target } + + it 'returns true' do + expect(pull_request).to be_actual_branch_head + end + end + + context 'when source sha does not match the head of the branch' do + let(:source_sha) { project.repository.commit('HEAD').sha } + + it 'returns true' do + expect(pull_request).not_to be_actual_branch_head + end + end + end + + describe '#from_fork?' do + it 'returns true if source_repository differs from target_repository' do + pull_request = build(:external_pull_request, + source_repository: 'repository-1', + target_repository: 'repository-2') + + expect(pull_request).to be_from_fork + end + + it 'returns false if source_repository is the same as target_repository' do + pull_request = build(:external_pull_request, + source_repository: 'repository-1', + target_repository: 'repository-1') + + expect(pull_request).not_to be_from_fork + 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/models/project_spec.rb b/spec/models/project_spec.rb index bfbcac60fea..e2a684c42ae 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -99,6 +99,7 @@ describe Project do it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } it { is_expected.to have_many(:cycle_analytics_stages) } + it { is_expected.to have_many(:external_pull_requests) } it 'has an inverse relationship with merge requests' do expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index d1b58aac104..97de26650db 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -11,6 +11,8 @@ require 'spec_helper' # because they are 3 edge cases of using wiki pages. describe API::Wikis do + include WorkhorseHelpers + let(:user) { create(:user) } let(:group) { create(:group).tap { |g| g.add_owner(user) } } let(:project_wiki) { create(:project_wiki, project: project, user: user) } @@ -155,7 +157,7 @@ describe API::Wikis do it 'pushes attachment to the wiki repository' do allow(SecureRandom).to receive(:hex).and_return('fixed_hex') - post(api(url, user), params: payload) + workhorse_post_with_file(api(url, user), file_key: :file, params: payload) expect(response).to have_gitlab_http_status(201) expect(json_response).to eq result_hash.deep_stringify_keys @@ -180,6 +182,15 @@ describe API::Wikis do expect(json_response.size).to eq(1) expect(json_response['error']).to eq('file is invalid') end + + it 'is backward compatible with regular multipart uploads' do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq result_hash.deep_stringify_keys + end end describe 'GET /projects/:id/wikis' do diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb new file mode 100644 index 00000000000..aca4644289d --- /dev/null +++ b/spec/requests/projects/uploads_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'File uploads' do + include WorkhorseHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { create(:user) } + + describe 'POST /:namespace/:project/create/:branch' do + let(:branch) { 'master' } + let(:create_url) { project_blob_path(project, branch) } + let(:blob_url) { project_blob_path(project, "#{branch}/dk.png") } + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + id: branch, + branch_name: branch, + file: fixture_file_upload('spec/fixtures/dk.png'), + commit_message: 'Add an image' + } + end + + before do + project.add_maintainer(user) + + login_as(user) + end + + it 'redirects to blob' do + workhorse_post_with_file(create_url, file_key: :file, params: params) + + expect(response).to redirect_to(blob_url) + end + end +end diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb index 478f09a7881..cf459ba99c1 100644 --- a/spec/requests/rack_attack_global_spec.rb +++ b/spec/requests/rack_attack_global_spec.rb @@ -20,11 +20,6 @@ describe 'Rack Attack global throttles' do let(:period_in_seconds) { 10000 } let(:period) { period_in_seconds.seconds } - let(:url_that_does_not_require_authentication) { '/users/sign_in' } - let(:url_that_requires_authentication) { '/dashboard/snippets' } - let(:url_api_internal) { '/api/v4/internal/check' } - let(:api_partial_url) { '/todos' } - around do |example| # Instead of test environment's :null_store so the throttles can increment Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new @@ -35,112 +30,10 @@ describe 'Rack Attack global throttles' do Rack::Attack.cache.store = Rails.cache end - # Requires let variables: - # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web") - # * get_args - # * other_user_get_args - shared_examples_for 'rate-limited token-authenticated requests' do - before do - # Set low limits - settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period - settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds - end - - context 'when the throttle is enabled' do - before do - settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true - stub_application_setting(settings_to_set) - end - - it 'rejects requests over the rate limit' do - # At first, allow requests under the rate limit. - requests_per_period.times do - get(*get_args) - expect(response).to have_http_status 200 - end - - # the last straw - expect_rejection { get(*get_args) } - end - - it 'allows requests after throttling and then waiting for the next period' do - requests_per_period.times do - get(*get_args) - expect(response).to have_http_status 200 - end - - expect_rejection { get(*get_args) } - - Timecop.travel(period.from_now) do - requests_per_period.times do - get(*get_args) - expect(response).to have_http_status 200 - end - - expect_rejection { get(*get_args) } - end - end - - it 'counts requests from different users separately, even from the same IP' do - requests_per_period.times do - get(*get_args) - expect(response).to have_http_status 200 - end - - # would be over the limit if this wasn't a different user - get(*other_user_get_args) - expect(response).to have_http_status 200 - end - - it 'counts all requests from the same user, even via different IPs' do - requests_per_period.times do - get(*get_args) - expect(response).to have_http_status 200 - end - - expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') - - expect_rejection { get(*get_args) } - end - - it 'logs RackAttack info into structured logs' do - requests_per_period.times do - get(*get_args) - expect(response).to have_http_status 200 - end - - arguments = { - message: 'Rack_Attack', - env: :throttle, - remote_ip: '127.0.0.1', - request_method: 'GET', - path: get_args.first, - user_id: user.id, - username: user.username - } - - expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once - - expect_rejection { get(*get_args) } - end - end - - context 'when the throttle is disabled' do - before do - settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false - stub_application_setting(settings_to_set) - end - - it 'allows requests over the rate limit' do - (1 + requests_per_period).times do - get(*get_args) - expect(response).to have_http_status 200 - end - end - end - end - describe 'unauthenticated requests' do + let(:url_that_does_not_require_authentication) { '/users/sign_in' } + let(:url_api_internal) { '/api/v4/internal/check' } + before do # Set low limits settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period @@ -245,6 +138,7 @@ describe 'Rack Attack global throttles' do let(:other_user) { create(:user) } let(:other_user_token) { create(:personal_access_token, user: other_user) } let(:throttle_setting_prefix) { 'throttle_authenticated_api' } + let(:api_partial_url) { '/todos' } context 'with the token in the query string' do let(:get_args) { [api(api_partial_url, personal_access_token: token)] } @@ -265,10 +159,13 @@ describe 'Rack Attack global throttles' do let(:user) { create(:user) } let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") } + let(:other_user) { create(:user) } let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) } let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") } + let(:throttle_setting_prefix) { 'throttle_authenticated_api' } + let(:api_partial_url) { '/todos' } context 'with the token in the query string' do let(:get_args) { [api(api_partial_url, oauth_access_token: token)] } @@ -299,110 +196,11 @@ describe 'Rack Attack global throttles' do end describe 'web requests authenticated with regular login' do + let(:throttle_setting_prefix) { 'throttle_authenticated_web' } let(:user) { create(:user) } + let(:url_that_requires_authentication) { '/dashboard/snippets' } - before do - login_as(user) - - # Set low limits - settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period - settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds - end - - context 'when the throttle is enabled' do - before do - settings_to_set[:throttle_authenticated_web_enabled] = true - stub_application_setting(settings_to_set) - end - - it 'rejects requests over the rate limit' do - # At first, allow requests under the rate limit. - requests_per_period.times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - # the last straw - expect_rejection { get url_that_requires_authentication } - end - - it 'allows requests after throttling and then waiting for the next period' do - requests_per_period.times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - expect_rejection { get url_that_requires_authentication } - - Timecop.travel(period.from_now) do - requests_per_period.times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - expect_rejection { get url_that_requires_authentication } - end - end - - it 'counts requests from different users separately, even from the same IP' do - requests_per_period.times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - # would be over the limit if this wasn't a different user - login_as(create(:user)) - - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - it 'counts all requests from the same user, even via different IPs' do - requests_per_period.times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') - - expect_rejection { get url_that_requires_authentication } - end - - it 'logs RackAttack info into structured logs' do - requests_per_period.times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - - arguments = { - message: 'Rack_Attack', - env: :throttle, - remote_ip: '127.0.0.1', - request_method: 'GET', - path: '/dashboard/snippets', - user_id: user.id, - username: user.username - } - - expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once - - get url_that_requires_authentication - end - end - - context 'when the throttle is disabled' do - before do - settings_to_set[:throttle_authenticated_web_enabled] = false - stub_application_setting(settings_to_set) - end - - it 'allows requests over the rate limit' do - (1 + requests_per_period).times do - get url_that_requires_authentication - expect(response).to have_http_status 200 - end - end - end + it_behaves_like 'rate-limited web authenticated requests' end def api_get_args_with_token_headers(partial_url, token_headers) diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb index d1483c3c41e..cf0b8ea9b40 100644 --- a/spec/serializers/merge_request_serializer_spec.rb +++ b/spec/serializers/merge_request_serializer_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe MergeRequestSerializer do - let(:user) { create(:user) } - let(:resource) { create(:merge_request) } + set(:user) { create(:user) } + set(:resource) { create(:merge_request, description: "Description") } + let(:json_entity) do described_class.new(current_user: user) .represent(resource, serializer: serializer) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 6cec93a53fd..d8880819d9f 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -23,6 +23,7 @@ describe Ci::CreatePipelineService do trigger_request: nil, variables_attributes: nil, merge_request: nil, + external_pull_request: nil, push_options: nil, source_sha: nil, target_sha: nil, @@ -36,8 +37,11 @@ describe Ci::CreatePipelineService do source_sha: source_sha, target_sha: target_sha } - described_class.new(project, user, params).execute( - source, save_on_errors: save_on_errors, trigger_request: trigger_request, merge_request: merge_request) + described_class.new(project, user, params).execute(source, + save_on_errors: save_on_errors, + trigger_request: trigger_request, + merge_request: merge_request, + external_pull_request: external_pull_request) end # rubocop:enable Metrics/ParameterLists @@ -969,6 +973,152 @@ describe Ci::CreatePipelineService do end end + describe 'Pipeline for external pull requests' do + let(:pipeline) do + execute_service(source: source, + external_pull_request: pull_request, + ref: ref_name, + source_sha: source_sha, + target_sha: target_sha) + end + + before do + stub_ci_pipeline_yaml_file(YAML.dump(config)) + end + + let(:ref_name) { 'refs/heads/feature' } + let(:source_sha) { project.commit(ref_name).id } + let(:target_sha) { nil } + + context 'when source is external pull request' do + let(:source) { :external_pull_request_event } + + context 'when config has external_pull_requests keywords' do + let(:config) do + { + build: { + stage: 'build', + script: 'echo' + }, + test: { + stage: 'test', + script: 'echo', + only: ['external_pull_requests'] + }, + pages: { + stage: 'deploy', + script: 'echo', + except: ['external_pull_requests'] + } + } + end + + context 'when external pull request is specified' do + let(:pull_request) { create(:external_pull_request, project: project, source_branch: 'feature', target_branch: 'master') } + let(:ref_name) { pull_request.source_ref } + + it 'creates an external pull request pipeline' do + expect(pipeline).to be_persisted + expect(pipeline).to be_external_pull_request_event + expect(pipeline.external_pull_request).to eq(pull_request) + expect(pipeline.source_sha).to eq(source_sha) + expect(pipeline.builds.order(:stage_id) + .map(&:name)) + .to eq(%w[build test]) + end + + context 'when ref is tag' do + let(:ref_name) { 'refs/tags/v1.1.0' } + + it 'does not create an extrnal pull request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:tag]).to eq(["is not included in the list"]) + end + end + + context 'when pull request is created from fork' do + it 'does not create an external pull request pipeline' + end + + context "when there are no matched jobs" do + let(:config) do + { + test: { + stage: 'test', + script: 'echo', + except: ['external_pull_requests'] + } + } + end + + it 'does not create a detached merge request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."]) + end + end + end + + context 'when external pull request is not specified' do + let(:pull_request) { nil } + + it 'does not create an external pull request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:external_pull_request]).to eq(["can't be blank"]) + end + end + end + + context "when config does not have external_pull_requests keywords" do + let(:config) do + { + build: { + stage: 'build', + script: 'echo' + }, + test: { + stage: 'test', + script: 'echo' + }, + pages: { + stage: 'deploy', + script: 'echo' + } + } + end + + context 'when external pull request is specified' do + let(:pull_request) do + create(:external_pull_request, + project: project, + source_branch: Gitlab::Git.ref_name(ref_name), + target_branch: 'master') + end + + it 'creates an external pull request pipeline' do + expect(pipeline).to be_persisted + expect(pipeline).to be_external_pull_request_event + expect(pipeline.external_pull_request).to eq(pull_request) + expect(pipeline.source_sha).to eq(source_sha) + expect(pipeline.builds.order(:stage_id) + .map(&:name)) + .to eq(%w[build test pages]) + end + end + + context 'when external pull request is not specified' do + let(:pull_request) { nil } + + it 'does not create an external pull request pipeline' do + expect(pipeline).not_to be_persisted + + expect(pipeline.errors[:base]) + .to eq(['Failed to build the pipeline!']) + end + end + end + end + end + describe 'Pipelines for merge requests' do let(:pipeline) do execute_service(source: source, diff --git a/spec/services/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/external_pull_requests/create_pipeline_service_spec.rb new file mode 100644 index 00000000000..a4da5b38b97 --- /dev/null +++ b/spec/services/external_pull_requests/create_pipeline_service_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ExternalPullRequests::CreatePipelineService do + describe '#execute' do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + let(:pull_request) { create(:external_pull_request, project: project) } + + before do + project.add_maintainer(user) + end + + subject { described_class.new(project, user).execute(pull_request) } + + context 'when pull request is open' do + before do + pull_request.update!(status: :open) + end + + context 'when source sha is the head of the source branch' do + let(:source_branch) { project.repository.branches.last } + let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) } + + before do + pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target) + end + + it 'creates a pipeline for external pull request' do + expect(subject).to be_valid + expect(subject).to be_persisted + expect(subject).to be_external_pull_request_event + expect(subject).to eq(project.ci_pipelines.last) + expect(subject.external_pull_request).to eq(pull_request) + expect(subject.user).to eq(user) + expect(subject.status).to eq('pending') + expect(subject.ref).to eq(pull_request.source_branch) + expect(subject.sha).to eq(pull_request.source_sha) + expect(subject.source_sha).to eq(pull_request.source_sha) + end + end + + context 'when source sha is not the head of the source branch (force push upon rebase)' do + let(:source_branch) { project.repository.branches.first } + let(:commit) { project.repository.commits(source_branch.name, limit: 2).last } + + before do + pull_request.update!(source_branch: source_branch.name, source_sha: commit.sha) + end + + it 'does nothing' do + expect(Ci::CreatePipelineService).not_to receive(:new) + + expect(subject).to be_nil + end + end + end + + context 'when pull request is not opened' do + before do + pull_request.update!(status: :closed) + end + + it 'does nothing' do + expect(Ci::CreatePipelineService).not_to receive(:new) + + expect(subject).to be_nil + end + end + end +end diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index d9e607cd251..c3a4f3dbe3f 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -99,6 +99,20 @@ describe Git::BranchPushService, services: true do expect(pipeline).to be_push expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref) end + + context 'when pipeline has errors' do + before do + config = YAML.dump({ test: { script: 'ls', only: ['feature'] } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'reports an error' do + allow(Sidekiq).to receive(:server?).and_return(true) + expect(Sidekiq.logger).to receive(:warn) + + expect { subject }.not_to change { Ci::Pipeline.count } + end + end end describe "Updates merge requests" do diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index a27fea0c90f..ff4cdd3e7e2 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -13,6 +13,9 @@ describe MergeRequests::PushOptionsHandlerService do let(:target_branch) { 'feature' } let(:title) { 'my title' } let(:description) { 'my description' } + let(:label1) { 'mylabel1' } + let(:label2) { 'mylabel2' } + let(:label3) { 'mylabel3' } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } @@ -122,6 +125,16 @@ describe MergeRequests::PushOptionsHandlerService do end end + shared_examples_for 'a service that can change labels of a merge request' do |count| + subject(:last_mr) { MergeRequest.last } + + it 'changes label count' do + service.execute + + expect(last_mr.label_ids.count).to eq(count) + end + end + shared_examples_for 'a service that does not create a merge request' do it do expect { service.execute }.not_to change { MergeRequest.count } @@ -504,6 +517,138 @@ describe MergeRequests::PushOptionsHandlerService do end end + describe '`label` push option' do + let(:push_options) { { label: { label1 => 1, label2 => 1 } } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 } } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can change labels of a merge request', 2 + end + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 } } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can change labels of a merge request', 2 + end + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can change labels of a merge request', 2 + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + + describe '`unlabel` push option' do + let(:push_options) { { label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can change labels of a merge request', 1 + end + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can change labels of a merge request', 1 + end + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can change labels of a merge request', 1 + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + describe 'multiple pushed branches' do let(:push_options) { { create: true } } let(:changes) do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 6ca0a3fa448..b65ee16c189 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -1140,6 +1140,19 @@ describe QuickActions::InterpretService do let(:todo_label) { create(:label, project: project, title: 'To Do') } let(:inreview_label) { create(:label, project: project, title: 'In Review') } + it 'is available when the user is a developer' do + expect(service.available_commands(issue)).to include(a_hash_including(name: :copy_metadata)) + end + + context 'when the user does not have permission' do + let(:guest) { create(:user) } + let(:service) { described_class.new(project, guest) } + + it 'is not available' do + expect(service.available_commands(issue)).not_to include(a_hash_including(name: :copy_metadata)) + end + end + it_behaves_like 'empty command' do let(:content) { '/copy_metadata' } let(:issuable) { issue } diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb index 4488e5f227e..fdbfe53fa39 100644 --- a/spec/support/helpers/workhorse_helpers.rb +++ b/spec/support/helpers/workhorse_helpers.rb @@ -17,7 +17,36 @@ module WorkhorseHelpers end def workhorse_internal_api_request_header - jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token } end + + # workhorse_post_with_file will transform file_key inside params as if it was disk accelerated by workhorse + def workhorse_post_with_file(url, file_key:, params:) + workhorse_params = params.dup + file = workhorse_params.delete(file_key) + + workhorse_params.merge!(workhorse_disk_accelerated_file_params(file_key, file)) + + post(url, + params: workhorse_params, + headers: workhorse_rewritten_fields_header('file' => file.path) + ) + end + + private + + def jwt_token(data = {}) + JWT.encode({ 'iss' => 'gitlab-workhorse' }.merge(data), Gitlab::Workhorse.secret, 'HS256') + end + + def workhorse_rewritten_fields_header(fields) + { Gitlab::Middleware::Multipart::RACK_ENV_KEY => jwt_token('rewritten_fields' => fields) } + end + + def workhorse_disk_accelerated_file_params(key, file) + { + "#{key}.name" => file.original_filename, + "#{key}.path" => file.path + } + end end 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/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb new file mode 100644 index 00000000000..afc6f59b773 --- /dev/null +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true +# +# Requires let variables: +# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths" +# * get_args +# * other_user_get_args +# * requests_per_period +# * period_in_seconds +# * period +shared_examples_for 'rate-limited token-authenticated requests' do + before do + # Set low limits + settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period + settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get(*get_args) } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_rejection { get(*get_args) } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_rejection { get(*get_args) } + end + end + + it 'counts requests from different users separately, even from the same IP' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + # would be over the limit if this wasn't a different user + get(*other_user_get_args) + expect(response).to have_http_status 200 + end + + it 'counts all requests from the same user, even via different IPs' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + expect_rejection { get(*get_args) } + end + + it 'logs RackAttack info into structured logs' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + arguments = { + message: 'Rack_Attack', + env: :throttle, + remote_ip: '127.0.0.1', + request_method: 'GET', + path: get_args.first, + user_id: user.id, + username: user.username + } + + expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once + + expect_rejection { get(*get_args) } + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get(*get_args) + expect(response).to have_http_status 200 + end + end + end +end + +# Requires let variables: +# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths" +# * user +# * url_that_requires_authentication +# * requests_per_period +# * period_in_seconds +# * period +shared_examples_for 'rate-limited web authenticated requests' do + before do + login_as(user) + + # Set low limits + settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period + settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get url_that_requires_authentication } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_requires_authentication } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_requires_authentication } + end + end + + it 'counts requests from different users separately, even from the same IP' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + # would be over the limit if this wasn't a different user + login_as(create(:user)) + + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + it 'counts all requests from the same user, even via different IPs' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + expect_rejection { get url_that_requires_authentication } + end + + it 'logs RackAttack info into structured logs' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + arguments = { + message: 'Rack_Attack', + env: :throttle, + remote_ip: '127.0.0.1', + request_method: 'GET', + path: '/dashboard/snippets', + user_id: user.id, + username: user.username + } + + expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once + + get url_that_requires_authentication + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + end + end +end 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/spec/workers/update_external_pull_requests_worker_spec.rb b/spec/workers/update_external_pull_requests_worker_spec.rb new file mode 100644 index 00000000000..f3956bb3514 --- /dev/null +++ b/spec/workers/update_external_pull_requests_worker_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UpdateExternalPullRequestsWorker do + describe '#perform' do + set(:project) { create(:project, import_source: 'tanuki/repository') } + set(:user) { create(:user) } + let(:worker) { described_class.new } + + before do + create(:external_pull_request, + project: project, + source_repository: project.import_source, + target_repository: project.import_source, + source_branch: 'feature-1', + target_branch: 'master') + + create(:external_pull_request, + project: project, + source_repository: project.import_source, + target_repository: project.import_source, + source_branch: 'feature-1', + target_branch: 'develop') + end + + subject { worker.perform(project.id, user.id, ref) } + + context 'when ref is a branch' do + let(:ref) { 'refs/heads/feature-1' } + let(:create_pipeline_service) { instance_double(ExternalPullRequests::CreatePipelineService) } + + it 'runs CreatePipelineService for each pull request matching the source branch and repository' do + expect(ExternalPullRequests::CreatePipelineService) + .to receive(:new) + .and_return(create_pipeline_service) + .twice + expect(create_pipeline_service).to receive(:execute).twice + + subject + end + end + + context 'when ref is not a branch' do + let(:ref) { 'refs/tags/v1.2.3' } + + it 'does nothing' do + expect(ExternalPullRequests::CreatePipelineService).not_to receive(:new) + + subject + end + end + end +end diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 00000000000..a383b23fb78 --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,5 @@ +* +!*/ +!.gitignore +!.gitkeep +/tests/ diff --git a/yarn.lock b/yarn.lock index 92da409f544..c64c3a6acaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -991,15 +991,15 @@ dependencies: vue-eslint-parser "^6.0.4" -"@gitlab/svgs@^1.71.0": - version "1.71.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.71.0.tgz#c8e6e8f500ea91e5cbba4ac08df533fb2e622a00" - integrity sha512-kkeNic/FFwaqKnzwio4NE7whBOZ/toRJ8cS0587DBotajAzSYhph5ij4TCY2GTjPa33zIJ5OUr/k90C0Kr71hQ== +"@gitlab/svgs@^1.72.0": + version "1.72.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.72.0.tgz#78a29fd383a5a2b31ef91670068a6fea05ba234e" + integrity sha512-EVKRqrXsCY6tUiVAh+lpFMJAyNXZwfEqv5NeKH5ginhALMlOunRkY5rsDllyNvgZ0DWHJS1KEKJj2oVU1ouwAg== -"@gitlab/ui@5.21.0": - version "5.21.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.21.0.tgz#975cf0bca3d16dd080d67ed392b9d24cd64695ac" - integrity sha512-8TMVM+pJXf7omHgKMMZ1FiltuyMOTwfQ3iFgorQzcuhio9u35DJpWi45S2TF7m6CrlpJi7dMX3BsXLbF7ViSUw== +"@gitlab/ui@5.21.1": + version "5.21.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.21.1.tgz#8215ab3eae4296845596d5b3a987d5460b030569" + integrity sha512-TjPVhex9sQGUVwebaJK5XuopDHWx4+Sh9N7yH5u8eXSFWa8vk11voR4qYVt7DZB7powAO/+iiXxYMLLNtXmC/g== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" @@ -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" |