diff options
100 files changed, 2494 insertions, 155 deletions
diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js new file mode 100644 index 00000000000..2d1ec238274 --- /dev/null +++ b/app/assets/javascripts/boards/config_toggle.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 23b107abefa..d6a5372b22d 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -7,28 +7,30 @@ import { __ } from '~/locale'; import './models/label'; import './models/assignee'; -import FilteredSearchBoards from './filtered_search_boards'; -import eventHub from './eventhub'; +import FilteredSearchBoards from '~/boards/filtered_search_boards'; +import eventHub from '~/boards/eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; -import './models/issue'; -import './models/list'; -import './models/milestone'; -import './models/project'; -import boardsStore from './stores/boards_store'; -import ModalStore from './stores/modal_store'; -import BoardService from './services/board_service'; -import modalMixin from './mixins/modal_mixins'; -import './filters/due_date_filters'; -import Board from './components/board'; -import BoardSidebar from './components/board_sidebar'; +import 'ee_else_ce/boards/models/issue'; +import 'ee_else_ce/boards/models/list'; +import '~/boards/models/milestone'; +import '~/boards/models/project'; +import boardsStore from '~/boards/stores/boards_store'; +import ModalStore from '~/boards/stores/modal_store'; +import BoardService from 'ee_else_ce/boards/services/board_service'; +import modalMixin from '~/boards/mixins/modal_mixins'; +import '~/boards/filters/due_date_filters'; +import Board from 'ee_else_ce/boards/components/board'; +import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar'; import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; -import BoardAddIssuesModal from './components/modal/index.vue'; +import BoardAddIssuesModal from '~/boards/components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; import { NavigationType, convertObjectPropsToCamelCase, parseBoolean, } from '~/lib/utils/common_utils'; +import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; +import toggleFocusMode from 'ee_else_ce/boards/toggle_focus'; let issueBoardsApp; @@ -207,6 +209,8 @@ export default () => { }, }); + boardConfigToggle(boardsStore); + const issueBoardsModal = document.getElementById('js-add-issues-btn'); if (issueBoardsModal) { @@ -281,5 +285,6 @@ export default () => { }); } + toggleFocusMode(ModalStore, boardsStore); mountMultipleBoardsSwitcher(); }; diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js new file mode 100644 index 00000000000..2d1ec238274 --- /dev/null +++ b/app/assets/javascripts/boards/toggle_focus.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index 47a6f07cce2..bc0f5c19b9d 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -38,9 +38,6 @@ export default { return Vue.http.get(endpoint, options); }, - toggleAward(endpoint, data) { - return Vue.http.post(endpoint, data, { emulateJSON: true }); - }, toggleIssueState(endpoint, data) { return Vue.http.put(endpoint, data); }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index fef962f008e..2eefef8bd6e 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -384,12 +384,9 @@ export const toggleAward = ({ commit, getters }, { awardName, noteId }) => { export const toggleAwardRequest = ({ dispatch }, data) => { const { endpoint, awardName } = data; - return service - .toggleAward(endpoint, { name: awardName }) - .then(res => res.json()) - .then(() => { - dispatch('toggleAward', data); - }); + return axios.post(endpoint, { name: awardName }).then(() => { + dispatch('toggleAward', data); + }); }; export const scrollToNoteIfNeeded = (context, el) => { diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss index acbd909d595..e27bf282247 100644 --- a/app/assets/stylesheets/components/toast.scss +++ b/app/assets/stylesheets/components/toast.scss @@ -15,11 +15,15 @@ .toasted.gl-toast { border-radius: $border-radius-default; font-size: $gl-font-size; - padding: $gl-padding-8 $gl-padding-24; + padding: $gl-padding-8 $gl-padding $gl-padding-8 $gl-padding-24; margin-top: $toast-default-margin; line-height: $gl-line-height; background-color: rgba($gray-900, $toast-background-opacity); + span { + padding-right: $gl-padding-8; + } + @include media-breakpoint-down(xs) { .action:first-of-type { // Ensures actions buttons are right aligned on mobile @@ -29,19 +33,14 @@ .action { color: $blue-300; - margin: 0 0 0 $toast-action-margin-left; + margin: 0 0 0 $toast-default-margin; text-transform: none; font-size: $gl-font-size; - - &:first-of-type { - padding-right: 0; - } } .toast-close { font-size: $default-icon-size; margin-left: $toast-default-margin; - padding-left: $gl-padding; } } } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 975dca168d5..1d00372d04d 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -53,6 +53,10 @@ display: flex; flex-direction: row; + .btn { + margin: 0; + } + .btn + .btn:not(.dropdown-toggle-split), .btn + .btn-group, .btn-group + .btn { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 406bcda418e..4521643ce08 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -507,7 +507,6 @@ $toast-height: 48px; $toast-max-width: 586px; $toast-padding-right: 42px; $toast-default-margin: 8px; -$toast-action-margin-left: 16px; $toast-background-opacity: 0.95; /* diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 152ebb930e2..7edd14e48f7 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -66,6 +66,8 @@ class GitlabSchema < GraphQL::Schema if gid.model_class < ApplicationRecord Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find + elsif gid.model_class.respond_to?(:lazy_find) + gid.model_class.lazy_find(gid.model_id) else gid.find end diff --git a/app/graphql/mutations/notes/base.rb b/app/graphql/mutations/notes/base.rb new file mode 100644 index 00000000000..a7198f5fba6 --- /dev/null +++ b/app/graphql/mutations/notes/base.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Mutations + module Notes + class Base < BaseMutation + include Gitlab::Graphql::Authorize::AuthorizeResource + + field :note, + Types::Notes::NoteType, + null: true, + description: 'The note after mutation' + + private + + def find_object(id:) + GitlabSchema.object_from_id(id) + end + + def check_object_is_noteable!(object) + unless object.is_a?(Noteable) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'Cannot add notes to this resource' + end + end + + def check_object_is_note!(object) + unless object.is_a?(Note) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'Resource is not a note' + end + end + end + end +end diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb new file mode 100644 index 00000000000..d3a5dae2188 --- /dev/null +++ b/app/graphql/mutations/notes/create/base.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Create + # This is a Base class for the Note creation Mutations and is not + # mounted as a GraphQL mutation itself. + class Base < Mutations::Notes::Base + authorize :create_note + + argument :noteable_id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the resource to add a note to' + + argument :body, + GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::Notes::NoteType, :body) + + private + + def resolve(args) + noteable = authorized_find!(id: args[:noteable_id]) + + check_object_is_noteable!(noteable) + + note = ::Notes::CreateService.new( + noteable.project, + current_user, + create_note_params(noteable, args) + ).execute + + { + note: (note if note.persisted?), + errors: errors_on_object(note) + } + end + + def create_note_params(noteable, args) + { + noteable: noteable, + note: args[:body] + } + end + end + end + end +end diff --git a/app/graphql/mutations/notes/create/diff_note.rb b/app/graphql/mutations/notes/create/diff_note.rb new file mode 100644 index 00000000000..9b5f3092006 --- /dev/null +++ b/app/graphql/mutations/notes/create/diff_note.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Create + class DiffNote < Base + graphql_name 'CreateDiffNote' + + argument :position, + Types::Notes::DiffPositionInputType, + required: true, + description: copy_field_description(Types::Notes::NoteType, :position) + + private + + def create_note_params(noteable, args) + super(noteable, args).merge({ + type: 'DiffNote', + position: position(noteable, args) + }) + end + + def position(noteable, args) + position = args[:position].to_h + position[:position_type] = 'text' + position.merge!(position[:paths].to_h) + + Gitlab::Diff::Position.new(position) + end + end + end + end +end diff --git a/app/graphql/mutations/notes/create/image_diff_note.rb b/app/graphql/mutations/notes/create/image_diff_note.rb new file mode 100644 index 00000000000..d94fd4d6ff8 --- /dev/null +++ b/app/graphql/mutations/notes/create/image_diff_note.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Create + class ImageDiffNote < Base + graphql_name 'CreateImageDiffNote' + + argument :position, + Types::Notes::DiffImagePositionInputType, + required: true, + description: copy_field_description(Types::Notes::NoteType, :position) + + private + + def create_note_params(noteable, args) + super(noteable, args).merge({ + type: 'DiffNote', + position: position(noteable, args) + }) + end + + def position(noteable, args) + position = args[:position].to_h + position[:position_type] = 'image' + position.merge!(position[:paths].to_h) + + Gitlab::Diff::Position.new(position) + end + end + end + end +end diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb new file mode 100644 index 00000000000..5236e48026e --- /dev/null +++ b/app/graphql/mutations/notes/create/note.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Mutations + module Notes + module Create + class Note < Base + graphql_name 'CreateNote' + + argument :discussion_id, + GraphQL::ID_TYPE, + required: false, + description: 'The global id of the discussion this note is in reply to' + + private + + def create_note_params(noteable, args) + discussion_id = nil + + if args[:discussion_id] + discussion = GitlabSchema.object_from_id(args[:discussion_id]) + authorize_discussion!(discussion) + + discussion_id = discussion.id + end + + super(noteable, args).merge({ + in_reply_to_discussion_id: discussion_id + }) + end + + def authorize_discussion!(discussion) + unless Ability.allowed?(current_user, :read_note, discussion, scope: :user) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + "The discussion does not exist or you don't have permission to perform this action" + end + end + end + end + end +end diff --git a/app/graphql/mutations/notes/destroy.rb b/app/graphql/mutations/notes/destroy.rb new file mode 100644 index 00000000000..a81322bc9b7 --- /dev/null +++ b/app/graphql/mutations/notes/destroy.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + module Notes + class Destroy < Base + graphql_name 'DestroyNote' + + authorize :admin_note + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the note to destroy' + + def resolve(id:) + note = authorized_find!(id: id) + + check_object_is_note!(note) + + ::Notes::DestroyService.new(note.project, current_user).execute(note) + + { + errors: [] + } + end + end + end +end diff --git a/app/graphql/mutations/notes/update.rb b/app/graphql/mutations/notes/update.rb new file mode 100644 index 00000000000..ebf57b800c0 --- /dev/null +++ b/app/graphql/mutations/notes/update.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Mutations + module Notes + class Update < Base + graphql_name 'UpdateNote' + + authorize :admin_note + + argument :id, + GraphQL::ID_TYPE, + required: true, + description: 'The global id of the note to update' + + argument :body, + GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::Notes::NoteType, :body) + + def resolve(args) + note = authorized_find!(id: args[:id]) + + check_object_is_note!(note) + + note = ::Notes::UpdateService.new( + note.project, + current_user, + { note: args[:body] } + ).execute(note) + + { + note: note.reset, + errors: errors_on_object(note) + } + end + end + end +end diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb index aebed035d3b..90a29b0cfb8 100644 --- a/app/graphql/types/base_input_object.rb +++ b/app/graphql/types/base_input_object.rb @@ -2,5 +2,6 @@ module Types class BaseInputObject < GraphQL::Schema::InputObject + prepend Gitlab::Graphql::CopyFieldDescription end end diff --git a/app/graphql/types/diff_paths_input_type.rb b/app/graphql/types/diff_paths_input_type.rb new file mode 100644 index 00000000000..43feddd9827 --- /dev/null +++ b/app/graphql/types/diff_paths_input_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class DiffPathsInputType < BaseInputObject + argument :old_path, GraphQL::STRING_TYPE, required: false, + description: 'The path of the file on the start sha' + argument :new_path, GraphQL::STRING_TYPE, required: false, + description: 'The path of the file on the head sha' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/diff_refs_type.rb b/app/graphql/types/diff_refs_type.rb new file mode 100644 index 00000000000..33a5780cd68 --- /dev/null +++ b/app/graphql/types/diff_refs_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + # Types that use DiffRefsType should have their own authorization + class DiffRefsType < BaseObject + graphql_name 'DiffRefs' + + field :head_sha, GraphQL::STRING_TYPE, null: false, description: 'The sha of the head at the time the comment was made' + field :base_sha, GraphQL::STRING_TYPE, null: false, description: 'The merge base of the branch the comment was made on' + field :start_sha, GraphQL::STRING_TYPE, null: false, description: 'The sha of the branch being compared against' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 6734d4761c2..b8f63a750c5 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -23,6 +23,7 @@ module Types field :updated_at, Types::TimeType, null: false field :source_project, Types::ProjectType, null: true field :target_project, Types::ProjectType, null: false + field :diff_refs, Types::DiffRefsType, null: true # Alias for target_project field :project, Types::ProjectType, null: false field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index bc5fb709522..f843d6ad86f 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -10,5 +10,10 @@ module Types mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true + mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true + mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true + mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true + mount_mutation Mutations::Notes::Update + mount_mutation Mutations::Notes::Destroy end end diff --git a/app/graphql/types/notes/diff_image_position_input_type.rb b/app/graphql/types/notes/diff_image_position_input_type.rb new file mode 100644 index 00000000000..23b53b20815 --- /dev/null +++ b/app/graphql/types/notes/diff_image_position_input_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Notes + # rubocop: disable Graphql/AuthorizeTypes + class DiffImagePositionInputType < DiffPositionBaseInputType + graphql_name 'DiffImagePositionInput' + + argument :x, GraphQL::INT_TYPE, required: true, + description: copy_field_description(Types::Notes::DiffPositionType, :x) + argument :y, GraphQL::INT_TYPE, required: true, + description: copy_field_description(Types::Notes::DiffPositionType, :y) + argument :width, GraphQL::INT_TYPE, required: true, + description: copy_field_description(Types::Notes::DiffPositionType, :width) + argument :height, GraphQL::INT_TYPE, required: true, + description: copy_field_description(Types::Notes::DiffPositionType, :height) + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/notes/diff_position_base_input_type.rb b/app/graphql/types/notes/diff_position_base_input_type.rb new file mode 100644 index 00000000000..a9b4e1a8948 --- /dev/null +++ b/app/graphql/types/notes/diff_position_base_input_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module Notes + # rubocop: disable Graphql/AuthorizeTypes + class DiffPositionBaseInputType < BaseInputObject + argument :head_sha, GraphQL::STRING_TYPE, required: true, + description: copy_field_description(Types::DiffRefsType, :head_sha) + argument :base_sha, GraphQL::STRING_TYPE, required: false, + description: copy_field_description(Types::DiffRefsType, :base_sha) + argument :start_sha, GraphQL::STRING_TYPE, required: true, + description: copy_field_description(Types::DiffRefsType, :start_sha) + + argument :paths, + Types::DiffPathsInputType, + required: true, + description: 'The paths of the file that was changed. ' \ + 'Both of the properties of this input are optional, but at least one of them is required' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/notes/diff_position_input_type.rb b/app/graphql/types/notes/diff_position_input_type.rb new file mode 100644 index 00000000000..02c91e173cb --- /dev/null +++ b/app/graphql/types/notes/diff_position_input_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Notes + # rubocop: disable Graphql/AuthorizeTypes + class DiffPositionInputType < DiffPositionBaseInputType + graphql_name 'DiffPositionInput' + + argument :old_line, GraphQL::INT_TYPE, required: false, + description: copy_field_description(Types::Notes::DiffPositionType, :old_line) + argument :new_line, GraphQL::INT_TYPE, required: true, + description: copy_field_description(Types::Notes::DiffPositionType, :new_line) + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb index ebc24451715..6a0377fbfdf 100644 --- a/app/graphql/types/notes/diff_position_type.rb +++ b/app/graphql/types/notes/diff_position_type.rb @@ -7,12 +7,7 @@ module Types class DiffPositionType < BaseObject graphql_name 'DiffPosition' - field :head_sha, GraphQL::STRING_TYPE, null: false, - description: "The sha of the head at the time the comment was made" - field :base_sha, GraphQL::STRING_TYPE, null: true, - description: "The merge base of the branch the comment was made on" - field :start_sha, GraphQL::STRING_TYPE, null: false, - description: "The sha of the branch being compared against" + field :diff_refs, Types::DiffRefsType, null: false field :file_path, GraphQL::STRING_TYPE, null: false, description: "The path of the file that was changed" diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb index c4691942f2d..a3fb28298f6 100644 --- a/app/graphql/types/notes/discussion_type.rb +++ b/app/graphql/types/notes/discussion_type.rb @@ -8,8 +8,16 @@ module Types authorize :read_note field :id, GraphQL::ID_TYPE, null: false + field :reply_id, GraphQL::ID_TYPE, null: false, description: 'The ID used to reply to this discussion' field :created_at, Types::TimeType, null: false field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion" + + # The gem we use to generate Global IDs is hard-coded to work with + # `id` properties. To generate a GID for the `reply_id` property, + # we must use the ::Gitlab::GlobalId module. + def reply_id + ::Gitlab::GlobalId.build(object, id: object.reply_id) + end end end end diff --git a/app/models/concerns/project_api_compatibility.rb b/app/models/concerns/project_api_compatibility.rb new file mode 100644 index 00000000000..cb00efb06df --- /dev/null +++ b/app/models/concerns/project_api_compatibility.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Add methods used by the projects API +module ProjectAPICompatibility + extend ActiveSupport::Concern + + def build_git_strategy=(value) + write_attribute(:build_allow_git_fetch, value == 'fetch') + end + + def auto_devops_enabled=(value) + self.build_auto_devops if self.auto_devops&.enabled.nil? + self.auto_devops.update! enabled: value + end + + def auto_devops_deploy_strategy=(value) + self.build_auto_devops if self.auto_devops&.enabled.nil? + self.auto_devops.update! deploy_strategy: value + end +end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index f268a842db4..551a2e56ecf 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -9,32 +9,70 @@ require 'gitlab/utils' module ProjectFeaturesCompatibility extend ActiveSupport::Concern + # TODO: remove in API v5, replaced by *_access_level def wiki_enabled=(value) - write_feature_attribute(:wiki_access_level, value) + write_feature_attribute_boolean(:wiki_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def builds_enabled=(value) - write_feature_attribute(:builds_access_level, value) + write_feature_attribute_boolean(:builds_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def merge_requests_enabled=(value) - write_feature_attribute(:merge_requests_access_level, value) + write_feature_attribute_boolean(:merge_requests_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def issues_enabled=(value) - write_feature_attribute(:issues_access_level, value) + write_feature_attribute_boolean(:issues_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def snippets_enabled=(value) - write_feature_attribute(:snippets_access_level, value) + write_feature_attribute_boolean(:snippets_access_level, value) + end + + def repository_access_level=(value) + write_feature_attribute_string(:repository_access_level, value) + end + + def wiki_access_level=(value) + write_feature_attribute_string(:wiki_access_level, value) + end + + def builds_access_level=(value) + write_feature_attribute_string(:builds_access_level, value) + end + + def merge_requests_access_level=(value) + write_feature_attribute_string(:merge_requests_access_level, value) + end + + def issues_access_level=(value) + write_feature_attribute_string(:issues_access_level, value) + end + + def snippets_access_level=(value) + write_feature_attribute_string(:snippets_access_level, value) end private - def write_feature_attribute(field, value) + def write_feature_attribute_boolean(field, value) + access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + write_feature_attribute_raw(field, access_level) + end + + def write_feature_attribute_string(field, value) + access_level = ProjectFeature.access_level_from_str(value) + write_feature_attribute_raw(field, access_level) + end + + def write_feature_attribute_raw(field, value) build_project_feature unless project_feature - access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend + project_feature.__send__(:write_attribute, field, value) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index ae13cdfd85f..dd896f77084 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -38,6 +38,17 @@ class Discussion grouped_notes.values.map { |notes| build(notes, context_noteable) } end + def self.lazy_find(discussion_id) + BatchLoader.for(discussion_id).batch do |discussion_ids, loader| + results = Note.where(discussion_id: discussion_ids).fresh.to_a.group_by(&:discussion_id) + results.each do |discussion_id, notes| + next if notes.empty? + + loader.call(discussion_id, Discussion.build(notes)) + end + end + end + # Returns an alphanumeric discussion ID based on `build_discussion_id` def self.discussion_id(note) Digest::SHA1.hexdigest(build_discussion_id(note).join("-")) diff --git a/app/models/note.rb b/app/models/note.rb index 4e9ea146485..5c31cff9816 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -158,6 +158,8 @@ class Note < ApplicationRecord Discussion.build_collection(all.includes(:noteable).fresh, context_noteable) end + # Note: Where possible consider using Discussion#lazy_find to return + # Discussions in order to benefit from having records batch loaded. def find_discussion(discussion_id) notes = where(discussion_id: discussion_id).fresh.to_a diff --git a/app/models/project.rb b/app/models/project.rb index 075f7882d72..bfc35b77b8f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -15,6 +15,7 @@ class Project < ApplicationRecord include CaseSensitivity include TokenAuthenticatable include ValidAttribute + include ProjectAPICompatibility include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Presentable diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 0542581c6e0..7ff06655de0 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -24,6 +24,12 @@ class ProjectFeature < ApplicationRecord FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze + STRING_OPTIONS = HashWithIndifferentAccess.new({ + 'disabled' => DISABLED, + 'private' => PRIVATE, + 'enabled' => ENABLED, + 'public' => PUBLIC + }).freeze class << self def access_level_attribute(feature) @@ -45,6 +51,14 @@ class ProjectFeature < ApplicationRecord PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST) end + def access_level_from_str(level) + STRING_OPTIONS.fetch(level) + end + + def str_from_access_level(level) + STRING_OPTIONS.key(level) + end + private def ensure_feature!(feature) @@ -83,6 +97,10 @@ class ProjectFeature < ApplicationRecord public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend end + def string_access_level(feature) + ProjectFeature.str_from_access_level(access_level(feature)) + end + def builds_enabled? builds_access_level > DISABLED end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index dedab98b56d..9d4cf5df713 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -6,7 +6,7 @@ module Ci class RegisterJobService attr_reader :runner - JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300].freeze + JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300, 900].freeze JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze Result = Struct.new(:build, :valid?) diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 37ba2143eba..b9be6028b72 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -8,7 +8,7 @@ - if current_user .page-title-controls = render 'shared/new_project_item_select', - path: 'milestones/new', label: 'New milestone', + path: '-/milestones/new', label: 'New milestone', include_groups: true, type: :milestones .top-area diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index e423631ec99..7541737f79c 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -36,17 +36,17 @@ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", required: true - if current_user.can_create_group? .form-text.text-muted - Want to house several dependent projects under the same namespace? - = link_to "Create a group.", new_group_path + - link_start_group_path = '<a href="%{path}">' % { path: new_group_path } + - project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' } + = project_tip.html_safe .form-group = f.label :description, class: 'label-bold' do - Project description - %span (optional) - = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" } + = s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe } + = f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" } = f.label :visibility_level, class: 'label-bold' do - Visibility Level + = s_('ProjectsNew|Visibility Level') = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false @@ -57,9 +57,9 @@ = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" } = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do .option-title - %strong Initialize repository with a README + %strong= s_('ProjectsNew|Initialize repository with a README') .option-description - Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository. + = s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.') -= f.submit 'Create project', class: "btn btn-success project-submit", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" } -= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } += f.submit _('Create project'), class: "btn btn-success project-submit", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" } += link_to _('Cancel'), dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 1cfe302fdc7..33de0aa153b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,7 +1,7 @@ - @hide_breadcrumbs = true - @hide_top_links = true -- page_title 'New Project' -- header_title "Projects", dashboard_projects_path +- page_title _('New Project') +- header_title _("Projects"), dashboard_projects_path - active_tab = local_assigns.fetch(:active_tab, 'blank') .project-edit-container.prepend-top-default @@ -33,16 +33,16 @@ %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } %li.nav-item{ role: 'presentation' } %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' } - %span.d-none.d-sm-block Blank project - %span.d-block.d-sm-none Blank + %span.d-none.d-sm-block= s_('ProjectsNew|Blank project') + %span.d-block.d-sm-none= s_('ProjectsNew|Blank') %li.nav-item{ role: 'presentation' } %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' } - %span.d-none.d-sm-block.qa-project-create-from-template-tab Create from template - %span.d-block.d-sm-none Template + %span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template') + %span.d-block.d-sm-none= s_('ProjectsNew|Template') %li.nav-item{ role: 'presentation' } %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' } - %span.d-none.d-sm-block Import project - %span.d-block.d-sm-none Import + %span.d-none.d-sm-block= s_('ProjectsNew|Import project') + %span.d-block.d-sm-none= s_('ProjectsNew|Import') = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab .tab-content.gitlab-tab-content @@ -67,8 +67,8 @@ = render 'import_project_pane', active_tab: active_tab - else .nothing-here-block - %h4 No import options available - %p Contact an administrator to enable options for importing your project. + %h4= s_('ProjectsNew|No import options available') + %p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.') = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab @@ -76,5 +76,6 @@ .center %h2 %i.fa.fa-spinner.fa-spin - Creating project & repository. - %p Please wait a moment, this page will automatically refresh when ready. + = s_('ProjectsNew|Creating project & repository.') + %p + = s_('ProjectsNew|Please wait a moment, this page will automatically refresh when ready.') diff --git a/changelogs/unreleased/62826-graphql-note-mutations.yml b/changelogs/unreleased/62826-graphql-note-mutations.yml new file mode 100644 index 00000000000..85273186bb6 --- /dev/null +++ b/changelogs/unreleased/62826-graphql-note-mutations.yml @@ -0,0 +1,5 @@ +--- +title: GraphQL mutations for managing Notes +merge_request: 30210 +author: +type: added diff --git a/changelogs/unreleased/64066-fix-uneven-click-areas.yml b/changelogs/unreleased/64066-fix-uneven-click-areas.yml new file mode 100644 index 00000000000..ce0572cad34 --- /dev/null +++ b/changelogs/unreleased/64066-fix-uneven-click-areas.yml @@ -0,0 +1,5 @@ +--- +title: Fix spacing issues for toasts +merge_request: 30345 +author: +type: fixed diff --git a/changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml b/changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml new file mode 100644 index 00000000000..825247db3e7 --- /dev/null +++ b/changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Fix wrong URL when creating milestones from instance milestones dashboard +merge_request: 30512 +author: +type: fixed diff --git a/changelogs/unreleased/caneldem-master-patch-77839.yml b/changelogs/unreleased/caneldem-master-patch-77839.yml new file mode 100644 index 00000000000..6239bcf67c4 --- /dev/null +++ b/changelogs/unreleased/caneldem-master-patch-77839.yml @@ -0,0 +1,5 @@ +--- +title: Propagate python version variable +merge_request: +author: Can Eldem +type: changed diff --git a/changelogs/unreleased/embedded-metrics-be-2.yml b/changelogs/unreleased/embedded-metrics-be-2.yml new file mode 100644 index 00000000000..2623b4a2e0c --- /dev/null +++ b/changelogs/unreleased/embedded-metrics-be-2.yml @@ -0,0 +1,5 @@ +--- +title: Expose placeholder element for metrics charts in GFM +merge_request: 29861 +author: +type: added diff --git a/changelogs/unreleased/fix-unicorn-sampler-workers-count.yml b/changelogs/unreleased/fix-unicorn-sampler-workers-count.yml new file mode 100644 index 00000000000..9a263b7f460 --- /dev/null +++ b/changelogs/unreleased/fix-unicorn-sampler-workers-count.yml @@ -0,0 +1,5 @@ +--- +title: Make unicorn_workers to return meaningful results +merge_request: 30506 +author: +type: fixed diff --git a/changelogs/unreleased/project_api.yml b/changelogs/unreleased/project_api.yml new file mode 100644 index 00000000000..a04f9bb5608 --- /dev/null +++ b/changelogs/unreleased/project_api.yml @@ -0,0 +1,5 @@ +--- +title: Improve Project API +merge_request: 28327 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/update-clair-version.yml b/changelogs/unreleased/update-clair-version.yml new file mode 100644 index 00000000000..59b6e113fd5 --- /dev/null +++ b/changelogs/unreleased/update-clair-version.yml @@ -0,0 +1,6 @@ +--- +title: Extract clair version as CLAIR_EXECUTABLE_VERSION variable and update clair + executable from v8 to v11 +merge_request: 30396 +author: +type: changed diff --git a/changelogs/unreleased/winh-notes-service-toggleAward.yml b/changelogs/unreleased/winh-notes-service-toggleAward.yml new file mode 100644 index 00000000000..0471888c285 --- /dev/null +++ b/changelogs/unreleased/winh-notes-service-toggleAward.yml @@ -0,0 +1,5 @@ +--- +title: Remove toggleAward from notes service +merge_request: 30536 +author: Frank van Rest +type: other diff --git a/config/brakeman.ignore b/config/brakeman.ignore new file mode 100644 index 00000000000..0e4fef65781 --- /dev/null +++ b/config/brakeman.ignore @@ -0,0 +1,24 @@ +{ + "ignored_warnings": [ + { + "warning_type": "Cross-Site Request Forgery", + "warning_code": 7, + "fingerprint": "dc562678129557cdb8b187217da304044547a3605f05fe678093dcb4b4d8bbe4", + "message": "'protect_from_forgery' should be called in Oauth::GeoAuthController", + "file": "app/controllers/oauth/geo_auth_controller.rb", + "line": 1, + "link": "http://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/", + "code": null, + "render_path": null, + "location": { + "type": "controller", + "controller": "Oauth::GeoAuthController" + }, + "user_input": null, + "confidence": "High", + "note": "" + } + ], + "updated": "2017-01-20 02:06:54 +0000", + "brakeman_version": "3.4.1" +} diff --git a/config/database_geo.yml.postgresql b/config/database_geo.yml.postgresql new file mode 100644 index 00000000000..2918879f7ed --- /dev/null +++ b/config/database_geo.yml.postgresql @@ -0,0 +1,51 @@ +# +# PRODUCTION +# +production: + adapter: postgresql + encoding: unicode + database: gitlabhq_geo_production + pool: 10 + username: git + password: "secure password" + host: localhost + fdw: true + +# +# Development specific +# +development: + adapter: postgresql + encoding: unicode + database: gitlabhq_geo_development + pool: 5 + username: postgres + password: "secure password" + host: localhost + fdw: true + +# +# Staging specific +# +staging: + adapter: postgresql + encoding: unicode + database: gitlabhq_geo_staging + pool: 10 + username: git + password: "secure password" + host: localhost + fdw: true + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: &test + adapter: postgresql + encoding: unicode + database: gitlabhq_geo_test + pool: 5 + username: postgres + password: + host: localhost + fdw: true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c82d9b5ceef..334c241bcaa 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -664,6 +664,9 @@ production: &base # Port where the client side certificate is requested by the webserver (NGINX/Apache) # client_certificate_required_port: 3444 + # Browser session with smartcard sign-in is required for Git access + # required_for_git_access: false + ## Kerberos settings kerberos: # Allow the HTTP Negotiate authentication method for Git clients diff --git a/config/initializers/ar_speed_up_migration_checking.rb b/config/initializers/ar_speed_up_migration_checking.rb index aae774daa35..f98b246db0b 100644 --- a/config/initializers/ar_speed_up_migration_checking.rb +++ b/config/initializers/ar_speed_up_migration_checking.rb @@ -10,7 +10,8 @@ if Rails.env.test? # it reads + parses `db/migrate/*` each time. Memoizing it can save 0.5 # seconds per spec. def migrations(paths) - (@migrations ||= migrations_unmemoized(paths)).dup + @migrations ||= {} + (@migrations[paths] ||= migrations_unmemoized(paths)).dup end end end diff --git a/config/prometheus/cluster_metrics.yml b/config/prometheus/cluster_metrics.yml new file mode 100644 index 00000000000..3df76b0974f --- /dev/null +++ b/config/prometheus/cluster_metrics.yml @@ -0,0 +1,63 @@ +- group: Cluster Health + priority: 1 + metrics: + - title: "CPU Usage" + y_label: "CPU" + required_metrics: ['container_cpu_usage_seconds_total'] + weight: 1 + queries: + - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)' + label: Usage + unit: "cores" + appearance: + line: + width: 2 + area: + opacity: 0 + - query_range: 'sum(kube_pod_container_resource_requests_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})' + label: Requested + unit: "cores" + appearance: + line: + width: 2 + area: + opacity: 0 + - query_range: 'sum(kube_node_status_capacity_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})' + label: Capacity + unit: "cores" + appearance: + line: + type: 'dashed' + width: 2 + area: + opacity: 0 + - title: "Memory usage" + y_label: "Memory" + required_metrics: ['container_memory_usage_bytes'] + weight: 1 + queries: + - query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30' + label: Usage + unit: "GiB" + appearance: + line: + width: 2 + area: + opacity: 0 + - query_range: 'sum(kube_pod_container_resource_requests_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30' + label: Requested + unit: "GiB" + appearance: + line: + width: 2 + area: + opacity: 0 + - query_range: 'sum(kube_node_status_capacity_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30' + label: Capacity + unit: "GiB" + appearance: + line: + type: 'dashed' + width: 2 + area: + opacity: 0 diff --git a/config/pseudonymizer.yml b/config/pseudonymizer.yml new file mode 100644 index 00000000000..1d85ac1db45 --- /dev/null +++ b/config/pseudonymizer.yml @@ -0,0 +1,475 @@ +tables: + approvals: + whitelist: + - id + - merge_request_id + - user_id + - created_at + - updated_at + approver_groups: + whitelist: + - id + - target_type + - group_id + - created_at + - updated_at + board_assignees: + whitelist: + - id + - board_id + - assignee_id + board_labels: + whitelist: + - id + - board_id + - label_id + boards: + whitelist: + - id + - project_id + - created_at + - updated_at + - milestone_id + - group_id + - weight + epic_issues: + whitelist: + - id + - epic_id + - issue_id + - relative_position + epic_metrics: + whitelist: + - id + - epic_id + - created_at + - updated_at + epics: + whitelist: + - id + - milestone_id + - group_id + - author_id + - assignee_id + - iid + - updated_by_id + - last_edited_by_id + - lock_version + - start_date + - end_date + - last_edited_at + - created_at + - updated_at + - title + - description + issue_assignees: + whitelist: + - user_id + - issue_id + issue_links: + whitelist: + - id + - source_id + - target_id + - created_at + - updated_at + issue_metrics: + whitelist: + - id + - issue_id + - first_mentioned_in_commit_at + - first_associated_with_milestone_at + - first_added_to_board_at + - created_at + - updated_at + issues: + whitelist: + - id + - title + - author_id + - project_id + - created_at + - confidential + - updated_at + - description + - milestone_id + - state + - updated_by_id + - weight + - due_date + - moved_to_id + - lock_version + - time_estimate + - last_edited_at + - last_edited_by_id + - discussion_locked + - closed_at + label_links: + whitelist: + - id + - label_id + - target_id + - target_type + - created_at + - updated_at + label_priorities: + whitelist: + - id + - project_id + - label_id + - priority + - created_at + - updated_at + labels: + whitelist: + - id + - title + - color + - project_id + - created_at + - updated_at + - template + - type + - group_id + licenses: + whitelist: + - id + - created_at + - updated_at + merge_request_diffs: + whitelist: + - id + - state + - merge_request_id + - created_at + - updated_at + - base_commit_sha + - real_size + - head_commit_sha + - start_commit_sha + - commits_count + merge_request_metrics: + whitelist: + - id + - merge_request_id + - latest_build_started_at + - latest_build_finished_at + - first_deployed_to_production_at + - merged_at + - created_at + - updated_at + - pipeline_id + - merged_by_id + - latest_closed_by_id + - latest_closed_at + merge_requests: + whitelist: + - id + - target_branch + - source_branch + - source_project_id + - author_id + - assignee_id + - created_at + - updated_at + - milestone_id + - state + - merge_status + - target_project_id + - updated_by_id + - merge_error + - merge_params + - merge_when_pipeline_succeeds + - merge_user_id + - approvals_before_merge + - lock_version + - time_estimate + - squash + - last_edited_at + - last_edited_by_id + - head_pipeline_id + - discussion_locked + - latest_merge_request_diff_id + - allow_maintainer_to_push + merge_requests_closing_issues: + whitelist: + - id + - merge_request_id + - issue_id + - created_at + - updated_at + milestones: + whitelist: + - id + - project_id + - due_date + - created_at + - updated_at + - state + - start_date + - group_id + namespace_statistics: + whitelist: + - id + - namespace_id + - shared_runners_seconds + - shared_runners_seconds_last_reset + namespaces: + whitelist: + - id + - name + - path + - owner_id + - created_at + - updated_at + - type + - avatar + - membership_lock + - share_with_group_lock + - visibility_level + - request_access_enabled + - ldap_sync_status + - ldap_sync_error + - ldap_sync_last_update_at + - ldap_sync_last_successful_update_at + - ldap_sync_last_sync_at + - lfs_enabled + - parent_id + - shared_runners_minutes_limit + - repository_size_limit + - require_two_factor_authentication + - two_factor_grace_period + - plan_id + - project_creation_level + members: + whitelist: + - id + - access_level + - source_id + - source_type + - user_id + - notification_level + - type + - created_by_id + - invite_email + - invite_accepted_at + - requested_at + - expires_at + - ldap + - override + notification_settings: + whitelist: + - id + - user_id + - source_id + - source_type + - level + - created_at + - updated_at + - new_note + - new_issue + - reopen_issue + - close_issue + - reassign_issue + - new_merge_request + - reopen_merge_request + - close_merge_request + - reassign_merge_request + - merge_merge_request + - failed_pipeline + - success_pipeline + project_authorizations: + whitelist: + - user_id + - project_id + - access_level + project_auto_devops: + whitelist: + - id + - project_id + - created_at + - updated_at + - enabled + project_custom_attributes: + whitelist: + - id + - created_at + - updated_at + - project_id + - key + - value + project_features: + whitelist: + - id + - project_id + - merge_requests_access_level + - issues_access_level + - wiki_access_level + - snippets_access_level + - builds_access_level + - created_at + - updated_at + - repository_access_level + project_group_links: + whitelist: + - id + - project_id + - group_id + - created_at + - updated_at + - group_access + - expires_at + project_import_data: + whitelist: + - id + - project_id + project_mirror_data: + whitelist: + - id + - project_id + - retry_count + - last_update_started_at + - last_update_scheduled_at + - next_execution_timestamp + project_repository_states: + whitelist: + - id + - project_id + - repository_verification_checksum + - wiki_verification_checksum + - last_repository_verification_failure + - last_wiki_verification_failure + project_statistics: + whitelist: + - id + - project_id + - namespace_id + - commit_count + - storage_size + - repository_size + - lfs_objects_size + - build_artifacts_size + - shared_runners_seconds + - shared_runners_seconds_last_reset + projects: + whitelist: + - id + - name + - path + - description + - created_at + - updated_at + - creator_id + - namespace_id + - last_activity_at + - import_url + - visibility_level + - archived + - avatar + - merge_requests_template + - star_count + - merge_requests_rebase_enabled + - import_type + - import_source + - approvals_before_merge + - reset_approvals_on_push + - merge_requests_ff_only_enabled + - issues_template + - mirror + - mirror_user_id + - shared_runners_enabled + - build_coverage_regex + - build_allow_git_fetch + - build_timeout + - mirror_trigger_builds + - pending_delete + - public_builds + - last_repository_check_failed + - last_repository_check_at + - container_registry_enabled + - only_allow_merge_if_pipeline_succeeds + - has_external_issue_tracker + - repository_storage + - repository_read_only + - request_access_enabled + - has_external_wiki + - ci_config_path + - lfs_enabled + - only_allow_merge_if_all_discussions_are_resolved + - repository_size_limit + - printing_merge_request_link_enabled + - auto_cancel_pending_pipelines + - service_desk_enabled + - delete_error + - last_repository_updated_at + - disable_overriding_approvers_per_merge_request + - storage_version + - resolve_outdated_diff_discussions + - remote_mirror_available_overridden + - only_mirror_protected_branches + - pull_mirror_available_overridden + - mirror_overwrites_diverged_branches + - external_authorization_classification_label + subscriptions: + whitelist: + - id + - user_id + - subscribable_id + - subscribable_type + - subscribed + - created_at + - updated_at + - project_id + users: + whitelist: + - id + - remember_created_at + - sign_in_count + - current_sign_in_at + - last_sign_in_at + - current_sign_in_ip + - last_sign_in_ip + - created_at + - updated_at + - admin + - projects_limit + - failed_attempts + - locked_at + - can_create_group + - can_create_team + - state + - color_scheme_id + - password_expires_at + - created_by_id + - last_credential_check_at + - avatar + - confirmed_at + - confirmation_sent_at + - unconfirmed_email + - hide_no_ssh_key + - website_url + - admin_email_unsubscribed_at + - notification_email + - hide_no_password + - password_automatically_set + - location + - public_email + - dashboard + - project_view + - consumed_timestep + - layout + - hide_project_limit + - note + - otp_grace_period_started_at + - external + - organization + - auditor + - require_two_factor_authentication_from_group + - two_factor_grace_period + - ghost + - last_activity_on + - notified_of_own_activity + - bot_type + - preferred_language + - theme_id + diff --git a/config/settings.rb b/config/settings.rb index 6df2132332c..da459afcce2 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -62,6 +62,31 @@ class Settings < Settingslogic (base_url(gitlab) + [gitlab.relative_url_root]).join('') end + def kerberos_protocol + kerberos.https ? "https" : "http" + end + + def kerberos_port + kerberos.use_dedicated_port ? kerberos.port : gitlab.port + end + + # Curl expects username/password for authentication. However when using GSS-Negotiate not credentials should be needed. + # By inserting in the Kerberos dedicated URL ":@", we give to curl an empty username and password and GSS auth goes ahead + # Known bug reported in http://sourceforge.net/p/curl/bugs/440/ and http://curl.haxx.se/docs/knownbugs.html + def build_gitlab_kerberos_url + [ + kerberos_protocol, + "://:@", + gitlab.host, + ":#{kerberos_port}", + gitlab.relative_url_root + ].join('') + end + + def alternative_gitlab_kerberos_url? + kerberos.enabled && (build_gitlab_kerberos_url != build_gitlab_url) + end + # check that values in `current` (string or integer) is a contant in `modul`. def verify_constant_array(modul, current, default) values = default || [] diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 3a7ca517d56..102656b01ec 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -4,21 +4,24 @@ description: 'Learn how to administer GitLab Pages.' # GitLab Pages administration -> **Notes:** -> > - [Introduced][ee-80] in GitLab EE 8.3. > - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. -> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. -> - This guide is for Omnibus GitLab installations. If you have installed -> GitLab from source, follow the [Pages source installation document](source.md). -> - To learn how to use GitLab Pages, read the [user documentation][pages-userguide]. -> - Support for subgroup project's websites was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8. - -This document describes how to set up the _latest_ GitLab Pages feature. Make -sure to read the [changelog](#changelog) if you are upgrading to a new GitLab +> - GitLab Pages [was ported][ce-14605] to Community Edition in GitLab 8.17. +> - Support for subgroup project's websites was +> [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8. + +GitLab Pages allows for hosting of static sites. It must be configured by an +administrator. Separate [user documentation][pages-userguide] is available. + +Read the [changelog](#changelog) if you are upgrading to a new GitLab version as it may include new features and changes needed to be made in your configuration. +NOTE: **Note:** +This guide is for Omnibus GitLab installations. If you have installed +GitLab from source, see +[GitLab Pages administration for source installations](source.md). + ## Overview GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server diff --git a/doc/api/projects.md b/doc/api/projects.md index 6468d73e0af..781192fb92e 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -708,11 +708,17 @@ POST /projects | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | -| `issues_enabled` | boolean | no | Enable issues for this project | -| `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `jobs_enabled` | boolean | no | Enable jobs for this project | -| `wiki_enabled` | boolean | no | Enable wiki for this project | -| `snippets_enabled` | boolean | no | Enable snippets for this project | +| `issues_enabled` | boolean | no | (deprecated) Enable issues for this project. Use `issues_access_level` instead | +| `merge_requests_enabled` | boolean | no | (deprecated) Enable merge requests for this project. Use `merge_requests_access_level` instead | +| `jobs_enabled` | boolean | no | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | +| `wiki_enabled` | boolean | no | (deprecated) Enable wiki for this project. Use `wiki_access_level` instead | +| `snippets_enabled` | boolean | no | (deprecated) Enable snippets for this project. Use `snippets_access_level` instead | +| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -721,13 +727,19 @@ POST /projects | `public_builds` | boolean | no | If `true`, jobs can be viewed by non-project-members | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | -| `merge_method` | string | no | Set the merge method used | +| `merge_method` | string | no | Set the [merge method](#project-merge-method) used | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | +| `build_git_strategy` | string | no | The Git strategy. Defaults to `fetch` | +| `build_timeout` | integer | no | The maximum amount of time in minutes that a job is able run (in seconds) | +| `auto_cancel_pending_pipelines` | string | no | Auto-cancel pending pipelines (Note: this is not a boolean, but enabled/disabled | +| `build_coverage_regex` | string | no | Test coverage parsing | | `ci_config_path` | string | no | The path to CI config file | +| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project | +| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) | | `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins | | `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default | | `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project | @@ -753,11 +765,17 @@ POST /projects/user/:user_id | `path` | string | no | Custom repository name for new project. By default generated based on name | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `description` | string | no | Short project description | -| `issues_enabled` | boolean | no | Enable issues for this project | -| `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `jobs_enabled` | boolean | no | Enable jobs for this project | -| `wiki_enabled` | boolean | no | Enable wiki for this project | -| `snippets_enabled` | boolean | no | Enable snippets for this project | +| `issues_enabled` | boolean | no | (deprecated) Enable issues for this project. Use `issues_access_level` instead | +| `merge_requests_enabled` | boolean | no | (deprecated) Enable merge requests for this project. Use `merge_requests_access_level` instead | +| `jobs_enabled` | boolean | no | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | +| `wiki_enabled` | boolean | no | (deprecated) Enable wiki for this project. Use `wiki_access_level` instead | +| `snippets_enabled` | boolean | no | (deprecated) Enable snippets for this project. Use `snippets_access_level` instead | +| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -766,13 +784,19 @@ POST /projects/user/:user_id | `public_builds` | boolean | no | If `true`, jobs can be viewed by non-project-members | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | -| `merge_method` | string | no | Set the merge method used | +| `merge_method` | string | no | Set the [merge method](#project-merge-method) used | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | +| `build_git_strategy` | string | no | The Git strategy. Defaults to `fetch` | +| `build_timeout` | integer | no | The maximum amount of time in minutes that a job is able run (in seconds) | +| `auto_cancel_pending_pipelines` | string | no | Auto-cancel pending pipelines (Note: this is not a boolean, but enabled/disabled | +| `build_coverage_regex` | string | no | Test coverage parsing | | `ci_config_path` | string | no | The path to CI config file | +| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project | +| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) | | `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins | | `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default | | `external_authorization_classification_label` | string | no | **(CORE ONLY)** The classification label for the project | @@ -798,11 +822,17 @@ PUT /projects/:id | `path` | string | no | Custom repository name for the project. By default generated based on name | | `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | -| `issues_enabled` | boolean | no | Enable issues for this project | -| `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `jobs_enabled` | boolean | no | Enable jobs for this project | -| `wiki_enabled` | boolean | no | Enable wiki for this project | -| `snippets_enabled` | boolean | no | Enable snippets for this project | +| `issues_enabled` | boolean | no | (deprecated) Enable issues for this project. Use `issues_access_level` instead | +| `merge_requests_enabled` | boolean | no | (deprecated) Enable merge requests for this project. Use `merge_requests_access_level` instead | +| `jobs_enabled` | boolean | no | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | +| `wiki_enabled` | boolean | no | (deprecated) Enable wiki for this project. Use `wiki_access_level` instead | +| `snippets_enabled` | boolean | no | (deprecated) Enable snippets for this project. Use `snippets_access_level` instead | +| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -811,13 +841,19 @@ PUT /projects/:id | `public_builds` | boolean | no | If `true`, jobs can be viewed by non-project-members | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | -| `merge_method` | string | no | Set the merge method used | +| `merge_method` | string | no | Set the [merge method](#project-merge-method) used | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | +| `build_git_strategy` | string | no | The Git strategy. Defaults to `fetch` | +| `build_timeout` | integer | no | The maximum amount of time in minutes that a job is able run (in seconds) | +| `auto_cancel_pending_pipelines` | string | no | Auto-cancel pending pipelines (Note: this is not a boolean, but enabled/disabled | +| `build_coverage_regex` | string | no | Test coverage parsing | | `ci_config_path` | string | no | The path to CI config file | | `ci_default_git_depth` | integer | no | Default number of revisions for [shallow cloning](../user/project/pipelines/settings.md#git-shallow-clone) | +| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project | +| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) | | `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins | | `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge request by default | | `external_authorization_classification_label` | string | no | **(CORE ONLY)** The classification label for the project | diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 418e58b22d5..cbdc0a3a174 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -18,7 +18,7 @@ In addition to this page, the following resources to help craft and contribute d ## Source files and rendered web locations -Documentation for GitLab Community Edition (CE) and Enterprise Edition (EE), along with GitLab Runner and Omnibus, is published to [docs.gitlab.com](https://docs.gitlab.com). The documentation for CE and EE is also published within the application at `/help` on the domain of the GitLab instance. +Documentation for GitLab Community Edition (CE) and Enterprise Edition (EE), along with GitLab Runner and Omnibus, is published to [docs.gitlab.com](https://docs.gitlab.com). The documentation for CE and EE is also published within the application at `/help` on the domain of the GitLab instance, though there are [plans](https://gitlab.com/groups/gitlab-org/-/epics/693) to end this practice and instead link out from the GitLab application to docs.gitlab.com URLs. At `/help`, only content for your current edition and version is included, whereas multiple versions' content is available at docs.gitlab.com. @@ -274,8 +274,11 @@ Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.m ## GitLab `/help` -Every GitLab instance includes the documentation, which is available from `/help` -(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>. +Every GitLab instance includes the documentation, which is available at `/help` +(`https://gitlab.example.com/help`). For example, <https://gitlab.com/help>. + +There are [plans](https://gitlab.com/groups/gitlab-org/-/epics/693) to end this +practice and instead link out from the GitLab application to docs.gitlab.com URLs. The documentation available online on docs.gitlab.com is continuously deployed every hour from the `master` branch of CE, EE, Omnibus, and Runner. Therefore, diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index e2d51f673b5..503ad784a77 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -316,7 +316,7 @@ If a project's repository contains a `Dockerfile`, Auto Build will use If you are also using Auto Review Apps and Auto Deploy and choose to provide your own `Dockerfile`, make sure you expose your application to port `5000` as this is the port assumed by the -[default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app). +[default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app). Alternatively you can override the default values by [customizing the Auto Deploy helm chart](#custom-helm-chart) #### Auto Build using Heroku buildpacks @@ -452,7 +452,7 @@ be deleted. Review apps are deployed using the [auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with -Helm. The app will be deployed into the [Kubernetes +Helm, which can be [customized](#custom-helm-chart). The app will be deployed into the [Kubernetes namespace](../../user/project/clusters/index.md#deployment-variables) for the environment. @@ -514,7 +514,7 @@ Auto Deploy doesn't include deployments to staging or canary by default, but the enable them. You can make use of [environment variables](#environment-variables) to automatically -scale your pod replicas. +scale your pod replicas and to apply custom arguments to the Auto DevOps `helm upgrade` commands. This is an easy way to [customize the Auto Deploy helm chart](#custom-helm-chart). Apps are deployed using the [auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with @@ -655,6 +655,9 @@ repo or by specifying a project variable: - **Project variable** - Create a [project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables) `AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart. +You can also make use of the `HELM_UPGRADE_EXTRA_ARGS` environment variable to override the default values in the `values.yaml` file in the [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app). +To apply your own `values.yaml` file to all Helm upgrade commands in Auto Deploy set `HELM_UPGRADE_EXTRA_ARGS` to `--values my-values.yaml`. + ### Custom Helm chart per environment **(PREMIUM)** You can specify the use of a custom Helm chart per environment by scoping the environment variable @@ -761,7 +764,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. | | `ROLLOUT_RESOURCE_TYPE` | From GitLab 11.9, this variable allows specification of the resource type being deployed when using a custom helm chart. Default value is `deployment`. | | `ROLLOUT_STATUS_DISABLED` | From GitLab 12.0, this variable allows to disable rollout status check because it doesn't support all resource types, for example, `cronjob`. | -| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. | +| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. **Tip:** you can use this variable to [customize the Auto Deploy helm chart](https://docs.gitlab.com/ee/topics/autodevops/index.html#custom-helm-chart) by applying custom override values with `--values my-values.yaml`. | TIP: **Tip:** Set up the replica variables using a diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index f2156720af7..c6bc580fb8f 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -444,7 +444,7 @@ Clicking on the **Reply to comment** button will bring the reply area into focus ![Reply to comment feature](img/reply_to_comment.gif) -Relying to a non-discussion comment will convert the non-discussion comment to a +Replying to a non-discussion comment will convert the non-discussion comment to a threaded discussion once the reply is submitted. This conversion is considered an edit to the original comment, so a note about when it was last edited will appear underneath it. diff --git a/jest.config.js b/jest.config.js index 84481642250..986b8465eef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,9 +15,18 @@ if (process.env.CI) { ]); } +let testMatch = ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js']; + +// workaround for eslint-import-resolver-jest only resolving in test files +// see https://github.com/JoinColony/eslint-import-resolver-jest#note +const isESLint = module.parent.path.includes('/eslint-import-resolver-jest/'); +if (isESLint) { + testMatch = testMatch.map(path => path.replace('_spec.js', '')); +} + // eslint-disable-next-line import/no-commonjs module.exports = { - testMatch: ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js'], + testMatch, moduleFileExtensions: ['js', 'json', 'vue'], moduleNameMapper: { '^~(/.*)$': '<rootDir>/app/assets/javascripts$1', diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b9aa387ba61..765819e6bf1 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -201,6 +201,7 @@ module API # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 projects_relation.preload(:project_feature, :route) .preload(:import_state, :tags) + .preload(:auto_devops) .preload(namespace: [:route, :owner]) end # rubocop: enable CodeReuse/ActiveRecord @@ -247,12 +248,20 @@ module API expose :container_registry_enabled # Expose old field names with the new permissions methods to keep API compatible + # TODO: remove in API v5, replaced by *_access_level expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) } + expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) } + expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) } + expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) } + expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) } + expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } + expose :shared_runners_enabled expose :lfs_enabled?, as: :lfs_enabled expose :creator_id @@ -267,6 +276,12 @@ module API expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :ci_default_git_depth expose :public_builds, as: :public_jobs + expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options| + project.build_allow_git_fetch ? 'fetch' : 'clone' + end + expose :build_timeout + expose :auto_cancel_pending_pipelines + expose :build_coverage_regex expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links, options) @@ -280,6 +295,10 @@ module API options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) } expose :external_authorization_classification_label + expose :auto_devops_enabled?, as: :auto_devops_enabled + expose :auto_devops_deploy_strategy do |project, options| + project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy + end # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) @@ -289,6 +308,7 @@ module API # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 super(projects_relation).preload(:group) .preload(:ci_cd_settings) + .preload(:auto_devops) .preload(project_group_links: { group: :route }, fork_network: :root_project, fork_network_member: :forked_from_project, @@ -491,7 +511,7 @@ module API end end - class ProjectEntity < Grape::Entity + class IssuableEntity < Grape::Entity expose :id, :iid expose(:project_id) { |entity| entity&.project.try(:id) } expose :title, :description @@ -544,7 +564,7 @@ module API end end - class IssueBasic < ProjectEntity + class IssueBasic < IssuableEntity expose :closed_at expose :closed_by, using: Entities::UserBasic @@ -650,14 +670,14 @@ module API end end - class MergeRequestSimple < ProjectEntity + class MergeRequestSimple < IssuableEntity expose :title expose :web_url do |merge_request, options| Gitlab::UrlBuilder.build(merge_request) end end - class MergeRequestBasic < ProjectEntity + class MergeRequestBasic < IssuableEntity expose :merged_by, using: Entities::UserBasic do |merge_request, _options| merge_request.metrics&.merged_by end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index f242f1fea0e..0e21a7a66fd 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -8,12 +8,26 @@ module API params :optional_project_params_ce do optional :description, type: String, desc: 'The description of the project' + optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`' + optional :build_timeout, type: Integer, desc: 'Build timeout' + optional :auto_cancel_pending_pipelines, type: String, values: %w(disabled enabled), desc: 'Auto-cancel pending pipelines' + optional :build_coverage_regex, type: String, desc: 'Test coverage parsing' optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`' + + # TODO: remove in API v5, replaced by *_access_level optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled' optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' + + optional :issues_access_level, type: String, values: %w(disabled private enabled), desc: 'Issues access level. One of `disabled`, `private` or `enabled`' + optional :repository_access_level, type: String, values: %w(disabled private enabled), desc: 'Repository access level. One of `disabled`, `private` or `enabled`' + optional :merge_requests_access_level, type: String, values: %w(disabled private enabled), desc: 'Merge requests access level. One of `disabled`, `private` or `enabled`' + optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`' + optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`' + optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`' + optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' @@ -30,6 +44,8 @@ module API optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md" optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project' optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning' + optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled' + optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy' end params :optional_project_params_ee do @@ -48,15 +64,20 @@ module API def self.update_params_at_least_one_of [ - :jobs_enabled, - :resolve_outdated_diff_discussions, + :auto_devops_enabled, + :auto_devops_deploy_strategy, + :auto_cancel_pending_pipelines, + :build_coverage_regex, + :build_git_strategy, + :build_timeout, + :builds_access_level, :ci_config_path, :container_registry_enabled, :default_branch, :description, - :issues_enabled, + :issues_access_level, :lfs_enabled, - :merge_requests_enabled, + :merge_requests_access_level, :merge_method, :name, :only_allow_merge_if_all_discussions_are_resolved, @@ -64,14 +85,24 @@ module API :path, :printing_merge_request_link_enabled, :public_builds, + :repository_access_level, :request_access_enabled, + :resolve_outdated_diff_discussions, :shared_runners_enabled, - :snippets_enabled, + :snippets_access_level, :tag_list, :visibility, - :wiki_enabled, + :wiki_access_level, :avatar, - :external_authorization_classification_label + :external_authorization_classification_label, + + # TODO: remove in API v5, replaced by *_access_level + :issues_enabled, + :jobs_enabled, + :merge_requests_enabled, + :wiki_enabled, + :jobs_enabled, + :snippets_enabled ] end end diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index e7504051808..21d4928193e 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -28,7 +28,7 @@ module API desc 'Import a GitHub project' do detail 'This feature was introduced in GitLab 11.3.4.' - success Entities::ProjectEntity + success ::ProjectEntity end params do requires :personal_access_token, type: String, desc: 'GitHub personal access token' diff --git a/lib/banzai/filter/inline_embeds_filter.rb b/lib/banzai/filter/inline_embeds_filter.rb new file mode 100644 index 00000000000..97394fd8f82 --- /dev/null +++ b/lib/banzai/filter/inline_embeds_filter.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML filter that inserts a node for each occurence of + # a given link format. To transform references to DB + # resources in place, prefer to inherit from AbstractReferenceFilter. + class InlineEmbedsFilter < HTML::Pipeline::Filter + # Find every relevant link, create a new node based on + # the link, and insert this node after any html content + # surrounding the link. + def call + return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project]) + + doc.xpath(xpath_search).each do |node| + next unless element = element_to_embed(node) + + # We want this to follow any surrounding content. For example, + # if a link is inline in a paragraph. + node.parent.children.last.add_next_sibling(element) + end + + doc + end + + # Implement in child class. + # + # Return a Nokogiri::XML::Element to embed in the + # markdown. + def create_element(params) + end + + # Implement in child class unless overriding #embed_params + # + # Returns the regex pattern used to filter + # to only matching urls. + def link_pattern + end + + # Returns the xpath query string used to select nodes + # from the html document on which the embed is based. + # + # Override to select nodes other than links. + def xpath_search + 'descendant-or-self::a[@href]' + end + + # Creates a new element based on the parameters + # obtained from the target link + def element_to_embed(node) + return unless params = embed_params(node) + + create_element(params) + end + + # Returns a hash of named parameters based on the + # provided regex with string keys. + # + # Override to select nodes other than links. + def embed_params(node) + url = node['href'] + + link_pattern.match(url) { |m| m.named_captures } + end + end + end +end diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb new file mode 100644 index 00000000000..0120cc37d6f --- /dev/null +++ b/lib/banzai/filter/inline_metrics_filter.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML filter that inserts a placeholder element for each + # reference to a metrics dashboard. + class InlineMetricsFilter < Banzai::Filter::InlineEmbedsFilter + # Placeholder element for the frontend to use as an + # injection point for charts. + def create_element(params) + doc.document.create_element( + 'div', + class: 'js-render-metrics', + 'data-dashboard-url': metrics_dashboard_url(params) + ) + end + + # Endpoint FE should hit to collect the appropriate + # chart information + def metrics_dashboard_url(params) + Gitlab::Metrics::Dashboard::Url.build_dashboard_url( + params['namespace'], + params['project'], + params['environment'], + embedded: true + ) + end + + # Search params for selecting metrics links. A few + # simple checks is enough to boost performance without + # the cost of doing a full regex match. + def xpath_search + "descendant-or-self::a[contains(@href,'metrics') and \ + starts-with(@href, '#{Gitlab.config.gitlab.url}')]" + end + + # Regular expression matching metrics urls + def link_pattern + Gitlab::Metrics::Dashboard::Url.regex + end + end + end +end diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb new file mode 100644 index 00000000000..ff91be2cbb7 --- /dev/null +++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML filter that removes embeded elements that the current user does + # not have permission to view. + class InlineMetricsRedactorFilter < HTML::Pipeline::Filter + include Gitlab::Utils::StrongMemoize + + METRICS_CSS_CLASS = '.js-render-metrics' + + # Finds all embeds based on the css class the FE + # uses to identify the embedded content, removing + # only unnecessary nodes. + def call + return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project]) + + nodes.each do |node| + path = paths_by_node[node] + user_has_access = user_access_by_path[path] + + node.remove unless user_has_access + end + + doc + end + + private + + def user + context[:current_user] + end + + # Returns all nodes which the FE will identify as + # a metrics dashboard placeholder element + # + # @return [Nokogiri::XML::NodeSet] + def nodes + @nodes ||= doc.css(METRICS_CSS_CLASS) + end + + # Maps a node to the full path of a project. + # Memoized so we only need to run the regex to get + # the project full path from the url once per node. + # + # @return [Hash<Nokogiri::XML::Node, String>] + def paths_by_node + strong_memoize(:paths_by_node) do + nodes.each_with_object({}) do |node, paths| + paths[node] = path_for_node(node) + end + end + end + + # Gets a project's full_path from the dashboard url + # in the placeholder node. The FE will use the attr + # `data-dashboard-url`, so we want to check against that + # attribute directly in case a user has manually + # created a metrics element (rather than supporting + # an alternate attr in InlineMetricsFilter). + # + # @return [String] + def path_for_node(node) + url = node.attribute('data-dashboard-url').to_s + + Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m| + "#{$~[:namespace]}/#{$~[:project]}" + end + end + + # Maps a project's full path to a Project object. + # Contains all of the Projects referenced in the + # metrics placeholder elements of the current document + # + # @return [Hash<String, Project>] + def projects_by_path + strong_memoize(:projects_by_path) do + Project.eager_load(:route, namespace: [:route]) + .where_full_path_in(paths_by_node.values.uniq) + .index_by(&:full_path) + end + end + + # Returns a mapping representing whether the current user + # has permission to view the metrics for the project. + # Determined in a batch + # + # @return [Hash<Project, Boolean>] + def user_access_by_path + strong_memoize(:user_access_by_path) do + projects_by_path.each_with_object({}) do |(path, project), access| + access[path] = Ability.allowed?(user, :read_environment, project) + end + end + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index d67f461be57..2c1006f708a 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -25,6 +25,7 @@ module Banzai Filter::VideoLinkFilter, Filter::ImageLazyLoadFilter, Filter::ImageLinkFilter, + Filter::InlineMetricsFilter, Filter::TableOfContentsFilter, Filter::AutolinkFilter, Filter::ExternalLinkFilter, diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb index 7eaad6d7560..5c199453638 100644 --- a/lib/banzai/pipeline/post_process_pipeline.rb +++ b/lib/banzai/pipeline/post_process_pipeline.rb @@ -13,6 +13,7 @@ module Banzai def self.internal_link_filters [ Filter::RedactorFilter, + Filter::InlineMetricsRedactorFilter, Filter::RelativeLinkFilter, Filter::IssuableStateFilter, Filter::SuggestionFilter diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml index d1a34c515fa..5ad624bb15f 100644 --- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml @@ -23,6 +23,7 @@ container_scanning: DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/ # https://hub.docker.com/r/arminc/clair-local-scan/tags CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1 + CLAIR_EXECUTABLE_VERSION: v11 ## Disable the proxy for clair-local-scan, otherwise Container Scanning will ## fail when a proxy is used. NO_PROXY: ${DOCKER_SERVICE},localhost @@ -41,7 +42,7 @@ container_scanning: - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION} - apk add -U wget ca-certificates - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} - - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 + - wget https://github.com/arminc/clair-scanner/releases/download/${CLAIR_EXECUTABLE_VERSION}/clair-scanner_linux_amd64 - mv clair-scanner_linux_amd64 clair-scanner - chmod +x clair-scanner - touch clair-whitelist.yml diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index 8dd9775c583..f176771775e 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -40,6 +40,7 @@ dependency_scanning: DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ DS_PULL_ANALYZER_IMAGE_TIMEOUT \ DS_RUN_ANALYZER_TIMEOUT \ + DS_PYTHON_VERSION \ ) \ --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ diff --git a/lib/gitlab/global_id.rb b/lib/gitlab/global_id.rb new file mode 100644 index 00000000000..cc82b6c5897 --- /dev/null +++ b/lib/gitlab/global_id.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module GlobalId + def self.build(object = nil, model_name: nil, id: nil, params: nil) + if object + model_name ||= object.class.name + id ||= object.id + end + + ::URI::GID.build(app: GlobalID.app, model_name: model_name, model_id: id, params: params) + end + end +end diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb new file mode 100644 index 00000000000..b197e7ca86b --- /dev/null +++ b/lib/gitlab/metrics/dashboard/url.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Manages url matching for metrics dashboards. +module Gitlab + module Metrics + module Dashboard + class Url + class << self + # Matches urls for a metrics dashboard. This could be + # either the /metrics endpoint or the /metrics_dashboard + # endpoint. + # + # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics + def regex + %r{ + (?<url> + #{Regexp.escape(Gitlab.config.gitlab.url)} + \/#{Project.reference_pattern} + (?:\/\-)? + \/environments + \/(?<environment>\d+) + \/metrics + (?<query> + \?[a-z0-9_=-]+ + (&[a-z0-9_=-]+)* + )? + (?<anchor>\#[a-z0-9_-]+)? + ) + }x + end + + # Builds a metrics dashboard url based on the passed in arguments + def build_dashboard_url(*args) + Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args) + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb index 9af7e0afed4..355f938704e 100644 --- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb +++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb @@ -54,7 +54,16 @@ module Gitlab end def unicorn_workers_count - `pgrep -f '[u]nicorn_rails worker.+ #{Rails.root.to_s}'`.split.count + http_servers.sum(&:worker_processes) # rubocop: disable CodeReuse/ActiveRecord + end + + # Traversal of ObjectSpace is expensive, on fully loaded application + # it takes around 80ms. The instances of HttpServers are not a subject + # to change so we can cache the list of servers. + def http_servers + return [] unless defined?(::Unicorn::HttpServer) + + @http_servers ||= ObjectSpace.each_object(::Unicorn::HttpServer).to_a end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9b6e8d8c8a4..a4fee96753d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3263,6 +3263,9 @@ msgstr "" msgid "Create new..." msgstr "" +msgid "Create project" +msgstr "" + msgid "Create project label" msgstr "" @@ -6752,6 +6755,9 @@ msgstr "" msgid "New Pipeline Schedule" msgstr "" +msgid "New Project" +msgstr "" + msgid "New Snippet" msgstr "" @@ -8425,6 +8431,54 @@ msgstr "" msgid "ProjectsDropdown|This feature requires browser localStorage support" msgstr "" +msgid "ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository." +msgstr "" + +msgid "ProjectsNew|Blank" +msgstr "" + +msgid "ProjectsNew|Blank project" +msgstr "" + +msgid "ProjectsNew|Contact an administrator to enable options for importing your project." +msgstr "" + +msgid "ProjectsNew|Create from template" +msgstr "" + +msgid "ProjectsNew|Creating project & repository." +msgstr "" + +msgid "ProjectsNew|Description format" +msgstr "" + +msgid "ProjectsNew|Import" +msgstr "" + +msgid "ProjectsNew|Import project" +msgstr "" + +msgid "ProjectsNew|Initialize repository with a README" +msgstr "" + +msgid "ProjectsNew|No import options available" +msgstr "" + +msgid "ProjectsNew|Please wait a moment, this page will automatically refresh when ready." +msgstr "" + +msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}" +msgstr "" + +msgid "ProjectsNew|Template" +msgstr "" + +msgid "ProjectsNew|Visibility Level" +msgstr "" + +msgid "ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}" +msgstr "" + msgid "PrometheusService|%{exporters} with %{metrics} were found" msgstr "" diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index defd85a5740..0918445d119 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -18,7 +18,7 @@ module QA element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern - element :project_create_button, "submit 'Create project'" # rubocop:disable QA/ElementWithPattern + element :project_create_button, "submit _('Create project')" # rubocop:disable QA/ElementWithPattern element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index d50f618ff82..e78b9bece19 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -177,8 +177,8 @@ module QA ENV.fetch("GCLOUD_ACCOUNT_EMAIL") end - def gcloud_zone - ENV.fetch('GCLOUD_ZONE') + def gcloud_region + ENV.fetch('GCLOUD_REGION') end def has_gcloud_credentials? diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb index 16a89591eb2..40263e94065 100644 --- a/qa/qa/service/kubernetes_cluster.rb +++ b/qa/qa/service/kubernetes_cluster.rb @@ -28,17 +28,17 @@ module QA create #{cluster_name} #{auth_options} --enable-basic-auth - --zone #{Runtime::Env.gcloud_zone} + --region #{Runtime::Env.gcloud_region} && gcloud container clusters get-credentials - --zone #{Runtime::Env.gcloud_zone} + --region #{Runtime::Env.gcloud_region} #{cluster_name} CMD @api_url = `kubectl config view --minify -o jsonpath='{.clusters[].cluster.server}'` @admin_user = "#{cluster_name}-admin" - master_auth = JSON.parse(`gcloud container clusters describe #{cluster_name} --zone #{Runtime::Env.gcloud_zone} --format 'json(masterAuth.username, masterAuth.password)'`) + master_auth = JSON.parse(`gcloud container clusters describe #{cluster_name} --region #{Runtime::Env.gcloud_region} --format 'json(masterAuth.username, masterAuth.password)'`) shell <<~CMD.tr("\n", ' ') kubectl config set-credentials #{@admin_user} --username #{master_auth['masterAuth']['username']} @@ -66,10 +66,10 @@ module QA def remove! shell <<~CMD.tr("\n", ' ') gcloud container clusters delete - --zone #{Runtime::Env.gcloud_zone} - #{cluster_name} - --quiet --async - CMD + --region #{Runtime::Env.gcloud_region} + #{cluster_name} + --quiet --async + CMD end private diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb index a9de64e357a..2363836d5e3 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb @@ -7,15 +7,15 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - source_group = Resource::Group.fabricate! do |group| + source_group = Resource::Group.fabricate_via_api! do |group| group.path = 'source-group' end - target_group = Resource::Group.fabricate! do |group| + target_group = Resource::Group.fabricate_via_api! do |group| group.path = 'target-group' end - project = Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate_via_api! do |project| project.group = source_group project.name = 'transfer-project' project.initialize_with_readme = true diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index d1747280227..f51c16f472c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -9,7 +9,7 @@ module QA user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) - project = Resource::Project.fabricate! do |resource| + project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'add-member-project' end project.visit! diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb index 8fb2e37e269..c3310a4a132 100644 --- a/spec/features/dashboard/milestones_spec.rb +++ b/spec/features/dashboard/milestones_spec.rb @@ -29,5 +29,19 @@ describe 'Dashboard > Milestones' do expect(page).to have_content(milestone.title) expect(page).to have_content(group.name) end + + describe 'new milestones dropdown', :js do + it 'takes user to a new milestone page', :js do + find('.new-project-item-select-button').click + + page.within('.select2-results') do + first('.select2-result-label').click + end + + find('.new-project-item-link').click + + expect(current_path).to eq(new_group_milestone_path(group)) + end + end end end diff --git a/spec/frontend/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js index 42233f82d54..8c5bfd08beb 100644 --- a/spec/frontend/error_tracking_settings/mock.js +++ b/spec/frontend/error_tracking_settings/mock.js @@ -1,5 +1,5 @@ import createStore from '~/error_tracking_settings/store'; -import { TEST_HOST } from '../helpers/test_constants'; +import { TEST_HOST } from 'helpers/test_constants'; const defaultStore = createStore(); diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index 93b86b9b812..dec6b23d72a 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -143,6 +143,26 @@ describe GitlabSchema do end end + context 'for classes that are not ActiveRecord subclasses and have implemented .lazy_find' do + it 'returns the correct record' do + note = create(:discussion_note_on_merge_request) + + result = described_class.object_from_id(note.to_global_id) + + expect(result.__sync).to eq(note) + end + + it 'batchloads the queries' do + note1 = create(:discussion_note_on_merge_request) + note2 = create(:discussion_note_on_merge_request) + + expect do + [described_class.object_from_id(note1.to_global_id), + described_class.object_from_id(note2.to_global_id)].map(&:__sync) + end.not_to exceed_query_limit(1) + end + end + context 'for other classes' do # We cannot use an anonymous class here as `GlobalID` expects `.name` not # to return `nil` diff --git a/spec/graphql/types/diff_refs_type_spec.rb b/spec/graphql/types/diff_refs_type_spec.rb new file mode 100644 index 00000000000..91017c827ad --- /dev/null +++ b/spec/graphql/types/diff_refs_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['DiffRefs'] do + it { expect(described_class.graphql_name).to eq('DiffRefs') } + + it { expect(described_class).to have_graphql_fields(:base_sha, :head_sha, :start_sha) } +end diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index f73bd062369..59bd0123d88 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -13,7 +13,7 @@ describe GitlabSchema.types['MergeRequest'] do description_html state created_at updated_at source_project target_project project project_id source_project_id target_project_id source_branch target_branch work_in_progress merge_when_pipeline_succeeds diff_head_sha - merge_commit_sha user_notes_count should_remove_source_branch + merge_commit_sha user_notes_count should_remove_source_branch diff_refs force_remove_source_branch merge_status in_progress_merge_commit_sha merge_error allow_collaboration should_be_rebased rebase_commit_sha rebase_in_progress merge_commit_message default_merge_commit_message diff --git a/spec/graphql/types/notes/diff_position_type_spec.rb b/spec/graphql/types/notes/diff_position_type_spec.rb index 2f8724d7f0d..345bca8f702 100644 --- a/spec/graphql/types/notes/diff_position_type_spec.rb +++ b/spec/graphql/types/notes/diff_position_type_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe GitlabSchema.types['DiffPosition'] do it 'exposes the expected fields' do - expected_fields = [:head_sha, :base_sha, :start_sha, :file_path, :old_path, + expected_fields = [:diff_refs, :file_path, :old_path, :new_path, :position_type, :old_line, :new_line, :x, :y, :width, :height] diff --git a/spec/graphql/types/notes/discussion_type_spec.rb b/spec/graphql/types/notes/discussion_type_spec.rb index 2a1eb0efd35..ba7fc961212 100644 --- a/spec/graphql/types/notes/discussion_type_spec.rb +++ b/spec/graphql/types/notes/discussion_type_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe GitlabSchema.types['Discussion'] do - it { is_expected.to have_graphql_fields(:id, :created_at, :notes) } + it { is_expected.to have_graphql_fields(:id, :created_at, :notes, :reply_id) } it { is_expected.to require_graphql_authorizations(:read_note) } end diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb new file mode 100644 index 00000000000..772c94e3180 --- /dev/null +++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::InlineMetricsFilter do + include FilterSpecHelper + + let(:input) { %(<a href="#{url}">example</a>) } + let(:doc) { filter(input) } + + context 'when the document has an external link' do + let(:url) { 'https://foo.com' } + + it 'leaves regular non-metrics links unchanged' do + expect(doc.to_s).to eq input + end + end + + context 'when the document has a metrics dashboard link' do + let(:params) { ['foo', 'bar', 12] } + let(:url) { urls.metrics_namespace_project_environment_url(*params) } + + it 'leaves the original link unchanged' do + expect(doc.at_css('a').to_s).to eq input + end + + it 'appends a metrics charts placeholder with dashboard url after metrics links' do + node = doc.at_css('.js-render-metrics') + expect(node).to be_present + + dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params, embedded: true) + expect(node.attribute('data-dashboard-url').to_s).to eq dashboard_url + end + + context 'when the metrics dashboard link is part of a paragraph' do + let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) } + let(:input) { %(<p>#{paragraph}</p>) } + + it 'appends the charts placeholder after the enclosing paragraph' do + expect(doc.at_css('p').to_s).to include paragraph + expect(doc.at_css('.js-render-metrics')).to be_present + end + + context 'when the feature is disabled' do + before do + stub_feature_flags(gfm_embedded_metrics: false) + end + + it 'does nothing' do + expect(doc.to_s).to eq input + end + end + end + end +end diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb new file mode 100644 index 00000000000..fb2186e9d12 --- /dev/null +++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::InlineMetricsRedactorFilter do + include FilterSpecHelper + + set(:project) { create(:project) } + + let(:url) { urls.metrics_dashboard_project_environment_url(project, 1, embedded: true) } + let(:input) { %(<a href="#{url}">example</a>) } + let(:doc) { filter(input) } + + context 'when the feature is disabled' do + before do + stub_feature_flags(gfm_embedded_metrics: false) + end + + it 'does nothing' do + expect(doc.to_s).to eq input + end + end + + context 'without a metrics charts placeholder' do + it 'leaves regular non-metrics links unchanged' do + expect(doc.to_s).to eq input + end + end + + context 'with a metrics charts placeholder' do + let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) } + + context 'no user is logged in' do + it 'redacts the placeholder' do + expect(doc.to_s).to be_empty + end + end + + context 'the user does not have permission do see charts' do + let(:doc) { filter(input, current_user: build(:user)) } + + it 'redacts the placeholder' do + expect(doc.to_s).to be_empty + end + end + + context 'the user has requisite permissions' do + let(:user) { create(:user) } + let(:doc) { filter(input, current_user: user) } + + it 'leaves the placeholder' do + project.add_maintainer(user) + + expect(doc.to_s).to eq input + end + end + end +end diff --git a/spec/lib/gitlab/global_id_spec.rb b/spec/lib/gitlab/global_id_spec.rb new file mode 100644 index 00000000000..d35b5da0b75 --- /dev/null +++ b/spec/lib/gitlab/global_id_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::GlobalId do + describe '.build' do + set(:object) { create(:issue) } + + it 'returns a standard GlobalId if only object is passed' do + expect(described_class.build(object).to_s).to eq(object.to_global_id.to_s) + end + + it 'returns a GlobalId from params' do + expect(described_class.build(model_name: 'MyModel', id: 'myid').to_s).to eq( + 'gid://gitlab/MyModel/myid' + ) + end + + it 'returns a GlobalId from object and `id` param' do + expect(described_class.build(object, id: 'myid').to_s).to eq( + 'gid://gitlab/Issue/myid' + ) + end + + it 'returns a GlobalId from object and `model_name` param' do + expect(described_class.build(object, model_name: 'MyModel').to_s).to eq( + "gid://gitlab/MyModel/#{object.id}" + ) + end + + it 'returns an error if model_name and id are not able to be determined' do + expect { described_class.build(id: 'myid') }.to raise_error(URI::InvalidComponentError) + expect { described_class.build(model_name: 'MyModel') }.to raise_error(URI::InvalidComponentError) + expect { described_class.build }.to raise_error(URI::InvalidComponentError) + end + end +end diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb new file mode 100644 index 00000000000..34bc6359414 --- /dev/null +++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Dashboard::Url do + describe '#regex' do + it 'returns a regular expression' do + expect(described_class.regex).to be_a Regexp + end + + it 'matches a metrics dashboard link with named params' do + url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url('foo', 'bar', 1, start: 123345456, anchor: 'title') + + expected_params = { + 'url' => url, + 'namespace' => 'foo', + 'project' => 'bar', + 'environment' => '1', + 'query' => '?start=123345456', + 'anchor' => '#title' + } + + expect(described_class.regex).to match url + + described_class.regex.match(url) do |m| + expect(m.named_captures).to eq expected_params + end + end + + it 'does not match other gitlab urls that contain the term metrics' do + url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json) + + expect(described_class.regex).not_to match url + end + + it 'does not match other gitlab urls' do + url = Gitlab.config.gitlab.url + + expect(described_class.regex).not_to match url + end + + it 'does not match non-gitlab urls' do + url = 'https://www.super_awesome_site.com/' + + expect(described_class.regex).not_to match url + end + end + + describe '#build_dashboard_url' do + it 'builds the url for the dashboard endpoint' do + url = described_class.build_dashboard_url('foo', 'bar', 1) + + expect(url).to match described_class.regex + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb index 090e456644f..4b697b2ba0f 100644 --- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do subject { described_class.new(1.second) } describe '#sample' do - let(:unicorn) { double('unicorn') } + let(:unicorn) { Module.new } let(:raindrops) { double('raindrops') } let(:stats) { double('stats') } @@ -78,19 +78,32 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do end end - context 'additional metrics' do - let(:unicorn_workers) { 2 } - + context 'unicorn workers' do before do - allow(unicorn).to receive(:listener_names).and_return([""]) - allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14) - allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers) + allow(unicorn).to receive(:listener_names).and_return([]) end - it "sets additional metrics" do - expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers) + context 'without http server' do + it "does set unicorn_workers to 0" do + expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 0) - subject.sample + subject.sample + end + end + + context 'with http server' do + let(:http_server_class) { Struct.new(:worker_processes) } + let!(:http_server) { http_server_class.new(5) } + + before do + stub_const('Unicorn::HttpServer', http_server_class) + end + + it "sets additional metrics" do + expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 5) + + subject.sample + end end end end diff --git a/spec/models/concerns/project_api_compatibility_spec.rb b/spec/models/concerns/project_api_compatibility_spec.rb new file mode 100644 index 00000000000..8cecd4fe7bc --- /dev/null +++ b/spec/models/concerns/project_api_compatibility_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectAPICompatibility do + let(:project) { create(:project) } + + # git_strategy + it "converts build_git_strategy=fetch to build_allow_git_fetch=true" do + project.update!(build_git_strategy: 'fetch') + expect(project.build_allow_git_fetch).to eq(true) + end + + it "converts build_git_strategy=clone to build_allow_git_fetch=false" do + project.update!(build_git_strategy: 'clone') + expect(project.build_allow_git_fetch).to eq(false) + end + + # auto_devops_enabled + it "converts auto_devops_enabled=false to auto_devops_enabled?=false" do + expect(project.auto_devops_enabled?).to eq(true) + project.update!(auto_devops_enabled: false) + expect(project.auto_devops_enabled?).to eq(false) + end + + it "converts auto_devops_enabled=true to auto_devops_enabled?=true" do + expect(project.auto_devops_enabled?).to eq(true) + project.update!(auto_devops_enabled: true) + expect(project.auto_devops_enabled?).to eq(true) + end + + # auto_devops_deploy_strategy + it "converts auto_devops_deploy_strategy=timed_incremental to auto_devops.deploy_strategy=timed_incremental" do + expect(project.auto_devops).to be_nil + project.update!(auto_devops_deploy_strategy: 'timed_incremental') + expect(project.auto_devops.deploy_strategy).to eq('timed_incremental') + end +end diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 5aa43b58217..1fe176ab5af 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -4,7 +4,8 @@ require 'spec_helper' describe ProjectFeaturesCompatibility do let(:project) { create(:project) } - let(:features) { %w(issues wiki builds merge_requests snippets) } + let(:features_except_repository) { %w(issues wiki builds merge_requests snippets) } + let(:features) { features_except_repository + ['repository'] } # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table # All those fields got moved to a new table called project_feature and are now integers instead of booleans @@ -12,30 +13,37 @@ describe ProjectFeaturesCompatibility do # So we can keep it compatible it "converts fields from 'true' to ProjectFeature::ENABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, "true") expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) end end it "converts fields from 'false' to ProjectFeature::DISABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, "false") expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end it "converts fields from true to ProjectFeature::ENABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, true) expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) end end it "converts fields from false to ProjectFeature::DISABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, false) expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end + + it "accepts private as ProjectFeature::PRIVATE" do + features.each do |feature| + project.update!("#{feature}_access_level".to_sym => 'private') + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::PRIVATE) + end + end end diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 22d4dab0617..950bdec4d00 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -10,6 +10,20 @@ describe Discussion do let(:second_note) { create(:diff_note_on_merge_request, in_reply_to: first_note) } let(:third_note) { create(:diff_note_on_merge_request) } + describe '.lazy_find' do + let!(:note1) { create(:discussion_note_on_merge_request).to_discussion } + let!(:note2) { create(:discussion_note_on_merge_request, in_reply_to: note1).to_discussion } + + subject { [note1, note2].map { |note| described_class.lazy_find(note.discussion_id) } } + + it 'batches requests' do + expect do + [described_class.lazy_find(note1.id), + described_class.lazy_find(note2.id)].map(&:__sync) + end.not_to exceed_query_limit(1) + end + end + describe '.build' do it 'returns a discussion of the right type' do discussion = described_class.build([first_note, second_note], merge_request) diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb new file mode 100644 index 00000000000..b04fcb9aece --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Adding a DiffNote' do + include GraphqlHelpers + + set(:current_user) { create(:user) } + let(:noteable) { create(:merge_request, source_project: project, target_project: project) } + let(:project) { create(:project, :repository) } + let(:diff_refs) { noteable.diff_refs } + let(:mutation) do + variables = { + noteable_id: GitlabSchema.id_from_object(noteable).to_s, + body: 'Body text', + position: { + paths: { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen2.rb' + }, + new_line: 14, + base_sha: diff_refs.base_sha, + head_sha: diff_refs.head_sha, + start_sha: diff_refs.start_sha + } + } + + graphql_mutation(:create_diff_note, variables) + end + + def mutation_response + graphql_mutation_response(:create_diff_note) + end + + it_behaves_like 'a Note mutation when the user does not have permission' + + context 'when the user has permission' do + before do + project.add_developer(current_user) + end + + it_behaves_like 'a Note mutation that creates a Note' + + it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote + + context do + let(:diff_refs) { build(:merge_request).diff_refs } # Allow fake diff refs so arguments are valid + + it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' + end + + it 'returns the note with the correct position' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['body']).to eq('Body text') + mutation_position_response = mutation_response['note']['position'] + expect(mutation_position_response['positionType']).to eq('text') + expect(mutation_position_response['filePath']).to eq('files/ruby/popen2.rb') + expect(mutation_position_response['oldPath']).to eq('files/ruby/popen.rb') + expect(mutation_position_response['newPath']).to eq('files/ruby/popen2.rb') + expect(mutation_position_response['newLine']).to eq(14) + end + end +end diff --git a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb new file mode 100644 index 00000000000..3ba6c689024 --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Adding an image DiffNote' do + include GraphqlHelpers + + set(:current_user) { create(:user) } + let(:noteable) { create(:merge_request, source_project: project, target_project: project) } + let(:project) { create(:project, :repository) } + let(:diff_refs) { noteable.diff_refs } + let(:mutation) do + variables = { + noteable_id: GitlabSchema.id_from_object(noteable).to_s, + body: 'Body text', + position: { + paths: { + old_path: 'files/images/any_image.png', + new_path: 'files/images/any_image2.png' + }, + width: 100, + height: 200, + x: 1, + y: 2, + base_sha: diff_refs.base_sha, + head_sha: diff_refs.head_sha, + start_sha: diff_refs.start_sha + } + } + + graphql_mutation(:create_image_diff_note, variables) + end + + def mutation_response + graphql_mutation_response(:create_image_diff_note) + end + + it_behaves_like 'a Note mutation when the user does not have permission' + + context 'when the user has permission' do + before do + project.add_developer(current_user) + end + + it_behaves_like 'a Note mutation that creates a Note' + + it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote + + context do + let(:diff_refs) { build(:merge_request).diff_refs } # Allow fake diff refs so arguments are valid + + it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' + end + + it 'returns the note with the correct position' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['body']).to eq('Body text') + mutation_position_response = mutation_response['note']['position'] + expect(mutation_position_response['filePath']).to eq('files/images/any_image2.png') + expect(mutation_position_response['oldPath']).to eq('files/images/any_image.png') + expect(mutation_position_response['newPath']).to eq('files/images/any_image2.png') + expect(mutation_position_response['positionType']).to eq('image') + expect(mutation_position_response['width']).to eq(100) + expect(mutation_position_response['height']).to eq(200) + expect(mutation_position_response['x']).to eq(1) + expect(mutation_position_response['y']).to eq(2) + end + end +end diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb new file mode 100644 index 00000000000..14aaa430ac9 --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Adding a Note' do + include GraphqlHelpers + + set(:current_user) { create(:user) } + let(:noteable) { create(:merge_request, source_project: project, target_project: project) } + let(:project) { create(:project) } + let(:discussion) { nil } + let(:mutation) do + variables = { + noteable_id: GitlabSchema.id_from_object(noteable).to_s, + discussion_id: (GitlabSchema.id_from_object(discussion).to_s if discussion), + body: 'Body text' + } + + graphql_mutation(:create_note, variables) + end + + def mutation_response + graphql_mutation_response(:create_note) + end + + it_behaves_like 'a Note mutation when the user does not have permission' + + context 'when the user has permission' do + before do + project.add_developer(current_user) + end + + it_behaves_like 'a Note mutation that creates a Note' + + it_behaves_like 'a Note mutation when there are active record validation errors' + + it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' + + it 'returns the note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['body']).to eq('Body text') + end + + describe 'creating Notes in reply to a discussion' do + context 'when the user does not have permission to create notes on the discussion' do + let(:discussion) { create(:discussion_note).to_discussion } + + it_behaves_like 'a mutation that returns top-level errors', + errors: ["The discussion does not exist or you don't have permission to perform this action"] + end + + context 'when the user has permission to create notes on the discussion' do + let(:discussion) { create(:discussion_note, project: project).to_discussion } + + it 'creates a Note in a discussion' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['discussion']['id']).to eq(discussion.to_global_id.to_s) + end + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb new file mode 100644 index 00000000000..337a6e6f6e6 --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Destroying a Note' do + include GraphqlHelpers + + let!(:note) { create(:note) } + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(note).to_s + } + + graphql_mutation(:destroy_note, variables) + end + + def mutation_response + graphql_mutation_response(:destroy_note) + end + + context 'when the user does not have permission' do + let(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + + it 'does not destroy the Note' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Note.count } + end + end + + context 'when the user has permission' do + let(:current_user) { note.author } + + it_behaves_like 'a Note mutation when the given resource id is not for a Note' + + it 'destroys the Note' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { Note.count }.by(-1) + end + + it 'returns an empty Note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('note') + expect(mutation_response['note']).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/mutations/notes/update_spec.rb b/spec/requests/api/graphql/mutations/notes/update_spec.rb new file mode 100644 index 00000000000..958f640995a --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/update_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Updating a Note' do + include GraphqlHelpers + + let!(:note) { create(:note, note: original_body) } + let(:original_body) { 'Initial body text' } + let(:updated_body) { 'Updated body text' } + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(note).to_s, + body: updated_body + } + + graphql_mutation(:update_note, variables) + end + + def mutation_response + graphql_mutation_response(:update_note) + end + + context 'when the user does not have permission' do + let(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] + + it 'does not update the Note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(note.reload.note).to eq(original_body) + end + end + + context 'when the user has permission' do + let(:current_user) { note.author } + + it_behaves_like 'a Note mutation when the given resource id is not for a Note' + + it 'updates the Note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(note.reload.note).to eq(updated_body) + end + + it 'returns the updated Note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['body']).to eq(updated_body) + end + + context 'when there are ActiveRecord validation errors' do + let(:updated_body) { '' } + + it_behaves_like 'a mutation that returns errors in the response', errors: ["Note can't be blank"] + + it 'does not update the Note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(note.reload.note).to eq(original_body) + end + + it 'returns the Note with its original body' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['body']).to eq(original_body) + end + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c67412a44c1..a2aae257352 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1102,6 +1102,12 @@ describe API::Projects do expect(json_response['wiki_enabled']).to be_present expect(json_response['jobs_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present + expect(json_response['snippets_access_level']).to be_present + expect(json_response['repository_access_level']).to be_present + expect(json_response['issues_access_level']).to be_present + expect(json_response['merge_requests_access_level']).to be_present + expect(json_response['wiki_access_level']).to be_present + expect(json_response['builds_access_level']).to be_present expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present @@ -1913,6 +1919,34 @@ describe API::Projects do end end + it 'updates builds_access_level' do + project_param = { builds_access_level: 'private' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['builds_access_level']).to eq('private') + end + + it 'updates build_git_strategy' do + project_param = { build_git_strategy: 'clone' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['build_git_strategy']).to eq('clone') + end + + it 'rejects to update build_git_strategy when build_git_strategy is invalid' do + project_param = { build_git_strategy: 'invalid' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(400) + end + it 'updates merge_method' do project_param = { merge_method: 'ff' } @@ -1946,6 +1980,26 @@ describe API::Projects do '-/system/project/avatar/'\ "#{project3.id}/banana_sample.gif") end + + it 'updates auto_devops_deploy_strategy' do + project_param = { auto_devops_deploy_strategy: 'timed_incremental' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['auto_devops_deploy_strategy']).to eq('timed_incremental') + end + + it 'updates auto_devops_enabled' do + project_param = { auto_devops_enabled: false } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['auto_devops_enabled']).to eq(false) + end end context 'when authenticated as project maintainer' do diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 1a09d48f4cd..ec3c460cd37 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -57,7 +57,8 @@ module GraphqlHelpers end def variables_for_mutation(name, input) - graphql_input = input.map { |name, value| [GraphqlHelpers.fieldnamerize(name), value] }.to_h + graphql_input = prepare_input_for_mutation(input) + result = { input_variable_name_for_mutation(name) => graphql_input } # Avoid trying to serialize multipart data into JSON @@ -68,6 +69,18 @@ module GraphqlHelpers end end + # Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys + # + # prepare_input_for_mutation({ 'my_key' => 1 }) + # => { 'myKey' => 1} + def prepare_input_for_mutation(input) + input.map do |name, value| + value = prepare_input_for_mutation(value) if value.is_a?(Hash) + + [GraphqlHelpers.fieldnamerize(name), value] + end.to_h + end + def input_variable_name_for_mutation(mutation_name) mutation_name = GraphqlHelpers.fieldnamerize(mutation_name) mutation_field = GitlabSchema.mutation.fields[mutation_name] diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb new file mode 100644 index 00000000000..f2e1a95345b --- /dev/null +++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a Note mutation that does not create a Note' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Note.count } + end +end + +RSpec.shared_examples 'a Note mutation that creates a Note' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { Note.count }.by(1) + end +end + +RSpec.shared_examples 'a Note mutation when the user does not have permission' do + it_behaves_like 'a Note mutation that does not create a Note' + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action'] +end + +RSpec.shared_examples 'a Note mutation when there are active record validation errors' do |model: Note| + before do + expect_next_instance_of(model) do |note| + expect(note).to receive(:valid?).at_least(:once).and_return(false) + expect(note).to receive_message_chain( + :errors, + :full_messages + ).and_return(['Error 1', 'Error 2']) + end + end + + it_behaves_like 'a Note mutation that does not create a Note' + + it_behaves_like 'a mutation that returns errors in the response', errors: ['Error 1', 'Error 2'] + + it 'returns an empty Note' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('note') + expect(mutation_response['note']).to be_nil + end +end + +RSpec.shared_examples 'a Note mutation when the given resource id is not for a Noteable' do + let(:noteable) { create(:label, project: project) } + + it_behaves_like 'a Note mutation that does not create a Note' + + it_behaves_like 'a mutation that returns top-level errors', errors: ['Cannot add notes to this resource'] +end + +RSpec.shared_examples 'a Note mutation when the given resource id is not for a Note' do + let(:note) { create(:issue) } + + it_behaves_like 'a mutation that returns top-level errors', errors: ['Resource is not a note'] +end |