Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-11 18:10:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-11 18:10:08 +0300
commit9dde2726710184f066387d044fce4ae2b3684210 (patch)
tree141da0dfc25da6b1724329a3d5cf2d51c7d45937
parent03b5d94c2c145491bd493837ec50a36e5d1d2612 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue82
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql5
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql11
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql10
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql8
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_actions.vue12
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb7
-rw-r--r--app/graphql/resolvers/ci/pipeline_stages_resolver.rb21
-rw-r--r--app/graphql/types/ci/group_type.rb17
-rw-r--r--app/graphql/types/ci/job_type.rb15
-rw-r--r--app/graphql/types/ci/pipeline_type.rb4
-rw-r--r--app/graphql/types/ci/stage_type.rb15
-rw-r--r--app/presenters/alert_management/alert_presenter.rb8
-rw-r--r--app/presenters/projects/prometheus/alert_presenter.rb10
-rw-r--r--app/services/award_emojis/copy_service.rb30
-rw-r--r--app/services/incident_management/create_issue_service.rb21
-rw-r--r--app/services/issuable/clone/base_service.rb30
-rw-r--r--app/services/issuable/clone/content_rewriter.rb74
-rw-r--r--app/services/markdown_content_rewriter_service.rb28
-rw-r--r--app/services/notes/copy_service.rb70
-rw-r--r--app/services/packages/maven/find_or_create_package_service.rb39
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/workers/incident_management/process_alert_worker.rb10
-rw-r--r--changelogs/unreleased/215088-add-alert-url-to-issue-description.yml5
-rw-r--r--changelogs/unreleased/222359-alert-todo.yml5
-rw-r--r--changelogs/unreleased/225213-unfurl-search-page.yml5
-rw-r--r--changelogs/unreleased/38338-gradle-packages-group.yml5
-rw-r--r--changelogs/unreleased/bug-incorrect-issue-url-in-vsa.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql231
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json643
-rw-r--r--doc/api/graphql/reference/index.md19
-rw-r--r--doc/raketasks/cleanup.md4
-rw-r--r--doc/university/training/end-user/README.md369
-rw-r--r--doc/user/application_security/configuration/index.md7
-rw-r--r--doc/user/application_security/index.md6
-rw-r--r--doc/user/application_security/sast/index.md15
-rw-r--r--lib/gitlab/alerting/alert.rb12
-rw-r--r--lib/gitlab/analytics/cycle_analytics/records_fetcher.rb4
-rw-r--r--lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml2
-rw-r--r--lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml23
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json6
-rw-r--r--qa/qa/page/project/operations/metrics/show.rb4
-rw-r--r--qa/qa/service/praefect_manager.rb1
-rw-r--r--qa/qa/specs/features/api/3_create/repository/backend_node_recovery_spec.rb1
-rw-r--r--spec/controllers/groups/shared_projects_controller_spec.rb6
-rw-r--r--spec/factories/packages.rb2
-rw-r--r--spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js57
-rw-r--r--spec/frontend/alert_management/mocks/alerts.json9
-rw-r--r--spec/graphql/types/ci/group_type_spec.rb17
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb16
-rw-r--r--spec/graphql/types/ci/stage_type_spec.rb16
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb14
-rw-r--r--spec/presenters/alert_management/alert_presenter_spec.rb9
-rw-r--r--spec/presenters/alert_management/prometheus_alert_presenter_spec.rb7
-rw-r--r--spec/presenters/projects/prometheus/alert_presenter_spec.rb13
-rw-r--r--spec/requests/api/graphql/ci/groups_spec.rb55
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb93
-rw-r--r--spec/requests/api/graphql/ci/stages_spec.rb46
-rw-r--r--spec/serializers/analytics_issue_entity_spec.rb25
-rw-r--r--spec/services/award_emojis/copy_service_spec.rb34
-rw-r--r--spec/services/incident_management/create_issue_service_spec.rb6
-rw-r--r--spec/services/issuable/clone/content_rewriter_spec.rb184
-rw-r--r--spec/services/issues/move_service_spec.rb66
-rw-r--r--spec/services/markdown_content_rewriter_service_spec.rb48
-rw-r--r--spec/services/notes/copy_service_spec.rb157
-rw-r--r--spec/services/packages/maven/find_or_create_package_service_spec.rb85
-rw-r--r--spec/spec_helper.rb15
-rw-r--r--spec/support/helpers/stub_feature_flags.rb31
-rw-r--r--spec/support/helpers/stubbed_feature.rb49
-rw-r--r--spec/support_specs/helpers/stub_feature_flags_spec.rb36
-rw-r--r--spec/views/search/show.html.haml_spec.rb32
-rw-r--r--spec/workers/incident_management/process_alert_worker_spec.rb4
-rw-r--r--yarn.lock26
75 files changed, 2233 insertions, 836 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a34b3216f7a..dfee4c122f3 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0e49dee763f596ca9223e88cd68b2f09a56d68a7
+6730c101d0be2db5155b6e2c4de689dc906337f4
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
index f5b708cf48f..5bd69a1f0ec 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
@@ -1,8 +1,9 @@
<script>
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
-import axios from '~/lib/utils/axios_utils';
-import createAlertTodo from '../../graphql/mutations/alert_todo_create.graphql';
+import createAlertTodo from '../../graphql/mutations/alert_todo_create.mutation.graphql';
+import todoMarkDone from '../../graphql/mutations/alert_todo_mark_done.mutation.graphql';
+import alertQuery from '../../graphql/queries/details.query.graphql';
export default {
i18n: {
@@ -30,14 +31,24 @@ export default {
data() {
return {
isUpdating: false,
- isTodo: false,
- todo: '',
};
},
computed: {
alertID() {
return parseInt(this.alert.iid, 10);
},
+ firstToDoId() {
+ return this.alert?.todos?.nodes[0]?.id;
+ },
+ hasPendingTodos() {
+ return this.alert?.todos?.nodes.length > 0;
+ },
+ getAlertQueryVariables() {
+ return {
+ fullPath: this.projectPath,
+ alertId: this.alert.iid,
+ };
+ },
},
methods: {
updateToDoCount(add) {
@@ -51,11 +62,7 @@ export default {
return document.dispatchEvent(headerTodoEvent);
},
- toggleTodo() {
- if (this.todo) {
- return this.markAsDone();
- }
-
+ addToDo() {
this.isUpdating = true;
return this.$apollo
.mutate({
@@ -65,24 +72,14 @@ export default {
projectPath: this.projectPath,
},
})
- .then(({ data: { alertTodoCreate: { todo = {}, errors = [] } } = {} } = {}) => {
+ .then(({ data: { errors = [] } }) => {
if (errors[0]) {
- return this.$emit(
- 'alert-error',
- `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${errors[0]}.`,
- );
+ return this.throwError(errors[0]);
}
-
- this.todo = todo.id;
return this.updateToDoCount(true);
})
.catch(() => {
- this.$emit(
- 'alert-error',
- `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${s__(
- 'AlertManagement|Please try again.',
- )}`,
- );
+ this.throwError();
})
.finally(() => {
this.isUpdating = false;
@@ -90,20 +87,45 @@ export default {
},
markAsDone() {
this.isUpdating = true;
-
- return axios
- .delete(`/dashboard/todos/${this.todo.split('/').pop()}`)
- .then(() => {
- this.todo = '';
+ return this.$apollo
+ .mutate({
+ mutation: todoMarkDone,
+ variables: {
+ id: this.firstToDoId,
+ },
+ update: this.updateCache,
+ })
+ .then(({ data: { errors = [] } }) => {
+ if (errors[0]) {
+ return this.throwError(errors[0]);
+ }
return this.updateToDoCount(false);
})
.catch(() => {
- this.$emit('alert-error', this.$options.i18n.UPDATE_ALERT_TODO_ERROR);
+ this.throwError();
})
.finally(() => {
this.isUpdating = false;
});
},
+ updateCache(store) {
+ const data = store.readQuery({
+ query: alertQuery,
+ variables: this.getAlertQueryVariables,
+ });
+
+ data.project.alertManagementAlerts.nodes[0].todos.nodes.shift();
+
+ store.writeQuery({
+ query: alertQuery,
+ variables: this.getAlertQueryVariables,
+ data,
+ });
+ },
+ throwError(err = '') {
+ const error = err || s__('AlertManagement|Please try again.');
+ this.$emit('alert-error', `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${error}`);
+ },
},
};
</script>
@@ -114,10 +136,10 @@ export default {
data-testid="alert-todo-button"
:collapsed="sidebarCollapsed"
:issuable-id="alertID"
- :is-todo="todo !== ''"
+ :is-todo="hasPendingTodos"
:is-action-active="isUpdating"
issuable-type="alert"
- @toggleTodo="toggleTodo"
+ @toggleTodo="hasPendingTodos ? markAsDone() : addToDo()"
/>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
index 18fab429164..92eb828bdf8 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
+++ b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
@@ -11,6 +11,11 @@ fragment AlertDetailItem on AlertManagementAlert {
updatedAt
endedAt
details
+ todos {
+ nodes {
+ id
+ }
+ }
notes {
nodes {
...AlertNote
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql
deleted file mode 100644
index cdf3d763302..00000000000
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql
+++ /dev/null
@@ -1,11 +0,0 @@
-mutation($projectPath: ID!, $iid: String!) {
- alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
- errors
- alert {
- iid
- }
- todo {
- id
- }
- }
-}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql
new file mode 100644
index 00000000000..ac9858c104f
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/detail_item.fragment.graphql"
+
+mutation alertTodoCreate($projectPath: ID!, $iid: String!) {
+ alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
+ errors
+ alert {
+ ...AlertDetailItem
+ }
+ }
+}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql
new file mode 100644
index 00000000000..4d59b4d94cd
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql
@@ -0,0 +1,8 @@
+mutation todoMarkDone($id: ID!) {
+ todoMarkDone(input: { id: $id }) {
+ errors
+ todo {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
index 0e3fd70b17b..df03cc126b2 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
@@ -44,27 +44,27 @@ export default {
<template>
<div>
- <gl-button-group class="flex-column flex-md-row ml-0 ml-md-n4">
+ <gl-button-group class="gl-flex-direction-column flex-md-row gl-ml-0 ml-md-n4">
<gl-deprecated-button
:key="ignoreBtn.status"
:ref="`${ignoreBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
- class="d-block mb-2 mb-md-0 w-100"
+ class="gl-display-block gl-mb-4 mb-md-0 gl-w-full"
:title="ignoreBtn.title"
@click="$emit('update-issue-status', { errorId: error.id, status: ignoreBtn.status })"
>
- <gl-icon class="d-none d-md-inline m-0" :name="ignoreBtn.icon" :size="12" />
+ <gl-icon class="gl-display-none d-md-inline gl-m-0" :name="ignoreBtn.icon" :size="12" />
<span class="d-md-none">{{ ignoreBtn.title }}</span>
</gl-deprecated-button>
<gl-deprecated-button
:key="resolveBtn.status"
:ref="`${resolveBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
- class="d-block mb-2 mb-md-0 w-100"
+ class="gl-display-block gl-mb-4 mb-md-0 gl-w-full"
:title="resolveBtn.title"
@click="$emit('update-issue-status', { errorId: error.id, status: resolveBtn.status })"
>
- <gl-icon class="d-none d-md-inline m-0" :name="resolveBtn.icon" :size="12" />
+ <gl-icon class="gl-display-none d-md-inline gl-m-0" :name="resolveBtn.icon" :size="12" />
<span class="d-md-none">{{ resolveBtn.title }}</span>
</gl-deprecated-button>
</gl-button-group>
@@ -72,7 +72,7 @@ export default {
:href="detailsLink"
category="secondary"
variant="info"
- class="d-block d-md-none mb-2 mb-md-0"
+ class="gl-display-block d-md-none gl-mb-4 mb-md-0"
>
{{ __('More details') }}
</gl-deprecated-button>
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 706a4843117..6a393405e4d 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -27,6 +27,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
user = User.by_login(params[:username])
user&.increment_failed_attempts!
+ log_failed_login(params[:username], failed_strategy.name)
end
super
@@ -90,6 +91,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
+ def log_failed_login(user, provider)
+ # overridden in EE
+ end
+
def after_omniauth_failure_path_for(scope)
if Feature.enabled?(:user_mode_in_session)
return new_admin_session_path if current_user_mode.admin_mode_requested?
@@ -198,6 +203,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def fail_login(user)
+ log_failed_login(user.username, oauth['provider'])
+
error_message = user.errors.full_messages.to_sentence
redirect_to omniauth_error_path(oauth['provider'], error: error_message)
diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
new file mode 100644
index 00000000000..f9817d8b97b
--- /dev/null
+++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class PipelineStagesResolver < BaseResolver
+ include LooksAhead
+
+ alias_method :pipeline, :object
+
+ def resolve_with_lookahead
+ apply_lookahead(pipeline.stages)
+ end
+
+ def preloads
+ {
+ statuses: [:needs]
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/group_type.rb b/app/graphql/types/ci/group_type.rb
new file mode 100644
index 00000000000..04c0eb93068
--- /dev/null
+++ b/app/graphql/types/ci/group_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class GroupType < BaseObject
+ graphql_name 'CiGroup'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job group'
+ field :size, GraphQL::INT_TYPE, null: true,
+ description: 'Size of the group'
+ field :jobs, Ci::JobType.connection_type, null: true,
+ description: 'Jobs in group'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
new file mode 100644
index 00000000000..4c18f3ffd52
--- /dev/null
+++ b/app/graphql/types/ci/job_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class JobType < BaseObject
+ graphql_name 'CiJob'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job'
+ field :needs, JobType.connection_type, null: true,
+ description: 'Builds that must complete before the jobs run'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 32050766e5b..179a5393b17 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -37,6 +37,10 @@ module Types
description: "Timestamp of the pipeline's completion"
field :committed_at, Types::TimeType, null: true,
description: "Timestamp of the pipeline's commit"
+ field :stages, Types::Ci::StageType.connection_type, null: true,
+ description: 'Stages of the pipeline',
+ extras: [:lookahead],
+ resolver: Resolvers::Ci::PipelineStagesResolver
# TODO: Add triggering user as a type
end
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
new file mode 100644
index 00000000000..278c4d4d748
--- /dev/null
+++ b/app/graphql/types/ci/stage_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class StageType < BaseObject
+ graphql_name 'CiStage'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the stage'
+ field :groups, Ci::GroupType.connection_type, null: true,
+ description: 'Group of jobs for the stage'
+ end
+ end
+end
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index d6ebc8e18b1..c3067e6377f 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -47,6 +47,13 @@ module AlertManagement
private
+ def details_url
+ ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
+ project,
+ alert.iid
+ )
+ end
+
attr_reader :alert, :project
def alerting_alert
@@ -67,6 +74,7 @@ module AlertManagement
metadata << list_item('Monitoring tool', monitoring_tool) if monitoring_tool
metadata << list_item('Hosts', host_links) if hosts.any?
metadata << list_item('Description', description) if description.present?
+ metadata << list_item('GitLab alert', details_url) if details_url.present?
metadata.join(MARKDOWN_LINE_BREAK)
end
diff --git a/app/presenters/projects/prometheus/alert_presenter.rb b/app/presenters/projects/prometheus/alert_presenter.rb
index 1cf8b202810..14a51b4a4fc 100644
--- a/app/presenters/projects/prometheus/alert_presenter.rb
+++ b/app/presenters/projects/prometheus/alert_presenter.rb
@@ -77,6 +77,15 @@ module Projects
end
end
+ def details_url
+ return unless am_alert
+
+ ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
+ project,
+ am_alert.iid
+ )
+ end
+
private
def alert_title
@@ -97,6 +106,7 @@ module Projects
metadata << list_item(service.label.humanize, service.value) if service
metadata << list_item(monitoring_tool.label.humanize, monitoring_tool.value) if monitoring_tool
metadata << list_item(hosts.label.humanize, host_links) if hosts
+ metadata << list_item('GitLab alert', details_url) if details_url
metadata.join(MARKDOWN_LINE_BREAK)
end
diff --git a/app/services/award_emojis/copy_service.rb b/app/services/award_emojis/copy_service.rb
new file mode 100644
index 00000000000..2e500d4c697
--- /dev/null
+++ b/app/services/award_emojis/copy_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# This service copies AwardEmoji from one Awardable to another.
+#
+# It expects the calling code to have performed the necessary authorization
+# checks in order to allow the copy to happen.
+module AwardEmojis
+ class CopyService
+ def initialize(from_awardable, to_awardable)
+ raise ArgumentError, 'Awardables must be different' if from_awardable == to_awardable
+
+ @from_awardable = from_awardable
+ @to_awardable = to_awardable
+ end
+
+ def execute
+ from_awardable.award_emoji.find_each do |award|
+ new_award = award.dup
+ new_award.awardable = to_awardable
+ new_award.save!
+ end
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_accessor :from_awardable, :to_awardable
+ end
+end
diff --git a/app/services/incident_management/create_issue_service.rb b/app/services/incident_management/create_issue_service.rb
index 34c70b002b6..d1c5c6752d4 100644
--- a/app/services/incident_management/create_issue_service.rb
+++ b/app/services/incident_management/create_issue_service.rb
@@ -5,13 +5,16 @@ module IncidentManagement
include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
- def initialize(project, params)
- super(project, User.alert_bot, params)
+ attr_reader :alert
+
+ def initialize(project, alert)
+ super(project, User.alert_bot)
+ @alert = alert
end
def execute
return error('setting disabled') unless incident_management_setting.create_issue?
- return error('invalid alert') unless alert.valid?
+ return error('invalid alert') unless alert_presenter.valid?
result = create_incident
return error(result.message, result.payload[:issue]) unless result.success?
@@ -31,7 +34,7 @@ module IncidentManagement
end
def issue_title
- alert.full_title
+ alert_presenter.full_title
end
def issue_description
@@ -45,16 +48,16 @@ module IncidentManagement
end
def alert_summary
- alert.issue_summary_markdown
+ alert_presenter.issue_summary_markdown
end
def alert_markdown
- alert.alert_markdown
+ alert_presenter.alert_markdown
end
- def alert
- strong_memoize(:alert) do
- Gitlab::Alerting::Alert.new(project: project, payload: params).present
+ def alert_presenter
+ strong_memoize(:alert_presenter) do
+ Gitlab::Alerting::Alert.for_alert_management_alert(project: project, alert: alert).present
end
end
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index 0d1640924e5..639423ed4bf 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -24,12 +24,34 @@ module Issuable
private
+ def copy_award_emoji
+ AwardEmojis::CopyService.new(original_entity, new_entity).execute
+ end
+
+ def copy_notes
+ Notes::CopyService.new(current_user, original_entity, new_entity).execute
+ end
+
def update_new_entity
- rewriters = [ContentRewriter, AttributesRewriter]
+ update_new_entity_description
+ update_new_entity_attributes
+ copy_award_emoji
+ copy_notes
+ end
- rewriters.each do |rewriter|
- rewriter.new(current_user, original_entity, new_entity).execute
- end
+ def update_new_entity_description
+ rewritten_description = MarkdownContentRewriterService.new(
+ current_user,
+ original_entity.description,
+ original_entity.project,
+ new_entity.project
+ ).execute
+
+ new_entity.update!(description: rewritten_description)
+ end
+
+ def update_new_entity_attributes
+ AttributesRewriter.new(current_user, original_entity, new_entity).execute
end
def update_old_entity
diff --git a/app/services/issuable/clone/content_rewriter.rb b/app/services/issuable/clone/content_rewriter.rb
deleted file mode 100644
index 67d2f9fd3fe..00000000000
--- a/app/services/issuable/clone/content_rewriter.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module Issuable
- module Clone
- class ContentRewriter < ::Issuable::Clone::BaseService
- def initialize(current_user, original_entity, new_entity)
- @current_user = current_user
- @original_entity = original_entity
- @new_entity = new_entity
- @project = original_entity.project
- end
-
- def execute
- rewrite_description
- rewrite_award_emoji(original_entity, new_entity)
- rewrite_notes
- end
-
- private
-
- def rewrite_description
- new_entity.update(description: rewrite_content(original_entity.description))
- end
-
- def rewrite_notes
- new_discussion_ids = {}
- original_entity.notes_with_associations.find_each do |note|
- new_note = note.dup
- new_discussion_ids[note.discussion_id] ||= Discussion.discussion_id(new_note)
- new_params = {
- project: new_entity.project,
- noteable: new_entity,
- discussion_id: new_discussion_ids[note.discussion_id],
- note: rewrite_content(new_note.note),
- note_html: nil,
- created_at: note.created_at,
- updated_at: note.updated_at
- }
-
- if note.system_note_metadata
- new_params[:system_note_metadata] = note.system_note_metadata.dup
-
- # TODO: Implement copying of description versions when an issue is moved
- # https://gitlab.com/gitlab-org/gitlab/issues/32300
- new_params[:system_note_metadata].description_version = nil
- end
-
- new_note.update(new_params)
-
- rewrite_award_emoji(note, new_note)
- end
- end
-
- def rewrite_content(content)
- return unless content
-
- rewriters = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter]
-
- rewriters.inject(content) do |text, klass|
- rewriter = klass.new(text, old_project, current_user)
- rewriter.rewrite(new_parent)
- end
- end
-
- def rewrite_award_emoji(old_awardable, new_awardable)
- old_awardable.award_emoji.each do |award|
- new_award = award.dup
- new_award.awardable = new_awardable
- new_award.save
- end
- end
- end
- end
-end
diff --git a/app/services/markdown_content_rewriter_service.rb b/app/services/markdown_content_rewriter_service.rb
new file mode 100644
index 00000000000..f945990a1b4
--- /dev/null
+++ b/app/services/markdown_content_rewriter_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# This service passes Markdown content through our GFM rewriter classes
+# which rewrite references to GitLab objects and uploads within the content
+# based on their visibility by the `target_parent`.
+class MarkdownContentRewriterService
+ REWRITERS = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter].freeze
+
+ def initialize(current_user, content, source_parent, target_parent)
+ @current_user = current_user
+ @content = content.presence
+ @source_parent = source_parent
+ @target_parent = target_parent
+ end
+
+ def execute
+ return unless content
+
+ REWRITERS.inject(content) do |text, klass|
+ rewriter = klass.new(text, source_parent, current_user)
+ rewriter.rewrite(target_parent)
+ end
+ end
+
+ private
+
+ attr_reader :current_user, :content, :source_parent, :target_parent
+end
diff --git a/app/services/notes/copy_service.rb b/app/services/notes/copy_service.rb
new file mode 100644
index 00000000000..31aaa219922
--- /dev/null
+++ b/app/services/notes/copy_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+# This service copies Notes from one Noteable to another.
+#
+# It expects the calling code to have performed the necessary authorization
+# checks in order to allow the copy to happen.
+module Notes
+ class CopyService
+ def initialize(current_user, from_noteable, to_noteable)
+ raise ArgumentError, 'Noteables must be different' if from_noteable == to_noteable
+
+ @current_user = current_user
+ @from_noteable = from_noteable
+ @to_noteable = to_noteable
+ @from_project = from_noteable.project
+ @to_project = to_noteable.project
+ @new_discussion_ids = {}
+ end
+
+ def execute
+ from_noteable.notes_with_associations.find_each do |note|
+ copy_note(note)
+ end
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :from_noteable, :to_noteable, :from_project, :to_project,
+ :current_user, :new_discussion_ids
+
+ def copy_note(note)
+ new_note = note.dup
+ new_params = params_from_note(note, new_note)
+ new_note.update!(new_params)
+
+ copy_award_emoji(note, new_note)
+ end
+
+ def params_from_note(note, new_note)
+ new_discussion_ids[note.discussion_id] ||= Discussion.discussion_id(new_note)
+ rewritten_note = MarkdownContentRewriterService.new(current_user, note.note, from_project, to_project).execute
+
+ new_params = {
+ project: to_noteable.project,
+ noteable: to_noteable,
+ discussion_id: new_discussion_ids[note.discussion_id],
+ note: rewritten_note,
+ note_html: nil,
+ created_at: note.created_at,
+ updated_at: note.updated_at
+ }
+
+ if note.system_note_metadata
+ new_params[:system_note_metadata] = note.system_note_metadata.dup
+
+ # TODO: Implement copying of description versions when an issue is moved
+ # https://gitlab.com/gitlab-org/gitlab/issues/32300
+ new_params[:system_note_metadata].description_version = nil
+ end
+
+ new_params
+ end
+
+ def copy_award_emoji(from_note, to_note)
+ AwardEmojis::CopyService.new(from_note, to_note).execute
+ end
+ end
+end
diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb
index 50a008843ad..505f45a7b21 100644
--- a/app/services/packages/maven/find_or_create_package_service.rb
+++ b/app/services/packages/maven/find_or_create_package_service.rb
@@ -3,21 +3,33 @@ module Packages
module Maven
class FindOrCreatePackageService < BaseService
MAVEN_METADATA_FILE = 'maven-metadata.xml'.freeze
+ SNAPSHOT_TERM = '-SNAPSHOT'.freeze
def execute
- package = ::Packages::Maven::PackageFinder
- .new(params[:path], current_user, project: project).execute
+ package =
+ ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project)
+ .execute
unless package
- if params[:file_name] == MAVEN_METADATA_FILE
- # Maven uploads several files during `mvn deploy` in next order:
- # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
- # - my-company/my-app/1.0-SNAPSHOT/my-app.pom
- # - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
- # - my-company/my-app/maven-metadata.xml
- #
- # The last xml file does not have VERSION in URL because it contains
- # information about all versions.
+ # Maven uploads several files during `mvn deploy` in next order:
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.pom
+ # - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
+ # - my-company/my-app/maven-metadata.xml
+ #
+ # The last xml file does not have VERSION in URL because it contains
+ # information about all versions. When uploading such file, we create
+ # a package with a version set to `nil`. The xml file with a version
+ # is only created and uploaded for snapshot versions.
+ #
+ # Gradle has a different upload order:
+ # - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.pom
+ # - my-company/my-app/maven-metadata.xml
+ #
+ # The first upload has to create the proper package (the one with the version set).
+ if params[:file_name] == MAVEN_METADATA_FILE && !params[:path]&.ends_with?(SNAPSHOT_TERM)
package_name, version = params[:path], nil
else
package_name, _, version = params[:path].rpartition('/')
@@ -30,8 +42,9 @@ module Packages
build: params[:build]
}
- package = ::Packages::Maven::CreatePackageService
- .new(project, current_user, package_params).execute
+ package =
+ ::Packages::Maven::CreatePackageService.new(project, current_user, package_params)
+ .execute
end
package
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 869890cdf31..18eaccb46b2 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -2,6 +2,10 @@
- page_title @search_term
- @hide_breadcrumbs = true
+- if @search_results
+ - page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
+ - page_card_attributes("Namespace" => @group&.full_path, "Project" => @project&.full_path)
+
.page-title-holder.d-sm-flex.align-items-sm-center
%h1.page-title<
= _('Search')
diff --git a/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
index 26c86a3aa2b..a8c6c9aa121 100644
--- a/app/workers/incident_management/process_alert_worker.rb
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -29,17 +29,9 @@ module IncidentManagement
AlertManagement::Alert.find_by_id(alert_id)
end
- def parsed_payload(alert)
- if alert.prometheus?
- alert.payload
- else
- Gitlab::Alerting::NotificationPayloadParser.call(alert.payload.to_h, alert.project)
- end
- end
-
def create_issue_for(alert)
IncidentManagement::CreateIssueService
- .new(alert.project, parsed_payload(alert))
+ .new(alert.project, alert)
.execute
end
diff --git a/changelogs/unreleased/215088-add-alert-url-to-issue-description.yml b/changelogs/unreleased/215088-add-alert-url-to-issue-description.yml
new file mode 100644
index 00000000000..c3682301fab
--- /dev/null
+++ b/changelogs/unreleased/215088-add-alert-url-to-issue-description.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert url into incident issue markdown
+merge_request: 38649
+author:
+type: added
diff --git a/changelogs/unreleased/222359-alert-todo.yml b/changelogs/unreleased/222359-alert-todo.yml
new file mode 100644
index 00000000000..a666226b529
--- /dev/null
+++ b/changelogs/unreleased/222359-alert-todo.yml
@@ -0,0 +1,5 @@
+---
+title: Add Mark as done capability to Alert To Do's
+merge_request: 38595
+author:
+type: changed
diff --git a/changelogs/unreleased/225213-unfurl-search-page.yml b/changelogs/unreleased/225213-unfurl-search-page.yml
new file mode 100644
index 00000000000..0ff459879cc
--- /dev/null
+++ b/changelogs/unreleased/225213-unfurl-search-page.yml
@@ -0,0 +1,5 @@
+---
+title: Improve unfurling support for /search
+merge_request: 38699
+author:
+type: other
diff --git a/changelogs/unreleased/38338-gradle-packages-group.yml b/changelogs/unreleased/38338-gradle-packages-group.yml
new file mode 100644
index 00000000000..8e63ccaf4ba
--- /dev/null
+++ b/changelogs/unreleased/38338-gradle-packages-group.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a Gradle bug where a package without a version would be created and thus not displayed on the UI.
+merge_request: 38338
+author:
+type: fixed
diff --git a/changelogs/unreleased/bug-incorrect-issue-url-in-vsa.yml b/changelogs/unreleased/bug-incorrect-issue-url-in-vsa.yml
new file mode 100644
index 00000000000..bae76988db7
--- /dev/null
+++ b/changelogs/unreleased/bug-incorrect-issue-url-in-vsa.yml
@@ -0,0 +1,5 @@
+---
+title: Fix URLs of issues in VSA dashboard
+merge_request: 38703
+author:
+type: fixed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index ee55360e5f5..19fddf795b1 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -1347,6 +1347,212 @@ type Branch {
name: String!
}
+type CiGroup {
+ """
+ Jobs in group
+ """
+ jobs(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): CiJobConnection
+
+ """
+ Name of the job group
+ """
+ name: String
+
+ """
+ Size of the group
+ """
+ size: Int
+}
+
+"""
+The connection type for CiGroup.
+"""
+type CiGroupConnection {
+ """
+ A list of edges.
+ """
+ edges: [CiGroupEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [CiGroup]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type CiGroupEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: CiGroup
+}
+
+type CiJob {
+ """
+ Name of the job
+ """
+ name: String
+
+ """
+ Builds that must complete before the jobs run
+ """
+ needs(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): CiJobConnection
+}
+
+"""
+The connection type for CiJob.
+"""
+type CiJobConnection {
+ """
+ A list of edges.
+ """
+ edges: [CiJobEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [CiJob]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type CiJobEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: CiJob
+}
+
+type CiStage {
+ """
+ Group of jobs for the stage
+ """
+ groups(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): CiGroupConnection
+
+ """
+ Name of the stage
+ """
+ name: String
+}
+
+"""
+The connection type for CiStage.
+"""
+type CiStageConnection {
+ """
+ A list of edges.
+ """
+ edges: [CiStageEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [CiStage]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type CiStageEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: CiStage
+}
+
type Commit {
"""
Author of the commit
@@ -9851,6 +10057,31 @@ type Pipeline {
sha: String!
"""
+ Stages of the pipeline
+ """
+ stages(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): CiStageConnection
+
+ """
Timestamp when the pipeline was started
"""
startedAt: Time
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 3fe75338d84..4dc59cdfeb7 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -3641,6 +3641,596 @@
},
{
"kind": "OBJECT",
+ "name": "CiGroup",
+ "description": null,
+ "fields": [
+ {
+ "name": "jobs",
+ "description": "Jobs in group",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiJobConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the job group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "size",
+ "description": "Size of the group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiGroupConnection",
+ "description": "The connection type for CiGroup.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiGroupEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiGroup",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiGroupEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiGroup",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiJob",
+ "description": null,
+ "fields": [
+ {
+ "name": "name",
+ "description": "Name of the job",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "needs",
+ "description": "Builds that must complete before the jobs run",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiJobConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiJobConnection",
+ "description": "The connection type for CiJob.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiJobEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiJob",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiJobEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiJob",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiStage",
+ "description": null,
+ "fields": [
+ {
+ "name": "groups",
+ "description": "Group of jobs for the stage",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiGroupConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the stage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiStageConnection",
+ "description": "The connection type for CiStage.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiStageEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "CiStage",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CiStageEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiStage",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Commit",
"description": null,
"fields": [
@@ -29497,6 +30087,59 @@
"deprecationReason": null
},
{
+ "name": "stages",
+ "description": "Stages of the pipeline",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiStageConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "startedAt",
"description": "Timestamp when the pipeline was started",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 5ae0d7abb1a..468d8d6557b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -231,6 +231,25 @@ Autogenerated return type of BoardListUpdateLimitMetrics
| `commit` | Commit | Commit for the branch |
| `name` | String! | Name of the branch |
+## CiGroup
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `name` | String | Name of the job group |
+| `size` | Int | Size of the group |
+
+## CiJob
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `name` | String | Name of the job |
+
+## CiStage
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `name` | String | Name of the stage |
+
## Commit
| Name | Type | Description |
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index cf4edea383b..c4046b36c55 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -142,6 +142,10 @@ I, [2018-08-02T10:26:47.764356 #45087] INFO -- : Moved to lost and found: @hash
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29681) in GitLab 12.1.
> - [`ionice` support fixed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28023) in GitLab 12.10.
+NOTE: **Note:**
+These commands will not work for artifacts stored on
+[object storage](../administration/object_storage.md).
+
When you notice there are more job artifacts files on disk than there
should be, you can run:
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index 8d25b865855..c0251229916 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -1,370 +1,5 @@
---
-comments: false
+redirect_to: 'https://docs.gitlab.com'
---
-# Training
-
-This training material is the Markdown used to generate training slides
-which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-training-slides/#/)
-through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides)
-project.
-
-## Git Intro
-
-### What is a Version Control System (VCS)
-
-- Records changes to a file
-- Maintains history of changes
-- Disaster Recovery
-- Types of VCS: Local, Centralized and Distributed
-
-### Short Story of Git
-
-- 1991-2002: The Linux kernel was being maintained by sharing archived files
- and patches.
-- 2002: The Linux kernel project began using a DVCS called BitKeeper
-- 2005: BitKeeper revoked the free-of-charge status and Git was created
-
-### What is Git
-
-- Distributed Version Control System
-- Great branching model that adapts well to most workflows
-- Fast and reliable
-- Keeps a complete history
-- Disaster recovery friendly
-- Open Source
-
-### Getting Help
-
-- Use the tools at your disposal when you get stuck.
- - Use `git help <command>` command
- - Use Google (i.e. StackOverflow, Google groups)
- - Read documentation at <https://git-scm.com>
-
-## Git Setup
-
-Workshop Time!
-
-### Setup
-
-- Windows: Install 'Git for Windows'
- - <https://gitforwindows.org>
-- Mac: Type `git` in the Terminal application.
- - If it's not installed, it will prompt you to install it.
-- Linux
- - Debian: `sudo apt-get install git-all`
- - Red Hat `sudo yum install git-all`
-
-### Configure
-
-- One-time configuration of the Git client:
-
-```shell
-git config --global user.name "Your Name"
-git config --global user.email you@example.com
-```
-
-- If you don't use the global flag you can set up a different author for
- each project
-- Check settings with:
-
-```shell
-git config --global --list
-```
-
-- You might want or be required to use an SSH key.
- - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html)
-
-### Workspace
-
-- Choose a directory on you machine easy to access
-- Create a workspace or development directory
-- This is where we'll be working and adding content
-
-```shell
-mkdir ~/development
-cd ~/development
-
--or-
-
-mkdir ~/workspace
-cd ~/workspace
-```
-
-## Git Basics
-
-### Git Workflow
-
-- Untracked files
- - New files that Git has not been told to track previously.
-- Working area (Workspace)
- - Files that have been modified but are not committed.
-- Staging area (Index)
- - Modified files that have been marked to go in the next commit.
-- Upstream
- - Hosted repository on a shared server
-
-### GitLab
-
-- GitLab is an application to code, test and deploy.
-- Provides repository management with access controls, code reviews,
- issue tracking, Merge Requests, and other features.
-- The hosted version of GitLab is <https://gitlab.com>
-
-### New Project
-
-- Sign in into your <https://gitlab.com> account
-- Create a project
-- Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git>
-- On your machine clone the `training-examples` project
-
-### Git and GitLab basics
-
-1. Edit `edit_this_file.rb` in `training-examples`
-1. See it listed as a changed file (working area)
-1. View the differences
-1. Stage the file
-1. Commit
-1. Push the commit to the remote
-1. View the Git log
-
-```shell
-# Edit `edit_this_file.rb`
-git status
-git diff
-git add <file>
-git commit -m 'My change'
-git push origin master
-git log
-```
-
-### Feature Branching
-
-1. Create a new feature branch called `squash_some_bugs`
-1. Edit `bugs.rb` and remove all the bugs.
-1. Commit
-1. Push
-
-```shell
-git checkout -b squash_some_bugs
-# Edit `bugs.rb`
-git status
-git add bugs.rb
-git commit -m 'Fix some buggy code'
-git push origin squash_some_bugs
-```
-
-## Merge Request
-
-- When you want feedback create a merge request
-- Target is the ‘default’ branch (usually master)
-- Assign or mention the person you would like to review
-- Add `Draft:` to the title if it's a work in progress
-- When accepting, always delete the branch
-- Anyone can comment, not just the assignee
-- Push corrections to the same branch
-
-### Merge request example
-
-- Create your first merge request
- - Use the blue button in the activity feed
- - View the diff (changes) and leave a comment
- - Push a new commit to the same branch
- - Review the changes again and notice the update
-
-### Feedback and Collaboration
-
-- Merge requests are a time for feedback and collaboration
-- Giving feedback is hard
-- Be as kind as possible
-- Receiving feedback is hard
-- Be as receptive as possible
-- Feedback is about the best code, not the person. You are not your code
-- Feedback and Collaboration
-
----
-
-- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
- [Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review)
-- See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests)
-
-## Merge Conflicts
-
-- Happen often
-- Learning to fix conflicts is hard
-- Practice makes perfect
-- Force push after fixing conflicts. Be careful!
-
-### Example Plan
-
-1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'.
-1. Commit and push
-1. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'.
-1. Commit and push to master
-1. Create a merge request and watch it fail
-1. Rebase our new branch with master
-1. Fix conflicts on the conflicts.rb file.
-1. Stage the file and continue rebasing
-1. Force push the changes
-1. Finally continue with the Merge Request
-
-### Example 1/2
-
-```shell
-git checkout -b conflicts_branch
-
-# vi conflicts.rb
-# Add 'Line4' and 'Line5'
-
-git commit -am "add line4 and line5"
-git push origin conflicts_branch
-
-git checkout master
-
-# vi conflicts.rb
-# Add 'Line6' and 'Line7'
-git commit -am "add line6 and line7"
-git push origin master
-```
-
-### Example 2/2
-
-Create a merge request on the GitLab web UI. You'll see a conflict warning.
-
-```shell
-git checkout conflicts_branch
-git fetch
-git rebase master
-
-# Fix conflicts by editing the files.
-
-git add conflicts.rb
-# No need to commit this file
-
-git rebase --continue
-
-# Remember that we have rewritten our commit history so we
-# need to force push so that our remote branch is restructured
-git push origin conflicts_branch -f
-```
-
-### Notes
-
-- When to use `git merge` and when to use `git rebase`
-- Rebase when updating your branch with master
-- Merge when bringing changes from feature to master
-- Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing>
-
-## Revert and Unstage
-
-### Unstage
-
-To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch:
-
-```shell
-git reset HEAD <file>
-```
-
-This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
-
-```shell
-git checkout -- <file>
-```
-
-To remove a file from disk and repo use `git rm` and to remove a directory use the `-r` flag:
-
-```shell
-git rm '*.txt'
-git rm -r <dirname>
-```
-
-If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`:
-
-```shell
-git rm <filename> --cache
-```
-
-### Undo Commits
-
-Undo last commit putting everything back into the staging area:
-
-```shell
-git reset --soft HEAD^
-```
-
-Add files and change message with:
-
-```shell
-git commit --amend -m "New Message"
-```
-
-Undo last and remove changes
-
-```shell
-git reset --hard HEAD^
-```
-
-Same as last one but for two commits back:
-
-```shell
-git reset --hard HEAD^^
-```
-
-Don't reset after pushing
-
-### Reset Workflow
-
-1. Edit file again 'edit_this_file.rb'
-1. Check status
-1. Add and commit with wrong message
-1. Check log
-1. Amend commit
-1. Check log
-1. Soft reset
-1. Check log
-1. Pull for updates
-1. Push changes
-
-```shell
-# Change file edit_this_file.rb
-git status
-git commit -am "kjkfjkg"
-git log
-git commit --amend -m "New comment added"
-git log
-git reset --soft HEAD^
-git log
-git pull origin master
-git push origin master
-```
-
-### `git revert` vs `git reset`
-
-Reset removes the commit while revert removes the changes but leaves the commit
-Revert is safer considering we can revert a revert
-
-```shell
-# Changed file
-git commit -am "bug introduced"
-git revert HEAD
-# New commit created reverting changes
-# Now we want to re apply the reverted commit
-git log # take hash from the revert commit
-git revert <rev commit hash>
-# reverted commit is back (new commit created again)
-```
-
-## Questions
-
-## Instructor Notes
-
-### Version Control
-
-- Local VCS was used with a filesystem or a simple db.
-- Centralized VCS such as Subversion includes collaboration but
- still is prone to data loss as the main server is the single point of
- failure.
-- Distributed VCS enables the team to have a complete copy of the project
- and work with little dependency to the main server. In case of a main
- server failing the project can be recovered by any of the latest copies
- from the team
+Visit our [documentation page](https://docs.gitlab.com) for information about GitLab.
diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md
index 229a8572206..647d6276554 100644
--- a/doc/user/application_security/configuration/index.md
+++ b/doc/user/application_security/configuration/index.md
@@ -24,6 +24,13 @@ NOTE: **Note:**
If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md),
all security features will be configured by default.
+## SAST Configuration
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3659) in GitLab Ultimate 13.3.
+
+For projects that do not already have a `.gitlab-ci.yml` file,
+[configure SAST in the UI](../sast/index.md#configure-sast-in-the-ui).
+
## Limitations
It is not yet possible to enable or disable most features using the
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index cd3e45c9ad3..ded72021cc4 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -45,6 +45,12 @@ To add Container Scanning, follow the steps listed in the [Container Scanning do
To further configure any of the other scanners, refer to each scanner's documentation.
+### SAST configuration
+
+You can set up and configure Static Application Security Testing
+(SAST) for your project, without opening a text editor. For more details,
+see [configure SAST in the UI](sast/index.md#configure-sast-in-the-ui).
+
### Override the default registry base address
By default, GitLab security scanners use `registry.gitlab.com/gitlab-org/security-products/analyzers` as the
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index d0793d0f3e4..fd0e1cc5e05 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -24,6 +24,8 @@ You can take advantage of SAST by doing one of the following:
- [Including the SAST template](#configuration) in your existing `.gitlab-ci.yml` file.
- Implicitly using [Auto SAST](../../../topics/autodevops/stages.md#auto-sast-ultimate) provided by
[Auto DevOps](../../../topics/autodevops/index.md).
+- Using the [SAST Configuration tool](#configure-sast-in-the-ui) to create the necessary
+ `.gitlab-ci.yml` file for you.
GitLab checks the SAST report, compares the found vulnerabilities between the
source and target branches.
@@ -151,6 +153,19 @@ The results will be saved as a
that you can later download and analyze. Due to implementation limitations, we
always take the latest SAST artifact available.
+### Configure SAST in the UI
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3659) in GitLab Ultimate 13.3.
+
+For projects that do not already have a `.gitlab-ci.yml` file, the above
+configuration can also be achieved by using the **SAST Configuration** tool.
+
+1. Navigate to **Security & Compliance > Configuration**.
+1. Click **Enable** on the Static Application Security Testing (SAST)
+row.
+
+A merge request is created, containing the necessary changes for you to review and merge.
+
### Customizing the SAST settings
The SAST settings can be changed through [environment variables](#available-variables)
diff --git a/lib/gitlab/alerting/alert.rb b/lib/gitlab/alerting/alert.rb
index dad3dabb4fc..94b81b7d290 100644
--- a/lib/gitlab/alerting/alert.rb
+++ b/lib/gitlab/alerting/alert.rb
@@ -7,7 +7,17 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
include Presentable
- attr_accessor :project, :payload
+ attr_accessor :project, :payload, :am_alert
+
+ def self.for_alert_management_alert(project:, alert:)
+ params = if alert.prometheus?
+ alert.payload
+ else
+ Gitlab::Alerting::NotificationPayloadParser.call(alert.payload.to_h, alert.project)
+ end
+
+ self.new(project: project, payload: params, am_alert: alert)
+ end
def gitlab_alert
strong_memoize(:gitlab_alert) do
diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
index e45a345d25a..be5d9be3d64 100644
--- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
@@ -13,7 +13,7 @@ module Gitlab
MAPPINGS = {
Issue => {
serializer_class: AnalyticsIssueSerializer,
- includes_for_query: { project: [:namespace], author: [] },
+ includes_for_query: { project: { namespace: [:route] }, author: [] },
columns_for_select: %I[title iid id created_at author_id project_id]
},
MergeRequest => {
@@ -41,7 +41,7 @@ module Gitlab
project = record.project
attributes = record.attributes.merge({
project_path: project.path,
- namespace_path: project.namespace.path,
+ namespace_path: project.namespace.route.path,
author: record.author
})
serializer.represent(attributes)
diff --git a/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml b/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml
index 0796329bcb5..1c17a3a4d40 100644
--- a/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml
+++ b/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml
@@ -1,6 +1,6 @@
# Only one dashboard should be defined per file
# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Guage Panel Example'
+dashboard: 'Gauge Panel Example'
# For more information about the required properties of panel_groups
# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
diff --git a/lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml b/lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml
new file mode 100644
index 00000000000..7f97719765b
--- /dev/null
+++ b/lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml
@@ -0,0 +1,23 @@
+# Only one dashboard should be defined per file
+# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
+dashboard: 'Gauge K8s Panel Example'
+
+# For more information about the required properties of panel_groups
+# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
+panel_groups:
+ - group: 'Server Statistics'
+ panels:
+ - title: "Memory usage"
+ # More information about gauge panel types can be found here:
+ # https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#gauge
+ type: "gauge-chart"
+ min_value: 0
+ max_value: 1024
+ split: 10
+ thresholds:
+ mode: "percentage"
+ values: [60, 90]
+ format: "megabytes"
+ metrics:
+ - query: 'avg(sum(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024 OR avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
+ unit: 'MB'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 09468fbdde6..576515c5b18 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -333,6 +333,9 @@ msgstr ""
msgid "%{cores} cores"
msgstr ""
+msgid "%{count} %{scope} for term '%{term}'"
+msgstr ""
+
msgid "%{count} LOC/commit"
msgstr ""
@@ -21420,6 +21423,9 @@ msgstr ""
msgid "SecurityConfiguration|Customize common SAST settings to suit your requirements. Configuration changes made here override those provided by GitLab and are excluded from updates. For details of more advanced configuration options, see the %{linkStart}GitLab SAST documentation%{linkEnd}."
msgstr ""
+msgid "SecurityConfiguration|Enable"
+msgstr ""
+
msgid "SecurityConfiguration|Enable via Merge Request"
msgstr ""
diff --git a/package.json b/package.json
index 17d9f47e763..20a19462875 100644
--- a/package.json
+++ b/package.json
@@ -43,13 +43,13 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.158.0",
- "@gitlab/ui": "18.3.0",
+ "@gitlab/ui": "18.6.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",
"@sourcegraph/code-host-integration": "0.0.49",
- "@toast-ui/editor": "^2.3.0",
- "@toast-ui/vue-editor": "^2.3.0",
+ "@toast-ui/editor": "^2.3.1",
+ "@toast-ui/vue-editor": "^2.3.1",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link": "^1.2.14",
diff --git a/qa/qa/page/project/operations/metrics/show.rb b/qa/qa/page/project/operations/metrics/show.rb
index 7576e11bf59..22d22af5a9a 100644
--- a/qa/qa/page/project/operations/metrics/show.rb
+++ b/qa/qa/page/project/operations/metrics/show.rb
@@ -58,7 +58,9 @@ module QA
end
def has_edit_dashboard_enabled?
- within_element :prometheus_graphs do
+ click_element :actions_menu_dropdown
+
+ within_element :actions_menu_dropdown do
has_element? :edit_dashboard_button_enabled
end
end
diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb
index d415c246021..1c0002fd76d 100644
--- a/qa/qa/service/praefect_manager.rb
+++ b/qa/qa/service/praefect_manager.rb
@@ -39,6 +39,7 @@ module QA
break line if line.start_with?('gitaly_cluster')
break nil if line.include?('Something went wrong when getting replicas')
end
+ next false unless replicas
# We want to know if the checksums are identical
replicas&.split('|')&.map(&:strip)&.slice(1..3)&.uniq&.one?
diff --git a/qa/qa/specs/features/api/3_create/repository/backend_node_recovery_spec.rb b/qa/qa/specs/features/api/3_create/repository/backend_node_recovery_spec.rb
index 77d72bc1bb3..89ce4a820b4 100644
--- a/qa/qa/specs/features/api/3_create/repository/backend_node_recovery_spec.rb
+++ b/qa/qa/specs/features/api/3_create/repository/backend_node_recovery_spec.rb
@@ -55,7 +55,6 @@ module QA
praefect_manager.wait_for_health_check_current_primary_node
# Confirm dataloss (i.e., inconsistent nodes)
- expect(praefect_manager.dataloss?).to be true
expect(praefect_manager.replicated?(project.id)).to be false
# Reconcile nodes to recover from dataloss
diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb
index dafce094b14..528d5c073b7 100644
--- a/spec/controllers/groups/shared_projects_controller_spec.rb
+++ b/spec/controllers/groups/shared_projects_controller_spec.rb
@@ -17,9 +17,9 @@ RSpec.describe Groups::SharedProjectsController do
).execute(group)
end
- let_it_be(:group) { create(:group) }
- let_it_be(:user) { create(:user) }
- let_it_be(:shared_project) do
+ let!(:group) { create(:group) }
+ let!(:user) { create(:user) }
+ let!(:shared_project) do
shared_project = create(:project, namespace: user.namespace)
share_project(shared_project)
diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb
index 562269a67bc..a7902f6f105 100644
--- a/spec/factories/packages.rb
+++ b/spec/factories/packages.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
maven_metadatum
after :build do |package|
- package.maven_metadatum.path = "#{package.name}/#{package.version}"
+ package.maven_metadatum.path = package.version? ? "#{package.name}/#{package.version}" : package.name
end
after :create do |package|
diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
index 7fd2a6755cd..2814b5ce357 100644
--- a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue';
-import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.graphql';
+import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -34,6 +34,8 @@ describe('Alert Details Sidebar To Do', () => {
wrapper.destroy();
});
+ const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]');
+
describe('updating the alert to do', () => {
const mockUpdatedMutationResult = {
data: {
@@ -44,25 +46,27 @@ describe('Alert Details Sidebar To Do', () => {
},
};
- beforeEach(() => {
- mountComponent({
- data: { alert: mockAlert },
- sidebarCollapsed: false,
- loading: false,
+ describe('adding a todo', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: mockAlert },
+ sidebarCollapsed: false,
+ loading: false,
+ });
});
- });
- it('renders a button for adding a To-Do', () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('[data-testid="alert-todo-button"]').text()).toBe('Add a To-Do');
+ it('renders a button for adding a To-Do', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(findToDoButton().text()).toBe('Add a To-Do');
});
- });
- it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+
+ findToDoButton().trigger('click');
+ await wrapper.vm.$nextTick();
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find('button').trigger('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: AlertMarkTodo,
variables: {
@@ -72,5 +76,28 @@ describe('Alert Details Sidebar To Do', () => {
});
});
});
+ describe('removing a todo', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: { ...mockAlert, todos: { nodes: [{ id: '1234' }] } } },
+ sidebarCollapsed: false,
+ loading: false,
+ });
+ });
+
+ it('renders a Mark As Done button when todo is present', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(findToDoButton().text()).toBe('Mark as done');
+ });
+
+ it('calls `$apollo.mutate` with `AlertMarkTodoDone` mutation and variables containing `id`', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+
+ findToDoButton().trigger('click');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ });
+ });
});
});
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json
index f63019d1e5c..fec101a52b4 100644
--- a/spec/frontend/alert_management/mocks/alerts.json
+++ b/spec/frontend/alert_management/mocks/alerts.json
@@ -9,7 +9,8 @@
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED",
"assignees": { "nodes": [] },
- "notes": { "nodes": [] }
+ "notes": { "nodes": [] },
+ "todos": { "nodes": [] }
},
{
"iid": "1527543",
@@ -37,7 +38,8 @@
"systemNoteIconName": "user"
}
]
- }
+ },
+ "todos": { "nodes": [] }
},
{
"iid": "1527544",
@@ -63,6 +65,7 @@
}
}
]
- }
+ },
+ "todos": { "nodes": [] }
}
]
diff --git a/spec/graphql/types/ci/group_type_spec.rb b/spec/graphql/types/ci/group_type_spec.rb
new file mode 100644
index 00000000000..8d547b19af3
--- /dev/null
+++ b/spec/graphql/types/ci/group_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::GroupType do
+ specify { expect(described_class.graphql_name).to eq('CiGroup') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ size
+ jobs
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
new file mode 100644
index 00000000000..faf3a95cf25
--- /dev/null
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::JobType do
+ specify { expect(described_class.graphql_name).to eq('CiJob') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ needs
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/stage_type_spec.rb b/spec/graphql/types/ci/stage_type_spec.rb
new file mode 100644
index 00000000000..0c352ed27aa
--- /dev/null
+++ b/spec/graphql/types/ci/stage_type_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::StageType do
+ specify { expect(described_class.graphql_name).to eq('CiStage') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ name
+ groups
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 1c9004262c5..f4875aa0ebc 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -110,6 +110,20 @@ RSpec.describe Gitlab::Gfm::ReferenceRewriter do
end
end
+ context 'when description contains a local reference' do
+ let(:local_issue) { create(:issue, project: old_project) }
+ let(:text) { "See ##{local_issue.iid}" }
+
+ it { is_expected.to eq("See #{old_project.path}##{local_issue.iid}") }
+ end
+
+ context 'when description contains a cross reference' do
+ let(:merge_request) { create(:merge_request) }
+ let(:text) { "See #{merge_request.project.full_path}!#{merge_request.iid}" }
+
+ it { is_expected.to eq(text) }
+ end
+
context 'with a commit' do
let(:old_project) { create(:project, :repository, name: 'old-project', group: group) }
let(:commit) { old_project.commit }
diff --git a/spec/presenters/alert_management/alert_presenter_spec.rb b/spec/presenters/alert_management/alert_presenter_spec.rb
index ccea0d36a28..4281babee61 100644
--- a/spec/presenters/alert_management/alert_presenter_spec.rb
+++ b/spec/presenters/alert_management/alert_presenter_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe AlertManagement::AlertPresenter do
let_it_be(:project) { create(:project) }
+
let_it_be(:generic_payload) do
{
'title' => 'Alert title',
@@ -12,10 +13,13 @@ RSpec.describe AlertManagement::AlertPresenter do
'runbook' => 'https://runbook.com'
}
end
+
let_it_be(:alert) do
- build(:alert_management_alert, :with_description, :with_host, :with_service, :with_monitoring_tool, project: project, payload: generic_payload)
+ create(:alert_management_alert, :with_description, :with_host, :with_service, :with_monitoring_tool, project: project, payload: generic_payload)
end
+ let(:alert_url) { "http://localhost/#{project.full_path}/-/alert_management/#{alert.iid}/details" }
+
subject(:presenter) { described_class.new(alert) }
describe '#issue_description' do
@@ -31,7 +35,8 @@ RSpec.describe AlertManagement::AlertPresenter do
**Service:** #{alert.service}#{markdown_line_break}
**Monitoring tool:** #{alert.monitoring_tool}#{markdown_line_break}
**Hosts:** #{alert.hosts.join(' ')}#{markdown_line_break}
- **Description:** #{alert.description}
+ **Description:** #{alert.description}#{markdown_line_break}
+ **GitLab alert:** #{alert_url}
#### Alert Details
diff --git a/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb b/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb
index 70c85619fd1..3cfff3c1b2f 100644
--- a/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb
+++ b/spec/presenters/alert_management/prometheus_alert_presenter_spec.rb
@@ -16,10 +16,12 @@ RSpec.describe AlertManagement::PrometheusAlertPresenter do
}
end
- let(:alert) do
+ let!(:alert) do
create(:alert_management_alert, :prometheus, project: project, payload: payload)
end
+ let(:alert_url) { "http://localhost/#{project.full_path}/-/alert_management/#{alert.iid}/details" }
+
subject(:presenter) { described_class.new(alert) }
describe '#issue_description' do
@@ -33,7 +35,8 @@ RSpec.describe AlertManagement::PrometheusAlertPresenter do
**Start time:** #{presenter.start_time}#{markdown_line_break}
**Severity:** #{presenter.severity}#{markdown_line_break}
**full_query:** `vector(1)`#{markdown_line_break}
- **Monitoring tool:** Prometheus
+ **Monitoring tool:** Prometheus#{markdown_line_break}
+ **GitLab alert:** #{alert_url}
#### Alert Details
diff --git a/spec/presenters/projects/prometheus/alert_presenter_spec.rb b/spec/presenters/projects/prometheus/alert_presenter_spec.rb
index 89c5438b074..2d58a7f2cfa 100644
--- a/spec/presenters/projects/prometheus/alert_presenter_spec.rb
+++ b/spec/presenters/projects/prometheus/alert_presenter_spec.rb
@@ -293,6 +293,19 @@ RSpec.describe Projects::Prometheus::AlertPresenter do
end
end
+ describe '#details_url' do
+ subject { presenter.details_url }
+
+ it { is_expected.to eq(nil) }
+
+ context 'alert management alert present' do
+ let_it_be(:am_alert) { create(:alert_management_alert, project: project) }
+ let(:alert) { create(:alerting_alert, project: project, payload: payload, am_alert: am_alert) }
+
+ it { is_expected.to eq("http://localhost/#{project.full_path}/-/alert_management/#{am_alert.iid}/details") }
+ end
+ end
+
context 'with gitlab alert' do
include_context 'gitlab alert'
diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb
new file mode 100644
index 00000000000..9e81358a152
--- /dev/null
+++ b/spec/requests/api/graphql/ci/groups_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Query.project.pipeline.stages.groups' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository, :public) }
+ let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let(:group_graphql_data) { graphql_data.dig('project', 'pipeline', 'stages', 'nodes', 0, 'groups', 'nodes') }
+
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('CiGroup')}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ nodes {
+ groups {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create(:commit_status, pipeline: pipeline, name: 'rspec 0 2')
+ create(:commit_status, pipeline: pipeline, name: 'rspec 0 1')
+ create(:commit_status, pipeline: pipeline, name: 'spinach 0 1')
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns a array of jobs belonging to a pipeline' do
+ expect(group_graphql_data.map { |g| g.slice('name', 'size') }).to eq([
+ { 'name' => 'rspec', 'size' => 2 },
+ { 'name' => 'spinach', 'size' => 1 }
+ ])
+ end
+end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
new file mode 100644
index 00000000000..7d416f4720b
--- /dev/null
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository, :public) }
+ let(:user) { create(:user) }
+ let(:pipeline) do
+ pipeline = create(:ci_pipeline, project: project, user: user)
+ stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
+ create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job')
+
+ pipeline
+ end
+
+ def first(field)
+ [field.pluralize, 'nodes', 0]
+ end
+
+ let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ nodes {
+ name
+ groups {
+ nodes {
+ name
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the jobs of a pipeline stage' do
+ post_graphql(query, current_user: user)
+
+ expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job'))
+ end
+
+ context 'when fetching jobs from the pipeline' do
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: user)
+ end
+
+ build_stage = create(:ci_stage_entity, name: 'build', pipeline: pipeline)
+ test_stage = create(:ci_stage_entity, name: 'test', pipeline: pipeline)
+ create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 1 2')
+ create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 2 2')
+ create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 1 2')
+ create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 2 2')
+
+ expect do
+ post_graphql(query, current_user: user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ build_stage = graphql_data.dig('project', 'pipeline', 'stages', 'nodes').find do |stage|
+ stage['name'] == 'build'
+ end
+ test_stage = graphql_data.dig('project', 'pipeline', 'stages', 'nodes').find do |stage|
+ stage['name'] == 'test'
+ end
+ docker_group = build_stage.dig('groups', 'nodes').first
+ rspec_group = test_stage.dig('groups', 'nodes').first
+
+ expect(docker_group['name']).to eq('docker')
+ expect(rspec_group['name']).to eq('rspec')
+
+ docker_jobs = docker_group.dig('jobs', 'nodes')
+ rspec_jobs = rspec_group.dig('jobs', 'nodes')
+
+ expect(docker_jobs).to eq([{ 'name' => 'docker 1 2' }, { 'name' => 'docker 2 2' }])
+ expect(rspec_jobs).to eq([{ 'name' => 'rspec 1 2' }, { 'name' => 'rspec 2 2' }])
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb
new file mode 100644
index 00000000000..cd48a24b9c8
--- /dev/null
+++ b/spec/requests/api/graphql/ci/stages_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Query.project.pipeline.stages' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository, :public) }
+ let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let(:stage_graphql_data) { graphql_data['project']['pipeline']['stages'] }
+
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('CiStage')}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ #{fields}
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create(:ci_stage_entity, pipeline: pipeline, name: 'deploy')
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the stage of a pipeline' do
+ expect(stage_graphql_data['nodes'].first['name']).to eq('deploy')
+ end
+end
diff --git a/spec/serializers/analytics_issue_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb
index 2518eec8c23..447c5e7d02a 100644
--- a/spec/serializers/analytics_issue_entity_spec.rb
+++ b/spec/serializers/analytics_issue_entity_spec.rb
@@ -17,16 +17,13 @@ RSpec.describe AnalyticsIssueEntity do
}
end
- let(:project) { create(:project, name: 'my project') }
let(:request) { EntityRequest.new(entity: :merge_request) }
let(:entity) do
described_class.new(entity_hash, request: request, project: project)
end
- context 'generic entity' do
- subject { entity.as_json }
-
+ shared_examples 'generic entity' do
it 'contains the entity URL' do
expect(subject).to include(:url)
end
@@ -40,4 +37,24 @@ RSpec.describe AnalyticsIssueEntity do
expect(subject).not_to include(/variables/)
end
end
+
+ context 'without subgroup' do
+ let_it_be(:project) { create(:project, name: 'my project') }
+
+ subject { entity.as_json }
+
+ it_behaves_like 'generic entity'
+ end
+
+ context 'with subgroup' do
+ let_it_be(:project) { create(:project, :in_subgroup, name: 'my project') }
+
+ subject { entity.as_json }
+
+ it_behaves_like 'generic entity'
+
+ it 'has URL containing subgroup' do
+ expect(subject[:url]).to include("#{project.group.parent.name}/#{project.group.name}/my_project/")
+ end
+ end
end
diff --git a/spec/services/award_emojis/copy_service_spec.rb b/spec/services/award_emojis/copy_service_spec.rb
new file mode 100644
index 00000000000..e85c548968e
--- /dev/null
+++ b/spec/services/award_emojis/copy_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AwardEmojis::CopyService do
+ let_it_be(:from_awardable) do
+ create(:issue, award_emoji: [
+ build(:award_emoji, name: 'thumbsup'),
+ build(:award_emoji, name: 'thumbsdown')
+ ])
+ end
+
+ describe '#initialize' do
+ it 'validates that we cannot copy AwardEmoji to the same Awardable' do
+ expect { described_class.new(from_awardable, from_awardable) }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe '#execute' do
+ let(:to_awardable) { create(:issue) }
+
+ subject(:execute_service) { described_class.new(from_awardable, to_awardable).execute }
+
+ it 'copies AwardEmojis', :aggregate_failures do
+ expect { execute_service }.to change { AwardEmoji.count }.by(2)
+ expect(to_awardable.award_emoji.map(&:name)).to match_array(%w(thumbsup thumbsdown))
+ end
+
+ it 'returns success', :aggregate_failures do
+ expect(execute_service).to be_kind_of(ServiceResponse)
+ expect(execute_service).to be_success
+ end
+ end
+end
diff --git a/spec/services/incident_management/create_issue_service_spec.rb b/spec/services/incident_management/create_issue_service_spec.rb
index 60b3a513a67..6c77779fd92 100644
--- a/spec/services/incident_management/create_issue_service_spec.rb
+++ b/spec/services/incident_management/create_issue_service_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe IncidentManagement::CreateIssueService do
let(:project) { create(:project, :repository, :private) }
let_it_be(:user) { User.alert_bot }
- let(:service) { described_class.new(project, alert_payload) }
let(:alert_starts_at) { Time.current }
let(:alert_title) { 'TITLE' }
let(:alert_annotations) { { title: alert_title } }
@@ -17,8 +16,11 @@ RSpec.describe IncidentManagement::CreateIssueService do
)
end
+ let(:alert) { create(:alert_management_alert, :prometheus, project: project, payload: alert_payload) }
+ let(:service) { described_class.new(project, alert) }
+
let(:alert_presenter) do
- Gitlab::Alerting::Alert.new(project: project, payload: alert_payload).present
+ Gitlab::Alerting::Alert.for_alert_management_alert(project: project, alert: alert).present
end
let!(:setting) do
diff --git a/spec/services/issuable/clone/content_rewriter_spec.rb b/spec/services/issuable/clone/content_rewriter_spec.rb
deleted file mode 100644
index b1cef0789b3..00000000000
--- a/spec/services/issuable/clone/content_rewriter_spec.rb
+++ /dev/null
@@ -1,184 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Issuable::Clone::ContentRewriter do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:project1) { create(:project, :public, group: group) }
- let(:project2) { create(:project, :public, group: group) }
-
- let(:other_issue) { create(:issue, project: project1) }
- let(:merge_request) { create(:merge_request) }
-
- subject { described_class.new(user, original_issue, new_issue)}
-
- let(:description) { 'Simple text' }
- let(:original_issue) { create(:issue, description: description, project: project1) }
- let(:new_issue) { create(:issue, project: project2) }
-
- context 'rewriting award emojis' do
- it 'copies the award emojis' do
- create(:award_emoji, awardable: original_issue, name: 'thumbsup')
- create(:award_emoji, awardable: original_issue, name: 'thumbsdown')
-
- expect { subject.execute }.to change { AwardEmoji.count }.by(2)
-
- expect(new_issue.award_emoji.map(&:name)).to match_array(%w(thumbsup thumbsdown))
- end
- end
-
- context 'rewriting description' do
- before do
- subject.execute
- end
-
- context 'when description is a simple text' do
- it 'does not rewrite the description' do
- expect(new_issue.reload.description).to eq(original_issue.description)
- end
- end
-
- context 'when description contains a local reference' do
- let(:description) { "See ##{other_issue.iid}" }
-
- it 'rewrites the local reference correctly' do
- expected_description = "See #{project1.path}##{other_issue.iid}"
-
- expect(new_issue.reload.description).to eq(expected_description)
- end
- end
-
- context 'when description contains a cross reference' do
- let(:description) { "See #{merge_request.project.full_path}!#{merge_request.iid}" }
-
- it 'rewrites the cross reference correctly' do
- expected_description = "See #{merge_request.project.full_path}!#{merge_request.iid}"
-
- expect(new_issue.reload.description).to eq(expected_description)
- end
- end
-
- context 'when description contains a user reference' do
- let(:description) { "FYU #{user.to_reference}" }
-
- it 'works with a user reference' do
- expect(new_issue.reload.description).to eq("FYU #{user.to_reference}")
- end
- end
-
- context 'when description contains uploads' do
- let(:uploader) { build(:file_uploader, project: project1) }
- let(:description) { "Text and #{uploader.markdown_link}" }
-
- it 'rewrites uploads in the description' do
- upload = Upload.last
-
- expect(new_issue.description).not_to eq(description)
- expect(new_issue.description).to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
- expect(upload.secret).not_to eq(uploader.secret)
- expect(new_issue.description).to include(upload.secret)
- expect(new_issue.description).to include(upload.path)
- end
- end
- end
-
- context 'rewriting notes' do
- context 'simple notes' do
- let!(:notes) do
- [
- create(:note, noteable: original_issue, project: project1,
- created_at: 2.weeks.ago, updated_at: 1.week.ago),
- create(:note, noteable: original_issue, project: project1),
- create(:note, system: true, noteable: original_issue, project: project1)
- ]
- end
-
- let!(:system_note_metadata) { create(:system_note_metadata, note: notes.last) }
- let!(:award_emoji) { create(:award_emoji, awardable: notes.first, name: 'thumbsup')}
-
- before do
- subject.execute
- end
-
- it 'rewrites existing notes in valid order' do
- expect(new_issue.notes.order('id ASC').pluck(:note).first(3)).to eq(notes.map(&:note))
- end
-
- it 'copies all the issue notes' do
- expect(new_issue.notes.count).to eq(3)
- end
-
- it 'does not change the note attributes' do
- subject.execute
-
- new_note = new_issue.notes.first
-
- expect(new_note.note).to eq(notes.first.note)
- expect(new_note.author).to eq(notes.first.author)
- end
-
- it 'copies the award emojis' do
- subject.execute
-
- new_note = new_issue.notes.first
- new_note.award_emoji.first.name = 'thumbsup'
- end
-
- it 'copies system_note_metadata for system note' do
- new_note = new_issue.notes.last
-
- expect(new_note.system_note_metadata.action).to eq(system_note_metadata.action)
- expect(new_note.system_note_metadata.id).not_to eq(system_note_metadata.id)
- end
- end
-
- context 'notes with reference' do
- let(:text) do
- "See ##{other_issue.iid} and #{merge_request.project.full_path}!#{merge_request.iid}"
- end
-
- let!(:note) { create(:note, noteable: original_issue, note: text, project: project1) }
-
- it 'rewrites the references correctly' do
- subject.execute
-
- new_note = new_issue.notes.first
-
- expected_text = "See #{other_issue.project.path}##{other_issue.iid} and #{merge_request.project.full_path}!#{merge_request.iid}"
-
- expect(new_note.note).to eq(expected_text)
- expect(new_note.author).to eq(note.author)
- end
- end
-
- context 'notes with upload' do
- let(:uploader) { build(:file_uploader, project: project1) }
- let(:text) { "Simple text with image: #{uploader.markdown_link} "}
- let!(:note) { create(:note, noteable: original_issue, note: text, project: project1) }
-
- it 'rewrites note content correctly' do
- subject.execute
- new_note = new_issue.notes.first
-
- expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
- expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
- expect(note.note).not_to eq(new_note.note)
- expect(note.note_html).not_to eq(new_note.note_html)
- end
- end
-
- context "discussion notes" do
- let(:note) { create(:note, noteable: original_issue, note: "sample note", project: project1) }
- let!(:discussion) { create(:discussion_note_on_issue, in_reply_to: note, note: "reply to sample note") }
-
- it 'rewrites discussion correctly' do
- subject.execute
-
- expect(new_issue.notes.count).to eq(original_issue.notes.count)
- expect(new_issue.notes.where(discussion_id: discussion.discussion_id).count).to eq(0)
- expect(original_issue.notes.where(discussion_id: discussion.discussion_id).count).to eq(1)
- end
- end
- end
-end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 8929907a179..5f944d1213b 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -3,15 +3,15 @@
require 'spec_helper'
RSpec.describe Issues::MoveService do
- let(:user) { create(:user) }
- let(:author) { create(:user) }
- let(:title) { 'Some issue' }
- let(:description) { "Some issue description with mention to #{user.to_reference}" }
- let(:group) { create(:group, :private) }
- let(:sub_group_1) { create(:group, :private, parent: group) }
- let(:sub_group_2) { create(:group, :private, parent: group) }
- let(:old_project) { create(:project, namespace: sub_group_1) }
- let(:new_project) { create(:project, namespace: sub_group_2) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:title) { 'Some issue' }
+ let_it_be(:description) { "Some issue description with mention to #{user.to_reference}" }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:sub_group_1) { create(:group, :private, parent: group) }
+ let_it_be(:sub_group_2) { create(:group, :private, parent: group) }
+ let_it_be(:old_project) { create(:project, namespace: sub_group_1) }
+ let_it_be(:new_project) { create(:project, namespace: sub_group_2) }
let(:old_issue) do
create(:issue, title: title, description: description, project: old_project, author: author)
@@ -30,15 +30,10 @@ RSpec.describe Issues::MoveService do
describe '#execute' do
shared_context 'issue move executed' do
- let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
-
let!(:new_issue) { move_service.execute(old_issue, new_project) }
end
context 'issue movable' do
- let!(:note_with_mention) { create(:note, noteable: old_issue, author: author, project: old_project, note: "note with mention #{user.to_reference}") }
- let!(:note_with_no_mention) { create(:note, noteable: old_issue, author: author, project: old_project, note: "note without mention") }
-
include_context 'user can move issue'
context 'generic issue' do
@@ -48,11 +43,11 @@ RSpec.describe Issues::MoveService do
expect(new_issue.project).to eq new_project
end
- it 'rewrites issue title' do
+ it 'copies issue title' do
expect(new_issue.title).to eq title
end
- it 'rewrites issue description' do
+ it 'copies issue description' do
expect(new_issue.description).to eq description
end
@@ -93,23 +88,21 @@ RSpec.describe Issues::MoveService do
it 'preserves create time' do
expect(old_issue.created_at).to eq new_issue.created_at
end
+ end
- it 'moves the award emoji' do
- expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
- end
+ context 'issue with award emoji' do
+ let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
- context 'when issue has notes with mentions' do
- it 'saves user mentions with actual mentions for new issue' do
- expect(new_issue.user_mentions.find_by(note_id: nil).mentioned_users_ids).to match_array([user.id])
- expect(new_issue.user_mentions.where.not(note_id: nil).first.mentioned_users_ids).to match_array([user.id])
- expect(new_issue.user_mentions.where.not(note_id: nil).count).to eq 1
- expect(new_issue.user_mentions.count).to eq 2
- end
+ it 'copies the award emoji' do
+ old_issue.reload
+ new_issue = move_service.execute(old_issue, new_project)
+
+ expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
end
end
context 'issue with assignee' do
- let(:assignee) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
before do
old_issue.assignees = [assignee]
@@ -154,6 +147,25 @@ RSpec.describe Issues::MoveService do
.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
end
end
+
+ # These tests verify that notes are copied. More thorough tests are in
+ # the unit test for Notes::CopyService.
+ context 'issue with notes' do
+ let!(:notes) do
+ [
+ create(:note, noteable: old_issue, project: old_project, created_at: 2.weeks.ago, updated_at: 1.week.ago),
+ create(:note, noteable: old_issue, project: old_project)
+ ]
+ end
+
+ let(:copied_notes) { new_issue.notes.limit(notes.size) } # Remove the system note added by the copy itself
+
+ include_context 'issue move executed'
+
+ it 'copies existing notes in order' do
+ expect(copied_notes.order('id ASC').pluck(:note)).to eq(notes.map(&:note))
+ end
+ end
end
describe 'move permissions' do
diff --git a/spec/services/markdown_content_rewriter_service_spec.rb b/spec/services/markdown_content_rewriter_service_spec.rb
new file mode 100644
index 00000000000..a5cd2a25a37
--- /dev/null
+++ b/spec/services/markdown_content_rewriter_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MarkdownContentRewriterService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_parent) { create(:project, :public) }
+ let_it_be(:target_parent) { create(:project, :public) }
+ let(:content) { 'My content' }
+
+ subject { described_class.new(user, content, source_parent, target_parent).execute }
+
+ it 'calls the rewriter classes successfully', :aggregate_failures do
+ [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter].each do |rewriter_class|
+ service = double
+
+ expect(service).to receive(:rewrite).with(target_parent)
+ expect(rewriter_class).to receive(:new).and_return(service)
+ end
+
+ subject
+ end
+
+ # Perform simple integration-style tests for each rewriter class.
+ # to prove they run correctly.
+ context 'when content contains a reference' do
+ let_it_be(:issue) { create(:issue, project: source_parent) }
+ let(:content) { "See ##{issue.iid}" }
+
+ it 'rewrites content' do
+ expect(subject).to eq("See #{source_parent.full_path}##{issue.iid}")
+ end
+ end
+
+ context 'when content contains an upload' do
+ let(:image_uploader) { build(:file_uploader, project: source_parent) }
+ let(:content) { "Text and #{image_uploader.markdown_link}" }
+
+ it 'rewrites content' do
+ new_content = subject
+
+ expect(new_content).not_to eq(content)
+ expect(new_content.length).to eq(content.length)
+ end
+ end
+ end
+end
diff --git a/spec/services/notes/copy_service_spec.rb b/spec/services/notes/copy_service_spec.rb
new file mode 100644
index 00000000000..fd44aa7cf40
--- /dev/null
+++ b/spec/services/notes/copy_service_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Notes::CopyService do
+ describe '#initialize' do
+ let_it_be(:noteable) { create(:issue) }
+
+ it 'validates that we cannot copy notes to the same Noteable' do
+ expect { described_class.new(noteable, noteable) }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:from_project) { create(:project, :public, group: group) }
+ let_it_be(:to_project) { create(:project, :public, group: group) }
+ let(:from_noteable) { create(:issue, project: from_project) }
+ let(:to_noteable) { create(:issue, project: to_project) }
+
+ subject(:execute_service) { described_class.new(user, from_noteable, to_noteable).execute }
+
+ context 'rewriting the note body' do
+ context 'simple notes' do
+ let!(:notes) do
+ [
+ create(:note, noteable: from_noteable, project: from_noteable.project,
+ created_at: 2.weeks.ago, updated_at: 1.week.ago),
+ create(:note, noteable: from_noteable, project: from_noteable.project),
+ create(:note, system: true, noteable: from_noteable, project: from_noteable.project)
+ ]
+ end
+
+ it 'rewrites existing notes in valid order' do
+ execute_service
+
+ expect(to_noteable.notes.order('id ASC').pluck(:note).first(3)).to eq(notes.map(&:note))
+ end
+
+ it 'copies all the issue notes' do
+ execute_service
+
+ expect(to_noteable.notes.count).to eq(3)
+ end
+
+ it 'does not change the note attributes' do
+ execute_service
+
+ new_note = to_noteable.notes.first
+
+ expect(new_note).to have_attributes(
+ note: notes.first.note,
+ author: notes.first.author
+ )
+ end
+
+ it 'copies the award emojis' do
+ create(:award_emoji, awardable: notes.first, name: 'thumbsup')
+
+ execute_service
+
+ new_award_emoji = to_noteable.notes.first.award_emoji.first
+
+ expect(new_award_emoji.name).to eq('thumbsup')
+ end
+
+ it 'copies system_note_metadata for system note' do
+ system_note_metadata = create(:system_note_metadata, note: notes.last)
+
+ execute_service
+
+ new_note = to_noteable.notes.last
+
+ aggregate_failures do
+ expect(new_note.system_note_metadata.action).to eq(system_note_metadata.action)
+ expect(new_note.system_note_metadata.id).not_to eq(system_note_metadata.id)
+ end
+ end
+
+ it 'returns success' do
+ aggregate_failures do
+ expect(execute_service).to be_kind_of(ServiceResponse)
+ expect(execute_service).to be_success
+ end
+ end
+ end
+
+ context 'notes with mentions' do
+ let!(:note_with_mention) { create(:note, noteable: from_noteable, author: from_noteable.author, project: from_noteable.project, note: "note with mention #{user.to_reference}") }
+ let!(:note_with_no_mention) { create(:note, noteable: from_noteable, author: from_noteable.author, project: from_noteable.project, note: "note without mention") }
+
+ it 'saves user mentions with actual mentions for new issue' do
+ execute_service
+
+ aggregate_failures do
+ expect(to_noteable.user_mentions.first.mentioned_users_ids).to match_array([user.id])
+ expect(to_noteable.user_mentions.count).to eq(1)
+ end
+ end
+ end
+
+ context 'notes with reference' do
+ let(:other_issue) { create(:issue, project: from_noteable.project) }
+ let(:merge_request) { create(:merge_request) }
+ let(:text) { "See ##{other_issue.iid} and #{merge_request.project.full_path}!#{merge_request.iid}" }
+ let!(:note) { create(:note, noteable: from_noteable, note: text, project: from_noteable.project) }
+
+ it 'rewrites the references correctly' do
+ execute_service
+
+ new_note = to_noteable.notes.first
+
+ expected_text = "See #{other_issue.project.path}##{other_issue.iid} and #{merge_request.project.full_path}!#{merge_request.iid}"
+
+ aggregate_failures do
+ expect(new_note.note).to eq(expected_text)
+ expect(new_note.author).to eq(note.author)
+ end
+ end
+ end
+
+ context 'notes with upload' do
+ let(:uploader) { build(:file_uploader, project: from_noteable.project) }
+ let(:text) { "Simple text with image: #{uploader.markdown_link} "}
+ let!(:note) { create(:note, noteable: from_noteable, note: text, project: from_noteable.project) }
+
+ it 'rewrites note content correctly' do
+ execute_service
+ new_note = to_noteable.notes.first
+
+ aggregate_failures do
+ expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(note.note).not_to eq(new_note.note)
+ expect(note.note_html).not_to eq(new_note.note_html)
+ end
+ end
+ end
+
+ context 'discussion notes' do
+ let(:note) { create(:note, noteable: from_noteable, note: 'sample note', project: from_noteable.project) }
+ let!(:discussion) { create(:discussion_note_on_issue, in_reply_to: note, note: 'reply to sample note') }
+
+ it 'rewrites discussion correctly' do
+ execute_service
+
+ aggregate_failures do
+ expect(to_noteable.notes.count).to eq(from_noteable.notes.count)
+ expect(to_noteable.notes.where(discussion_id: discussion.discussion_id).count).to eq(0)
+ expect(from_noteable.notes.where(discussion_id: discussion.discussion_id).count).to eq(1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/maven/find_or_create_package_service_spec.rb b/spec/services/packages/maven/find_or_create_package_service_spec.rb
index c9441324216..4406e4037e2 100644
--- a/spec/services/packages/maven/find_or_create_package_service_spec.rb
+++ b/spec/services/packages/maven/find_or_create_package_service_spec.rb
@@ -4,34 +4,77 @@ require 'spec_helper'
RSpec.describe Packages::Maven::FindOrCreatePackageService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
- let_it_be(:app_name) { 'my-app' }
- let_it_be(:version) { '1.0-SNAPSHOT' }
- let_it_be(:path) { "my/company/app/#{app_name}" }
- let_it_be(:path_with_version) { "#{path}/#{version}" }
- let_it_be(:params) do
- {
- path: path_with_version,
- name: path,
- version: version
- }
- end
+
+ let(:app_name) { 'my-app' }
+ let(:path) { "sandbox/test/app/#{app_name}" }
+ let(:version) { '1.0.0' }
+ let(:file_name) { 'test.jar' }
+ let(:param_path) { "#{path}/#{version}" }
describe '#execute' do
- subject { described_class.new(project, user, params).execute }
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(project, user, { path: param_path, file_name: file_name }).execute }
+
+ RSpec.shared_examples 'reuse existing package' do
+ it { expect { subject}.not_to change { Packages::Package.count } }
+
+ it { is_expected.to eq(existing_package) }
+ end
+
+ RSpec.shared_examples 'create package' do
+ it { expect { subject}.to change { Packages::Package.count }.by(1) }
+
+ it 'sets the proper name and version' do
+ pkg = subject
+
+ expect(pkg.name).to eq(path)
+ expect(pkg.version).to eq(version)
+ end
+ end
- context 'without any existing package' do
- it 'creates a package' do
- expect { subject }.to change { Packages::Package.count }.by(1)
+ context 'path with version' do
+ # Note that "path with version" and "file type maven metadata xml" only exists for snapshot versions
+ # In other words, we will never have an metadata xml upload on a path with version for a non snapshot version
+ where(:package_exist, :file_type, :snapshot_version, :shared_example_name) do
+ true | :jar | false | 'reuse existing package'
+ false | :jar | false | 'create package'
+ true | :jar | true | 'reuse existing package'
+ false | :jar | true | 'create package'
+ true | :maven_xml | true | 'reuse existing package'
+ false | :maven_xml | true | 'create package'
+ end
+
+ with_them do
+ let(:version) { snapshot_version ? '1.0-SNAPSHOT' : '1.0.0' }
+ let(:file_name) { file_type == :maven_xml ? 'maven-metadata.xml' : 'test.jar' }
+
+ let!(:existing_package) do
+ if package_exist
+ create(:maven_package, name: path, version: version, project: project)
+ end
+ end
+
+ it_behaves_like params[:shared_example_name]
end
end
- context 'with an existing package' do
- let_it_be(:existing_package) { create(:maven_package, name: path, version: version, project: project) }
+ context 'path without version' do
+ let(:param_path) { path }
+ let(:version) { nil }
+
+ context 'maven-metadata.xml file' do
+ let(:file_name) { 'maven-metadata.xml' }
+
+ context 'with existing package' do
+ let!(:existing_package) { create(:maven_package, name: path, version: version, project: project) }
+
+ it_behaves_like 'reuse existing package'
+ end
- it { is_expected.to eq existing_package }
- it "doesn't create a new package" do
- expect { subject }
- .to not_change { Packages::Package.count }
+ context 'without existing package' do
+ it_behaves_like 'create package'
+ end
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ed3211a9c87..68beef40c0b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -107,7 +107,6 @@ RSpec.configure do |config|
config.include FixtureHelpers
config.include NonExistingRecordsHelpers
config.include GitlabRoutingHelper
- config.include StubFeatureFlags
config.include StubExperiments
config.include StubGitlabCalls
config.include StubGitlabData
@@ -140,6 +139,8 @@ RSpec.configure do |config|
config.include SidekiqMiddleware
config.include StubActionCableConnection, type: :channel
+ include StubFeatureFlags
+
if ENV['CI'] || ENV['RETRIES']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = ENV.fetch('RETRIES', 3).to_i + 1
@@ -158,6 +159,10 @@ RSpec.configure do |config|
# Reload all feature flags definitions
Feature.register_definitions
+
+ # Enable all features by default for testing
+ # Reset any changes in after hook.
+ stub_all_feature_flags
end
config.after(:all) do
@@ -176,9 +181,6 @@ RSpec.configure do |config|
config.before do |example|
if example.metadata.fetch(:stub_feature_flags, true)
- # Enable all features by default for testing
- stub_all_feature_flags
-
# The following can be removed when we remove the staged rollout strategy
# and we can just enable it using instance wide settings
# (ie. ApplicationSetting#auto_devops_enabled)
@@ -203,6 +205,8 @@ RSpec.configure do |config|
stub_feature_flags(file_identifier_hash: false)
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
+ else
+ unstub_all_feature_flags
end
# Enable Marginalia feature for all specs in the test suite.
@@ -314,6 +318,9 @@ RSpec.configure do |config|
config.after do
Fog.unmock! if Fog.mock?
Gitlab::CurrentSettings.clear_in_memory_application_settings!
+
+ # Reset all feature flag stubs to default for testing
+ stub_all_feature_flags
end
config.before(:example, :mailer) do
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index 696148cacaf..792a1c21c31 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -1,6 +1,11 @@
# frozen_string_literal: true
module StubFeatureFlags
+ def self.included(base)
+ # Extend Feature class with methods that can stub feature flags.
+ Feature.prepend(StubbedFeature)
+ end
+
class StubFeatureGate
attr_reader :flipper_id
@@ -9,28 +14,14 @@ module StubFeatureFlags
end
end
+ # Ensure feature flags are stubbed and reset.
def stub_all_feature_flags
- adapter = Flipper::Adapters::Memory.new
- flipper = Flipper.new(adapter)
-
- allow(Feature).to receive(:flipper).and_return(flipper)
-
- # All new requested flags are enabled by default
- allow(Feature).to receive(:enabled?).and_wrap_original do |m, *args|
- feature_flag = m.call(*args)
-
- # If feature flag is not persisted we mark the feature flag as enabled
- # We do `m.call` as we want to validate the execution of method arguments
- # and a feature flag state if it is not persisted
- unless Feature.persisted_name?(args.first)
- # TODO: this is hack to support `promo_feature_available?`
- # We enable all feature flags by default unless they are `promo_`
- # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667
- feature_flag = true unless args.first.to_s.start_with?('promo_')
- end
+ Feature.stub = true
+ Feature.reset_flipper
+ end
- feature_flag
- end
+ def unstub_all_feature_flags
+ Feature.stub = false
end
# Stub Feature flags with `flag_name: true/false`
diff --git a/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb
new file mode 100644
index 00000000000..e78efcf6b75
--- /dev/null
+++ b/spec/support/helpers/stubbed_feature.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+# Extend the Feature class with the ability to stub feature flags.
+module StubbedFeature
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Turn stubbed feature flags on or off.
+ def stub=(stub)
+ @stub = stub
+ end
+
+ def stub?
+ @stub.nil? ? true : @stub
+ end
+
+ # Wipe any previously set feature flags.
+ def reset_flipper
+ @flipper = nil
+ end
+
+ # Replace #flipper method with the optional stubbed/unstubbed version.
+ def flipper
+ if stub?
+ @flipper ||= Flipper.new(Flipper::Adapters::Memory.new)
+ else
+ super
+ end
+ end
+
+ # Replace #enabled? method with the optional stubbed/unstubbed version.
+ def enabled?(*args)
+ feature_flag = super(*args)
+ return feature_flag unless stub?
+
+ # If feature flag is not persisted we mark the feature flag as enabled
+ # We do `m.call` as we want to validate the execution of method arguments
+ # and a feature flag state if it is not persisted
+ unless Feature.persisted_name?(args.first)
+ # TODO: this is hack to support `promo_feature_available?`
+ # We enable all feature flags by default unless they are `promo_`
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667
+ feature_flag = true unless args.first.to_s.start_with?('promo_')
+ end
+
+ feature_flag
+ end
+ end
+end
diff --git a/spec/support_specs/helpers/stub_feature_flags_spec.rb b/spec/support_specs/helpers/stub_feature_flags_spec.rb
index 8d5f16751ae..5d1e4e1627d 100644
--- a/spec/support_specs/helpers/stub_feature_flags_spec.rb
+++ b/spec/support_specs/helpers/stub_feature_flags_spec.rb
@@ -119,6 +119,42 @@ RSpec.describe StubFeatureFlags do
end
end
+ describe 'stub timing' do
+ context 'let_it_be variable' do
+ let_it_be(:let_it_be_var) { Feature.enabled?(:any_feature_flag) }
+
+ it { expect(let_it_be_var).to eq true }
+ end
+
+ context 'before_all variable' do
+ before_all do
+ @suite_var = Feature.enabled?(:any_feature_flag)
+ end
+
+ it { expect(@suite_var).to eq true }
+ end
+
+ context 'before(:all) variable' do
+ before(:all) do
+ @suite_var = Feature.enabled?(:any_feature_flag)
+ end
+
+ it { expect(@suite_var).to eq true }
+ end
+
+ context 'with stub_feature_flags meta' do
+ let(:var) { Feature.enabled?(:any_feature_flag) }
+
+ context 'as true', :stub_feature_flags do
+ it { expect(var).to eq true }
+ end
+
+ context 'as false', stub_feature_flags: false do
+ it { expect(var).to eq false }
+ end
+ end
+ end
+
def actor(actor)
case actor
when Array
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 9ddfe08c8f3..eb763d424d3 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -33,5 +33,37 @@ RSpec.describe 'search/show' do
expect(rendered).to render_template('search/_category')
expect(rendered).to render_template('search/_results')
end
+
+ context 'unfurling support' do
+ let(:group) { build(:group) }
+ let(:search_results) do
+ instance_double(Gitlab::GroupSearchResults).tap do |double|
+ allow(double).to receive(:formatted_count).and_return(0)
+ end
+ end
+
+ before do
+ assign(:search_results, search_results)
+ assign(:scope, 'issues')
+ assign(:group, group)
+ end
+
+ it 'renders meta tags for a group' do
+ render
+
+ expect(view.page_description).to match(/\d+ issues for term '#{search_term}'/)
+ expect(view.page_card_attributes).to eq("Namespace" => group.full_path)
+ end
+
+ it 'renders meta tags for both group and project' do
+ project = build(:project, group: group)
+ assign(:project, project)
+
+ render
+
+ expect(view.page_description).to match(/\d+ issues for term '#{search_term}'/)
+ expect(view.page_card_attributes).to eq("Namespace" => group.full_path, "Project" => project.full_path)
+ end
+ end
end
end
diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb
index bed6dc59ac7..9aac253c767 100644
--- a/spec/workers/incident_management/process_alert_worker_spec.rb
+++ b/spec/workers/incident_management/process_alert_worker_spec.rb
@@ -19,14 +19,14 @@ RSpec.describe IncidentManagement::ProcessAlertWorker do
allow(Gitlab::AppLogger).to receive(:warn).and_call_original
allow(IncidentManagement::CreateIssueService)
- .to receive(:new).with(alert.project, parsed_payload)
+ .to receive(:new).with(alert.project, alert)
.and_call_original
end
shared_examples 'creates issue successfully' do
it 'creates an issue' do
expect(IncidentManagement::CreateIssueService)
- .to receive(:new).with(alert.project, parsed_payload)
+ .to receive(:new).with(alert.project, alert)
expect { subject }.to change { Issue.count }.by(1)
end
diff --git a/yarn.lock b/yarn.lock
index 04d4312fcd9..9886c8653a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -848,10 +848,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.158.0.tgz#300d416184a2b0e05f15a96547f726e1825b08a1"
integrity sha512-5OJl+7TsXN9PJhY6/uwi+mTwmDZa9n/6119rf77orQ/joFYUypaYhBmy/1TcKVPsy5Zs6KCxE1kmGsfoXc1TYA==
-"@gitlab/ui@18.3.0":
- version "18.3.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-18.3.0.tgz#c582eca1a0a851823700dabc7f4456feef882d9a"
- integrity sha512-H0I3ExZJIqDd9rFDzyZwUerS3ZHDxRf2wHmAzMzK9smq/kr8aL5Pvb2E0KPcgDsVhGQCt7coCBN5NI0p+kf8oQ==
+"@gitlab/ui@18.6.0":
+ version "18.6.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-18.6.0.tgz#b0009ddbd0c6a058b3648786c8dbc5e038dd78c9"
+ integrity sha512-kFrJIVeAyAjrEQuadReuFmSV4Lw1V9LkgnSzhE0TdIkjLtST9y0QLH1Z0REH3QQZe7GbJIRQfZ8ItXpVWJsrgw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
@@ -1162,20 +1162,20 @@
dom-accessibility-api "^0.4.5"
pretty-format "^25.5.0"
-"@toast-ui/editor@^2.3.0":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-2.3.0.tgz#47a0bb4f7cec8248dda64cbbd2edf63294debcd8"
- integrity sha512-rCb35CMxYS6U2aiwWhdLZMzbgzoVHm2YxGrlmH4OdNQNfzAM03DHl4lTwq7EP7E4MG0FEMgMyn0Ovo5DI7G8+w==
+"@toast-ui/editor@^2.3.1":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-2.3.1.tgz#add692840011efcdbd9b4b6de93dda2da12c36a0"
+ integrity sha512-0akQUnyCg24SBBb+weRX6VJ6ZjltxYEivBWOHft0birkrPF0YQgKjebPtFGPB9THBSUY+0qyr6k/KIN2rZqUog==
dependencies:
"@types/codemirror" "0.0.71"
codemirror "^5.48.4"
-"@toast-ui/vue-editor@^2.3.0":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/@toast-ui/vue-editor/-/vue-editor-2.3.0.tgz#8b80896f1132ca229ab28167c78f607d06e4d3ef"
- integrity sha512-oXuy4YqaF9RHBqYutNg8xI9XvcrWQv9xRKQJLSPtR3wh9/5AeTuXm/b8qkJdoccCsJl5lUq/Qh7l/+o8zcbvFA==
+"@toast-ui/vue-editor@^2.3.1":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/@toast-ui/vue-editor/-/vue-editor-2.3.1.tgz#ca8771a77513d1998123db717626a2ec4267b6f5"
+ integrity sha512-vEElTwJ3CwUL2da2y3gxKVK7bRWC+d6SrUyOQNeCDtN95vEs/7MUNK4cUAxAGDZuDYg1KB2ZpBtc/dV1LkO9XQ==
dependencies:
- "@toast-ui/editor" "^2.3.0"
+ "@toast-ui/editor" "^2.3.1"
"@types/babel__core@^7.1.0":
version "7.1.2"