Commit 4e8a7892 authored by Imre Farkas's avatar Imre Farkas

Merge branch '32935-preventing-accidental-project-deletion-application-settings' into 'master'

Resolve "Preventing accidental project deletion" - application settings

Closes #32935

See merge request gitlab-org/gitlab!18790
parents e13a21fe ae7a45cd
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import { GlBadge } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { import {
ITEM_TYPE, ITEM_TYPE,
...@@ -8,13 +9,16 @@ import { ...@@ -8,13 +9,16 @@ import {
PROJECT_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE,
} from '../constants'; } from '../constants';
import itemStatsValue from './item_stats_value.vue'; import itemStatsValue from './item_stats_value.vue';
import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
export default { export default {
components: { components: {
icon, icon,
timeAgoTooltip, timeAgoTooltip,
itemStatsValue, itemStatsValue,
GlBadge,
}, },
mixins: [isProjectPendingRemoval],
props: { props: {
item: { item: {
type: Object, type: Object,
...@@ -70,6 +74,9 @@ export default { ...@@ -70,6 +74,9 @@ export default {
css-class="project-stars" css-class="project-stars"
icon-name="star" icon-name="star"
/> />
<div v-if="isProjectPendingRemoval">
<gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
</div>
<div v-if="isProject" class="last-updated"> <div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" /> <time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
</div> </div>
......
export default {
computed: {
isProjectPendingRemoval() {
return false;
},
},
};
...@@ -93,6 +93,7 @@ export default class GroupsStore { ...@@ -93,6 +93,7 @@ export default class GroupsStore {
memberCount: rawGroupItem.number_users_with_delimiter, memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count, starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at, updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion_at,
}; };
} }
......
...@@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity ...@@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity
end end
end end
end end
GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold' = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control' = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f = render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
= render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
.form-group.visibility-level-setting .form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold' = f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
...@@ -53,6 +54,7 @@ ...@@ -53,6 +54,7 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.form-text.text-muted#clone-protocol-help %span.form-text.text-muted#clone-protocol-help
= _('Allow only the selected protocols to be used for Git access.') = _('Allow only the selected protocols to be used for Git access.')
.form-group .form-group
= f.label :custom_http_clone_url_root, _('Custom Git clone URL for HTTP(S)'), class: 'label-bold' = f.label :custom_http_clone_url_root, _('Custom Git clone URL for HTTP(S)'), class: 'label-bold'
= f.text_field :custom_http_clone_url_root, class: 'form-control', placeholder: 'https://git.example.com', :'aria-describedby' => 'custom_http_clone_url_root_help_block' = f.text_field :custom_http_clone_url_root, class: 'form-control', placeholder: 'https://git.example.com', :'aria-describedby' => 'custom_http_clone_url_root_help_block'
......
- if project.archived
%span.badge.badge-warning
= _('archived')
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
.stats .stats
%span.badge.badge-pill %span.badge.badge-pill
= storage_counter(project.statistics&.storage_size) = storage_counter(project.statistics&.storage_size)
- if project.archived = render_if_exists 'admin/projects/archived', project: project
%span.badge.badge-warning archived
.title .title
= link_to(admin_project_path(project)) do = link_to(admin_project_path(project)) do
.dash-project-avatar .dash-project-avatar
......
- if project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
= _('Archived project! Repository and other project resources are read only')
- return unless can?(current_user, :remove_project, project)
.sub-section
%h4.danger-title= _('Remove project')
%p
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(project), method: :delete) do
%p
%strong= _('Removed projects cannot be restored!')
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }
...@@ -73,23 +73,7 @@ ...@@ -73,23 +73,7 @@
= render 'export', project: @project = render 'export', project: @project
- if can? current_user, :archive_project, @project = render_if_exists 'projects/settings/archive'
.sub-section
%h4.warning-title
- if @project.archived?
= _('Unarchive project')
- else
= _('Archive project')
- if @project.archived?
%p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>").html_safe
= link_to _('Unarchive project'), unarchive_project_path(@project),
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
method: :post, class: "btn btn-success"
- else
%p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>").html_safe
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "btn btn-warning"
.sub-section.rename-repository .sub-section.rename-repository
%h4.warning-title= _('Change path') %h4.warning-title= _('Change path')
= render 'projects/errors' = render 'projects/errors'
...@@ -135,14 +119,7 @@ ...@@ -135,14 +119,7 @@
%strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.') %strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.')
= button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) } = button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
- if can?(current_user, :remove_project, @project) = render 'remove', project: @project
.sub-section
%h4.danger-title= _('Remove project')
%p= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(@project), method: :delete) do
%p
%strong= _('Removed projects cannot be restored!')
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
.save-project-loader.hide .save-project-loader.hide
.center .center
......
- return unless can?(current_user, :archive_project, @project)
.sub-section
%h4.warning-title
- if @project.archived?
= _('Unarchive project')
- else
= _('Archive project')
- if @project.archived?
%p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= link_to _('Unarchive project'), unarchive_project_path(@project),
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
method: :post, class: "btn btn-success"
- else
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "btn btn-warning"
...@@ -18,11 +18,8 @@ ...@@ -18,11 +18,8 @@
- if can?(current_user, :download_code, @project) && @project.repository_languages.present? - if can?(current_user, :download_code, @project) && @project.repository_languages.present?
= repository_languages_bar(@project.repository_languages) = repository_languages_bar(@project.repository_languages)
- if @project.archived? = render "archived_notice", project: @project
.text-warning.center.prepend-top-20 = render_if_exists "projects/marked_for_deletion_notice", project: @project
%p
= icon("exclamation-triangle fw")
#{ _('Archived project! Repository and other project resources are read-only') }
- view_path = @project.default_view - view_path = @project.default_view
......
- if project.archived
%span.d-flex.badge.badge-warning
= _('archived')
...@@ -67,8 +67,7 @@ ...@@ -67,8 +67,7 @@
%span.icon-wrapper.pipeline-status %span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path = render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
- if project.archived = render_if_exists 'shared/projects/archived', project: project
%span.d-flex.icon-wrapper.badge.badge-warning archived
- if stars - if stars
= link_to project_starrers_path(project), = link_to project_starrers_path(project),
class: "d-flex align-items-center icon-wrapper stars has-tooltip", class: "d-flex align-items-center icon-wrapper stars has-tooltip",
......
...@@ -475,6 +475,9 @@ Gitlab.ee do ...@@ -475,6 +475,9 @@ Gitlab.ee do
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['clear_shared_runners_minutes_worker']['cron'] ||= '0 0 1 * *' Settings.cron_jobs['clear_shared_runners_minutes_worker']['cron'] ||= '0 0 1 * *'
Settings.cron_jobs['clear_shared_runners_minutes_worker']['job_class'] = 'ClearSharedRunnersMinutesWorker' Settings.cron_jobs['clear_shared_runners_minutes_worker']['job_class'] = 'ClearSharedRunnersMinutesWorker'
Settings.cron_jobs['adjourned_projects_deletion_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['adjourned_projects_deletion_cron_worker']['cron'] ||= '0 4 * * *'
Settings.cron_jobs['adjourned_projects_deletion_cron_worker']['job_class'] = 'AdjournedProjectsDeletionCronWorker'
Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '*/1 * * * *' Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'Geo::FileDownloadDispatchWorker' Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'Geo::FileDownloadDispatchWorker'
......
...@@ -124,4 +124,4 @@ ...@@ -124,4 +124,4 @@
- [design_management_new_version, 1] - [design_management_new_version, 1]
- [epics, 2] - [epics, 2]
- [personal_access_tokens, 1] - [personal_access_tokens, 1]
- [adjourned_project_deletion, 1]
...@@ -1713,7 +1713,12 @@ Example response: ...@@ -1713,7 +1713,12 @@ Example response:
## Remove project ## Remove project
Removes a project including all associated resources (issues, merge requests etc). This endpoint either:
- Removes a project including all associated resources (issues, merge requests etc).
- From GitLab 12.6 on Premium or higher tiers, marks a project for deletion. Actual
deletion happens after number of days specified in
[instance settings](../user/admin_area/settings/visibility_and_access_controls.md#project-deletion-adjourned-period-premium-only).
``` ```
DELETE /projects/:id DELETE /projects/:id
...@@ -1723,6 +1728,18 @@ DELETE /projects/:id ...@@ -1723,6 +1728,18 @@ DELETE /projects/:id
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Restore project marked for deletion **(PREMIUM)**
Restores project marked for deletion.
```
POST /projects/:id/restore
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Upload a file ## Upload a file
Uploads a file to the specified project to be used in an issue or merge request description, or a comment. Uploads a file to the specified project to be used in an issue or merge request description, or a comment.
......
...@@ -72,14 +72,15 @@ Example response: ...@@ -72,14 +72,15 @@ Example response:
``` ```
Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
the `file_template_project_id` or the `geo_node_allowed_ips` parameters: the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
```json ```json
{ {
"id" : 1, "id" : 1,
"signup_enabled" : true, "signup_enabled" : true,
"file_template_project_id": 1, "file_template_project_id": 1,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0" "geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"deletion_adjourned_period": 7,
... ...
} }
``` ```
...@@ -162,6 +163,7 @@ these parameters: ...@@ -162,6 +163,7 @@ these parameters:
- `file_template_project_id` - `file_template_project_id`
- `geo_node_allowed_ips` - `geo_node_allowed_ips`
- `geo_status_timeout` - `geo_status_timeout`
- `deletion_adjourned_period`
Example responses: **(PREMIUM ONLY)** Example responses: **(PREMIUM ONLY)**
...@@ -292,6 +294,7 @@ are listed in the descriptions of the relevant settings. ...@@ -292,6 +294,7 @@ are listed in the descriptions of the relevant settings.
| `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. | | `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. | | `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. | | `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** How many days after marking project for deletion it is actually removed. Value between 0 and 90.
| `project_export_enabled` | boolean | no | Enable project export. | | `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. | | `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. | | `protected_ci_variables` | boolean | no | Environment variables are protected by default. |
......
...@@ -48,6 +48,17 @@ To ensure only admin users can delete projects: ...@@ -48,6 +48,17 @@ To ensure only admin users can delete projects:
1. Check the **Default project deletion protection** checkbox. 1. Check the **Default project deletion protection** checkbox.
1. Click **Save changes**. 1. Click **Save changes**.
## Project deletion adjourned period **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6.
By default, project marked for deletion will be permanently removed after 7 days. This period may be changed.
To change this period:
1. Select the desired option.
1. Click **Save changes**.
## Default project visibility ## Default project visibility
To set the default visibility levels for new projects: To set the default visibility levels for new projects:
......
import { ITEM_TYPE } from '~/groups/constants';
export default {
computed: {
isProjectPendingRemoval() {
return this.item.type === ITEM_TYPE.PROJECT && this.item.pendingRemoval;
},
},
};
...@@ -38,6 +38,10 @@ module EE ...@@ -38,6 +38,10 @@ module EE
attrs << :default_project_deletion_protection attrs << :default_project_deletion_protection
end end
if License.feature_available?(:marking_project_for_deletion)
attrs << :deletion_adjourned_period
end
if License.feature_available?(:required_ci_templates) if License.feature_available?(:required_ci_templates)
attrs << :required_instance_ci_template attrs << :required_instance_ci_template
end end
......
...@@ -11,6 +11,40 @@ module EE ...@@ -11,6 +11,40 @@ module EE
before_action :log_unarchive_audit_event, only: [:unarchive] before_action :log_unarchive_audit_event, only: [:unarchive]
end end
def restore
return access_denied! unless can?(current_user, :remove_project, project)
result = ::Projects::RestoreService.new(project, current_user, {}).execute
if result[:status] == :success
flash[:notice] = _("Project '%{project_name}' is restored.") % { project_name: project.full_name }
redirect_to(edit_project_path(project))
else
flash.now[:alert] = result[:message]
render 'edit'
end
end
override :destroy
def destroy
return super unless project.adjourned_deletion?
return access_denied! unless can?(current_user, :remove_project, project)
result = ::Projects::MarkForDeletionService.new(project, current_user, {}).execute
if result[:status] == :success
date = permanent_deletion_date(project.marked_for_deletion_at)
flash[:notice] = _("Project '%{project_name}' will be deleted on %{date}") % { date: date, project_name: project.full_name }
redirect_to(project_path(project), status: :found)
else
flash.now[:alert] = result[:message]
render 'edit'
end
end
override :project_params_attributes override :project_params_attributes
def project_params_attributes def project_params_attributes
super + project_params_ee super + project_params_ee
......
...@@ -88,6 +88,7 @@ module EE ...@@ -88,6 +88,7 @@ module EE
email_additional_text email_additional_text
file_template_project_id file_template_project_id
default_project_deletion_protection default_project_deletion_protection
deletion_adjourned_period
] ]
end end
end end
......
...@@ -116,6 +116,19 @@ module EE ...@@ -116,6 +116,19 @@ module EE
super || project_feature_flags_path(project) super || project_feature_flags_path(project)
end end
override :remove_project_message
def remove_project_message(project)
return super unless project.feature_available?(:marking_project_for_deletion)
date = permanent_deletion_date(Time.now.utc)
_("Removing a project places it into a read-only state until %{date}, at which point the project will be permanantly removed. Are you ABSOLUTELY sure?") %
{ date: date }
end
def permanent_deletion_date(date)
(date + ::Gitlab::CurrentSettings.deletion_adjourned_period.days).strftime('%F')
end
# Given the current GitLab configuration, check whether the GitLab URL for Kerberos is going to be different than the HTTP URL # Given the current GitLab configuration, check whether the GitLab URL for Kerberos is going to be different than the HTTP URL
def alternative_kerberos_url? def alternative_kerberos_url?
::Gitlab.config.alternative_gitlab_kerberos_url? ::Gitlab.config.alternative_gitlab_kerberos_url?
......
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
prepended do prepended do
EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000 EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000
INSTANCE_REVIEW_MIN_USERS = 100 INSTANCE_REVIEW_MIN_USERS = 100
DEFAULT_NUMBER_OF_DAYS_BEFORE_REMOVAL = 7
belongs_to :file_template_project, class_name: "Project" belongs_to :file_template_project, class_name: "Project"
...@@ -39,6 +40,10 @@ module EE ...@@ -39,6 +40,10 @@ module EE
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
validates :deletion_adjourned_period,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 90 }
validates :elasticsearch_replicas, validates :elasticsearch_replicas,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
...@@ -87,6 +92,7 @@ module EE ...@@ -87,6 +92,7 @@ module EE
mirror_capacity_threshold: Settings.gitlab['mirror_capacity_threshold'], mirror_capacity_threshold: Settings.gitlab['mirror_capacity_threshold'],
mirror_max_capacity: Settings.gitlab['mirror_max_capacity'], mirror_max_capacity: Settings.gitlab['mirror_max_capacity'],
mirror_max_delay: Settings.gitlab['mirror_max_delay'], mirror_max_delay: Settings.gitlab['mirror_max_delay'],
deletion_adjourned_period: DEFAULT_NUMBER_OF_DAYS_BEFORE_REMOVAL,
pseudonymizer_enabled: false, pseudonymizer_enabled: false,
repository_size_limit: 0, repository_size_limit: 0,
slack_app_enabled: false, slack_app_enabled: false,
......
# frozen_string_literal: true
module EE
module GroupChildEntity
extend ActiveSupport::Concern
prepended do
# Project only attributes
expose :marked_for_deletion_at,
if: lambda { |_instance, _options| project? }
end
end
end
# frozen_string_literal: true
module Projects
class MarkForDeletionService < BaseService
def execute
return if project.marked_for_deletion_at?
return unless project.feature_available?(:marking_project_for_deletion)
result = ::Projects::UpdateService.new(
project,
current_user,
{ archived: true,
marked_for_deletion_at: Time.now.utc,
deleting_user: current_user }
).execute
log_event if result[:status] == :success
log_error(result[:message]) if result[:status] == :error
result
end
def log_event
log_audit_event
log_info("User #{current_user.id} marked project #{project.full_path} for deletion")
end
def log_audit_event
::AuditEventService.new(
current_user,
project,
action: :custom,
custom_message: "Project marked for deletion"
).for_project.security_event
end
end
end
# frozen_string_literal: true
module Projects
class RestoreService < BaseService
def execute
return error(_('Project already deleted')) if project.pending_delete?
result = ::Projects::UpdateService.new(
project,
current_user,
{ archived: false,
marked_for_deletion_at: nil,
deleting_user: nil }
).execute
log_event if result[:status] == :success
result
end
def log_event
log_audit_event
log_info("User #{current_user.id} restored project #{project.full_path}")
end
def log_audit_event
::AuditEventService.new(
current_user,
project,
action: :custom,
custom_message: "Project restored"
).for_project.security_event
end
end
end
- return unless License.feature_available?(:marking_project_for_deletion)
- f = local_assigns.fetch(:form)
.form-group
= f.label s_('Default deletion adjourned period'), class: 'label-bold'
= f.select :deletion_adjourned_period, options_for_select(0..90, @application_setting.deletion_adjourned_period), {}, class: 'form-control'
= f.label :deletion_adjourned_period, class: 'form-check-label' do
= _('How many days need to pass between marking entity for deletion and actual removing it.')
- if project.marked_for_deletion?
%span.badge.badge-warning
= _('pending removal')
- elsif project.archived
%span.badge.badge-warning
= _('archived')
- return if project.marked_for_deletion?
- if project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
= _('Archived project! Repository and other project resources are read-only')
- if project.marked_for_deletion?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
= _("Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only.") % { date: permanent_deletion_date(project.marked_for_deletion_at) }
- return unless can?(current_user, :remove_project, project)
- unless project.marked_for_deletion?
.sub-section
%h4.danger-title= _('Remove project')
= render 'projects/settings/marked_for_removal'
%p
%strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
= form_tag(project_path(project), method: :delete) do
%p
%strong= _('Removed projects cannot be restored!')
= button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }
- else
= render 'projects/settings/restore', project: project
- return if @project.marked_for_deletion?
= render_ce 'projects/settings/archive'
- return unless @project.feature_available?(:marking_project_for_deletion)
- date = permanent_deletion_date(Time.now.utc)
%p
= _("Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed.") %{ date: date }
%br
= _("Until that time, the project can be restored.")
- return unless project.feature_available?(:marking_project_for_deletion)
- date = permanent_deletion_date(project.marked_for_deletion_at)
.sub-section
%h4.danger-title= _('Restore project')
%p
%strong= _('This project will be removed on %{date}') %{ date: date }
%p
= _("Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it.")
= _("The repository can be commited to, and issues, comments and other entities can be created.")
%strong= _('Only active this projects shows up in the search and on the dashboard.')
= link_to _('Restore project'), namespace_project_restore_path(project.namespace, project),
method: :post, class: "btn btn-remove"
- if project.marked_for_deletion?
%span.d-flex.badge.badge-warning
= _('pending removal')
- elsif project.archived
%span.d-flex.badge.badge-warning
= _('archived')
# frozen_string_literal: true
class AdjournedProjectDeletionWorker
include ApplicationWorker
include ExceptionBacktrace
feature_category :authentication_and_authorization
def perform(project_id)
project = Project.find(project_id)
user = project.deleting_user
return unless user
::Projects::DestroyService.new(project, user).async_execute
rescue ActiveRecord::RecordNotFound => error
logger.error("Failed to delete project (#{project_id}): #{error.message}")
end
end
# frozen_string_literal: true
class AdjournedProjectsDeletionCronWorker
include ApplicationWorker
include CronjobQueue
INTERVAL = 5.minutes.to_i
feature_category :authentication_and_authorization
def perform
deletion_cutoff = Gitlab::CurrentSettings.deletion_adjourned_period.days.ago.to_date
Project.aimed_for_deletion(deletion_cutoff).find_each(batch_size: 100).with_index do |project, index| # rubocop: disable CodeReuse/ActiveRecord
delay = index * INTERVAL
AdjournedProjectDeletionWorker.perform_in(delay, project.id)
end
end
end
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
- cronjob:update_all_mirrors - cronjob:update_all_mirrors
- cronjob:pseudonymizer - cronjob:pseudonymizer
- cronjob:update_max_seats_used_for_gitlab_com_subscriptions - cronjob:update_max_seats_used_for_gitlab_com_subscriptions
- cronjob:adjourned_projects_deletion_cron
- gcp_cluster:cluster_update_app - gcp_cluster:cluster_update_app
- gcp_cluster:cluster_wait_for_app_update - gcp_cluster:cluster_wait_for_app_update
...@@ -74,6 +75,7 @@ ...@@ -74,6 +75,7 @@
- new_epic - new_epic
- project_import_schedule - project_import_schedule
- project_update_repository_storage - project_update_repository_storage
- adjourned_project_deletion
- rebase - rebase
- refresh_license_compliance_checks - refresh_license_compliance_checks
- repository_update_mirror - repository_update_mirror
......
---
title: Add application settings needed for soft-deletion
merge_request: 18790
author:
type: added
...@@ -113,6 +113,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -113,6 +113,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
end end
post '/restore' => '/projects#restore', as: :restore
end end
# End of the /-/ scope. # End of the /-/ scope.
......
...@@ -50,6 +50,7 @@ module EE ...@@ -50,6 +50,7 @@ module EE
expose :packages_enabled, if: ->(project, _) { project.feature_available?(:packages) } expose :packages_enabled, if: ->(project, _) { project.feature_available?(:packages) }
expose :service_desk_enabled, if: ->(project, _) { project.feature_available?(:service_desk) } expose :service_desk_enabled, if: ->(project, _) { project.feature_available?(:service_desk) }
expose :service_desk_address, if: ->(project, _) { project.feature_available?(:service_desk) } expose :service_desk_address, if: ->(project, _) { project.feature_available?(:service_desk) }
expose :marked_for_deletion_at, if: ->(project, _) { project.feature_available?(:marking_project_for_deletion) }
end end
end end
...@@ -202,6 +203,7 @@ module EE ...@@ -202,6 +203,7 @@ module EE
expose :email_additional_text, if: ->(_instance, _opts) { ::License.feature_available?(:email_additional_text) } expose :email_additional_text, if: ->(_instance, _opts) { ::License.feature_available?(:email_additional_text) }
expose :file_template_project_id, if: ->(_instance, _opts) { ::License.feature_available?(:custom_file_templates) } expose :file_template_project_id, if: ->(_instance, _opts) { ::License.feature_available?(:custom_file_templates) }
expose :default_project_deletion_protection, if: ->(_instance, _opts) { ::License.feature_available?(:default_project_deletion_protection) } expose :default_project_deletion_protection, if: ->(_instance, _opts) { ::License.feature_available?(:default_project_deletion_protection) }
expose :deletion_adjourned_period, if: ->(_instance, _opts) { ::License.feature_available?(:marking_project_for_deletion) }
end end
end end
......
...@@ -31,6 +31,7 @@ module EE ...@@ -31,6 +31,7 @@ module EE
optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons' optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons'
optional :default_project_deletion_protection, type: Grape::API::Boolean, desc: 'Disable project owners ability to delete project' optional :default_project_deletion_protection, type: Grape::API::Boolean, desc: 'Disable project owners ability to delete project'
optional :deletion_adjourned_period, type: Integer, desc: 'Number of days between marking project as deleted and actual removal'
optional :help_text, type: String, desc: 'GitLab server administrator information' optional :help_text, type: String, desc: 'GitLab server administrator information'
optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)' optional :repository_size_limit, type: Integer, desc: 'Size limit per repository (MB)'
optional :file_template_project_id, type: Integer, desc: 'ID of project where instance-level file templates are stored.' optional :file_template_project_id, type: Integer, desc: 'ID of project where instance-level file templates are stored.'
......
...@@ -6,6 +6,23 @@ module EE ...@@ -6,6 +6,23 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
resource :projects do
desc 'Restore a project' do
success Entities::Project
end
post ':id/restore' do
authorize!(:remove_project, user_project)
break not_found! unless user_project.feature_available?(:marking_project_for_deletion)
result = ::Projects::RestoreService.new(user_project, current_user).execute
if result[:status] == :success
present user_project, with: ::API::Entities::Project, current_user: current_user
else
render_api_error!(result[:message], 400)
end
end
end
helpers do helpers do
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
...@@ -41,6 +58,21 @@ module EE ...@@ -41,6 +58,21 @@ module EE
attrs.delete(:import_data_attributes) attrs.delete(:import_data_attributes)
end end
end end
override :delete_project
def delete_project(user_project)
return super unless user_project.adjourned_deletion?
result = destroy_conditionally!(user_project) do
::Projects::MarkForDeletionService.new(user_project, current_user, {}).execute
end
if result[:status] == :success
accepted!
else
render_api_error!(result[:message], 400)
end
end
end end
end end
end end
......
...@@ -27,6 +27,10 @@ module EE ...@@ -27,6 +27,10 @@ module EE
attrs = attrs.except(:default_project_deletion_protection) attrs = attrs.except(:default_project_deletion_protection)
end end
unless License.feature_available?(:marking_project_for_deletion)
attrs = attrs.except(:deletion_adjourned_period)
end
attrs attrs
end end
end end
......
...@@ -104,6 +104,13 @@ describe Admin::ApplicationSettingsController do ...@@ -104,6 +104,13 @@ describe Admin::ApplicationSettingsController do
it_behaves_like 'settings for licensed features' it_behaves_like 'settings for licensed features'
end end
context 'project deletion adjourned period' do
let(:settings) { { deletion_adjourned_period: 6 } }
let(:feature) { :marking_project_for_deletion }
it_behaves_like 'settings for licensed features'
end
context 'additional email footer' do context 'additional email footer' do
let(:settings) { { email_additional_text: 'scary legal footer' } } let(:settings) { { email_additional_text: 'scary legal footer' } }
let(:feature) { :email_additional_text } let(:feature) { :email_additional_text }
......
...@@ -394,4 +394,96 @@ describe ProjectsController do ...@@ -394,4 +394,96 @@ describe ProjectsController do
end end
end end
end end
describe 'DELETE #destroy' do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace)}
before do
controller.instance_variable_set(:@project, project)
sign_in(owner)
end
context 'feature is available' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
it 'marks project for deletion' do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.reload.marked_for_deletion?).to be_truthy
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(project_path(project))
end
it 'does not mark project for deletion because of error' do
message = 'Error'
expect(::Projects::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:edit)
expect(flash[:alert]).to include(message)
end
context 'when instance setting is set to 0 days' do
it 'deletes project right away' do
allow(Gitlab::CurrentSettings).to receive(:deletion_adjourned_period).and_return(0)
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.marked_for_deletion?).to be_falsey
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
end
context 'feature is not available' do
before do
stub_licensed_features(marking_project_for_deletion: false)
end
it 'deletes project right away' do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.marked_for_deletion?).to be_falsey
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
end
describe 'POST #restore' do
let(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace)}
before do
controller.instance_variable_set(:@project, project)
sign_in(owner)
end
it 'restores project deletion' do
post :restore, params: { namespace_id: project.namespace, project_id: project }
expect(project.reload.marked_for_deletion_at).to be_nil
expect(project.reload.archived).to be_falsey
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(edit_project_path(project))
end
it 'does not restore project because of error' do
message = 'Error'
expect(::Projects::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
post :restore, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:edit)
expect(flash[:alert]).to include(message)
end
end
end end
...@@ -2255,6 +2255,7 @@ describe Project do ...@@ -2255,6 +2255,7 @@ describe Project do
context 'when number of days is set to more than 0' do context 'when number of days is set to more than 0' do
it 'returns true' do it 'returns true' do
stub_application_setting(deletion_adjourned_period: 1) stub_application_setting(deletion_adjourned_period: 1)
expect(project.adjourned_deletion?).to eq(true) expect(project.adjourned_deletion?).to eq(true)
end end
end end
...@@ -2262,6 +2263,7 @@ describe Project do ...@@ -2262,6 +2263,7 @@ describe Project do
context 'when number of days is set to 0' do context 'when number of days is set to 0' do
it 'returns false' do it 'returns false' do
stub_application_setting(deletion_adjourned_period: 0) stub_application_setting(deletion_adjourned_period: 0)
expect(project.adjourned_deletion?).to eq(false) expect(project.adjourned_deletion?).to eq(false)
end end
end end
......
...@@ -180,6 +180,24 @@ describe API::Projects do ...@@ -180,6 +180,24 @@ describe API::Projects do
end end
end end
describe 'marked_for_deletion attribute' do
it 'exposed when the feature is available' do
stub_licensed_features(marking_project_for_deletion: true)
get api("/projects/#{project.id}", user)
expect(json_response).to have_key 'marked_for_deletion_at'
end
it 'not exposed when the feature is not available' do
stub_licensed_features(marking_project_for_deletion: false)
get api("/projects/#{project.id}", user)
expect(json_response).not_to have_key 'marked_for_deletion_at'
end
end
describe 'repository_storage attribute' do describe 'repository_storage attribute' do
context 'when authenticated as an admin' do context 'when authenticated as an admin' do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
...@@ -653,4 +671,92 @@ describe API::Projects do ...@@ -653,4 +671,92 @@ describe API::Projects do
end end
end end
end end
describe 'POST /projects/:id/restore' do
context 'feature is available' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
it 'restores project' do
project.update(archived: true, marked_for_deletion_at: 1.day.ago, deleting_user: user)
post api("/projects/#{project.id}/restore", user)
expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_falsey
expect(json_response['marked_for_deletion_at']).to be_falsey
end
it 'returns error if project is already being deleted' do
message = 'Error'
expect(::Projects::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
post api("/projects/#{project.id}/restore", user)
expect(response).to have_gitlab_http_status(400)
expect(json_response["message"]).to eq(message)
end
end
context 'feature is not available' do
before do
stub_licensed_features(marking_project_for_deletion: false)
end
it 'returns error' do
post api("/projects/#{project.id}/restore", user)
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'DELETE /projects/:id' do
context 'when feature is available' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
it 'marks project for deletion' do
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(202)
expect(project.reload.marked_for_deletion?).to be_truthy
end
it 'returns error if project cannot be marked for deletion' do
message = 'Error'
expect(::Projects::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: message })
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(400)
expect(json_response["message"]).to eq(message)
end
context 'when instance setting is set to 0 days' do
it 'deletes project right away' do
allow(Gitlab::CurrentSettings).to receive(:deletion_adjourned_period).and_return(0)
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(202)
expect(project.reload.pending_delete).to eq(true)
end
end
end
context 'when feature is not available' do
before do
stub_licensed_features(marking_project_for_deletion: false)
end
it 'deletes project' do
delete api("/projects/#{project.id}", user)
expect(response).to have_gitlab_http_status(202)
expect(project.reload.pending_delete).to eq(true)
end
end
end
end end
...@@ -143,6 +143,13 @@ describe API::Settings, 'EE Settings' do ...@@ -143,6 +143,13 @@ describe API::Settings, 'EE Settings' do
it_behaves_like 'settings for licensed features' it_behaves_like 'settings for licensed features'
end end
context 'deletion adjourned period' do
let(:settings) { { deletion_adjourned_period: 5 } }
let(:feature) { :marking_project_for_deletion }
it_behaves_like 'settings for licensed features'
end
context 'custom file template project' do context 'custom file template project' do
let(:settings) { { file_template_project_id: project.id } } let(:settings) { { file_template_project_id: project.id } }
let(:feature) { :custom_file_templates } let(:feature) { :custom_file_templates }
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::MarkForDeletionService do
let(:user) { create(:user) }
let(:marked_for_deletion_at) { nil }
let(:project) do
create(:project,
:repository,
namespace: user.namespace,
marked_for_deletion_at: marked_for_deletion_at)
end
context 'with soft-delete feature turned on' do
before do
stub_licensed_features(marking_project_for_deletion: true)
end
context 'marking project for deletion' do
before do
described_class.new(project, user).execute
end
it 'marks project as archived and marked for deletion' do
expect(Project.unscoped.all).to include(project)
expect(project.archived).to eq(true)
expect(project.marked_for_deletion_at).not_to be_nil
expect(project.deleting_user).to eq(user)
end
end
context 'marking project for deletion once again' do
let(:marked_for_deletion_at) { 2.days.ago }
before do
described_class.new(project, user).execute
end
it 'does not change original date' do
expect(project.marked_for_deletion_at).to eq(marked_for_deletion_at.to_date)
end
end
context 'audit events' do
it 'saves audit event' do
expect { described_class.new(project, user).execute }
.to change { AuditEvent.count }.by(1)
end
end
end
context 'with soft-delete feature turned off' do
context 'marking project for deletion' do
before do
described_class.new(project, user).execute
end
it 'does not change project attributes' do
expect(Project.all).to include(project)
expect(project.archived).to eq(false)
expect(project.marked_for_deletion_at).to be_nil
expect(project.deleting_user).to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::RestoreService do
let(:user) { create(:user) }
let(:pending_delete) { nil }
let(:project) do
create(:project,
:repository,
namespace: user.namespace,
marked_for_deletion_at: 1.day.ago,
deleting_user: user,
archived: true,
pending_delete: pending_delete)
end
context 'restoring project' do
before do
described_class.new(project, user).execute
end
it 'marks project as unarchived and not marked for deletion' do
expect(Project.unscoped.all).to include(project)
expect(project.archived).to eq(false)
expect(project.marked_for_deletion_at).to be_nil
expect(project.deleting_user).to eq(nil)
end
end
context 'restoring project already in process of removal' do
let(:deletion_date) { 2.days.ago }
let(:pending_delete) { true }
it 'does not allow to restore' do
expect(described_class.new(project, user).execute).to include(status: :error)
end
end
context 'audit events' do
it 'saves audit event' do
expect { described_class.new(project, user).execute }
.to change { AuditEvent.count }.by(1)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AdjournedProjectDeletionWorker do
describe "#perform" do
subject(:worker) { described_class.new }
let(:user) { create(:user)}
let(:project) { create(:project, deleting_user: user) }
let(:service) { instance_double(Projects::DestroyService) }
it 'executes destroying project' do
expect(service).to receive(:async_execute)
expect(Projects::DestroyService).to receive(:new).with(project, user).and_return(service)
worker.perform(project.id)
end
it 'stops execution if user was deleted' do
project.update(deleting_user: nil)
expect(Projects::DestroyService).not_to receive(:new)
worker.perform(project.id)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AdjournedProjectsDeletionCronWorker do
describe "#perform" do
subject(:worker) { described_class.new }
let(:user) { create(:user)}
let(:marked_for_deletion_at) { 14.days.ago }
let!(:project_marked_for_deletion) { create(:project, marked_for_deletion_at: marked_for_deletion_at, deleting_user: user) }
before do
create(:project)
create(:project, marked_for_deletion_at: 3.days.ago)
end
it 'only schedules to delete projects marked for deletion before number of days from settings' do
expect(AdjournedProjectDeletionWorker).to receive(:perform_in).with(0, project_marked_for_deletion.id)
worker.perform
end
context 'marked for deletion exectly before number of days from settings' do
let(:marked_for_deletion_at) { 7.days.ago }
it 'schedules to delete project ' do
expect(AdjournedProjectDeletionWorker).to receive(:perform_in).with(0, project_marked_for_deletion.id)
worker.perform
end
end
context 'when settings are set to not-default number of days' do
before do
create(:project, marked_for_deletion_at: 5.days.ago)
allow(Gitlab::CurrentSettings).to receive(:deletion_adjourned_period).and_return(4)
end
it 'only schedules to delete projects marked for deletion before number of days from settings' do
expect(AdjournedProjectDeletionWorker).to receive(:perform_in).twice
worker.perform
end
end
end
end
...@@ -26,6 +26,14 @@ module API ...@@ -26,6 +26,14 @@ module API
def verify_update_project_attrs!(project, attrs) def verify_update_project_attrs!(project, attrs)
end end
def delete_project(user_project)
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
end
end end
helpers do helpers do
...@@ -404,11 +412,7 @@ module API ...@@ -404,11 +412,7 @@ module API
delete ":id" do delete ":id" do
authorize! :remove_project, user_project authorize! :remove_project, user_project
destroy_conditionally!(user_project) do delete_project(user_project)
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
end end
desc 'Mark this project as forked from another' desc 'Mark this project as forked from another'
......
...@@ -2031,13 +2031,16 @@ msgstr "" ...@@ -2031,13 +2031,16 @@ msgstr ""
msgid "Archive project" msgid "Archive project"
msgstr "" msgstr ""
msgid "Archived project! Repository and other project resources are read only"
msgstr ""
msgid "Archived project! Repository and other project resources are read-only" msgid "Archived project! Repository and other project resources are read-only"
msgstr "" msgstr ""
msgid "Archived projects" msgid "Archived projects"
msgstr "" msgstr ""
msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>" msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}"
msgstr "" msgstr ""
msgid "Are you setting up GitLab for a company?" msgid "Are you setting up GitLab for a company?"
...@@ -5554,6 +5557,9 @@ msgstr "" ...@@ -5554,6 +5557,9 @@ msgstr ""
msgid "Default classification label" msgid "Default classification label"
msgstr "" msgstr ""
msgid "Default deletion adjourned period"
msgstr ""
msgid "Default description template for issues" msgid "Default description template for issues"
msgstr "" msgstr ""
...@@ -5665,6 +5671,9 @@ msgstr "" ...@@ -5665,6 +5671,9 @@ msgstr ""
msgid "Deleting the license failed. You are not permitted to perform this action." msgid "Deleting the license failed. You are not permitted to perform this action."
msgstr "" msgstr ""
msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only."
msgstr ""
msgid "Denied authorization of chat nickname %{user_name}." msgid "Denied authorization of chat nickname %{user_name}."
msgstr "" msgstr ""
...@@ -9339,6 +9348,9 @@ msgstr "" ...@@ -9339,6 +9348,9 @@ msgstr ""
msgid "How it works" msgid "How it works"
msgstr "" msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr ""
msgid "How many replicas each Elasticsearch shard has." msgid "How many replicas each Elasticsearch shard has."
msgstr "" msgstr ""
...@@ -12242,6 +12254,9 @@ msgstr "" ...@@ -12242,6 +12254,9 @@ msgstr ""
msgid "Only Project Members" msgid "Only Project Members"
msgstr "" msgstr ""
msgid "Only active this projects shows up in the search and on the dashboard."
msgstr ""
msgid "Only admins" msgid "Only admins"
msgstr "" msgstr ""
...@@ -13640,6 +13655,9 @@ msgstr "" ...@@ -13640,6 +13655,9 @@ msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted." msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "" msgstr ""
msgid "Project '%{project_name}' is restored."
msgstr ""
msgid "Project '%{project_name}' queued for deletion." msgid "Project '%{project_name}' queued for deletion."
msgstr "" msgstr ""
...@@ -13649,6 +13667,9 @@ msgstr "" ...@@ -13649,6 +13667,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated." msgid "Project '%{project_name}' was successfully updated."
msgstr "" msgstr ""
msgid "Project '%{project_name}' will be deleted on %{date}"
msgstr ""
msgid "Project Badges" msgid "Project Badges"
msgstr "" msgstr ""
...@@ -13670,6 +13691,9 @@ msgstr "" ...@@ -13670,6 +13691,9 @@ msgstr ""
msgid "Project already created" msgid "Project already created"
msgstr "" msgstr ""
msgid "Project already deleted"
msgstr ""
msgid "Project and wiki repositories" msgid "Project and wiki repositories"
msgstr "" msgstr ""
...@@ -14931,6 +14955,12 @@ msgstr "" ...@@ -14931,6 +14955,12 @@ msgstr ""
msgid "Removes time estimate." msgid "Removes time estimate."
msgstr "" msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanantly removed. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed."
msgstr ""
msgid "Removing group will cause all child projects and resources to be removed." msgid "Removing group will cause all child projects and resources to be removed."
msgstr "" msgstr ""
...@@ -15226,6 +15256,12 @@ msgstr "" ...@@ -15226,6 +15256,12 @@ msgstr ""
msgid "Restart Terminal" msgid "Restart Terminal"
msgstr "" msgstr ""
msgid "Restore project"
msgstr ""
msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it."
msgstr ""
msgid "Restrict access by IP address" msgid "Restrict access by IP address"
msgstr "" msgstr ""
...@@ -17881,6 +17917,9 @@ msgstr "" ...@@ -17881,6 +17917,9 @@ msgstr ""
msgid "The remote repository is being updated..." msgid "The remote repository is being updated..."
msgstr "" msgstr ""
msgid "The repository can be commited to, and issues, comments and other entities can be created."
msgstr ""
msgid "The repository for this project does not exist." msgid "The repository for this project does not exist."
msgstr "" msgstr ""
...@@ -18397,6 +18436,9 @@ msgstr "" ...@@ -18397,6 +18436,9 @@ msgstr ""
msgid "This project path either does not exist or is private." msgid "This project path either does not exist or is private."
msgstr "" msgstr ""
msgid "This project will be removed on %{date}"
msgstr ""
msgid "This repository" msgid "This repository"
msgstr "" msgstr ""
...@@ -19157,7 +19199,7 @@ msgstr "" ...@@ -19157,7 +19199,7 @@ msgstr ""
msgid "Unarchive project" msgid "Unarchive project"
msgstr "" msgstr ""
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>" msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
msgstr "" msgstr ""
msgid "Unblock" msgid "Unblock"
...@@ -19268,6 +19310,9 @@ msgstr "" ...@@ -19268,6 +19310,9 @@ msgstr ""
msgid "Until" msgid "Until"
msgstr "" msgstr ""
msgid "Until that time, the project can be restored."
msgstr ""
msgid "Unverified" msgid "Unverified"
msgstr "" msgstr ""
...@@ -20949,6 +20994,9 @@ msgstr "" ...@@ -20949,6 +20994,9 @@ msgstr ""
msgid "among other things" msgid "among other things"
msgstr "" msgstr ""
msgid "archived"
msgstr ""
msgid "assign yourself" msgid "assign yourself"
msgstr "" msgstr ""
...@@ -21893,6 +21941,9 @@ msgstr "" ...@@ -21893,6 +21941,9 @@ msgstr ""
msgid "pending comment" msgid "pending comment"
msgstr "" msgstr ""
msgid "pending removal"
msgstr ""
msgid "pipeline" msgid "pipeline"
msgstr "" msgstr ""
......
...@@ -12,6 +12,9 @@ module QA ...@@ -12,6 +12,9 @@ module QA
element :project_path_field element :project_path_field
element :change_path_button element :change_path_button
element :transfer_button element :transfer_button
end
view 'app/views/projects/settings/_archive.html.haml' do
element :archive_project_link element :archive_project_link
element :unarchive_project_link element :unarchive_project_link
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment