Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-12 21:09:54 +00:00
parent 5982b74e32
commit 14245e7755
63 changed files with 693 additions and 551 deletions

View file

@ -1,9 +1,10 @@
<script>
import { GlFormInput } from '@gitlab/ui';
import { GlFormInput, GlButton } from '@gitlab/ui';
export default {
components: {
GlFormInput,
GlButton,
},
inheritAttrs: false,
props: {
@ -11,6 +12,16 @@ export default {
type: String,
required: true,
},
canDelete: {
type: Boolean,
required: false,
default: false,
},
showDelete: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -21,17 +32,27 @@ export default {
</script>
<template>
<div class="js-file-title file-title-flex-parent">
<gl-form-input
id="snippet_file_name"
v-model="name"
:placeholder="
s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby')
"
name="snippet_file_name"
class="form-control js-snippet-file-name"
type="text"
v-bind="$attrs"
@change="$emit('input', name)"
/>
<div class="gl-display-flex gl-align-items-center gl-w-full">
<gl-form-input
v-model="name"
:placeholder="
s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby')
"
name="snippet_file_name"
class="form-control js-snippet-file-name"
type="text"
v-bind="$attrs"
@change="$emit('input', name)"
/>
<gl-button
v-if="showDelete"
class="gl-ml-4"
variant="danger"
category="secondary"
:disabled="!canDelete"
@click="$emit('delete')"
>{{ s__('Snippets|Delete file') }}</gl-button
>
</div>
</div>
</template>

View file

@ -24,9 +24,9 @@ export default {
class="settings no-animate qa-incident-management-settings"
>
<div class="settings-header">
<h3 ref="sectionHeader" class="h4">
<h4 ref="sectionHeader" class="gl-my-3! gl-py-1">
{{ $options.i18n.headerText }}
</h3>
</h4>
<gl-button ref="toggleBtn" class="js-settings-toggle">{{
$options.i18n.expandBtnLabel
}}</gl-button>

View file

@ -54,7 +54,7 @@ export default {
},
},
strings: {
deleteProject: __('Remove project'),
deleteProject: __('Delete project'),
title: __('Delete project. Are you ABSOLUTELY SURE?'),
confirmText: __('Please type the following to confirm:'),
},

View file

@ -18,7 +18,7 @@ import {
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
} from '../constants';
import SnippetBlobEdit from './snippet_blob_edit.vue';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
@ -27,7 +27,7 @@ export default {
components: {
SnippetDescriptionEdit,
SnippetVisibilityEdit,
SnippetBlobEdit,
SnippetBlobActionsEdit,
TitleField,
FormFooterActions,
GlButton,
@ -261,15 +261,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
/>
<template v-if="blobs.length">
<snippet-blob-edit
v-for="blob in blobs"
:key="blob.name"
:blob="blob"
@blob-updated="updateBlobActions"
/>
</template>
<snippet-blob-edit v-else @blob-updated="updateBlobActions" />
<snippet-blob-actions-edit :blobs="blobs" @blob-updated="updateBlobActions" />
<snippet-visibility-edit
v-model="snippet.visibilityLevel"

View file

@ -0,0 +1,25 @@
<script>
import SnippetBlobEdit from './snippet_blob_edit.vue';
export default {
components: {
SnippetBlobEdit,
},
props: {
blobs: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div class="form-group file-editor">
<label for="snippet_file_path">{{ s__('Snippets|File') }}</label>
<template v-if="blobs.length">
<snippet-blob-edit v-for="blob in blobs" :key="blob.name" :blob="blob" v-on="$listeners" />
</template>
<snippet-blob-edit v-else v-on="$listeners" />
</div>
</template>

View file

@ -91,17 +91,18 @@ export default {
};
</script>
<template>
<div class="form-group file-editor">
<label>{{ s__('Snippets|File') }}</label>
<div class="file-holder snippet">
<blob-header-edit v-model="filePath" data-qa-selector="file_name_field" />
<gl-loading-icon
v-if="isContentLoading"
:label="__('Loading snippet')"
size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<blob-content-edit v-else v-model="content" :file-global-id="id" :file-name="filePath" />
</div>
<div class="file-holder snippet">
<blob-header-edit
id="snippet_file_path"
v-model="filePath"
data-qa-selector="file_name_field"
/>
<gl-loading-icon
v-if="isContentLoading"
:label="__('Loading snippet')"
size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<blob-content-edit v-else v-model="content" :file-global-id="id" :file-name="filePath" />
</div>
</template>

View file

@ -30,3 +30,4 @@ export const SNIPPET_BLOB_CONTENT_FETCH_ERROR = __("Can't fetch content for the
export const SNIPPET_BLOB_ACTION_CREATE = 'create';
export const SNIPPET_BLOB_ACTION_UPDATE = 'update';
export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete';

View file

@ -0,0 +1,66 @@
import { uniqueId } from 'lodash';
import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
} from '../constants';
const createLocalId = () => uniqueId('blob_local_');
export const decorateBlob = blob => ({
...blob,
id: createLocalId(),
isLoaded: false,
content: '',
});
export const createBlob = () => ({
id: createLocalId(),
content: '',
path: '',
isLoaded: true,
});
const diff = ({ content, path }, origBlob) => {
if (!origBlob) {
return {
action: SNIPPET_BLOB_ACTION_CREATE,
previousPath: path,
content,
filePath: path,
};
} else if (origBlob.path !== path || origBlob.content !== content) {
return {
action: origBlob.path === path ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE,
previousPath: origBlob.path,
content,
filePath: path,
};
}
return null;
};
/**
* This function returns an array of diff actions (to be sent to the BE) based on the current vs. original blobs
*
* @param {Object} blobs
* @param {Object} origBlobs
*/
export const diffAll = (blobs, origBlobs) => {
const deletedEntries = Object.values(origBlobs)
.filter(x => !blobs[x.id])
.map(({ path, content }) => ({
action: SNIPPET_BLOB_ACTION_DELETE,
previousPath: path,
filePath: path,
content,
}));
const newEntries = Object.values(blobs)
.map(blob => diff(blob, origBlobs[blob.id]))
.filter(x => x);
return [...deletedEntries, ...newEntries];
};

View file

@ -38,6 +38,9 @@ class ProjectsController < Projects::ApplicationController
before_action only: [:new, :create] do
frontend_experimentation_tracking_data(:new_create_project_ui, 'click_tab')
push_frontend_feature_flag(:new_create_project_ui) if experiment_enabled?(:new_create_project_ui)
end
before_action only: [:edit] do
push_frontend_feature_flag(:service_desk_custom_address, @project)
end

View file

@ -130,7 +130,7 @@ module GroupsHelper
end
def remove_group_message(group)
_("You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
_("You are going to remove %{group_name}, this will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end

View file

@ -104,7 +104,7 @@ module ProjectsHelper
end
def remove_project_message(project)
_("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
_("You are going to delete %{project_full_name}. Deleted projects CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
end

View file

@ -3,6 +3,7 @@
class PagesDomain < ApplicationRecord
include Presentable
include FromUnion
include AfterCommitQueue
VERIFICATION_KEY = 'gitlab-pages-verification-code'
VERIFICATION_THRESHOLD = 3.days.freeze
@ -222,6 +223,8 @@ class PagesDomain < ApplicationRecord
private
def pages_deployed?
return false unless project
# TODO: remove once `pages_metadatum` is migrated
# https://gitlab.com/gitlab-org/gitlab/issues/33106
unless project.pages_metadatum
@ -244,8 +247,13 @@ class PagesDomain < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def update_daemon
return if usage_serverless?
return unless pages_deployed?
::Projects::UpdatePagesConfigurationService.new(project).execute
if Feature.enabled?(:async_update_pages_config, project)
run_after_commit { PagesUpdateConfigurationWorker.perform_async(project_id) }
else
Projects::UpdatePagesConfigurationService.new(project).execute
end
end
# rubocop: enable CodeReuse/ServiceClass

View file

@ -2468,6 +2468,10 @@ class Project < ApplicationRecord
alias_method :service_desk_enabled?, :service_desk_enabled
def service_desk_address
service_desk_custom_address || service_desk_incoming_address
end
def service_desk_incoming_address
return unless service_desk_enabled?
config = Gitlab.config.incoming_email
@ -2476,6 +2480,16 @@ class Project < ApplicationRecord
config.address&.gsub(wildcard, "#{full_path_slug}-#{id}-issue-")
end
def service_desk_custom_address
return unless ::Gitlab::ServiceDeskEmail.enabled?
return unless ::Feature.enabled?(:service_desk_custom_address, self)
key = service_desk_setting&.project_key
return unless key.present?
::Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
end
def root_namespace
if namespace.has_parent?
namespace.root_ancestor

View file

@ -1,74 +0,0 @@
# frozen_string_literal: true
module IncidentManagement
class CreateIssueService < BaseService
include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
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_presenter.valid?
result = create_incident
return error(result.message, result.payload[:issue]) unless result.success?
result
end
private
def create_incident
::IncidentManagement::Incidents::CreateService.new(
project,
current_user,
title: issue_title,
description: issue_description
).execute
end
def issue_title
alert_presenter.full_title
end
def issue_description
horizontal_line = "\n\n---\n\n"
[
alert_summary,
alert_markdown,
issue_template_content
].compact.join(horizontal_line)
end
def alert_summary
alert_presenter.issue_summary_markdown
end
def alert_markdown
alert_presenter.alert_markdown
end
def alert_presenter
strong_memoize(:alert_presenter) do
Gitlab::Alerting::Alert.for_alert_management_alert(project: project, alert: alert).present
end
end
def issue_template_content
incident_management_setting.issue_template_content
end
def error(message, issue = nil)
log_error(%{Cannot create incident issue for "#{project.full_name}": #{message}})
ServiceResponse.error(payload: { issue: issue }, message: message)
end
end
end

View file

@ -31,7 +31,7 @@ module Projects
attempt_destroy_transaction(project)
system_hook_service.execute_hooks_for(project, :destroy)
log_info("Project \"#{project.full_path}\" was removed")
log_info("Project \"#{project.full_path}\" was deleted")
current_user.invalidate_personal_projects_count

View file

@ -9,6 +9,8 @@ module ServiceDeskSettings
params.delete(:project_key)
end
params[:project_key] = nil if params[:project_key].blank?
if settings.update(params)
success
else

View file

@ -15,7 +15,7 @@
.controls
= link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to _('Remove'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-remove"
= link_to _('Delete'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-remove"
.stats
%span.badge.badge-pill

View file

@ -2,9 +2,9 @@
- confirm_phrase = s_('DeleteProject|Delete %{name}') % { name: project.full_name }
.sub-section
%h4.danger-title= _('Remove project')
%h4.danger-title= _('Delete project')
%p
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
%strong= _('Deleting the project will delete its repository and all related resources including issues, merge requests etc.')
%p
%strong= _('Removed projects cannot be restored!')
%strong= _('Deleted projects cannot be restored!')
#js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: confirm_phrase } }

View file

@ -10,7 +10,8 @@
- if ::Gitlab::ServiceDesk.supported?
.js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project),
enabled: "#{@project.service_desk_enabled}",
incoming_email: (@project.service_desk_address if @project.service_desk_enabled),
incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled),
custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",

View file

@ -22,4 +22,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
= link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
= link_to _('Delete project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"

View file

@ -17,10 +17,9 @@ module IncidentManagement
return unless alert
result = create_issue_for(alert)
return unless result.success?
return if result.success?
new_issue = result.payload[:issue]
link_issue_with_alert(alert, new_issue.id)
log_warning(alert, result)
end
private
@ -30,19 +29,19 @@ module IncidentManagement
end
def create_issue_for(alert)
IncidentManagement::CreateIssueService
.new(alert.project, alert)
AlertManagement::CreateAlertIssueService
.new(alert, User.alert_bot)
.execute
end
def link_issue_with_alert(alert, issue_id)
return if alert.update(issue_id: issue_id)
def log_warning(alert, result)
issue_id = result.payload[:issue]&.id
Gitlab::AppLogger.warn(
message: 'Cannot link an Issue with Alert',
message: 'Cannot process an Incident',
issue_id: issue_id,
alert_id: alert.id,
alert_errors: alert.errors.messages
errors: result.message
)
end
end

View file

@ -0,0 +1,5 @@
---
title: Re-name project remove as project delete
merge_request: 38489
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Replace mis-used CSS class in operations settings
merge_request: 39338
author:
type: changed

View file

@ -167,7 +167,7 @@ user = User.find_by_username('root')
# Find the project, update the xxx-changeme values from above
project = Project.find_by_full_path('group-changeme/project-changeme')
# Delete the project
# Immediately delete the project
::Projects::DestroyService.new(project, user, {}).execute
```

View file

@ -1832,11 +1832,11 @@ Example response:
}
```
## Remove project
## Delete project
This endpoint:
- Removes a project including all associated resources (issues, merge requests etc).
- Deletes a project including all associated resources (issues, merge requests etc).
- From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers,
group admins can [configure](../user/group/index.md#enabling-delayed-project-removal-premium) projects within a group
to be deleted after a delayed period.

View file

@ -221,7 +221,7 @@ are listed in the descriptions of the relevant settings.
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** The number of days to wait before removing a project or group that is marked for deletion. Value must be between 0 and 90.
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). |
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |

View file

@ -24,7 +24,8 @@ should be leveraged:
- When development of a feature will be spread across multiple merge
requests, you can use the following workflow:
1. Introduce a feature flag which is **off** by default, in the first merge request.
1. [Create a new feature flag](development.md#create-a-new-feature-flag)
which is **off** by default, in the first merge request.
1. Submit incremental changes via one or more merge requests, ensuring that any
new code added can only be reached if the feature flag is **on**.
You can keep the feature flag enabled on your local GDK during development.

View file

@ -636,7 +636,7 @@ Errors in `locale/zh_HK/gitlab.po`:
Syntax error in msgstr
Syntax error in message_line
There should be only whitespace until the end of line after the double quote character of a message text.
Parsing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
Parsing result before error: '{:msgid=>["", "You are going to delete %{project_name_with_namespace}.\\n", "Deleted projects CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
SimplePoParser filtered backtrace: SimplePoParser::ParserError
Errors in `locale/zh_TW/gitlab.po`:
1 pipeline

View file

@ -0,0 +1,36 @@
---
stage: Monitor
group: APM
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab-defined metrics dashboards **(CORE)**
GitLab provides some dashboards out-of-the-box for any project with
[Prometheus available](../../../user/project/integrations/prometheus.md). You can
[duplicate these GitLab-defined dashboards](index.md#duplicate-a-gitlab-defined-dashboard):
- [Overview dashboard](#overview-dashboard).
- [Kubernetes pod health dashboard](#kubernetes-pod-health-dashboard).
To learn about the components of a dashboard, read
[Metrics dashboard for your CI/CD environment](../index.md).
## Overview dashboard
This dashboard is the default metrics dashboard. It displays a large number of
metrics about the [deployed application](../index.md#configure-prometheus-to-gather-metrics).
![Example of metrics dashboard](../img/example-dashboard_v13_3.png)
## Kubernetes pod health dashboard
NOTE: **Note:**
This dashboard requires Kubernetes v1.14 or higher, due to the
[change in metric labels](https://github.com/kubernetes/kubernetes/pull/69099)
in Kubernetes 1.14.
This dashboard displays CPU, memory, network and disk metrics for the pods in your
[connected K8s cluster](../../../user/project/clusters/index.md). It provides a
[variable selector](templating_variables.md#metric_label_values-variable-type)
at the top of the dashboard to select which pod's metrics to display.

View file

@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/59974) in GitLab 12.1.
By default, all projects include a GitLab-defined Prometheus dashboard, which
By default, all projects include a [GitLab-defined Prometheus dashboard](default.md), which
includes a few key metrics, but you can also define your own custom dashboards.
You may create a [new dashboard from scratch](#add-a-new-dashboard-to-your-project)
@ -23,7 +23,8 @@ The metrics as defined below do not support alerts, unlike
> UI option [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228856) in GitLab 13.3.
You can configure a custom dashboard by adding a new YAML file into your project's
`.gitlab/dashboards/` directory. For the dashboard to display on your project's **Operations > Metrics** page, the files must have a `.yml`
`.gitlab/dashboards/` directory. For the dashboard to display on your project's
**Operations > Metrics** page, the files must have a `.yml`
extension and be present in your project's **default** branch.
To create a new dashboard from the GitLab user interface:
@ -145,7 +146,7 @@ Your custom dashboard is available at `https://example.com/project/-/metrics/cus
To manage the settings for your metrics dashboard:
1. Sign in as a user with project Maintainer or Admin
1. Sign in as a user with project Maintainer or Administrator
[permissions](../../../user/permissions.md#project-members-permissions).
1. Navigate to your dashboard at **Operations > Metrics**.
1. In the top-right corner of your dashboard, click **Metrics Settings**:

View file

@ -72,16 +72,15 @@ and NGINX, and attempts to identify individual environments. To learn more about
the supported metrics and scan processes, see the
[Prometheus Metrics Library documentation](../../user/project/integrations/prometheus_library/index.md).
To view the metrics dashboard for an environment that has
To view the metrics dashboard for an environment that is
To view the [default metrics dashboard](dashboards/default.md) for an environment that is
[configured to gather metrics](#configure-prometheus-to-gather-metrics):
1. *If the metrics dashboard is only visible to project members,* sign in to
GitLab as a member of a project. Learn more about [metrics dashboard visibility](#metrics-dashboard-visibility).
1. In your project, navigate to **Operations > Metrics**.
GitLab displays the default metrics dashboard for the environment, like the
following example:
GitLab displays the [default metrics dashboard](dashboards/default.md) for the environment,
like the following example:
![Example of metrics dashboard](img/example-dashboard_v13_3.png)

View file

@ -7,8 +7,6 @@ type: reference, howto
# Coverage Guided Fuzz Testing **(ULTIMATE)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3226) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.2 as an [Alpha feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha).
GitLab allows you to add coverage-guided fuzz testing to your pipelines. This helps you discover
bugs and potential security issues that other QA processes may miss. Coverage-guided fuzzing sends
random inputs to an instrumented version of your application in an effort to cause unexpected

View file

@ -156,7 +156,7 @@ The following table depicts the various user permission levels in a project.
| Transfer project to another namespace | | | | | ✓ |
| Rename project | | | | | ✓ |
| Remove fork relationship | | | | | ✓ |
| Remove project | | | | | ✓ |
| Delete project | | | | | ✓ |
| Archive project | | | | | ✓ |
| Delete issues | | | | | ✓ |
| Delete pipelines | | | | | ✓ |
@ -416,7 +416,7 @@ instance and project. In addition, all admins can use the admin interface under
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
| Erase job artifacts and trace | | ✓ (*1*) | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Delete project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
| Add specific runners | | | ✓ | ✓ |

View file

@ -180,22 +180,22 @@ Read through the documentation on [project settings](settings/index.md).
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md)
## Remove a project
## Delete a project
To remove a project, first navigate to the home page for that project.
To delete a project, first navigate to the home page for that project.
1. Navigate to **Settings > General**.
1. Expand the **Advanced** section.
1. Scroll down to the **Remove project** section.
1. Click **Remove project**
1. Scroll down to the **Delete project** section.
1. Click **Delete project**
1. Confirm this action by typing in the expected text.
### Delayed removal **(PREMIUM)**
### Delayed deletion **(PREMIUM)**
By default, clicking to remove a project is followed by a seven day delay. Admins can restore the project during this period of time.
By default, clicking to delete a project is followed by a seven day delay. Admins can restore the project during this period of time.
This delay [may be changed by an admin](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
Admins can view all projects pending deletion. If you're an administrator, go to the top navigation bar, click **Projects > Your projects**, and then select the **Removed projects** tab.
Admins can view all projects pending deletion. If you're an administrator, go to the top navigation bar, click **Projects > Your projects**, and then select the **Deleted projects** tab.
From this tab an admin can restore any project.
## CI/CD for external repositories **(PREMIUM)**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View file

@ -187,7 +187,10 @@ a rule is already defined.
When an [eligible approver](#eligible-approvers) approves a merge request, it will
reduce the number of approvals left for all rules that the approver belongs to.
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v12_7.png)
When an [eligible approver](#eligible-approvers) comments on a merge request, it
appears in the **Commented by** column. This feature was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in GitLab 13.3.
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v13_3.png)
#### Scoped to Protected Branch **(PREMIUM)**

View file

@ -219,20 +219,20 @@ NOTE: **Note:**
GitLab administrators can use the admin interface to move any project to any
namespace if needed.
#### Remove a project
#### Delete a project
NOTE: **Note:**
Only project owners and admins have [permissions](../../permissions.md#project-members-permissions) to remove a project.
Only project owners and admins have [permissions](../../permissions.md#project-members-permissions) to delete a project.
To remove a project:
To delete a project:
1. Navigate to your project, and select **Settings > General > Advanced**.
1. In the Remove project section, click the **Remove project** button.
1. In the "Delete project" section, click the **Delete project** button.
1. Confirm the action when asked to.
This action:
- Removes a project including all associated resources (issues, merge requests etc).
- Deletes a project including all associated resources (issues, merge requests etc).
- From [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/220382) on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers,
group admins can [configure](../../group/index.md#enabling-delayed-project-removal-premium) projects within a group
to be deleted after a delayed period.

View file

@ -448,7 +448,7 @@ module API
.execute.map { |lang| [lang.name, lang.share] }.to_h
end
desc 'Remove a project'
desc 'Delete a project'
delete ":id" do
authorize! :remove_project, user_project

View file

@ -17,6 +17,12 @@ module Gitlab
def config
Gitlab.config.service_desk_email
end
def address_for_key(key)
return if config.address.blank?
config.address.sub(Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER, key)
end
end
end
end

View file

@ -7884,15 +7884,27 @@ msgstr ""
msgid "Deleted"
msgstr ""
msgid "Deleted Projects"
msgstr ""
msgid "Deleted chat nickname: %{chat_name}!"
msgstr ""
msgid "Deleted in this version"
msgstr ""
msgid "Deleted projects"
msgstr ""
msgid "Deleted projects cannot be restored!"
msgstr ""
msgid "Deleting"
msgstr ""
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Deleting the license failed."
msgstr ""
@ -7902,6 +7914,9 @@ msgstr ""
msgid "Deleting the license failed. You are not permitted to perform this action."
msgstr ""
msgid "Deleting the project will delete its repository and all related resources including issues, merge requests etc."
msgstr ""
msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only."
msgstr ""
@ -20240,9 +20255,6 @@ msgstr ""
msgid "Remove priority"
msgstr ""
msgid "Remove project"
msgstr ""
msgid "Remove secondary node"
msgstr ""
@ -20276,9 +20288,6 @@ msgstr ""
msgid "Removed %{type} with id %{id}"
msgstr ""
msgid "Removed Projects"
msgstr ""
msgid "Removed all labels."
msgstr ""
@ -20291,12 +20300,6 @@ msgstr ""
msgid "Removed parent epic %{epic_ref}."
msgstr ""
msgid "Removed projects"
msgstr ""
msgid "Removed projects cannot be restored!"
msgstr ""
msgid "Removed spent time."
msgstr ""
@ -20345,15 +20348,9 @@ msgstr ""
msgid "Removes time estimate."
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Removing license…"
msgstr ""
msgid "Removing the project will delete its repository and all related resources including issues, merge requests etc."
msgstr ""
msgid "Removing this group also removes all child projects, including archived projects, and their resources."
msgstr ""
@ -22552,6 +22549,9 @@ msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show."
msgstr ""
msgid "Snippets|Delete file"
msgstr ""
msgid "Snippets|Description (optional)"
msgstr ""
@ -22744,10 +22744,10 @@ msgstr ""
msgid "Something went wrong, unable to add projects to dashboard"
msgstr ""
msgid "Something went wrong, unable to get projects"
msgid "Something went wrong, unable to delete project"
msgstr ""
msgid "Something went wrong, unable to remove project"
msgid "Something went wrong, unable to get projects"
msgstr ""
msgid "Something went wrong, unable to search projects"
@ -27657,10 +27657,10 @@ msgstr ""
msgid "You are connected to the Prometheus server, but there is currently no data to display."
msgstr ""
msgid "You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgid "You are going to delete %{project_full_name}. Deleted projects CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
msgid "You are going to remove %{group_name}, this will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove the fork relationship from %{project_full_name}. Are you ABSOLUTELY sure?"

View file

@ -35,7 +35,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
end
it 'renders form for incident management' do
expect(page).to have_selector('h3', text: 'Incidents')
expect(page).to have_selector('h4', text: 'Incidents')
end
it 'sets correct default values' do

View file

@ -254,13 +254,13 @@ RSpec.describe 'Project' do
end
it 'focuses on the confirmation field' do
click_button 'Remove project'
click_button 'Delete project'
expect(page).to have_selector '#confirm_name_input:focus'
end
it 'removes a project', :sidekiq_might_not_need_inline do
expect { remove_with_confirm('Remove project', "Delete #{project.full_name}", 'Yes, delete project') }.to change { Project.count }.by(-1)
it 'deletes a project', :sidekiq_might_not_need_inline do
expect { remove_with_confirm('Delete project', "Delete #{project.full_name}", 'Yes, delete project') }.to change { Project.count }.by(-1)
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty

View file

@ -4,13 +4,18 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = `
<div
class="js-file-title file-title-flex-parent"
>
<gl-form-input-stub
class="form-control js-snippet-file-name"
id="snippet_file_name"
name="snippet_file_name"
placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
type="text"
value="foo.md"
/>
<div
class="gl-display-flex gl-align-items-center gl-w-full"
>
<gl-form-input-stub
class="form-control js-snippet-file-name"
name="snippet_file_name"
placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
type="text"
value="foo.md"
/>
<!---->
</div>
</div>
`;

View file

@ -1,18 +1,21 @@
import { shallowMount } from '@vue/test-utils';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
import { GlFormInput } from '@gitlab/ui';
import { GlFormInput, GlButton } from '@gitlab/ui';
describe('Blob Header Editing', () => {
let wrapper;
const value = 'foo.md';
function createComponent() {
const createComponent = (props = {}) => {
wrapper = shallowMount(BlobEditHeader, {
propsData: {
value,
...props,
},
});
}
};
const findDeleteButton = () =>
wrapper.findAll(GlButton).wrappers.find(x => x.text() === 'Delete file');
beforeEach(() => {
createComponent();
@ -30,6 +33,10 @@ describe('Blob Header Editing', () => {
it('contains a form input field', () => {
expect(wrapper.contains(GlFormInput)).toBe(true);
});
it('does not show delete button', () => {
expect(findDeleteButton()).toBeUndefined();
});
});
describe('functionality', () => {
@ -47,4 +54,35 @@ describe('Blob Header Editing', () => {
});
});
});
describe.each`
props | expectedDisabled
${{ showDelete: true }} | ${true}
${{ showDelete: true, canDelete: true }} | ${false}
`('with $props', ({ props, expectedDisabled }) => {
beforeEach(() => {
createComponent(props);
});
it(`shows delete button (disabled=${expectedDisabled})`, () => {
const deleteButton = findDeleteButton();
expect(deleteButton.exists()).toBe(true);
expect(deleteButton.props('disabled')).toBe(expectedDisabled);
});
});
describe('with delete button', () => {
beforeEach(() => {
createComponent({ showDelete: true, canDelete: true });
});
it('emits delete when clicked', () => {
expect(wrapper.emitted().delete).toBeUndefined();
findDeleteButton().vm.$emit('click');
expect(wrapper.emitted().delete).toEqual([[]]);
});
});
});

View file

@ -9,13 +9,13 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
<div
class="settings-header"
>
<h3
class="h4"
<h4
class="gl-my-3! gl-py-1"
>
Incidents
</h3>
</h4>
<gl-button-stub
category="tertiary"

View file

@ -24,7 +24,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
tabindex="0"
variant="danger"
>
Remove project
Delete project
</gl-button-stub>
<gl-modal-stub

View file

@ -25,7 +25,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
tabindex="0"
variant="danger"
>
Remove project
Delete project
</gl-button-stub>
<b-modal-stub

View file

@ -2,25 +2,18 @@
exports[`Snippet Blob Edit component rendering matches the snapshot 1`] = `
<div
class="form-group file-editor"
class="file-holder snippet"
>
<label>
File
</label>
<blob-header-edit-stub
data-qa-selector="file_name_field"
id="snippet_file_path"
value="lorem.txt"
/>
<div
class="file-holder snippet"
>
<blob-header-edit-stub
data-qa-selector="file_name_field"
value="lorem.txt"
/>
<blob-content-edit-stub
fileglobalid="0a3d"
filename="lorem.txt"
value="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
/>
</div>
<blob-content-edit-stub
fileglobalid="0a3d"
filename="lorem.txt"
value="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
/>
</div>
`;

View file

@ -7,7 +7,7 @@ import { redirectTo } from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import TitleField from '~/vue_shared/components/form/title.vue';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants';
@ -141,7 +141,7 @@ describe('Snippet Edit app', () => {
expect(wrapper.contains(TitleField)).toBe(true);
expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
expect(wrapper.contains(SnippetBlobEdit)).toBe(true);
expect(wrapper.contains(SnippetBlobActionsEdit)).toBe(true);
expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
expect(wrapper.contains(FormFooterActions)).toBe(true);
});

View file

@ -0,0 +1,59 @@
import { shallowMount } from '@vue/test-utils';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
const TEST_BLOBS = [
{ name: 'foo', content: 'abc', rawPath: 'test/raw' },
{ name: 'bar', content: 'def', rawPath: 'test/raw' },
];
const TEST_EVENT = 'blob-update';
describe('snippets/components/snippet_blob_actions_edit', () => {
let onEvent;
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(SnippetBlobActionsEdit, {
propsData: {
blobs: [],
...props,
},
listeners: {
[TEST_EVENT]: onEvent,
},
});
};
const findBlobEdit = () => wrapper.find(SnippetBlobEdit);
const findBlobEditData = () => wrapper.findAll(SnippetBlobEdit).wrappers.map(x => x.props());
beforeEach(() => {
onEvent = jest.fn();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each`
props | expectedData
${{}} | ${[{ blob: null }]}
${{ blobs: TEST_BLOBS }} | ${TEST_BLOBS.map(blob => ({ blob }))}
`('with $props', ({ props, expectedData }) => {
beforeEach(() => {
createComponent(props);
});
it('renders blob edit', () => {
expect(findBlobEditData()).toEqual(expectedData);
});
it('emits event', () => {
expect(onEvent).not.toHaveBeenCalled();
findBlobEdit().vm.$emit('blob-update', TEST_BLOBS[0]);
expect(onEvent).toHaveBeenCalledWith(TEST_BLOBS[0]);
});
});
});

View file

@ -0,0 +1,134 @@
import { cloneDeep } from 'lodash';
import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
} from '~/snippets/constants';
import { decorateBlob, createBlob, diffAll } from '~/snippets/utils/blob';
jest.mock('lodash/uniqueId', () => arg => `${arg}fakeUniqueId`);
const TEST_RAW_BLOB = {
rawPath: '/test/blob/7/raw',
};
const CONTENT_1 = 'Lorem ipsum dolar\nSit amit\n\nGoodbye!\n';
const CONTENT_2 = 'Lorem ipsum dolar sit amit.\n\nGoodbye!\n';
describe('~/snippets/utils/blob', () => {
describe('decorateBlob', () => {
it('should decorate the given object with local blob properties', () => {
const orig = cloneDeep(TEST_RAW_BLOB);
expect(decorateBlob(orig)).toEqual({
...TEST_RAW_BLOB,
id: 'blob_local_fakeUniqueId',
isLoaded: false,
content: '',
});
});
});
describe('createBlob', () => {
it('should create an empty local blob', () => {
expect(createBlob()).toEqual({
id: 'blob_local_fakeUniqueId',
isLoaded: true,
content: '',
path: '',
});
});
});
describe('diffAll', () => {
// This object contains entries that contain an expected "diff" and the `id`
// or `origContent` that should be used to generate the expected diff.
const testEntries = {
created: {
id: 'blob_1',
diff: {
action: SNIPPET_BLOB_ACTION_CREATE,
filePath: '/new/file',
previousPath: '/new/file',
content: CONTENT_1,
},
},
deleted: {
id: 'blob_2',
diff: {
action: SNIPPET_BLOB_ACTION_DELETE,
filePath: '/src/delete/me',
previousPath: '/src/delete/me',
content: CONTENT_1,
},
},
updated: {
id: 'blob_3',
origContent: CONTENT_1,
diff: {
action: SNIPPET_BLOB_ACTION_UPDATE,
filePath: '/lorem.md',
previousPath: '/lorem.md',
content: CONTENT_2,
},
},
renamed: {
id: 'blob_4',
diff: {
action: SNIPPET_BLOB_ACTION_MOVE,
filePath: '/dolar.md',
previousPath: '/ipsum.md',
content: CONTENT_1,
},
},
renamedAndUpdated: {
id: 'blob_5',
origContent: CONTENT_1,
diff: {
action: SNIPPET_BLOB_ACTION_MOVE,
filePath: '/sit.md',
previousPath: '/sit/amit.md',
content: CONTENT_2,
},
},
};
const createBlobsFromTestEntries = (entries, isOrig = false) =>
entries.reduce(
(acc, { id, diff, origContent }) =>
Object.assign(acc, {
[id]: {
id,
content: isOrig && origContent ? origContent : diff.content,
path: isOrig ? diff.previousPath : diff.filePath,
},
}),
{},
);
it('should create diff from original files', () => {
const origBlobs = createBlobsFromTestEntries(
[
testEntries.deleted,
testEntries.updated,
testEntries.renamed,
testEntries.renamedAndUpdated,
],
true,
);
const blobs = createBlobsFromTestEntries([
testEntries.created,
testEntries.updated,
testEntries.renamed,
testEntries.renamedAndUpdated,
]);
expect(diffAll(blobs, origBlobs)).toEqual([
testEntries.deleted.diff,
testEntries.created.diff,
testEntries.updated.diff,
testEntries.renamed.diff,
testEntries.renamedAndUpdated.diff,
]);
});
});
});

View file

@ -56,4 +56,26 @@ RSpec.describe Gitlab::ServiceDeskEmail do
end
end
end
describe '.address_for_key' do
context 'when service desk address is set' do
before do
stub_service_desk_email_setting(address: 'address+%{key}@example.com')
end
it 'returns address' do
expect(described_class.address_for_key('foo')).to eq('address+foo@example.com')
end
end
context 'when service desk address is not set' do
before do
stub_service_desk_email_setting(address: nil)
end
it 'returns nil' do
expect(described_class.key_from_address('foo')).to be_nil
end
end
end
end

View file

@ -328,9 +328,11 @@ RSpec.describe PagesDomain do
end
describe '#update_daemon' do
let_it_be(:project) { create(:project).tap(&:mark_pages_as_deployed) }
context 'when usage is serverless' do
it 'does not call the UpdatePagesConfigurationService' do
expect(Projects::UpdatePagesConfigurationService).not_to receive(:new)
expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async)
create(:pages_domain, usage: :serverless)
end
@ -352,12 +354,30 @@ RSpec.describe PagesDomain do
domain.destroy!
end
it 'delegates to Projects::UpdatePagesConfigurationService' do
it 'delegates to Projects::UpdatePagesConfigurationService when not running async' do
stub_feature_flags(async_update_pages_config: false)
service = instance_double('Projects::UpdatePagesConfigurationService')
expect(Projects::UpdatePagesConfigurationService).to receive(:new) { service }
expect(service).to receive(:execute)
create(:pages_domain)
create(:pages_domain, project: project)
end
it "schedules a PagesUpdateConfigurationWorker" do
expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id)
create(:pages_domain, project: project)
end
context "when the pages aren't deployed" do
let_it_be(:project) { create(:project).tap(&:mark_pages_as_not_deployed) }
it "does not schedule a PagesUpdateConfigurationWorker" do
expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async).with(project.id)
create(:pages_domain, project: project)
end
end
context 'configuration updates when attributes change' do

View file

@ -1450,16 +1450,69 @@ RSpec.describe Project do
end
describe '#service_desk_address' do
let_it_be(:project) { create(:project, service_desk_enabled: true) }
let_it_be(:project, reload: true) { create(:project, service_desk_enabled: true) }
before do
allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(true)
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
allow(Gitlab.config.incoming_email).to receive(:address).and_return("test+%{key}@mail.com")
subject { project.service_desk_address }
shared_examples 'with incoming email address' do
context 'when incoming email is enabled' do
before do
config = double(enabled: true, address: 'test+%{key}@mail.com')
allow(::Gitlab.config).to receive(:incoming_email).and_return(config)
end
it 'uses project full path as service desk address key' do
expect(project.service_desk_address).to eq("test+#{project.full_path_slug}-#{project.project_id}-issue-@mail.com")
end
end
context 'when incoming email is disabled' do
before do
config = double(enabled: false)
allow(::Gitlab.config).to receive(:incoming_email).and_return(config)
end
it 'uses project full path as service desk address key' do
expect(project.service_desk_address).to be_nil
end
end
end
it 'uses project full path as service desk address key' do
expect(project.service_desk_address).to eq("test+#{project.full_path_slug}-#{project.project_id}-issue-@mail.com")
context 'when service_desk_email is disabled' do
before do
allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(false)
end
it_behaves_like 'with incoming email address'
end
context 'when service_desk_email is enabled' do
before do
config = double(enabled: true, address: 'foo+%{key}@bar.com')
allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config)
end
context 'when service_desk_custom_address flag is enabled' do
before do
stub_feature_flags(service_desk_custom_address: true)
end
it 'returns custom address when project_key is set' do
create(:service_desk_setting, project: project, project_key: 'key1')
expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
end
it_behaves_like 'with incoming email address'
end
context 'when service_desk_custom_address flag is disabled' do
before do
stub_feature_flags(service_desk_custom_address: false)
end
it_behaves_like 'with incoming email address'
end
end
end

View file

@ -1,240 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::CreateIssueService do
let(:project) { create(:project, :repository, :private) }
let_it_be(:user) { User.alert_bot }
let(:alert_starts_at) { Time.current }
let(:alert_title) { 'TITLE' }
let(:alert_annotations) { { title: alert_title } }
let(:alert_payload) do
build_alert_payload(
annotations: alert_annotations,
starts_at: alert_starts_at
)
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.for_alert_management_alert(project: project, alert: alert).present
end
let!(:setting) do
create(:project_incident_management_setting, project: project)
end
subject(:execute) { service.execute }
context 'when create_issue enabled' do
let(:issue) { execute.payload[:issue] }
before do
setting.update!(create_issue: true)
end
context 'without issue_template_content' do
it 'creates an issue with alert summary only' do
expect(execute).to be_success
expect(issue.author).to eq(user)
expect(issue.title).to eq(alert_title)
expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip)
expect(separator_count(issue.description)).to eq(0)
end
end
context 'with erroneous issue service' do
let(:invalid_issue) do
build(:issue, project: project, title: nil).tap(&:valid?)
end
let(:issue_error) { invalid_issue.errors.full_messages.to_sentence }
it 'returns and logs the issue error' do
expect_next_instance_of(Issues::CreateService) do |issue_service|
expect(issue_service).to receive(:execute).and_return(invalid_issue)
end
expect(service)
.to receive(:log_error)
.with(error_message(issue_error))
expect(execute).to be_error
expect(execute.message).to eq(issue_error)
end
end
shared_examples 'GFM template' do
context 'plain content' do
let(:template_content) { 'some content' }
it 'creates an issue appending issue template' do
expect(execute).to be_success
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
expect(separator_count(issue.description)).to eq(1)
expect(issue.description).to include(template_content)
end
end
context 'quick actions' do
let(:user) { create(:user) }
let(:plain_text) { 'some content' }
let(:template_content) do
<<~CONTENT
#{plain_text}
/due tomorrow
/assign @#{user.username}
CONTENT
end
before do
project.add_maintainer(user)
end
it 'creates an issue interpreting quick actions' do
expect(execute).to be_success
expect(issue.description).to include(plain_text)
expect(issue.due_date).to be_present
expect(issue.assignees).to eq([user])
end
end
end
context 'with gitlab_incident_markdown' do
let(:alert_annotations) do
{ title: alert_title, gitlab_incident_markdown: template_content }
end
it_behaves_like 'GFM template'
end
context 'with issue_template_content' do
before do
create_issue_template('bug', template_content)
setting.update!(issue_template_key: 'bug')
end
it_behaves_like 'GFM template'
context 'and gitlab_incident_markdown' do
let(:template_content) { 'plain text'}
let(:alt_template) { 'alternate text' }
let(:alert_annotations) do
{ title: alert_title, gitlab_incident_markdown: alt_template }
end
it 'includes both templates' do
expect(execute).to be_success
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
expect(issue.description).to include(template_content)
expect(issue.description).to include(alt_template)
expect(separator_count(issue.description)).to eq(2)
end
end
private
def create_issue_template(name, content)
project.repository.create_file(
project.creator,
".gitlab/issue_templates/#{name}.md",
content,
message: 'message',
branch_name: 'master'
)
end
end
context 'with gitlab alert' do
let(:gitlab_alert) { create(:prometheus_alert, project: project) }
before do
alert_payload['labels'] = {
'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s
}
end
it 'creates an issue' do
query_title = "#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold}"
expect(execute).to be_success
expect(issue.author).to eq(user)
expect(issue.title).to eq(alert_presenter.full_title)
expect(issue.title).to include(gitlab_alert.environment.name)
expect(issue.title).to include(query_title)
expect(issue.title).to include('for 5 minutes')
expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip)
expect(separator_count(issue.description)).to eq(0)
end
end
describe 'with invalid alert payload' do
shared_examples 'invalid alert' do
it 'does not create an issue' do
expect(service)
.to receive(:log_error)
.with(error_message('invalid alert'))
expect(execute).to be_error
expect(execute.message).to eq('invalid alert')
end
end
context 'without title' do
let(:alert_annotations) { {} }
it_behaves_like 'invalid alert'
end
context 'without startsAt' do
let(:alert_starts_at) { nil }
it_behaves_like 'invalid alert'
end
end
end
context 'when create_issue disabled' do
before do
setting.update!(create_issue: false)
end
it 'returns an error' do
expect(service)
.to receive(:log_error)
.with(error_message('setting disabled'))
expect(execute).to be_error
expect(execute.message).to eq('setting disabled')
end
end
private
def build_alert_payload(annotations: {}, starts_at: Time.current)
{
'annotations' => annotations.stringify_keys
}.tap do |payload|
payload['startsAt'] = starts_at.rfc3339 if starts_at
end
end
def error_message(message)
%{Cannot create incident issue for "#{project.full_name}": #{message}}
end
def separator_count(text)
summary_separator = "\n\n---\n\n"
text.scan(summary_separator).size
end
end

View file

@ -31,6 +31,17 @@ RSpec.describe ServiceDeskSettings::UpdateService do
end
end
context 'when project_key is an empty string' do
let(:params) { { project_key: '' } }
it 'sets nil project_key' do
result = described_class.new(settings.project, user, params).execute
expect(result[:status]).to eq :success
expect(settings.reload.project_key).to be_nil
end
end
context 'with invalid params' do
let(:params) { { outgoing_name: 'x' * 256 } }

View file

@ -18,15 +18,15 @@ RSpec.describe IncidentManagement::ProcessAlertWorker do
before do
allow(Gitlab::AppLogger).to receive(:warn).and_call_original
allow(IncidentManagement::CreateIssueService)
.to receive(:new).with(alert.project, alert)
allow(AlertManagement::CreateAlertIssueService)
.to receive(:new).with(alert, User.alert_bot)
.and_call_original
end
shared_examples 'creates issue successfully' do
it 'creates an issue' do
expect(IncidentManagement::CreateIssueService)
.to receive(:new).with(alert.project, alert)
expect(AlertManagement::CreateAlertIssueService)
.to receive(:new).with(alert, User.alert_bot)
expect { subject }.to change { Issue.count }.by(1)
end
@ -58,10 +58,10 @@ RSpec.describe IncidentManagement::ProcessAlertWorker do
subject
expect(Gitlab::AppLogger).to have_received(:warn).with(
message: 'Cannot link an Issue with Alert',
message: 'Cannot process an Incident',
issue_id: created_issue.id,
alert_id: alert.id,
alert_errors: { hosts: ['hosts array is over 255 chars'] }
errors: 'Hosts hosts array is over 255 chars'
)
end
end
@ -80,7 +80,7 @@ RSpec.describe IncidentManagement::ProcessAlertWorker do
subject { described_class.new.perform(nil, nil, invalid_alert_id) }
it 'does not create issues' do
expect(IncidentManagement::CreateIssueService).not_to receive(:new)
expect(AlertManagement::CreateAlertIssueService).not_to receive(:new)
expect { subject }.not_to change { Issue.count }
end

0
vendor/gitignore/C++.gitignore vendored Normal file → Executable file
View file

View file

@ -1,14 +0,0 @@
pom.xml
pom.xml.asc
*.jar
*.class
/lib/
/classes/
/target/
/checkouts/
.lein-deps-sum
.lein-repl-history
.lein-plugins/
.lein-failures
.nrepl-port
.cpcache/

1
vendor/gitignore/Clojure.gitignore vendored Symbolic link
View file

@ -0,0 +1 @@
Leiningen.gitignore

View file

@ -1,32 +0,0 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app

1
vendor/gitignore/Fortran.gitignore vendored Symbolic link
View file

@ -0,0 +1 @@
C++.gitignore

0
vendor/gitignore/Java.gitignore vendored Normal file → Executable file
View file

View file

@ -1,23 +0,0 @@
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

1
vendor/gitignore/Kotlin.gitignore vendored Symbolic link
View file

@ -0,0 +1 @@
Java.gitignore