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/confidential_merge_request/components/project_form_group.vue4
-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/dropdowns.scss4
-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/issuable_base_service.rb4
-rw-r--r--app/services/issues/update_service.rb4
-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--app/views/projects/_new_project_fields.html.haml20
-rw-r--r--app/views/projects/new.html.haml25
-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/api-doc-negative-commit-message-push-rule.yml5
-rw-r--r--changelogs/unreleased/caneldem-master-patch-77839.yml5
-rw-r--r--changelogs/unreleased/embedded-metrics-be-2.yml5
-rw-r--r--changelogs/unreleased/fix-unicorn-sampler-workers-count.yml5
-rw-r--r--changelogs/unreleased/issue_64021.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/update-clair-version.yml6
-rw-r--r--changelogs/unreleased/winh-notes-service-toggleAward.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/brakeman.ignore24
-rw-r--r--config/database_geo.yml.postgresql51
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/0_license.rb19
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/ar_speed_up_migration_checking.rb3
-rw-r--r--config/initializers/console_message.rb13
-rw-r--r--config/initializers/elastic_client_setup.rb50
-rw-r--r--config/initializers/geo.rb17
-rw-r--r--config/initializers/health_check.rb6
-rw-r--r--config/initializers/load_balancing.rb25
-rw-r--r--config/initializers/sidekiq.rb13
-rw-r--r--config/initializers/sidekiq_cluster.rb20
-rw-r--r--config/prometheus/cluster_metrics.yml63
-rw-r--r--config/pseudonymizer.yml475
-rw-r--r--config/settings.rb25
-rw-r--r--doc/administration/pages/index.md23
-rw-r--r--doc/administration/repository_storage_types.md19
-rw-r--r--doc/api/projects.md123
-rw-r--r--doc/development/documentation/index.md9
-rw-r--r--doc/topics/autodevops/index.md11
-rw-r--r--doc/user/discussions/index.md2
-rw-r--r--doc/user/project/packages/npm_registry.md15
-rw-r--r--doc/user/project/settings/import_export.md7
-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/banzai/filter/inline_embeds_filter.rb67
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb43
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb98
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb1
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml3
-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/dashboard/url.rb40
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb11
-rw-r--r--lib/peek/views/redis.rb8
-rw-r--r--locale/gitlab.pot54
-rw-r--r--qa/.rspec_parallel5
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock6
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/page/project/new.rb2
-rw-r--r--qa/qa/runtime/browser.rb6
-rw-r--r--qa/qa/runtime/env.rb12
-rw-r--r--qa/qa/runtime/scenario.rb6
-rw-r--r--qa/qa/scenario/shared_attributes.rb1
-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--qa/qa/specs/parallel_runner.rb33
-rw-r--r--qa/qa/specs/runner.rb58
-rw-r--r--qa/spec/page/logging_spec.rb8
-rw-r--r--qa/spec/spec_helper.rb4
-rw-r--r--qa/spec/specs/parallel_runner_spec.rb58
-rw-r--r--qa/spec/specs/runner_spec.rb8
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap34
-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/banzai/filter/inline_metrics_filter_spec.rb55
-rw-r--r--spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb58
-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/dashboard/url_spec.rb56
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb33
-rw-r--r--spec/lib/peek/views/redis_detailed_spec.rb36
-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/issues/update_service_spec.rb16
-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
146 files changed, 3016 insertions, 317 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/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 29d2cca6aed..99d77a75c23 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -126,10 +126,6 @@ export default {
{{ __('No forks available to you.') }}<br />
<span v-html="noForkText"></span>
</template>
- <gl-link :href="helpPagePath" class="help-link" target="_blank">
- <span class="sr-only">{{ __('Read more') }}</span>
- <i class="fa fa-question-circle" aria-hidden="true"></i>
- </gl-link>
</p>
</div>
</div>
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/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a12029d2419..e75c1379dfb 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -287,8 +287,8 @@
list-style: none;
padding: 0 1px;
- a:not(.help-link),
- button:not(.btn),
+ a,
+ button:not(.dropdown-toggle,.ci-action-icon-container),
.menu-item {
@include dropdown-link;
}
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/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 02de080e0ba..db673cace81 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -182,7 +182,7 @@ class IssuableBaseService < BaseService
# To be overridden by subclasses
end
- def before_update(issuable)
+ def before_update(issuable, skip_spam_check: false)
# To be overridden by subclasses
end
@@ -257,7 +257,7 @@ class IssuableBaseService < BaseService
last_edited_at: Time.now,
last_edited_by: current_user))
- before_update(issuable)
+ before_update(issuable, skip_spam_check: true)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 6b9f23f24cd..7cd825aa967 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -17,8 +17,8 @@ module Issues
super
end
- def before_update(issue)
- spam_check(issue, current_user)
+ def before_update(issue, skip_spam_check: false)
+ spam_check(issue, current_user) unless skip_spam_check
end
def handle_changes(issue, options)
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/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index e423631ec99..7541737f79c 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -36,17 +36,17 @@
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control", required: true
- if current_user.can_create_group?
.form-text.text-muted
- Want to house several dependent projects under the same namespace?
- = link_to "Create a group.", new_group_path
+ - link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
+ - project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' }
+ = project_tip.html_safe
.form-group
= f.label :description, class: 'label-bold' do
- Project description
- %span (optional)
- = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
+ = s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe }
+ = f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
= f.label :visibility_level, class: 'label-bold' do
- Visibility Level
+ = s_('ProjectsNew|Visibility Level')
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer'
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
@@ -57,9 +57,9 @@
= check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
.option-title
- %strong Initialize repository with a README
+ %strong= s_('ProjectsNew|Initialize repository with a README')
.option-description
- Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
+ = s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
-= f.submit 'Create project', class: "btn btn-success project-submit", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
-= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
+= f.submit _('Create project'), class: "btn btn-success project-submit", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
+= link_to _('Cancel'), dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 1cfe302fdc7..33de0aa153b 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,7 +1,7 @@
- @hide_breadcrumbs = true
- @hide_top_links = true
-- page_title 'New Project'
-- header_title "Projects", dashboard_projects_path
+- page_title _('New Project')
+- header_title _("Projects"), dashboard_projects_path
- active_tab = local_assigns.fetch(:active_tab, 'blank')
.project-edit-container.prepend-top-default
@@ -33,16 +33,16 @@
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' }
- %span.d-none.d-sm-block Blank project
- %span.d-block.d-sm-none Blank
+ %span.d-none.d-sm-block= s_('ProjectsNew|Blank project')
+ %span.d-block.d-sm-none= s_('ProjectsNew|Blank')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' }
- %span.d-none.d-sm-block.qa-project-create-from-template-tab Create from template
- %span.d-block.d-sm-none Template
+ %span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template')
+ %span.d-block.d-sm-none= s_('ProjectsNew|Template')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' }
- %span.d-none.d-sm-block Import project
- %span.d-block.d-sm-none Import
+ %span.d-none.d-sm-block= s_('ProjectsNew|Import project')
+ %span.d-block.d-sm-none= s_('ProjectsNew|Import')
= render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
.tab-content.gitlab-tab-content
@@ -67,8 +67,8 @@
= render 'import_project_pane', active_tab: active_tab
- else
.nothing-here-block
- %h4 No import options available
- %p Contact an administrator to enable options for importing your project.
+ %h4= s_('ProjectsNew|No import options available')
+ %p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
= render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab
@@ -76,5 +76,6 @@
.center
%h2
%i.fa.fa-spinner.fa-spin
- Creating project &amp; repository.
- %p Please wait a moment, this page will automatically refresh when ready.
+ = s_('ProjectsNew|Creating project & repository.')
+ %p
+ = s_('ProjectsNew|Please wait a moment, this page will automatically refresh when ready.')
diff --git a/changelogs/unreleased/62826-graphql-note-mutations.yml b/changelogs/unreleased/62826-graphql-note-mutations.yml
new file mode 100644
index 00000000000..85273186bb6
--- /dev/null
+++ b/changelogs/unreleased/62826-graphql-note-mutations.yml
@@ -0,0 +1,5 @@
+---
+title: GraphQL mutations for managing Notes
+merge_request: 30210
+author:
+type: added
diff --git a/changelogs/unreleased/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/api-doc-negative-commit-message-push-rule.yml b/changelogs/unreleased/api-doc-negative-commit-message-push-rule.yml
new file mode 100644
index 00000000000..0500978a2e1
--- /dev/null
+++ b/changelogs/unreleased/api-doc-negative-commit-message-push-rule.yml
@@ -0,0 +1,5 @@
+---
+title: "Document the negative commit message push rule for the API."
+merge_request: 14004
+author: Maikel Vlasman
+type: added
diff --git a/changelogs/unreleased/caneldem-master-patch-77839.yml b/changelogs/unreleased/caneldem-master-patch-77839.yml
new file mode 100644
index 00000000000..6239bcf67c4
--- /dev/null
+++ b/changelogs/unreleased/caneldem-master-patch-77839.yml
@@ -0,0 +1,5 @@
+---
+title: Propagate python version variable
+merge_request:
+author: Can Eldem
+type: changed
diff --git a/changelogs/unreleased/embedded-metrics-be-2.yml b/changelogs/unreleased/embedded-metrics-be-2.yml
new file mode 100644
index 00000000000..2623b4a2e0c
--- /dev/null
+++ b/changelogs/unreleased/embedded-metrics-be-2.yml
@@ -0,0 +1,5 @@
+---
+title: Expose placeholder element for metrics charts in GFM
+merge_request: 29861
+author:
+type: added
diff --git a/changelogs/unreleased/fix-unicorn-sampler-workers-count.yml b/changelogs/unreleased/fix-unicorn-sampler-workers-count.yml
new file mode 100644
index 00000000000..9a263b7f460
--- /dev/null
+++ b/changelogs/unreleased/fix-unicorn-sampler-workers-count.yml
@@ -0,0 +1,5 @@
+---
+title: Make unicorn_workers to return meaningful results
+merge_request: 30506
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_64021.yml b/changelogs/unreleased/issue_64021.yml
new file mode 100644
index 00000000000..088d9348619
--- /dev/null
+++ b/changelogs/unreleased/issue_64021.yml
@@ -0,0 +1,5 @@
+---
+title: Skip spam check for task list updates
+merge_request: 30279
+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/update-clair-version.yml b/changelogs/unreleased/update-clair-version.yml
new file mode 100644
index 00000000000..59b6e113fd5
--- /dev/null
+++ b/changelogs/unreleased/update-clair-version.yml
@@ -0,0 +1,6 @@
+---
+title: Extract clair version as CLAIR_EXECUTABLE_VERSION variable and update clair
+ executable from v8 to v11
+merge_request: 30396
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-notes-service-toggleAward.yml b/changelogs/unreleased/winh-notes-service-toggleAward.yml
new file mode 100644
index 00000000000..0471888c285
--- /dev/null
+++ b/changelogs/unreleased/winh-notes-service-toggleAward.yml
@@ -0,0 +1,5 @@
+---
+title: Remove toggleAward from notes service
+merge_request: 30536
+author: Frank van Rest
+type: other
diff --git a/config/application.rb b/config/application.rb
index c5ef6a2c60d..edf8b3e87f9 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -6,6 +6,7 @@ Bundler.require(:default, Rails.env)
module Gitlab
class Application < Rails::Application
+ require_dependency Rails.root.join('lib/gitlab')
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues')
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
new file mode 100644
index 00000000000..0e4fef65781
--- /dev/null
+++ b/config/brakeman.ignore
@@ -0,0 +1,24 @@
+{
+ "ignored_warnings": [
+ {
+ "warning_type": "Cross-Site Request Forgery",
+ "warning_code": 7,
+ "fingerprint": "dc562678129557cdb8b187217da304044547a3605f05fe678093dcb4b4d8bbe4",
+ "message": "'protect_from_forgery' should be called in Oauth::GeoAuthController",
+ "file": "app/controllers/oauth/geo_auth_controller.rb",
+ "line": 1,
+ "link": "http://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/",
+ "code": null,
+ "render_path": null,
+ "location": {
+ "type": "controller",
+ "controller": "Oauth::GeoAuthController"
+ },
+ "user_input": null,
+ "confidence": "High",
+ "note": ""
+ }
+ ],
+ "updated": "2017-01-20 02:06:54 +0000",
+ "brakeman_version": "3.4.1"
+}
diff --git a/config/database_geo.yml.postgresql b/config/database_geo.yml.postgresql
new file mode 100644
index 00000000000..2918879f7ed
--- /dev/null
+++ b/config/database_geo.yml.postgresql
@@ -0,0 +1,51 @@
+#
+# PRODUCTION
+#
+production:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_geo_production
+ pool: 10
+ username: git
+ password: "secure password"
+ host: localhost
+ fdw: true
+
+#
+# Development specific
+#
+development:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_geo_development
+ pool: 5
+ username: postgres
+ password: "secure password"
+ host: localhost
+ fdw: true
+
+#
+# Staging specific
+#
+staging:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_geo_staging
+ pool: 10
+ username: git
+ password: "secure password"
+ host: localhost
+ fdw: true
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test: &test
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_geo_test
+ pool: 5
+ username: postgres
+ password:
+ host: localhost
+ fdw: true
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index c82d9b5ceef..334c241bcaa 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -664,6 +664,9 @@ production: &base
# Port where the client side certificate is requested by the webserver (NGINX/Apache)
# client_certificate_required_port: 3444
+ # Browser session with smartcard sign-in is required for Git access
+ # required_for_git_access: false
+
## Kerberos settings
kerberos:
# Allow the HTTP Negotiate authentication method for Git clients
diff --git a/config/initializers/0_license.rb b/config/initializers/0_license.rb
new file mode 100644
index 00000000000..f750022dfdf
--- /dev/null
+++ b/config/initializers/0_license.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+Gitlab.ee do
+ begin
+ public_key_file = File.read(Rails.root.join(".license_encryption_key.pub"))
+ public_key = OpenSSL::PKey::RSA.new(public_key_file)
+ Gitlab::License.encryption_key = public_key
+ rescue
+ warn "WARNING: No valid license encryption key provided."
+ end
+
+ # Needed to run migration
+ if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.data_source_exists?('licenses')
+ message = LicenseHelper.license_message(signed_in: true, is_admin: true, in_html: false)
+ if ::License.block_changes? && message.present?
+ warn "WARNING: #{message}"
+ end
+ end
+end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index bf187e9a282..0b8a6607250 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -76,6 +76,7 @@ Gitlab.ee do
Settings['smartcard'] ||= Settingslogic.new({})
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil?
Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil?
+ Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil?
end
Settings['omniauth'] ||= Settingslogic.new({})
diff --git a/config/initializers/ar_speed_up_migration_checking.rb b/config/initializers/ar_speed_up_migration_checking.rb
index aae774daa35..f98b246db0b 100644
--- a/config/initializers/ar_speed_up_migration_checking.rb
+++ b/config/initializers/ar_speed_up_migration_checking.rb
@@ -10,7 +10,8 @@ if Rails.env.test?
# it reads + parses `db/migrate/*` each time. Memoizing it can save 0.5
# seconds per spec.
def migrations(paths)
- (@migrations ||= migrations_unmemoized(paths)).dup
+ @migrations ||= {}
+ (@migrations[paths] ||= migrations_unmemoized(paths)).dup
end
end
end
diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb
index 05eb395028d..04c109aa844 100644
--- a/config/initializers/console_message.rb
+++ b/config/initializers/console_message.rb
@@ -2,9 +2,18 @@
if defined?(Rails::Console)
# note that this will not print out when using `spring`
justify = 15
- puts "-------------------------------------------------------------------------------------"
+
+ puts '-' * 80
puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})"
puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}"
puts " #{Gitlab::Database.human_adapter_name}:".ljust(justify) + Gitlab::Database.version
- puts "-------------------------------------------------------------------------------------"
+
+ Gitlab.ee do
+ if Gitlab::Geo.enabled?
+ puts " Geo enabled:".ljust(justify) + 'yes'
+ puts " Geo server:".ljust(justify) + EE::GeoHelper.current_node_human_status
+ end
+ end
+
+ puts '-' * 80
end
diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb
new file mode 100644
index 00000000000..2ecb7956007
--- /dev/null
+++ b/config/initializers/elastic_client_setup.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+require 'gitlab/current_settings'
+
+Gitlab.ee do
+ Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records
+ Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records
+ Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods
+
+ module Elasticsearch
+ module Model
+ module Client
+ # This mutex is only used to synchronize *creation* of a new client, so
+ # all including classes can share the same client instance
+ CLIENT_MUTEX = Mutex.new
+
+ cattr_accessor :cached_client
+ cattr_accessor :cached_config
+
+ module ClassMethods
+ # Override the default ::Elasticsearch::Model::Client implementation to
+ # return a client configured from application settings. All including
+ # classes will use the same instance, which is refreshed automatically
+ # if the settings change.
+ #
+ # _client is present to match the arity of the overridden method, where
+ # it is also not used.
+ #
+ # @return [Elasticsearch::Transport::Client]
+ def client(_client = nil)
+ store = ::Elasticsearch::Model::Client
+
+ store::CLIENT_MUTEX.synchronize do
+ config = Gitlab::CurrentSettings.elasticsearch_config
+
+ if store.cached_client.nil? || config != store.cached_config
+ store.cached_client = ::Gitlab::Elastic::Client.build(config)
+ store.cached_config = config
+ end
+ end
+
+ store.cached_client
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/geo.rb b/config/initializers/geo.rb
new file mode 100644
index 00000000000..4cc9fbf49b2
--- /dev/null
+++ b/config/initializers/geo.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+Gitlab.ee do
+ if File.exist?(Rails.root.join('config/database_geo.yml'))
+ Rails.application.configure do
+ config.geo_database = config_for(:database_geo)
+ end
+ end
+
+ begin
+ if Gitlab::Geo.connected? && Gitlab::Geo.primary?
+ Gitlab::Geo.current_node&.update_clone_url!
+ end
+ rescue => e
+ warn "WARNING: Unable to check/update clone_url_prefix for Geo: #{e}"
+ end
+end
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
index 959daa93f78..9f466dc39de 100644
--- a/config/initializers/health_check.rb
+++ b/config/initializers/health_check.rb
@@ -1,4 +1,10 @@
HealthCheck.setup do |config|
config.standard_checks = %w(database migrations cache)
config.full_checks = %w(database migrations cache)
+
+ Gitlab.ee do
+ config.add_custom_check('geo') do
+ Gitlab::Geo::HealthCheck.new.perform_checks
+ end
+ end
end
diff --git a/config/initializers/load_balancing.rb b/config/initializers/load_balancing.rb
new file mode 100644
index 00000000000..029c0ff4277
--- /dev/null
+++ b/config/initializers/load_balancing.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# We need to run this initializer after migrations are done so it doesn't fail on CI
+
+Gitlab.ee do
+ if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.data_source_exists?('licenses')
+ if Gitlab::Database::LoadBalancing.enable?
+ Gitlab::Database.disable_prepared_statements
+
+ Gitlab::Application.configure do |config|
+ config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware)
+ end
+
+ Gitlab::Database::LoadBalancing.configure_proxy
+
+ # This needs to be executed after fork of clustered processes
+ Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ # Service discovery must be started after configuring the proxy, as service
+ # discovery depends on this.
+ Gitlab::Database::LoadBalancing.start_service_discovery
+ end
+
+ end
+ end
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 7b69cf11288..f9ef5d66bfa 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -85,6 +85,19 @@ Sidekiq.configure_server do |config|
ActiveRecord::Base.establish_connection(db_config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
+ Gitlab.ee do
+ Gitlab::Mirror.configure_cron_job!
+
+ Gitlab::Geo.configure_cron_jobs!
+
+ if Gitlab::Geo.geo_database_configured?
+ Rails.configuration.geo_database['pool'] = Sidekiq.options[:concurrency]
+ Geo::TrackingBase.establish_connection(Rails.configuration.geo_database)
+
+ Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{Geo::TrackingBase.connection_pool.size} (Geo tracking database)")
+ end
+ end
+
# Avoid autoload issue such as 'Mail::Parsers::AddressStruct'
# https://github.com/mikel/mail/issues/912#issuecomment-214850355
Mail.eager_autoload!
diff --git a/config/initializers/sidekiq_cluster.rb b/config/initializers/sidekiq_cluster.rb
new file mode 100644
index 00000000000..baa7495aa29
--- /dev/null
+++ b/config/initializers/sidekiq_cluster.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+if ENV['ENABLE_SIDEKIQ_CLUSTER'] && Gitlab.ee?
+ Thread.new do
+ Thread.current.abort_on_exception = true
+
+ parent = Process.ppid
+
+ loop do
+ sleep(5)
+
+ # In cluster mode it's possible that the master process is SIGKILL'd. In
+ # this case the parent PID changes and we need to terminate ourselves.
+ if Process.ppid != parent
+ Process.kill(:TERM, Process.pid)
+ break
+ end
+ end
+ end
+end
diff --git a/config/prometheus/cluster_metrics.yml b/config/prometheus/cluster_metrics.yml
new file mode 100644
index 00000000000..3df76b0974f
--- /dev/null
+++ b/config/prometheus/cluster_metrics.yml
@@ -0,0 +1,63 @@
+- group: Cluster Health
+ priority: 1
+ metrics:
+ - title: "CPU Usage"
+ y_label: "CPU"
+ required_metrics: ['container_cpu_usage_seconds_total']
+ weight: 1
+ queries:
+ - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)'
+ label: Usage
+ unit: "cores"
+ appearance:
+ line:
+ width: 2
+ area:
+ opacity: 0
+ - query_range: 'sum(kube_pod_container_resource_requests_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
+ label: Requested
+ unit: "cores"
+ appearance:
+ line:
+ width: 2
+ area:
+ opacity: 0
+ - query_range: 'sum(kube_node_status_capacity_cpu_cores{kubernetes_namespace="gitlab-managed-apps"})'
+ label: Capacity
+ unit: "cores"
+ appearance:
+ line:
+ type: 'dashed'
+ width: 2
+ area:
+ opacity: 0
+ - title: "Memory usage"
+ y_label: "Memory"
+ required_metrics: ['container_memory_usage_bytes']
+ weight: 1
+ queries:
+ - query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'
+ label: Usage
+ unit: "GiB"
+ appearance:
+ line:
+ width: 2
+ area:
+ opacity: 0
+ - query_range: 'sum(kube_pod_container_resource_requests_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
+ label: Requested
+ unit: "GiB"
+ appearance:
+ line:
+ width: 2
+ area:
+ opacity: 0
+ - query_range: 'sum(kube_node_status_capacity_memory_bytes{kubernetes_namespace="gitlab-managed-apps"})/2^30'
+ label: Capacity
+ unit: "GiB"
+ appearance:
+ line:
+ type: 'dashed'
+ width: 2
+ area:
+ opacity: 0
diff --git a/config/pseudonymizer.yml b/config/pseudonymizer.yml
new file mode 100644
index 00000000000..1d85ac1db45
--- /dev/null
+++ b/config/pseudonymizer.yml
@@ -0,0 +1,475 @@
+tables:
+ approvals:
+ whitelist:
+ - id
+ - merge_request_id
+ - user_id
+ - created_at
+ - updated_at
+ approver_groups:
+ whitelist:
+ - id
+ - target_type
+ - group_id
+ - created_at
+ - updated_at
+ board_assignees:
+ whitelist:
+ - id
+ - board_id
+ - assignee_id
+ board_labels:
+ whitelist:
+ - id
+ - board_id
+ - label_id
+ boards:
+ whitelist:
+ - id
+ - project_id
+ - created_at
+ - updated_at
+ - milestone_id
+ - group_id
+ - weight
+ epic_issues:
+ whitelist:
+ - id
+ - epic_id
+ - issue_id
+ - relative_position
+ epic_metrics:
+ whitelist:
+ - id
+ - epic_id
+ - created_at
+ - updated_at
+ epics:
+ whitelist:
+ - id
+ - milestone_id
+ - group_id
+ - author_id
+ - assignee_id
+ - iid
+ - updated_by_id
+ - last_edited_by_id
+ - lock_version
+ - start_date
+ - end_date
+ - last_edited_at
+ - created_at
+ - updated_at
+ - title
+ - description
+ issue_assignees:
+ whitelist:
+ - user_id
+ - issue_id
+ issue_links:
+ whitelist:
+ - id
+ - source_id
+ - target_id
+ - created_at
+ - updated_at
+ issue_metrics:
+ whitelist:
+ - id
+ - issue_id
+ - first_mentioned_in_commit_at
+ - first_associated_with_milestone_at
+ - first_added_to_board_at
+ - created_at
+ - updated_at
+ issues:
+ whitelist:
+ - id
+ - title
+ - author_id
+ - project_id
+ - created_at
+ - confidential
+ - updated_at
+ - description
+ - milestone_id
+ - state
+ - updated_by_id
+ - weight
+ - due_date
+ - moved_to_id
+ - lock_version
+ - time_estimate
+ - last_edited_at
+ - last_edited_by_id
+ - discussion_locked
+ - closed_at
+ label_links:
+ whitelist:
+ - id
+ - label_id
+ - target_id
+ - target_type
+ - created_at
+ - updated_at
+ label_priorities:
+ whitelist:
+ - id
+ - project_id
+ - label_id
+ - priority
+ - created_at
+ - updated_at
+ labels:
+ whitelist:
+ - id
+ - title
+ - color
+ - project_id
+ - created_at
+ - updated_at
+ - template
+ - type
+ - group_id
+ licenses:
+ whitelist:
+ - id
+ - created_at
+ - updated_at
+ merge_request_diffs:
+ whitelist:
+ - id
+ - state
+ - merge_request_id
+ - created_at
+ - updated_at
+ - base_commit_sha
+ - real_size
+ - head_commit_sha
+ - start_commit_sha
+ - commits_count
+ merge_request_metrics:
+ whitelist:
+ - id
+ - merge_request_id
+ - latest_build_started_at
+ - latest_build_finished_at
+ - first_deployed_to_production_at
+ - merged_at
+ - created_at
+ - updated_at
+ - pipeline_id
+ - merged_by_id
+ - latest_closed_by_id
+ - latest_closed_at
+ merge_requests:
+ whitelist:
+ - id
+ - target_branch
+ - source_branch
+ - source_project_id
+ - author_id
+ - assignee_id
+ - created_at
+ - updated_at
+ - milestone_id
+ - state
+ - merge_status
+ - target_project_id
+ - updated_by_id
+ - merge_error
+ - merge_params
+ - merge_when_pipeline_succeeds
+ - merge_user_id
+ - approvals_before_merge
+ - lock_version
+ - time_estimate
+ - squash
+ - last_edited_at
+ - last_edited_by_id
+ - head_pipeline_id
+ - discussion_locked
+ - latest_merge_request_diff_id
+ - allow_maintainer_to_push
+ merge_requests_closing_issues:
+ whitelist:
+ - id
+ - merge_request_id
+ - issue_id
+ - created_at
+ - updated_at
+ milestones:
+ whitelist:
+ - id
+ - project_id
+ - due_date
+ - created_at
+ - updated_at
+ - state
+ - start_date
+ - group_id
+ namespace_statistics:
+ whitelist:
+ - id
+ - namespace_id
+ - shared_runners_seconds
+ - shared_runners_seconds_last_reset
+ namespaces:
+ whitelist:
+ - id
+ - name
+ - path
+ - owner_id
+ - created_at
+ - updated_at
+ - type
+ - avatar
+ - membership_lock
+ - share_with_group_lock
+ - visibility_level
+ - request_access_enabled
+ - ldap_sync_status
+ - ldap_sync_error
+ - ldap_sync_last_update_at
+ - ldap_sync_last_successful_update_at
+ - ldap_sync_last_sync_at
+ - lfs_enabled
+ - parent_id
+ - shared_runners_minutes_limit
+ - repository_size_limit
+ - require_two_factor_authentication
+ - two_factor_grace_period
+ - plan_id
+ - project_creation_level
+ members:
+ whitelist:
+ - id
+ - access_level
+ - source_id
+ - source_type
+ - user_id
+ - notification_level
+ - type
+ - created_by_id
+ - invite_email
+ - invite_accepted_at
+ - requested_at
+ - expires_at
+ - ldap
+ - override
+ notification_settings:
+ whitelist:
+ - id
+ - user_id
+ - source_id
+ - source_type
+ - level
+ - created_at
+ - updated_at
+ - new_note
+ - new_issue
+ - reopen_issue
+ - close_issue
+ - reassign_issue
+ - new_merge_request
+ - reopen_merge_request
+ - close_merge_request
+ - reassign_merge_request
+ - merge_merge_request
+ - failed_pipeline
+ - success_pipeline
+ project_authorizations:
+ whitelist:
+ - user_id
+ - project_id
+ - access_level
+ project_auto_devops:
+ whitelist:
+ - id
+ - project_id
+ - created_at
+ - updated_at
+ - enabled
+ project_custom_attributes:
+ whitelist:
+ - id
+ - created_at
+ - updated_at
+ - project_id
+ - key
+ - value
+ project_features:
+ whitelist:
+ - id
+ - project_id
+ - merge_requests_access_level
+ - issues_access_level
+ - wiki_access_level
+ - snippets_access_level
+ - builds_access_level
+ - created_at
+ - updated_at
+ - repository_access_level
+ project_group_links:
+ whitelist:
+ - id
+ - project_id
+ - group_id
+ - created_at
+ - updated_at
+ - group_access
+ - expires_at
+ project_import_data:
+ whitelist:
+ - id
+ - project_id
+ project_mirror_data:
+ whitelist:
+ - id
+ - project_id
+ - retry_count
+ - last_update_started_at
+ - last_update_scheduled_at
+ - next_execution_timestamp
+ project_repository_states:
+ whitelist:
+ - id
+ - project_id
+ - repository_verification_checksum
+ - wiki_verification_checksum
+ - last_repository_verification_failure
+ - last_wiki_verification_failure
+ project_statistics:
+ whitelist:
+ - id
+ - project_id
+ - namespace_id
+ - commit_count
+ - storage_size
+ - repository_size
+ - lfs_objects_size
+ - build_artifacts_size
+ - shared_runners_seconds
+ - shared_runners_seconds_last_reset
+ projects:
+ whitelist:
+ - id
+ - name
+ - path
+ - description
+ - created_at
+ - updated_at
+ - creator_id
+ - namespace_id
+ - last_activity_at
+ - import_url
+ - visibility_level
+ - archived
+ - avatar
+ - merge_requests_template
+ - star_count
+ - merge_requests_rebase_enabled
+ - import_type
+ - import_source
+ - approvals_before_merge
+ - reset_approvals_on_push
+ - merge_requests_ff_only_enabled
+ - issues_template
+ - mirror
+ - mirror_user_id
+ - shared_runners_enabled
+ - build_coverage_regex
+ - build_allow_git_fetch
+ - build_timeout
+ - mirror_trigger_builds
+ - pending_delete
+ - public_builds
+ - last_repository_check_failed
+ - last_repository_check_at
+ - container_registry_enabled
+ - only_allow_merge_if_pipeline_succeeds
+ - has_external_issue_tracker
+ - repository_storage
+ - repository_read_only
+ - request_access_enabled
+ - has_external_wiki
+ - ci_config_path
+ - lfs_enabled
+ - only_allow_merge_if_all_discussions_are_resolved
+ - repository_size_limit
+ - printing_merge_request_link_enabled
+ - auto_cancel_pending_pipelines
+ - service_desk_enabled
+ - delete_error
+ - last_repository_updated_at
+ - disable_overriding_approvers_per_merge_request
+ - storage_version
+ - resolve_outdated_diff_discussions
+ - remote_mirror_available_overridden
+ - only_mirror_protected_branches
+ - pull_mirror_available_overridden
+ - mirror_overwrites_diverged_branches
+ - external_authorization_classification_label
+ subscriptions:
+ whitelist:
+ - id
+ - user_id
+ - subscribable_id
+ - subscribable_type
+ - subscribed
+ - created_at
+ - updated_at
+ - project_id
+ users:
+ whitelist:
+ - id
+ - remember_created_at
+ - sign_in_count
+ - current_sign_in_at
+ - last_sign_in_at
+ - current_sign_in_ip
+ - last_sign_in_ip
+ - created_at
+ - updated_at
+ - admin
+ - projects_limit
+ - failed_attempts
+ - locked_at
+ - can_create_group
+ - can_create_team
+ - state
+ - color_scheme_id
+ - password_expires_at
+ - created_by_id
+ - last_credential_check_at
+ - avatar
+ - confirmed_at
+ - confirmation_sent_at
+ - unconfirmed_email
+ - hide_no_ssh_key
+ - website_url
+ - admin_email_unsubscribed_at
+ - notification_email
+ - hide_no_password
+ - password_automatically_set
+ - location
+ - public_email
+ - dashboard
+ - project_view
+ - consumed_timestep
+ - layout
+ - hide_project_limit
+ - note
+ - otp_grace_period_started_at
+ - external
+ - organization
+ - auditor
+ - require_two_factor_authentication_from_group
+ - two_factor_grace_period
+ - ghost
+ - last_activity_on
+ - notified_of_own_activity
+ - bot_type
+ - preferred_language
+ - theme_id
+
diff --git a/config/settings.rb b/config/settings.rb
index 6df2132332c..da459afcce2 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -62,6 +62,31 @@ class Settings < Settingslogic
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
+ def kerberos_protocol
+ kerberos.https ? "https" : "http"
+ end
+
+ def kerberos_port
+ kerberos.use_dedicated_port ? kerberos.port : gitlab.port
+ end
+
+ # Curl expects username/password for authentication. However when using GSS-Negotiate not credentials should be needed.
+ # By inserting in the Kerberos dedicated URL ":@", we give to curl an empty username and password and GSS auth goes ahead
+ # Known bug reported in http://sourceforge.net/p/curl/bugs/440/ and http://curl.haxx.se/docs/knownbugs.html
+ def build_gitlab_kerberos_url
+ [
+ kerberos_protocol,
+ "://:@",
+ gitlab.host,
+ ":#{kerberos_port}",
+ gitlab.relative_url_root
+ ].join('')
+ end
+
+ def alternative_gitlab_kerberos_url?
+ kerberos.enabled && (build_gitlab_kerberos_url != build_gitlab_url)
+ end
+
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 3a7ca517d56..102656b01ec 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -4,21 +4,24 @@ description: 'Learn how to administer GitLab Pages.'
# GitLab Pages administration
-> **Notes:**
->
> - [Introduced][ee-80] in GitLab EE 8.3.
> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
-> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
-> - This guide is for Omnibus GitLab installations. If you have installed
-> GitLab from source, follow the [Pages source installation document](source.md).
-> - To learn how to use GitLab Pages, read the [user documentation][pages-userguide].
-> - Support for subgroup project's websites was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8.
-
-This document describes how to set up the _latest_ GitLab Pages feature. Make
-sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
+> - GitLab Pages [was ported][ce-14605] to Community Edition in GitLab 8.17.
+> - Support for subgroup project's websites was
+> [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8.
+
+GitLab Pages allows for hosting of static sites. It must be configured by an
+administrator. Separate [user documentation][pages-userguide] is available.
+
+Read the [changelog](#changelog) if you are upgrading to a new GitLab
version as it may include new features and changes needed to be made in your
configuration.
+NOTE: **Note:**
+This guide is for Omnibus GitLab installations. If you have installed
+GitLab from source, see
+[GitLab Pages administration for source installations](source.md).
+
## Overview
GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
diff --git a/doc/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 da05b090699..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 |
@@ -1631,6 +1667,7 @@ GET /projects/:id/push_rule
"id": 1,
"project_id": 3,
"commit_message_regex": "Fixes \d+\..*",
+ "commit_message_negative_regex": "ssh\:\/\/",
"branch_name_regex": "",
"deny_delete_tag": false,
"created_at": "2012-10-12T17:04:47Z",
@@ -1663,18 +1700,19 @@ Adds a push rule to a specified project.
POST /projects/:id/push_rule
```
-| Attribute | Type | Required | Description |
-| -------------------------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
-| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag |
-| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users |
-| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets |
-| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` |
-| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` |
-| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` |
-| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` |
-| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) |
-| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. |
+| Attribute | Type | Required | Description |
+| --------------------------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag |
+| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users |
+| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets |
+| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` |
+| `commit_message_negative_regex` **(STARTER)** | string | no | No commit message is allowed to match this, e.g. `ssh\:\/\/` |
+| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` |
+| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` |
+| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` |
+| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) |
+| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. |
### Edit project push rule
@@ -1684,18 +1722,19 @@ Edits a push rule for a specified project.
PUT /projects/:id/push_rule
```
-| Attribute | Type | Required | Description |
-| -------------------------------------- | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
-| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag |
-| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users |
-| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets |
-| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` |
-| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` |
-| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` |
-| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` |
-| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) |
-| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. |
+| Attribute | Type | Required | Description |
+| --------------------------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag |
+| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users |
+| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets |
+| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` |
+| `commit_message_negative_regex` **(STARTER)** | string | no | No commit message is allowed to match this, e.g. `ssh\:\/\/` |
+| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` |
+| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` |
+| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` |
+| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) |
+| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. |
### Delete project push rule
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 418e58b22d5..cbdc0a3a174 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -18,7 +18,7 @@ In addition to this page, the following resources to help craft and contribute d
## Source files and rendered web locations
-Documentation for GitLab Community Edition (CE) and Enterprise Edition (EE), along with GitLab Runner and Omnibus, is published to [docs.gitlab.com](https://docs.gitlab.com). The documentation for CE and EE is also published within the application at `/help` on the domain of the GitLab instance.
+Documentation for GitLab Community Edition (CE) and Enterprise Edition (EE), along with GitLab Runner and Omnibus, is published to [docs.gitlab.com](https://docs.gitlab.com). The documentation for CE and EE is also published within the application at `/help` on the domain of the GitLab instance, though there are [plans](https://gitlab.com/groups/gitlab-org/-/epics/693) to end this practice and instead link out from the GitLab application to docs.gitlab.com URLs.
At `/help`, only content for your current edition and version is included, whereas multiple versions' content is available at docs.gitlab.com.
@@ -274,8 +274,11 @@ Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.m
## GitLab `/help`
-Every GitLab instance includes the documentation, which is available from `/help`
-(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>.
+Every GitLab instance includes the documentation, which is available at `/help`
+(`https://gitlab.example.com/help`). For example, <https://gitlab.com/help>.
+
+There are [plans](https://gitlab.com/groups/gitlab-org/-/epics/693) to end this
+practice and instead link out from the GitLab application to docs.gitlab.com URLs.
The documentation available online on docs.gitlab.com is continuously
deployed every hour from the `master` branch of CE, EE, Omnibus, and Runner. Therefore,
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index e2d51f673b5..503ad784a77 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -316,7 +316,7 @@ If a project's repository contains a `Dockerfile`, Auto Build will use
If you are also using Auto Review Apps and Auto Deploy and choose to provide
your own `Dockerfile`, make sure you expose your application to port
`5000` as this is the port assumed by the
-[default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app).
+[default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app). Alternatively you can override the default values by [customizing the Auto Deploy helm chart](#custom-helm-chart)
#### Auto Build using Heroku buildpacks
@@ -452,7 +452,7 @@ be deleted.
Review apps are deployed using the
[auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with
-Helm. The app will be deployed into the [Kubernetes
+Helm, which can be [customized](#custom-helm-chart). The app will be deployed into the [Kubernetes
namespace](../../user/project/clusters/index.md#deployment-variables)
for the environment.
@@ -514,7 +514,7 @@ Auto Deploy doesn't include deployments to staging or canary by default, but the
enable them.
You can make use of [environment variables](#environment-variables) to automatically
-scale your pod replicas.
+scale your pod replicas and to apply custom arguments to the Auto DevOps `helm upgrade` commands. This is an easy way to [customize the Auto Deploy helm chart](#custom-helm-chart).
Apps are deployed using the
[auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with
@@ -655,6 +655,9 @@ repo or by specifying a project variable:
- **Project variable** - Create a [project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart.
+You can also make use of the `HELM_UPGRADE_EXTRA_ARGS` environment variable to override the default values in the `values.yaml` file in the [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app).
+To apply your own `values.yaml` file to all Helm upgrade commands in Auto Deploy set `HELM_UPGRADE_EXTRA_ARGS` to `--values my-values.yaml`.
+
### Custom Helm chart per environment **(PREMIUM)**
You can specify the use of a custom Helm chart per environment by scoping the environment variable
@@ -761,7 +764,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. |
| `ROLLOUT_RESOURCE_TYPE` | From GitLab 11.9, this variable allows specification of the resource type being deployed when using a custom helm chart. Default value is `deployment`. |
| `ROLLOUT_STATUS_DISABLED` | From GitLab 12.0, this variable allows to disable rollout status check because it doesn't support all resource types, for example, `cronjob`. |
-| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. |
+| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. **Tip:** you can use this variable to [customize the Auto Deploy helm chart](https://docs.gitlab.com/ee/topics/autodevops/index.html#custom-helm-chart) by applying custom override values with `--values my-values.yaml`. |
TIP: **Tip:**
Set up the replica variables using a
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index f2156720af7..c6bc580fb8f 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -444,7 +444,7 @@ Clicking on the **Reply to comment** button will bring the reply area into focus
![Reply to comment feature](img/reply_to_comment.gif)
-Relying to a non-discussion comment will convert the non-discussion comment to a
+Replying to a non-discussion comment will convert the non-discussion comment to a
threaded discussion once the reply is submitted. This conversion is considered an edit
to the original comment, so a note about when it was last edited will appear underneath it.
diff --git a/doc/user/project/packages/npm_registry.md b/doc/user/project/packages/npm_registry.md
index b2cfe10836f..481b1ce0337 100644
--- a/doc/user/project/packages/npm_registry.md
+++ b/doc/user/project/packages/npm_registry.md
@@ -11,11 +11,6 @@ project can have its own space to store NPM packages.
NOTE: **Note:**
Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported.
-
-NOTE: **Note:**
-As `@group/subgroup/project` is not a valid NPM package name, publishing a package
-within a subgroup is not supported yet.
-
## Enabling the NPM Registry
NOTE: **Note:**
@@ -36,12 +31,15 @@ get familiar with the package naming convention.
## Package naming convention
-**Only packages that have the same path as the project** are supported. For
- example:
+**Packages must be scoped in the root namespace of the project**. The package
+name may be anything but it is preferred that the project name be used unless
+it is not possible due to a naming collision. For example:
| Project | Package | Supported |
| ---------------------- | ----------------------- | --------- |
| `foo/bar` | `@foo/bar` | Yes |
+| `foo/bar/baz` | `@foo/baz` | Yes |
+| `foo/bar/buz` | `@foo/anything` | Yes |
| `gitlab-org/gitlab-ce` | `@gitlab-org/gitlab-ce` | Yes |
| `gitlab-org/gitlab-ce` | `@foo/bar` | No |
@@ -113,6 +111,9 @@ npm publish
You can then navigate to your project's **Packages** page and see the uploaded
packages or even delete them.
+If you attempt to publish a package with a name that already exists within
+a given scope, you will receive a `403 Forbidden!` error.
+
## Uploading a package with the same version twice
If you upload a package with a same name and version twice, GitLab will show
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 819515d7a4c..98bcc7cc09f 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -74,6 +74,13 @@ The following items will NOT be exported:
- CI variables
- Webhooks
- Any encrypted tokens
+- Merge Request Approvers
+- Push Rules
+- Awards
+
+NOTE: **Note:**
+For more details on the specific data persisted in a project export, see the
+[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/import_export/import_export.yml) file.
## Exporting a project and its data
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index b9aa387ba61..765819e6bf1 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -201,6 +201,7 @@ module API
# MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
projects_relation.preload(:project_feature, :route)
.preload(:import_state, :tags)
+ .preload(:auto_devops)
.preload(namespace: [:route, :owner])
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -247,12 +248,20 @@ module API
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
+ # TODO: remove in API v5, replaced by *_access_level
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+ expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) }
+ expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) }
+ expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) }
+ expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) }
+ expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) }
+ expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
+
expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
@@ -267,6 +276,12 @@ module API
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :ci_default_git_depth
expose :public_builds, as: :public_jobs
+ expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options|
+ project.build_allow_git_fetch ? 'fetch' : 'clone'
+ end
+ expose :build_timeout
+ expose :auto_cancel_pending_pipelines
+ expose :build_coverage_regex
expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links, options)
@@ -280,6 +295,10 @@ module API
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
}
expose :external_authorization_classification_label
+ expose :auto_devops_enabled?, as: :auto_devops_enabled
+ expose :auto_devops_deploy_strategy do |project, options|
+ project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
+ end
# rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {})
@@ -289,6 +308,7 @@ module API
# MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
super(projects_relation).preload(:group)
.preload(:ci_cd_settings)
+ .preload(:auto_devops)
.preload(project_group_links: { group: :route },
fork_network: :root_project,
fork_network_member: :forked_from_project,
@@ -491,7 +511,7 @@ module API
end
end
- class ProjectEntity < Grape::Entity
+ class IssuableEntity < Grape::Entity
expose :id, :iid
expose(:project_id) { |entity| entity&.project.try(:id) }
expose :title, :description
@@ -544,7 +564,7 @@ module API
end
end
- class IssueBasic < ProjectEntity
+ class IssueBasic < IssuableEntity
expose :closed_at
expose :closed_by, using: Entities::UserBasic
@@ -650,14 +670,14 @@ module API
end
end
- class MergeRequestSimple < ProjectEntity
+ class MergeRequestSimple < IssuableEntity
expose :title
expose :web_url do |merge_request, options|
Gitlab::UrlBuilder.build(merge_request)
end
end
- class MergeRequestBasic < ProjectEntity
+ class MergeRequestBasic < IssuableEntity
expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
merge_request.metrics&.merged_by
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index f242f1fea0e..0e21a7a66fd 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -8,12 +8,26 @@ module API
params :optional_project_params_ce do
optional :description, type: String, desc: 'The description of the project'
+ optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`'
+ optional :build_timeout, type: Integer, desc: 'Build timeout'
+ optional :auto_cancel_pending_pipelines, type: String, values: %w(disabled enabled), desc: 'Auto-cancel pending pipelines'
+ optional :build_coverage_regex, type: String, desc: 'Test coverage parsing'
optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
+
+ # TODO: remove in API v5, replaced by *_access_level
optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
+
+ optional :issues_access_level, type: String, values: %w(disabled private enabled), desc: 'Issues access level. One of `disabled`, `private` or `enabled`'
+ optional :repository_access_level, type: String, values: %w(disabled private enabled), desc: 'Repository access level. One of `disabled`, `private` or `enabled`'
+ optional :merge_requests_access_level, type: String, values: %w(disabled private enabled), desc: 'Merge requests access level. One of `disabled`, `private` or `enabled`'
+ optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`'
+ optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`'
+ optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`'
+
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
@@ -30,6 +44,8 @@ module API
optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project'
optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning'
+ optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled'
+ optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy'
end
params :optional_project_params_ee do
@@ -48,15 +64,20 @@ module API
def self.update_params_at_least_one_of
[
- :jobs_enabled,
- :resolve_outdated_diff_discussions,
+ :auto_devops_enabled,
+ :auto_devops_deploy_strategy,
+ :auto_cancel_pending_pipelines,
+ :build_coverage_regex,
+ :build_git_strategy,
+ :build_timeout,
+ :builds_access_level,
:ci_config_path,
:container_registry_enabled,
:default_branch,
:description,
- :issues_enabled,
+ :issues_access_level,
:lfs_enabled,
- :merge_requests_enabled,
+ :merge_requests_access_level,
:merge_method,
:name,
:only_allow_merge_if_all_discussions_are_resolved,
@@ -64,14 +85,24 @@ module API
:path,
:printing_merge_request_link_enabled,
:public_builds,
+ :repository_access_level,
:request_access_enabled,
+ :resolve_outdated_diff_discussions,
:shared_runners_enabled,
- :snippets_enabled,
+ :snippets_access_level,
:tag_list,
:visibility,
- :wiki_enabled,
+ :wiki_access_level,
:avatar,
- :external_authorization_classification_label
+ :external_authorization_classification_label,
+
+ # TODO: remove in API v5, replaced by *_access_level
+ :issues_enabled,
+ :jobs_enabled,
+ :merge_requests_enabled,
+ :wiki_enabled,
+ :jobs_enabled,
+ :snippets_enabled
]
end
end
diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb
index e7504051808..21d4928193e 100644
--- a/lib/api/import_github.rb
+++ b/lib/api/import_github.rb
@@ -28,7 +28,7 @@ module API
desc 'Import a GitHub project' do
detail 'This feature was introduced in GitLab 11.3.4.'
- success Entities::ProjectEntity
+ success ::ProjectEntity
end
params do
requires :personal_access_token, type: String, desc: 'GitHub personal access token'
diff --git a/lib/banzai/filter/inline_embeds_filter.rb b/lib/banzai/filter/inline_embeds_filter.rb
new file mode 100644
index 00000000000..97394fd8f82
--- /dev/null
+++ b/lib/banzai/filter/inline_embeds_filter.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that inserts a node for each occurence of
+ # a given link format. To transform references to DB
+ # resources in place, prefer to inherit from AbstractReferenceFilter.
+ class InlineEmbedsFilter < HTML::Pipeline::Filter
+ # Find every relevant link, create a new node based on
+ # the link, and insert this node after any html content
+ # surrounding the link.
+ def call
+ return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])
+
+ doc.xpath(xpath_search).each do |node|
+ next unless element = element_to_embed(node)
+
+ # We want this to follow any surrounding content. For example,
+ # if a link is inline in a paragraph.
+ node.parent.children.last.add_next_sibling(element)
+ end
+
+ doc
+ end
+
+ # Implement in child class.
+ #
+ # Return a Nokogiri::XML::Element to embed in the
+ # markdown.
+ def create_element(params)
+ end
+
+ # Implement in child class unless overriding #embed_params
+ #
+ # Returns the regex pattern used to filter
+ # to only matching urls.
+ def link_pattern
+ end
+
+ # Returns the xpath query string used to select nodes
+ # from the html document on which the embed is based.
+ #
+ # Override to select nodes other than links.
+ def xpath_search
+ 'descendant-or-self::a[@href]'
+ end
+
+ # Creates a new element based on the parameters
+ # obtained from the target link
+ def element_to_embed(node)
+ return unless params = embed_params(node)
+
+ create_element(params)
+ end
+
+ # Returns a hash of named parameters based on the
+ # provided regex with string keys.
+ #
+ # Override to select nodes other than links.
+ def embed_params(node)
+ url = node['href']
+
+ link_pattern.match(url) { |m| m.named_captures }
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
new file mode 100644
index 00000000000..0120cc37d6f
--- /dev/null
+++ b/lib/banzai/filter/inline_metrics_filter.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that inserts a placeholder element for each
+ # reference to a metrics dashboard.
+ class InlineMetricsFilter < Banzai::Filter::InlineEmbedsFilter
+ # Placeholder element for the frontend to use as an
+ # injection point for charts.
+ def create_element(params)
+ doc.document.create_element(
+ 'div',
+ class: 'js-render-metrics',
+ 'data-dashboard-url': metrics_dashboard_url(params)
+ )
+ end
+
+ # Endpoint FE should hit to collect the appropriate
+ # chart information
+ def metrics_dashboard_url(params)
+ Gitlab::Metrics::Dashboard::Url.build_dashboard_url(
+ params['namespace'],
+ params['project'],
+ params['environment'],
+ embedded: true
+ )
+ end
+
+ # Search params for selecting metrics links. A few
+ # simple checks is enough to boost performance without
+ # the cost of doing a full regex match.
+ def xpath_search
+ "descendant-or-self::a[contains(@href,'metrics') and \
+ starts-with(@href, '#{Gitlab.config.gitlab.url}')]"
+ end
+
+ # Regular expression matching metrics urls
+ def link_pattern
+ Gitlab::Metrics::Dashboard::Url.regex
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
new file mode 100644
index 00000000000..ff91be2cbb7
--- /dev/null
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that removes embeded elements that the current user does
+ # not have permission to view.
+ class InlineMetricsRedactorFilter < HTML::Pipeline::Filter
+ include Gitlab::Utils::StrongMemoize
+
+ METRICS_CSS_CLASS = '.js-render-metrics'
+
+ # Finds all embeds based on the css class the FE
+ # uses to identify the embedded content, removing
+ # only unnecessary nodes.
+ def call
+ return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])
+
+ nodes.each do |node|
+ path = paths_by_node[node]
+ user_has_access = user_access_by_path[path]
+
+ node.remove unless user_has_access
+ end
+
+ doc
+ end
+
+ private
+
+ def user
+ context[:current_user]
+ end
+
+ # Returns all nodes which the FE will identify as
+ # a metrics dashboard placeholder element
+ #
+ # @return [Nokogiri::XML::NodeSet]
+ def nodes
+ @nodes ||= doc.css(METRICS_CSS_CLASS)
+ end
+
+ # Maps a node to the full path of a project.
+ # Memoized so we only need to run the regex to get
+ # the project full path from the url once per node.
+ #
+ # @return [Hash<Nokogiri::XML::Node, String>]
+ def paths_by_node
+ strong_memoize(:paths_by_node) do
+ nodes.each_with_object({}) do |node, paths|
+ paths[node] = path_for_node(node)
+ end
+ end
+ end
+
+ # Gets a project's full_path from the dashboard url
+ # in the placeholder node. The FE will use the attr
+ # `data-dashboard-url`, so we want to check against that
+ # attribute directly in case a user has manually
+ # created a metrics element (rather than supporting
+ # an alternate attr in InlineMetricsFilter).
+ #
+ # @return [String]
+ def path_for_node(node)
+ url = node.attribute('data-dashboard-url').to_s
+
+ Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m|
+ "#{$~[:namespace]}/#{$~[:project]}"
+ end
+ end
+
+ # Maps a project's full path to a Project object.
+ # Contains all of the Projects referenced in the
+ # metrics placeholder elements of the current document
+ #
+ # @return [Hash<String, Project>]
+ def projects_by_path
+ strong_memoize(:projects_by_path) do
+ Project.eager_load(:route, namespace: [:route])
+ .where_full_path_in(paths_by_node.values.uniq)
+ .index_by(&:full_path)
+ end
+ end
+
+ # Returns a mapping representing whether the current user
+ # has permission to view the metrics for the project.
+ # Determined in a batch
+ #
+ # @return [Hash<Project, Boolean>]
+ def user_access_by_path
+ strong_memoize(:user_access_by_path) do
+ projects_by_path.each_with_object({}) do |(path, project), access|
+ access[path] = Ability.allowed?(user, :read_environment, project)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index d67f461be57..2c1006f708a 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -25,6 +25,7 @@ module Banzai
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
+ Filter::InlineMetricsFilter,
Filter::TableOfContentsFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 7eaad6d7560..5c199453638 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -13,6 +13,7 @@ module Banzai
def self.internal_link_filters
[
Filter::RedactorFilter,
+ Filter::InlineMetricsRedactorFilter,
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
Filter::SuggestionFilter
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index d1a34c515fa..5ad624bb15f 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -23,6 +23,7 @@ container_scanning:
DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/
# https://hub.docker.com/r/arminc/clair-local-scan/tags
CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
+ CLAIR_EXECUTABLE_VERSION: v11
## Disable the proxy for clair-local-scan, otherwise Container Scanning will
## fail when a proxy is used.
NO_PROXY: ${DOCKER_SERVICE},localhost
@@ -41,7 +42,7 @@ container_scanning:
- docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION}
- apk add -U wget ca-certificates
- docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
+ - wget https://github.com/arminc/clair-scanner/releases/download/${CLAIR_EXECUTABLE_VERSION}/clair-scanner_linux_amd64
- mv clair-scanner_linux_amd64 clair-scanner
- chmod +x clair-scanner
- touch clair-whitelist.yml
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index 8dd9775c583..f176771775e 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -40,6 +40,7 @@ dependency_scanning:
DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
DS_PULL_ANALYZER_IMAGE_TIMEOUT \
DS_RUN_ANALYZER_TIMEOUT \
+ DS_PYTHON_VERSION \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
diff --git a/lib/gitlab/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/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
new file mode 100644
index 00000000000..b197e7ca86b
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# Manages url matching for metrics dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class Url
+ class << self
+ # Matches urls for a metrics dashboard. This could be
+ # either the /metrics endpoint or the /metrics_dashboard
+ # endpoint.
+ #
+ # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics
+ def regex
+ %r{
+ (?<url>
+ #{Regexp.escape(Gitlab.config.gitlab.url)}
+ \/#{Project.reference_pattern}
+ (?:\/\-)?
+ \/environments
+ \/(?<environment>\d+)
+ \/metrics
+ (?<query>
+ \?[a-z0-9_=-]+
+ (&[a-z0-9_=-]+)*
+ )?
+ (?<anchor>\#[a-z0-9_-]+)?
+ )
+ }x
+ end
+
+ # Builds a metrics dashboard url based on the passed in arguments
+ def build_dashboard_url(*args)
+ Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index 9af7e0afed4..355f938704e 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -54,7 +54,16 @@ module Gitlab
end
def unicorn_workers_count
- `pgrep -f '[u]nicorn_rails worker.+ #{Rails.root.to_s}'`.split.count
+ http_servers.sum(&:worker_processes) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ # Traversal of ObjectSpace is expensive, on fully loaded application
+ # it takes around 80ms. The instances of HttpServers are not a subject
+ # to change so we can cache the list of servers.
+ def http_servers
+ return [] unless defined?(::Unicorn::HttpServer)
+
+ @http_servers ||= ObjectSpace.each_object(::Unicorn::HttpServer).to_a
end
end
end
diff --git a/lib/peek/views/redis.rb b/lib/peek/views/redis.rb
index ad3c3c9fe01..73de8672fa4 100644
--- a/lib/peek/views/redis.rb
+++ b/lib/peek/views/redis.rb
@@ -37,6 +37,8 @@ end
module Peek
module Views
module RedisDetailed
+ REDACTED_MARKER = "<redacted>"
+
def results
super.merge(details: details)
end
@@ -57,10 +59,12 @@ module Peek
end
def format_command(cmd)
+ if cmd.length >= 2 && cmd.first =~ /^auth$/i
+ cmd[-1] = REDACTED_MARKER
# Scrub out the value of the SET calls to avoid binary
# data or large data from spilling into the view
- if cmd.length >= 2 && cmd.first =~ /set/i
- cmd[-1] = "<redacted>"
+ elsif cmd.length >= 3 && cmd.first =~ /set/i
+ cmd[2..-1] = REDACTED_MARKER
end
cmd.join(' ')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9b6e8d8c8a4..a4fee96753d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3263,6 +3263,9 @@ msgstr ""
msgid "Create new..."
msgstr ""
+msgid "Create project"
+msgstr ""
+
msgid "Create project label"
msgstr ""
@@ -6752,6 +6755,9 @@ msgstr ""
msgid "New Pipeline Schedule"
msgstr ""
+msgid "New Project"
+msgstr ""
+
msgid "New Snippet"
msgstr ""
@@ -8425,6 +8431,54 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository."
+msgstr ""
+
+msgid "ProjectsNew|Blank"
+msgstr ""
+
+msgid "ProjectsNew|Blank project"
+msgstr ""
+
+msgid "ProjectsNew|Contact an administrator to enable options for importing your project."
+msgstr ""
+
+msgid "ProjectsNew|Create from template"
+msgstr ""
+
+msgid "ProjectsNew|Creating project & repository."
+msgstr ""
+
+msgid "ProjectsNew|Description format"
+msgstr ""
+
+msgid "ProjectsNew|Import"
+msgstr ""
+
+msgid "ProjectsNew|Import project"
+msgstr ""
+
+msgid "ProjectsNew|Initialize repository with a README"
+msgstr ""
+
+msgid "ProjectsNew|No import options available"
+msgstr ""
+
+msgid "ProjectsNew|Please wait a moment, this page will automatically refresh when ready."
+msgstr ""
+
+msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}"
+msgstr ""
+
+msgid "ProjectsNew|Template"
+msgstr ""
+
+msgid "ProjectsNew|Visibility Level"
+msgstr ""
+
+msgid "ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
diff --git a/qa/.rspec_parallel b/qa/.rspec_parallel
new file mode 100644
index 00000000000..e5927927eaa
--- /dev/null
+++ b/qa/.rspec_parallel
@@ -0,0 +1,5 @@
+--color
+--format documentation
+--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
+--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
+--require spec_helper
diff --git a/qa/Gemfile b/qa/Gemfile
index 12994b85322..c46be8a0362 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -1,5 +1,6 @@
source 'https://rubygems.org'
+gem 'gitlab-qa'
gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem 'capybara', '~> 2.16.1'
gem 'capybara-screenshot', '~> 1.0.18'
@@ -11,3 +12,4 @@ gem 'nokogiri', '~> 1.10.3'
gem 'rspec-retry', '~> 0.6.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
gem 'knapsack', '~> 1.17'
+gem 'parallel_tests', '~> 2.29'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 6b0635ed0e2..73aabf2c6ad 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -35,6 +35,7 @@ GEM
faker (1.9.3)
i18n (>= 0.7)
ffi (1.9.25)
+ gitlab-qa (4.0.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (0.9.1)
@@ -53,6 +54,9 @@ GEM
netrc (0.11.0)
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
+ parallel (1.17.0)
+ parallel_tests (2.29.0)
+ parallel
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@@ -104,8 +108,10 @@ DEPENDENCIES
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
faker (~> 1.6, >= 1.6.6)
+ gitlab-qa
knapsack (~> 1.17)
nokogiri (~> 1.10.3)
+ parallel_tests (~> 2.29)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
diff --git a/qa/qa.rb b/qa/qa.rb
index 10d44b6f6d9..be73776425b 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -360,6 +360,7 @@ module QA
module Specs
autoload :Config, 'qa/specs/config'
autoload :Runner, 'qa/specs/runner'
+ autoload :ParallelRunner, 'qa/specs/parallel_runner'
module Helpers
autoload :Quarantine, 'qa/specs/helpers/quarantine'
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index defd85a5740..0918445d119 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -18,7 +18,7 @@ module QA
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern
- element :project_create_button, "submit 'Create project'" # rubocop:disable QA/ElementWithPattern
+ element :project_create_button, "submit _('Create project')" # rubocop:disable QA/ElementWithPattern
element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index ed0779b93cc..2987bb1a213 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -13,6 +13,8 @@ module QA
NotRespondingError = Class.new(RuntimeError)
+ CAPYBARA_MAX_WAIT_TIME = 10
+
def initialize
self.class.configure!
end
@@ -43,6 +45,8 @@ module QA
end
end
+ Capybara.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i
+
return if Capybara.drivers.include?(:chrome)
Capybara.register_driver QA::Runtime::Env.browser do |app|
@@ -119,7 +123,7 @@ module QA
Capybara.configure do |config|
config.default_driver = QA::Runtime::Env.browser
config.javascript_driver = QA::Runtime::Env.browser
- config.default_max_wait_time = 10
+ config.default_max_wait_time = CAPYBARA_MAX_WAIT_TIME
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = ::File.expand_path('../../tmp', __dir__)
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 96f337dc081..e78b9bece19 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'gitlab/qa'
+
module QA
module Runtime
module Env
@@ -7,6 +9,8 @@ module QA
attr_writer :personal_access_token, :ldap_username, :ldap_password
+ ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES
+
# The environment variables used to indicate if the environment under test
# supports the given feature
SUPPORTED_FEATURES = {
@@ -173,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?
@@ -201,6 +205,10 @@ module QA
enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true)
end
+ def runtime_scenario_attributes
+ ENV['QA_RUNTIME_SCENARIO_ATTRIBUTES']
+ end
+
private
def remote_grid_credentials
diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb
index 5067322804b..3662ebe671b 100644
--- a/qa/qa/runtime/scenario.rb
+++ b/qa/qa/runtime/scenario.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'json'
+
module QA
module Runtime
##
@@ -24,6 +26,10 @@ module QA
end
end
+ def from_env(var)
+ JSON.parse(Runtime::Env.runtime_scenario_attributes).each { |k, v| define(k, v) }
+ end
+
def method_missing(name, *)
raise ArgumentError, "Scenario attribute `#{name}` not defined!"
end
diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb
index 40d5c6b1ff1..52f50ec8c27 100644
--- a/qa/qa/scenario/shared_attributes.rb
+++ b/qa/qa/scenario/shared_attributes.rb
@@ -7,6 +7,7 @@ module QA
attribute :gitlab_address, '--address URL', 'Address of the instance to test'
attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
+ attribute :parallel, '--parallel', 'Execute tests in parallel'
end
end
end
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/qa/qa/specs/parallel_runner.rb b/qa/qa/specs/parallel_runner.rb
new file mode 100644
index 00000000000..b92fdb610b6
--- /dev/null
+++ b/qa/qa/specs/parallel_runner.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'open3'
+
+module QA
+ module Specs
+ module ParallelRunner
+ module_function
+
+ def run(args)
+ unless args.include?('--')
+ index = args.index { |opt| opt.include?('features') }
+
+ args.insert(index, '--') if index
+ end
+
+ env = {}
+ Runtime::Env::ENV_VARIABLES.each_key do |key|
+ env[key] = ENV[key] if ENV[key]
+ end
+ env['QA_RUNTIME_SCENARIO_ATTRIBUTES'] = Runtime::Scenario.attributes.to_json
+ env['GITLAB_QA_ACCESS_TOKEN'] = Runtime::API::Client.new(:gitlab).personal_access_token unless env['GITLAB_QA_ACCESS_TOKEN']
+
+ cmd = "bundle exec parallel_test -t rspec --combine-stderr --serialize-stdout -- #{args.flatten.join(' ')}"
+ ::Open3.popen2e(env, cmd) do |_, out, wait|
+ out.each { |line| puts line }
+
+ exit wait.value.exitstatus
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index f1cb9378de8..6aa08cf77b4 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
+require 'knapsack'
require 'rspec/core'
require 'rspec/expectations'
-require 'knapsack'
module QA
module Specs
@@ -17,44 +17,56 @@ module QA
@options = []
end
- def perform
- args = []
- args.push('--tty') if tty
+ def paths_from_knapsack
+ allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator
+
+ QA::Runtime::Logger.info ''
+ QA::Runtime::Logger.info 'Report specs:'
+ QA::Runtime::Logger.info allocator.report_node_tests.join(', ')
+ QA::Runtime::Logger.info ''
+ QA::Runtime::Logger.info 'Leftover specs:'
+ QA::Runtime::Logger.info allocator.leftover_node_tests.join(', ')
+ QA::Runtime::Logger.info ''
+
+ ['--', allocator.node_tests]
+ end
+
+ def rspec_tags
+ tags_for_rspec = []
if tags.any?
- tags.each { |tag| args.push(['--tag', tag.to_s]) }
+ tags.each { |tag| tags_for_rspec.push(['--tag', tag.to_s]) }
else
- args.push(%w[--tag ~orchestrated]) unless (%w[-t --tag] & options).any?
+ tags_for_rspec.push(%w[--tag ~orchestrated]) unless (%w[-t --tag] & options).any?
end
- args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
+ tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
QA::Runtime::Env.supported_features.each_key do |key|
- args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key
+ tags_for_rspec.push(%W[--tag ~requires_#{key}]) unless QA::Runtime::Env.can_test? key
end
- args.push(options)
+ tags_for_rspec
+ end
- Runtime::Browser.configure!
+ def perform
+ args = []
+ args.push('--tty') if tty
+ args.push(rspec_tags)
+ args.push(options)
if Runtime::Env.knapsack?
- allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator
-
- QA::Runtime::Logger.info ''
- QA::Runtime::Logger.info 'Report specs:'
- QA::Runtime::Logger.info allocator.report_node_tests.join(', ')
- QA::Runtime::Logger.info ''
- QA::Runtime::Logger.info 'Leftover specs:'
- QA::Runtime::Logger.info allocator.leftover_node_tests.join(', ')
- QA::Runtime::Logger.info ''
-
- args.push(['--', allocator.node_tests])
+ args.push(paths_from_knapsack)
else
args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
end
- RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
- abort if status.nonzero?
+ if Runtime::Scenario.attributes[:parallel]
+ ParallelRunner.run(args.flatten)
+ else
+ RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+ abort if status.nonzero?
+ end
end
end
end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 0f1ed039149..92a4f7b40e6 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -91,26 +91,26 @@ describe QA::Support::Page::Logging do
it 'logs has_element?' do
expect { subject.has_element?(:element) }
- .to output(/has_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process
+ .to output(/has_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
end
it 'logs has_element? with text' do
expect { subject.has_element?(:element, text: "some text") }
- .to output(/has_element\? :element with text \"some text\" \(wait: 2\) returned: true/).to_stdout_from_any_process
+ .to output(/has_element\? :element with text \"some text\" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
end
it 'logs has_no_element?' do
allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element) }
- .to output(/has_no_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process
+ .to output(/has_no_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
end
it 'logs has_no_element? with text' do
allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element, text: "more text") }
- .to output(/has_no_element\? :element with text \"more text\" \(wait: 2\) returned: true/).to_stdout_from_any_process
+ .to output(/has_no_element\? :element with text \"more text\" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
end
it 'logs has_text?' do
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index f25dbf3a8ab..21bfd2876a9 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -8,6 +8,10 @@ if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK']
Knapsack::Adapters::RSpecAdapter.bind
end
+QA::Runtime::Browser.configure!
+
+QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes
+
%w[helpers shared_examples].each do |d|
Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f }
end
diff --git a/qa/spec/specs/parallel_runner_spec.rb b/qa/spec/specs/parallel_runner_spec.rb
new file mode 100644
index 00000000000..67d94a1f648
--- /dev/null
+++ b/qa/spec/specs/parallel_runner_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+describe QA::Specs::ParallelRunner do
+ include Helpers::StubENV
+
+ before do
+ allow(QA::Runtime::Scenario).to receive(:attributes).and_return(parallel: true)
+ stub_env('GITLAB_QA_ACCESS_TOKEN', 'skip_token_creation')
+ end
+
+ it 'passes args to parallel_tests' do
+ expect_cli_arguments(['--tag', '~orchestrated', *QA::Specs::Runner::DEFAULT_TEST_PATH_ARGS])
+
+ subject.run(['--tag', '~orchestrated', *QA::Specs::Runner::DEFAULT_TEST_PATH_ARGS])
+ end
+
+ it 'passes a given test path to parallel_tests and adds a separator' do
+ expect_cli_arguments(%w[-- qa/specs/features/foo])
+
+ subject.run(%w[qa/specs/features/foo])
+ end
+
+ it 'passes tags and test paths to parallel_tests and adds a separator' do
+ expect_cli_arguments(%w[--tag smoke -- qa/specs/features/foo qa/specs/features/bar])
+
+ subject.run(%w[--tag smoke qa/specs/features/foo qa/specs/features/bar])
+ end
+
+ it 'passes tags and test paths with separators to parallel_tests' do
+ expect_cli_arguments(%w[-- --tag smoke -- qa/specs/features/foo qa/specs/features/bar])
+
+ subject.run(%w[-- --tag smoke -- qa/specs/features/foo qa/specs/features/bar])
+ end
+
+ it 'passes supported environment variables' do
+ # Test only env vars starting with GITLAB because some of the others
+ # affect how the runner behaves, and we're not concerned with those
+ # behaviors in this test
+ gitlab_env_vars = QA::Runtime::Env::ENV_VARIABLES.reject { |v| !v.start_with?('GITLAB') }
+
+ gitlab_env_vars.each do |k, v|
+ stub_env(k, v)
+ end
+
+ gitlab_env_vars['QA_RUNTIME_SCENARIO_ATTRIBUTES'] = '{"parallel":true}'
+
+ expect_cli_arguments([], gitlab_env_vars)
+
+ subject.run([])
+ end
+
+ def expect_cli_arguments(arguments, env = { 'QA_RUNTIME_SCENARIO_ATTRIBUTES' => '{"parallel":true}' })
+ cmd = "bundle exec parallel_test -t rspec --combine-stderr --serialize-stdout -- #{arguments.join(' ')}"
+ expect(Open3).to receive(:popen2e)
+ .with(hash_including(env), cmd)
+ .and_return(0)
+ end
+end
diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb
index f94145d148e..6c533c6dc7d 100644
--- a/qa/spec/specs/runner_spec.rb
+++ b/qa/spec/specs/runner_spec.rb
@@ -58,11 +58,11 @@ describe QA::Specs::Runner do
end
end
- context 'when "-- qa/specs/features/foo" is set as options' do
- subject { described_class.new.tap { |runner| runner.options = %w[-- qa/specs/features/foo] } }
+ context 'when "--tag smoke" and "qa/specs/features/foo" are set as options' do
+ subject { described_class.new.tap { |runner| runner.options = %w[--tag smoke qa/specs/features/foo] } }
- it 'passes the given tests path and excludes the orchestrated tag' do
- expect_rspec_runner_arguments(['--tag', '~orchestrated', '--', 'qa/specs/features/foo'])
+ it 'focuses on the given tag and includes the path without excluding the orchestrated tag' do
+ expect_rspec_runner_arguments(['--tag', 'smoke', 'qa/specs/features/foo'])
subject.perform
end
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index ba0ee8dfd59..fd307ce5ab3 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -28,23 +28,6 @@ exports[`Confidential merge request project form group component renders empty s
</a>
and set the forks visiblity to private.
</span>
-
- <gllink-stub
- class="help-link"
- href="/help"
- target="_blank"
- >
- <span
- class="sr-only"
- >
- Read more
- </span>
-
- <i
- aria-hidden="true"
- class="fa fa-question-circle"
- />
- </gllink-stub>
</p>
</div>
</div>
@@ -78,23 +61,6 @@ exports[`Confidential merge request project form group component renders fork dr
</a>
and set the forks visiblity to private.
</span>
-
- <gllink-stub
- class="help-link"
- href="/help"
- target="_blank"
- >
- <span
- class="sr-only"
- >
- Read more
- </span>
-
- <i
- aria-hidden="true"
- class="fa fa-question-circle"
- />
- </gllink-stub>
</p>
</div>
</div>
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index 93b86b9b812..dec6b23d72a 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -143,6 +143,26 @@ describe GitlabSchema do
end
end
+ context 'for classes that are not ActiveRecord subclasses and have implemented .lazy_find' do
+ it 'returns the correct record' do
+ note = create(:discussion_note_on_merge_request)
+
+ result = described_class.object_from_id(note.to_global_id)
+
+ expect(result.__sync).to eq(note)
+ end
+
+ it 'batchloads the queries' do
+ note1 = create(:discussion_note_on_merge_request)
+ note2 = create(:discussion_note_on_merge_request)
+
+ expect do
+ [described_class.object_from_id(note1.to_global_id),
+ described_class.object_from_id(note2.to_global_id)].map(&:__sync)
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
context 'for other classes' do
# We cannot use an anonymous class here as `GlobalID` expects `.name` not
# to return `nil`
diff --git a/spec/graphql/types/diff_refs_type_spec.rb b/spec/graphql/types/diff_refs_type_spec.rb
new file mode 100644
index 00000000000..91017c827ad
--- /dev/null
+++ b/spec/graphql/types/diff_refs_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['DiffRefs'] do
+ it { expect(described_class.graphql_name).to eq('DiffRefs') }
+
+ it { expect(described_class).to have_graphql_fields(:base_sha, :head_sha, :start_sha) }
+end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index f73bd062369..59bd0123d88 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -13,7 +13,7 @@ describe GitlabSchema.types['MergeRequest'] do
description_html state created_at updated_at source_project target_project
project project_id source_project_id target_project_id source_branch
target_branch work_in_progress merge_when_pipeline_succeeds diff_head_sha
- merge_commit_sha user_notes_count should_remove_source_branch
+ merge_commit_sha user_notes_count should_remove_source_branch diff_refs
force_remove_source_branch merge_status in_progress_merge_commit_sha
merge_error allow_collaboration should_be_rebased rebase_commit_sha
rebase_in_progress merge_commit_message default_merge_commit_message
diff --git a/spec/graphql/types/notes/diff_position_type_spec.rb b/spec/graphql/types/notes/diff_position_type_spec.rb
index 2f8724d7f0d..345bca8f702 100644
--- a/spec/graphql/types/notes/diff_position_type_spec.rb
+++ b/spec/graphql/types/notes/diff_position_type_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe GitlabSchema.types['DiffPosition'] do
it 'exposes the expected fields' do
- expected_fields = [:head_sha, :base_sha, :start_sha, :file_path, :old_path,
+ expected_fields = [:diff_refs, :file_path, :old_path,
:new_path, :position_type, :old_line, :new_line, :x, :y,
:width, :height]
diff --git a/spec/graphql/types/notes/discussion_type_spec.rb b/spec/graphql/types/notes/discussion_type_spec.rb
index 2a1eb0efd35..ba7fc961212 100644
--- a/spec/graphql/types/notes/discussion_type_spec.rb
+++ b/spec/graphql/types/notes/discussion_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['Discussion'] do
- it { is_expected.to have_graphql_fields(:id, :created_at, :notes) }
+ it { is_expected.to have_graphql_fields(:id, :created_at, :notes, :reply_id) }
it { is_expected.to require_graphql_authorizations(:read_note) }
end
diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
new file mode 100644
index 00000000000..772c94e3180
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::InlineMetricsFilter do
+ include FilterSpecHelper
+
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ context 'when the document has an external link' do
+ let(:url) { 'https://foo.com' }
+
+ it 'leaves regular non-metrics links unchanged' do
+ expect(doc.to_s).to eq input
+ end
+ end
+
+ context 'when the document has a metrics dashboard link' do
+ let(:params) { ['foo', 'bar', 12] }
+ let(:url) { urls.metrics_namespace_project_environment_url(*params) }
+
+ it 'leaves the original link unchanged' do
+ expect(doc.at_css('a').to_s).to eq input
+ end
+
+ it 'appends a metrics charts placeholder with dashboard url after metrics links' do
+ node = doc.at_css('.js-render-metrics')
+ expect(node).to be_present
+
+ dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params, embedded: true)
+ expect(node.attribute('data-dashboard-url').to_s).to eq dashboard_url
+ end
+
+ context 'when the metrics dashboard link is part of a paragraph' do
+ let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
+ let(:input) { %(<p>#{paragraph}</p>) }
+
+ it 'appends the charts placeholder after the enclosing paragraph' do
+ expect(doc.at_css('p').to_s).to include paragraph
+ expect(doc.at_css('.js-render-metrics')).to be_present
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(gfm_embedded_metrics: false)
+ end
+
+ it 'does nothing' do
+ expect(doc.to_s).to eq input
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
new file mode 100644
index 00000000000..fb2186e9d12
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::InlineMetricsRedactorFilter do
+ include FilterSpecHelper
+
+ set(:project) { create(:project) }
+
+ let(:url) { urls.metrics_dashboard_project_environment_url(project, 1, embedded: true) }
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(gfm_embedded_metrics: false)
+ end
+
+ it 'does nothing' do
+ expect(doc.to_s).to eq input
+ end
+ end
+
+ context 'without a metrics charts placeholder' do
+ it 'leaves regular non-metrics links unchanged' do
+ expect(doc.to_s).to eq input
+ end
+ end
+
+ context 'with a metrics charts placeholder' do
+ let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
+
+ context 'no user is logged in' do
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+
+ context 'the user does not have permission do see charts' do
+ let(:doc) { filter(input, current_user: build(:user)) }
+
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+
+ context 'the user has requisite permissions' do
+ let(:user) { create(:user) }
+ let(:doc) { filter(input, current_user: user) }
+
+ it 'leaves the placeholder' do
+ project.add_maintainer(user)
+
+ expect(doc.to_s).to eq input
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/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/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
new file mode 100644
index 00000000000..34bc6359414
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Url do
+ describe '#regex' do
+ it 'returns a regular expression' do
+ expect(described_class.regex).to be_a Regexp
+ end
+
+ it 'matches a metrics dashboard link with named params' do
+ url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url('foo', 'bar', 1, start: 123345456, anchor: 'title')
+
+ expected_params = {
+ 'url' => url,
+ 'namespace' => 'foo',
+ 'project' => 'bar',
+ 'environment' => '1',
+ 'query' => '?start=123345456',
+ 'anchor' => '#title'
+ }
+
+ expect(described_class.regex).to match url
+
+ described_class.regex.match(url) do |m|
+ expect(m.named_captures).to eq expected_params
+ end
+ end
+
+ it 'does not match other gitlab urls that contain the term metrics' do
+ url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json)
+
+ expect(described_class.regex).not_to match url
+ end
+
+ it 'does not match other gitlab urls' do
+ url = Gitlab.config.gitlab.url
+
+ expect(described_class.regex).not_to match url
+ end
+
+ it 'does not match non-gitlab urls' do
+ url = 'https://www.super_awesome_site.com/'
+
+ expect(described_class.regex).not_to match url
+ end
+ end
+
+ describe '#build_dashboard_url' do
+ it 'builds the url for the dashboard endpoint' do
+ url = described_class.build_dashboard_url('foo', 'bar', 1)
+
+ expect(url).to match described_class.regex
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 090e456644f..4b697b2ba0f 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
subject { described_class.new(1.second) }
describe '#sample' do
- let(:unicorn) { double('unicorn') }
+ let(:unicorn) { Module.new }
let(:raindrops) { double('raindrops') }
let(:stats) { double('stats') }
@@ -78,19 +78,32 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
end
end
- context 'additional metrics' do
- let(:unicorn_workers) { 2 }
-
+ context 'unicorn workers' do
before do
- allow(unicorn).to receive(:listener_names).and_return([""])
- allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14)
- allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers)
+ allow(unicorn).to receive(:listener_names).and_return([])
end
- it "sets additional metrics" do
- expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers)
+ context 'without http server' do
+ it "does set unicorn_workers to 0" do
+ expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 0)
- subject.sample
+ subject.sample
+ end
+ end
+
+ context 'with http server' do
+ let(:http_server_class) { Struct.new(:worker_processes) }
+ let!(:http_server) { http_server_class.new(5) }
+
+ before do
+ stub_const('Unicorn::HttpServer', http_server_class)
+ end
+
+ it "sets additional metrics" do
+ expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 5)
+
+ subject.sample
+ end
end
end
end
diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb
new file mode 100644
index 00000000000..da13b6df53b
--- /dev/null
+++ b/spec/lib/peek/views/redis_detailed_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Peek::Views::RedisDetailed do
+ let(:redis_detailed_class) do
+ Class.new do
+ include Peek::Views::RedisDetailed
+ end
+ end
+
+ subject { redis_detailed_class.new }
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:cmd, :expected) do
+ [:auth, 'test'] | 'auth <redacted>'
+ [:set, 'key', 'value'] | 'set key <redacted>'
+ [:set, 'bad'] | 'set bad'
+ [:hmset, 'key1', 'value1', 'key2', 'value2'] | 'hmset key1 <redacted>'
+ [:get, 'key'] | 'get key'
+ end
+
+ with_them do
+ it 'scrubs Redis commands', :request_store do
+ subject.detail_store << { cmd: cmd, duration: 1.second }
+
+ expect(subject.details.count).to eq(1)
+ expect(subject.details.first)
+ .to eq({
+ cmd: expected,
+ duration: 1000
+ })
+ 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/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 28fa5d12d9c..468e7c286d5 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -480,6 +480,22 @@ describe Issues::UpdateService, :mailer do
update_issue(description: "- [x] Task 1\n- [X] Task 2")
end
+ it 'does not check for spam on task status change' do
+ params = {
+ update_task: {
+ index: 1,
+ checked: false,
+ line_source: '- [x] Task 1',
+ line_number: 1
+ }
+ }
+ service = described_class.new(project, user, params)
+
+ expect(service).not_to receive(:spam_check)
+
+ service.execute(issue)
+ end
+
it 'creates system note about task status change' do
note1 = find_note('marked the task **Task 1** as completed')
note2 = find_note('marked the task **Task 2** as completed')
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"