summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/boards/config_toggle.js1
-rw-r--r--app/assets/javascripts/boards/index.js33
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js1
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js9
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js4
-rw-r--r--app/assets/stylesheets/components/toast.scss13
-rw-r--r--app/assets/stylesheets/framework/modal.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/mutations/notes/base.rb34
-rw-r--r--app/graphql/mutations/notes/create/base.rb49
-rw-r--r--app/graphql/mutations/notes/create/diff_note.rb33
-rw-r--r--app/graphql/mutations/notes/create/image_diff_note.rb33
-rw-r--r--app/graphql/mutations/notes/create/note.rb40
-rw-r--r--app/graphql/mutations/notes/destroy.rb28
-rw-r--r--app/graphql/mutations/notes/update.rb38
-rw-r--r--app/graphql/types/base_input_object.rb1
-rw-r--r--app/graphql/types/diff_paths_input_type.rb12
-rw-r--r--app/graphql/types/diff_refs_type.rb14
-rw-r--r--app/graphql/types/merge_request_type.rb1
-rw-r--r--app/graphql/types/mutation_type.rb5
-rw-r--r--app/graphql/types/notes/diff_image_position_input_type.rb20
-rw-r--r--app/graphql/types/notes/diff_position_base_input_type.rb22
-rw-r--r--app/graphql/types/notes/diff_position_input_type.rb16
-rw-r--r--app/graphql/types/notes/diff_position_type.rb7
-rw-r--r--app/graphql/types/notes/discussion_type.rb8
-rw-r--r--app/models/concerns/project_api_compatibility.rb20
-rw-r--r--app/models/concerns/project_features_compatibility.rb54
-rw-r--r--app/models/discussion.rb11
-rw-r--r--app/models/label_note.rb18
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb3
-rw-r--r--app/models/project_feature.rb18
-rw-r--r--app/models/resource_label_event.rb7
-rw-r--r--app/services/ci/register_job_service.rb2
-rw-r--r--app/services/merge_requests/merge_base_service.rb11
-rw-r--r--app/services/merge_requests/merge_service.rb4
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb18
-rw-r--r--changelogs/unreleased/62826-graphql-note-mutations.yml5
-rw-r--r--changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml5
-rw-r--r--changelogs/unreleased/64066-fix-uneven-click-areas.yml5
-rw-r--r--changelogs/unreleased/caneldem-master-patch-77839.yml5
-rw-r--r--changelogs/unreleased/fix-unicorn-sampler-workers-count.yml5
-rw-r--r--changelogs/unreleased/jramsay-enable-object-dedupe-by-default.yml5
-rw-r--r--changelogs/unreleased/project_api.yml5
-rw-r--r--changelogs/unreleased/winh-notes-service-toggleAward.yml5
-rw-r--r--doc/administration/pages/index.md23
-rw-r--r--doc/administration/repository_storage_types.md19
-rw-r--r--doc/api/projects.md72
-rw-r--r--doc/topics/autodevops/index.md11
-rw-r--r--doc/user/discussions/index.md2
-rw-r--r--lib/api/entities.rb28
-rw-r--r--lib/api/helpers/projects_helpers.rb45
-rw-r--r--lib/api/import_github.rb2
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml1
-rw-r--r--lib/gitlab/gitaly_client.rb23
-rw-r--r--lib/gitlab/global_id.rb14
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb11
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/service/kubernetes_cluster.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb2
-rw-r--r--spec/graphql/gitlab_schema_spec.rb20
-rw-r--r--spec/graphql/types/diff_refs_type_spec.rb9
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb2
-rw-r--r--spec/graphql/types/notes/diff_position_type_spec.rb2
-rw-r--r--spec/graphql/types/notes/discussion_type_spec.rb2
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb2
-rw-r--r--spec/lib/gitlab/global_id_spec.rb37
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb33
-rw-r--r--spec/models/concerns/project_api_compatibility_spec.rb38
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb18
-rw-r--r--spec/models/discussion_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb64
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb70
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb64
-rw-r--r--spec/requests/api/graphql/mutations/notes/destroy_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/notes/update_spec.rb72
-rw-r--r--spec/requests/api/projects_spec.rb54
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb13
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb13
-rw-r--r--spec/support/helpers/graphql_helpers.rb15
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb61
-rw-r--r--yarn.lock6
87 files changed, 1406 insertions, 178 deletions
diff --git a/Gemfile b/Gemfile
index 33f9c4650ef..f32e899342b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -420,7 +420,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support
-gem 'net-ssh', '~> 5.0'
+gem 'net-ssh', '~> 5.2'
gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support
diff --git a/Gemfile.lock b/Gemfile.lock
index c8b8f0e4f90..85b4c32f168 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -523,7 +523,7 @@ GEM
mysql2 (0.4.10)
nakayoshi_fork (0.0.4)
net-ldap (0.16.0)
- net-ssh (5.0.1)
+ net-ssh (5.2.0)
netrc (0.11.0)
nio4r (2.3.1)
nokogiri (1.10.3)
@@ -1152,7 +1152,7 @@ DEPENDENCIES
mysql2 (~> 0.4.10)
nakayoshi_fork (~> 0.0.4)
net-ldap
- net-ssh (~> 5.0)
+ net-ssh (~> 5.2)
nokogiri (~> 1.10.3)
oauth2 (~> 1.4)
octokit (~> 4.9)
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/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index 2d1f7a1cfd0..73e92728cb9 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -3,8 +3,12 @@ import '~/commons/bootstrap';
export default {
bind(el) {
+ const glTooltipDelay = localStorage.getItem('gl-tooltip-delay');
+ const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0;
+
$(el).tooltip({
trigger: 'hover',
+ delay,
});
},
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/label_note.rb b/app/models/label_note.rb
index d6814f4a948..ba5f1f82a81 100644
--- a/app/models/label_note.rb
+++ b/app/models/label_note.rb
@@ -62,19 +62,27 @@ class LabelNote < Note
end
def note_text(html: false)
- added = labels_str('added', label_refs_by_action('add', html))
- removed = labels_str('removed', label_refs_by_action('remove', html))
+ added = labels_str(label_refs_by_action('add', html), prefix: 'added', suffix: added_suffix)
+ removed = labels_str(label_refs_by_action('remove', html), prefix: removed_prefix)
[added, removed].compact.join(' and ')
end
+ def removed_prefix
+ 'removed'
+ end
+
+ def added_suffix
+ ''
+ end
+
# returns string containing added/removed labels including
# count of deleted labels:
#
# added ~1 ~2 + 1 deleted label
# added 3 deleted labels
# added ~1 ~2 labels
- def labels_str(prefix, label_refs)
+ def labels_str(label_refs, prefix: '', suffix: '')
existing_refs = label_refs.select { |ref| ref.present? }.sort
refs_str = existing_refs.empty? ? nil : existing_refs.join(' ')
@@ -84,9 +92,9 @@ class LabelNote < Note
return unless refs_str || deleted_str
label_list_str = [refs_str, deleted_str].compact.join(' + ')
- suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count)
+ suffix += ' label'.pluralize(deleted > 0 ? deleted : existing_refs.count)
- "#{prefix} #{label_list_str} #{suffix}"
+ "#{prefix} #{label_list_str} #{suffix.squish}"
end
def label_refs_by_action(action, html)
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 0f4fba5d0b6..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
@@ -2144,7 +2145,7 @@ class Project < ApplicationRecord
public? &&
repository_exists? &&
Gitlab::CurrentSettings.hashed_storage_enabled &&
- Feature.enabled?(:object_pools, self)
+ Feature.enabled?(:object_pools, self, default_enabled: true)
end
def leave_pool_repository
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/models/resource_label_event.rb b/app/models/resource_label_event.rb
index f2c7cb6a65d..ad08f4763ae 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -36,10 +36,9 @@ class ResourceLabelEvent < ApplicationRecord
issue || merge_request
end
- # create same discussion id for all actions with the same user and time
def discussion_id(resource = nil)
strong_memoize(:discussion_id) do
- Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-"))
+ Digest::SHA1.hexdigest(discussion_id_key.join("-"))
end
end
@@ -121,4 +120,8 @@ class ResourceLabelEvent < ApplicationRecord
def resource_parent
issuable.project || issuable.group
end
+
+ def discussion_id_key
+ [self.class.name, created_at, user_id]
+ end
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/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb
index 095bdca5472..1ed396cee1e 100644
--- a/app/services/merge_requests/merge_base_service.rb
+++ b/app/services/merge_requests/merge_base_service.rb
@@ -28,6 +28,17 @@ module MergeRequests
private
+ def check_source
+ unless source
+ raise_error('No source for merge')
+ end
+ end
+
+ # Overridden in EE.
+ def check_size_limit
+ # No-op
+ end
+
# Overridden in EE.
def error_check!
# No-op
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index d8a78001b79..3e0f5aa181c 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -48,13 +48,13 @@ module MergeRequests
def error_check!
super
+ check_source
+
error =
if @merge_request.should_be_rebased?
'Only fast-forward merge is allowed for your project. Please update your source branch'
elsif !@merge_request.mergeable?
'Merge request is not mergeable'
- elsif !source
- 'No source for merge'
end
raise_error(error) if error
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index 0ea50a5dbf5..37b5805ae7e 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -16,7 +16,7 @@ module MergeRequests
def execute(merge_request)
@merge_request = merge_request
- validate!
+ error_check!
commit_id = commit
@@ -39,21 +39,9 @@ module MergeRequests
merge_request.diff_head_sha
end
- def validate!
- error_check!
- end
-
+ override :error_check!
def error_check!
- super
-
- error =
- if !hooks_validation_pass?(merge_request)
- hooks_validation_error(merge_request)
- elsif source.blank?
- 'No source for merge'
- end
-
- raise_error(error) if error
+ check_source
end
##
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/63945-update-mixin-deep-to-1-3-2.yml b/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml
new file mode 100644
index 00000000000..a0ef34f3700
--- /dev/null
+++ b/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update mixin-deep to 1.3.2
+merge_request: 30223
+author: Takuya Noguchi
+type: other
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/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/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/jramsay-enable-object-dedupe-by-default.yml b/changelogs/unreleased/jramsay-enable-object-dedupe-by-default.yml
new file mode 100644
index 00000000000..b953d7c0fc8
--- /dev/null
+++ b/changelogs/unreleased/jramsay-enable-object-dedupe-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Git object pools
+merge_request: 29595
+author: jramsay
+type: changed
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/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/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/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index f4cb89c84a4..9dea6074a3f 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -80,25 +80,20 @@ by another folder with the next 2 characters. They are both stored in a special
### Hashed object pools
-CAUTION: **Beta:**
-Hashed objects pools are considered beta, and are not ready for production use.
-Follow [gitaly#1548](https://gitlab.com/gitlab-org/gitaly/issues/1548) for
-updates.
+> [Introduced](https://gitlab.com/gitlab-org/gitaly/issues/1606) in GitLab 12.1.
-For deduplication of public forks and their parent repository, objects are pooled
-in an object pool. These object pools are a third repository where shared objects
-are stored.
+Forks of public projects are deduplicated by creating a third repository, the object pool, containing the objects from the source project. Using `objects/info/alternates`, the source project and forks use the object pool for shared objects. Objects are moved from the source project to the object pool when housekeeping is run on the source project.
```ruby
# object pool paths
"@pools/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git"
```
-The object pool feature is behind the `object_pools` feature flag, and can be
-enabled for individual projects by executing
-`Feature.enable(:object_pools, Project.find(<id>))`. Note that the project has to
-be on hashed storage, should not be a fork itself, and hashed storage should be
-enabled for all new projects.
+Object pools can be disabled using the `object_pools` feature flag, and can be
+disabled for individual projects by executing
+`Feature.disable(:object_pools, Project.find(<id>))`. Disabling object pools
+will not change existing deduplicated forks, but will prevent new forks from
+being deduplicated.
DANGER: **Danger:**
Do not run `git prune` or `git gc` in pool repositories! This can
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/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/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/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/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index cf0157269a8..cc9503fb6de 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -388,20 +388,21 @@ module Gitlab
end
def self.can_use_disk?(storage)
- cached_value = MUTEX.synchronize do
- @can_use_disk ||= {}
- @can_use_disk[storage]
- end
+ false
+ # cached_value = MUTEX.synchronize do
+ # @can_use_disk ||= {}
+ # @can_use_disk[storage]
+ # end
- return cached_value unless cached_value.nil?
+ # return cached_value unless cached_value.nil?
- gitaly_filesystem_id = filesystem_id(storage)
- direct_filesystem_id = filesystem_id_from_disk(storage)
+ # gitaly_filesystem_id = filesystem_id(storage)
+ # direct_filesystem_id = filesystem_id_from_disk(storage)
- MUTEX.synchronize do
- @can_use_disk[storage] = gitaly_filesystem_id.present? &&
- gitaly_filesystem_id == direct_filesystem_id
- end
+ # MUTEX.synchronize do
+ # @can_use_disk[storage] = gitaly_filesystem_id.present? &&
+ # gitaly_filesystem_id == direct_filesystem_id
+ # end
end
def self.filesystem_id(storage)
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/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/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/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/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
index f957ed00945..e7ef9d08f80 100644
--- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
+++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb
@@ -30,6 +30,7 @@ describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
end
it 'returns true when gitaly matches disk' do
+ pending('temporary disabled because of https://gitlab.com/gitlab-org/gitlab-ce/issues/64338')
expect(subject.use_rugged?(repository, feature_flag_name)).to be true
end
@@ -48,6 +49,7 @@ describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
end
it "doesn't lead to a second rpc call because gitaly client should use the cached value" do
+ pending('temporary disabled because of https://gitlab.com/gitlab-org/gitlab-ce/issues/64338')
expect(subject.use_rugged?(repository, feature_flag_name)).to be true
expect(Gitlab::GitalyClient).not_to receive(:filesystem_id)
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/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/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index aa759ac9edc..22578436c18 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -214,6 +214,19 @@ describe MergeRequests::MergeService do
allow(Rails.logger).to receive(:error)
end
+ context 'when source is missing' do
+ it 'logs and saves error' do
+ allow(merge_request).to receive(:diff_head_sha) { nil }
+
+ error_message = 'No source for merge'
+
+ service.execute(merge_request)
+
+ expect(merge_request.merge_error).to eq(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
+ end
+ end
+
it 'logs and saves error if there is an exception' do
error_message = 'error message'
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index 14012b4ea2d..758679edc45 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -191,6 +191,19 @@ describe MergeRequests::MergeToRefService do
it { expect(todo).not_to be_done }
end
+ context 'when source is missing' do
+ it 'returns error' do
+ allow(merge_request).to receive(:diff_head_sha) { nil }
+
+ error_message = 'No source for merge'
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(error_message)
+ end
+ end
+
context 'when target ref is passed as a parameter' do
let(:params) { { commit_message: 'merge train', target_ref: target_ref } }
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
diff --git a/yarn.lock b/yarn.lock
index bebc81d6847..dc5e0662396 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7476,9 +7476,9 @@ mississippi@^3.0.0:
through2 "^2.0.0"
mixin-deep@^1.2.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
- integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
dependencies:
for-in "^1.0.2"
is-extendable "^1.0.1"