Commit b0c431c0 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-08-28' into 'master'

CE upstream - 2018-08-28 13:21 UTC

Closes gitlab-ce#40059

See merge request gitlab-org/gitlab-ee!7009
parents 9095be90 8d67b118
...@@ -6,6 +6,7 @@ app/controllers/projects/approvers_controller.rb ...@@ -6,6 +6,7 @@ app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/helpers/system_note_helper.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb app/workers/stuck_merge_jobs_worker.rb
......
...@@ -146,13 +146,7 @@ export default { ...@@ -146,13 +146,7 @@ export default {
staged: false, staged: false,
prevPath: '', prevPath: '',
moved: false, moved: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, { lastCommitSha: lastCommit.commit.id,
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
}),
}); });
if (prevPath) { if (prevPath) {
......
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
export default {
components: {
CiIcon,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'latest':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
},
false,
);
} else {
environmentText = sprintf(
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
case 'failed':
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
{ environmentLink: this.environmentLink },
false,
);
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
environmentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.deploymentStatus.environment.path}">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
},
deploymentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeployment.path}">`,
name: _.escape(this.lastDeployment.name),
endLink: '</a>',
},
false,
);
},
hasLastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">
<div class="environment-information">
<ci-icon :status="deploymentStatus.icon" />
<p v-html="environment"></p>
</div>
</div>
</template>
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
import Milestone from '~/milestone'; import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow(); initMilestonesShow();
initDeleteMilestoneModal();
Milestone.initDeprecationMessage(); Milestone.initDeprecationMessage();
}); });
...@@ -40,8 +40,8 @@ ...@@ -40,8 +40,8 @@
if (this.issueCount === 0 && this.mergeRequestCount === 0) { if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf( return sprintf(
s__(`Milestones| s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project. You’re about to permanently delete the milestone %{milestoneTitle}.
%{milestoneTitle} is not currently used in any issues or merge requests.`), This milestone is not currently used in any issues or merge requests.`),
{ {
milestoneTitle, milestoneTitle,
}, },
...@@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p ...@@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p
return sprintf( return sprintf(
s__(`Milestones| s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
Once deleted, it cannot be undone or recovered.`), Once deleted, it cannot be undone or recovered.`),
{ {
milestoneTitle, milestoneTitle,
......
...@@ -3,15 +3,22 @@ import createFlash from '~/flash'; ...@@ -3,15 +3,22 @@ import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete'; import GfmAutoComplete from '~/gfm_auto_complete';
import EmojiMenu from './emoji_menu'; import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu'; const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector); const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field'); const statusMessageField = document.getElementById('js-status-message-field');
const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder');
const toggleNoEmojiPlaceholder = (isVisible) => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
const removeStatusEmoji = () => { const removeStatusEmoji = () => {
const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji'); const statusEmoji = findStatusEmoji();
if (statusEmoji) { if (statusEmoji) {
statusEmoji.remove(); statusEmoji.remove();
} }
...@@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => {
const selectEmojiCallback = (emoji, emojiTag) => { const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji; statusEmojiField.value = emoji;
findNoEmojiPlaceholder().classList.add('hidden'); toggleNoEmojiPlaceholder(false);
removeStatusEmoji(); removeStatusEmoji();
toggleEmojiMenuButton.innerHTML += emojiTag; toggleEmojiMenuButton.innerHTML += emojiTag;
}; };
...@@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => {
statusEmojiField.value = ''; statusEmojiField.value = '';
statusMessageField.value = ''; statusMessageField.value = '';
removeStatusEmoji(); removeStatusEmoji();
findNoEmojiPlaceholder().classList.remove('hidden'); toggleNoEmojiPlaceholder(true);
}); });
const emojiAutocomplete = new GfmAutoComplete(); const emojiAutocomplete = new GfmAutoComplete();
...@@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => {
selectEmojiCallback, selectEmojiCallback,
); );
emojiMenu.bindEvents(); emojiMenu.bindEvents();
const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
statusMessageField.addEventListener('input', () => {
const hasStatusMessage = statusMessageField.value.trim() !== '';
const statusEmoji = findStatusEmoji();
if (hasStatusMessage && statusEmoji) {
return;
}
if (hasStatusMessage) {
toggleNoEmojiPlaceholder(false);
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
toggleNoEmojiPlaceholder(true);
removeStatusEmoji();
}
});
}) })
.catch(() => createFlash('Failed to load emoji list!')); .catch(() => createFlash('Failed to load emoji list!'));
}); });
...@@ -4,8 +4,8 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -4,8 +4,8 @@ class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions include MilestoneActions
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels] before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update] before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
def index def index
respond_to do |format| respond_to do |format|
...@@ -58,10 +58,21 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -58,10 +58,21 @@ class Groups::MilestonesController < Groups::ApplicationController
redirect_to milestone_path redirect_to milestone_path
end end
def destroy
return render_404 if @milestone.legacy_group_milestone?
Milestones::DestroyService.new(group, current_user).execute(@milestone)
respond_to do |format|
format.html { redirect_to group_milestones_path(group), status: :see_other }
format.js { head :ok }
end
end
private private
def authorize_admin_milestones! def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group) return render_404 unless can?(current_user, :admin_milestone, group)
end end
def milestone_params def milestone_params
......
...@@ -89,8 +89,7 @@ class Project < ActiveRecord::Base ...@@ -89,8 +89,7 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) } after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! } before_destroy :untrack_site_statistics
after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
after_create :create_ci_cd_settings, after_create :create_ci_cd_settings,
unless: :ci_cd_settings, unless: :ci_cd_settings,
...@@ -2113,6 +2112,11 @@ class Project < ActiveRecord::Base ...@@ -2113,6 +2112,11 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path) Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path)
end end
def untrack_site_statistics
SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
self.project_feature.untrack_statistics_for_deletion!
end
def execute_rename_repository_hooks!(full_path_before) def execute_rename_repository_hooks!(full_path_before)
# When we import a project overwriting the original project, there # When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions. # is a move operation. In that case we don't want to send the instructions.
......
...@@ -56,7 +56,7 @@ class GroupPolicy < BasePolicy ...@@ -56,7 +56,7 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestones rule { developer }.enable :admin_milestone
rule { reporter }.policy do rule { reporter }.policy do
enable :admin_label enable :admin_label
......
...@@ -14,12 +14,15 @@ module Groups ...@@ -14,12 +14,15 @@ module Groups
def execute def execute
group.prepare_for_destroy group.prepare_for_destroy
group.projects.each do |project| group.projects.includes(:project_feature).each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup. # Execute the destruction of the models immediately to ensure atomic cleanup.
success = ::Projects::DestroyService.new(project, current_user).execute success = ::Projects::DestroyService.new(project, current_user).execute
raise DestroyError, "Project #{project.id} can't be deleted" unless success raise DestroyError, "Project #{project.id} can't be deleted" unless success
end end
# reload the relation to prevent triggering destroy hooks on the projects again
group.projects.reload
group.children.each do |group| group.children.each do |group|
# This needs to be synchronous since the namespace gets destroyed below # This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute DestroyService.new(group, current_user).execute
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
module Milestones module Milestones
class DestroyService < Milestones::BaseService class DestroyService < Milestones::BaseService
def execute(milestone) def execute(milestone)
return unless milestone.project_milestone?
Milestone.transaction do Milestone.transaction do
update_params = { milestone: nil } update_params = { milestone: nil }
...@@ -16,15 +14,21 @@ module Milestones ...@@ -16,15 +14,21 @@ module Milestones
MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request) MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request)
end end
event_service.destroy_milestone(milestone, current_user) log_destroy_event_for(milestone)
Event.for_milestone_id(milestone.id).each do |event|
event.target_id = nil
event.save
end
milestone.destroy milestone.destroy
end end
end end
def log_destroy_event_for(milestone)
return if milestone.group_milestone?
event_service.destroy_milestone(milestone, current_user)
Event.for_milestone_id(milestone.id).each do |event|
event.target_id = nil
event.save
end
end
end end
end end
...@@ -39,6 +39,10 @@ ...@@ -39,6 +39,10 @@
%strong= email.email %strong= email.email
= link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
%i.fa.fa-times %i.fa.fa-times
%li
%span.light ID:
%strong
= @user.id
%li.two-factor-status %li.two-factor-status
%span.light Two-factor Authentication: %span.light Two-factor Authentication:
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.nav-controls .nav-controls
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestones, @group) - if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
.milestones .milestones
......
#modal_merge_info.modal{ tabindex: '-1' } #modal_merge_info.modal{ tabindex: '-1' }
.modal-dialog .modal-dialog.modal-lg
.modal-content .modal-content
.modal-header .modal-header
%h3.modal-title Check out, review, and merge locally %h3.modal-title Check out, review, and merge locally
......
...@@ -43,18 +43,7 @@ ...@@ -43,18 +43,7 @@
- else - else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal', = render 'shared/milestones/delete_button'
target: '#delete-milestone-modal',
milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title),
milestone_url: project_milestone_path(@project, @milestone),
milestone_issue_count: @milestone.issues.count,
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
#delete-milestone-modal
%a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left') = icon('angle-double-left')
......
- milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone)
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal',
target: '#delete-milestone-modal',
milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title),
milestone_url: milestone_url,
milestone_issue_count: @milestone.issues.count,
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
#delete-milestone-modal
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
- unless milestone.active? - unless milestone.active?
= link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- if @group - if @group
- if can?(current_user, :admin_milestones, @group) - if can?(current_user, :admin_milestone, @group)
- if milestone.closed? - if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else - else
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= milestone_date_range(milestone) = milestone_date_range(milestone)
- if group - if group
.float-right .float-right
- if can?(current_user, :admin_milestones, group) - if can?(current_user, :admin_milestone, group)
- if milestone.group_milestone? - if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit Edit
...@@ -32,6 +32,9 @@ ...@@ -32,6 +32,9 @@
- else - else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- unless is_dynamic_milestone
= render 'shared/milestones/delete_button'
= render 'shared/milestones/deprecation_message' if is_dynamic_milestone = render 'shared/milestones/deprecation_message' if is_dynamic_milestone
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
......
---
title: Increase width of checkout branch modal box
merge_request:
author:
type: fixed
---
title: Creates vue component for environments information in job log view
merge_request:
author:
type: other
---
title: Removing a group no longer triggers hooks for project deletion twice
merge_request: 21366
author:
type: fixed
---
title: Fix Web IDE unable to commit to same file twice
merge_request: 21372
author:
type: fixed
---
title: Add Galician as an available language.
merge_request: 21202
author:
type: added
---
title: Expose user's id in /admin/users/ show page
merge_request:
author: Eva Kadlecova
type: changed
---
title: Allow to delete group milestones
merge_request:
author:
type: added
---
title: 'Rails5: fix can''t quote ActiveSupport::HashWithIndifferentAccess'
merge_request: 21397
author: Jasper Maes
type: other
---
title: Display default status emoji if only message is entered
merge_request: 21330
author:
type: changed
...@@ -38,7 +38,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -38,7 +38,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member post :toggle_subscription, on: :member
end end
resources :milestones, constraints: { id: %r{[^/]+} }, only: [:index, :show, :edit, :update, :new, :create] do resources :milestones, constraints: { id: %r{[^/]+} } do
member do member do
get :merge_requests get :merge_requests
get :participants get :participants
......
# frozen_string_literal: true
class RecalculateSiteStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
transaction do
execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
execute("UPDATE site_statistics SET repositories_count = (SELECT COUNT(*) FROM projects)")
end
transaction do
execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
end
end
def down
# No downside in keeping the counter up-to-date
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180823132905) do ActiveRecord::Schema.define(version: 20180826111825) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -96,6 +96,19 @@ Parameters: ...@@ -96,6 +96,19 @@ Parameters:
- `start_date` (optional) - The start date of the milestone - `start_date` (optional) - The start date of the milestone
- `state_event` (optional) - The state event of the milestone (close|activate) - `state_event` (optional) - The state event of the milestone (close|activate)
## Delete group milestone
Only for user with developer access to the group.
```
DELETE /groups/:id/milestones/:milestone_id
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of the group's milestone
## Get all issues assigned to a single milestone ## Get all issues assigned to a single milestone
Gets all issues assigned to a single group milestone. Gets all issues assigned to a single group milestone.
......
...@@ -474,7 +474,7 @@ POST /projects/:id/issues ...@@ -474,7 +474,7 @@ POST /projects/:id/issues
| `assignee_ids` | Array[integer] | no | The ID of a user to assign issue | | `assignee_ids` | Array[integer] | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The global ID of a milestone to assign issue | | `milestone_id` | integer | no | The global ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue | | `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.| | `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. | | `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
......
...@@ -41,7 +41,7 @@ module API ...@@ -41,7 +41,7 @@ module API
use :optional_params use :optional_params
end end
post ":id/milestones" do post ":id/milestones" do
authorize! :admin_milestones, user_group authorize! :admin_milestone, user_group
create_milestone_for(user_group) create_milestone_for(user_group)
end end
...@@ -53,11 +53,21 @@ module API ...@@ -53,11 +53,21 @@ module API
use :update_params use :update_params
end end
put ":id/milestones/:milestone_id" do put ":id/milestones/:milestone_id" do
authorize! :admin_milestones, user_group authorize! :admin_milestone, user_group
update_milestone_for(user_group) update_milestone_for(user_group)
end end
desc 'Remove a project milestone'
delete ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_group
milestone = user_group.milestones.find(params[:milestone_id])
Milestones::DestroyService.new(user_group, current_user).execute(milestone)
status(204)
end
desc 'Get all issues for a single group milestone' do desc 'Get all issues for a single group milestone' do
success Entities::IssueBasic success Entities::IssueBasic
end end
......
...@@ -64,7 +64,8 @@ module API ...@@ -64,7 +64,8 @@ module API
delete ":id/milestones/:milestone_id" do delete ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_project authorize! :admin_milestone, user_project
user_project.milestones.find(params[:milestone_id]).destroy milestone = user_project.milestones.find(params[:milestone_id])
Milestones::DestroyService.new(user_project, current_user).execute(milestone)
status(204) status(204)
end end
......
...@@ -5,6 +5,7 @@ module Gitlab ...@@ -5,6 +5,7 @@ module Gitlab
AVAILABLE_LANGUAGES = { AVAILABLE_LANGUAGES = {
'en' => 'English', 'en' => 'English',
'es' => 'Español', 'es' => 'Español',
'gl_ES' => 'Galego',
'de' => 'Deutsch', 'de' => 'Deutsch',
'fr' => 'Français', 'fr' => 'Français',
'pt_BR' => 'Português (Brasil)', 'pt_BR' => 'Português (Brasil)',
......
...@@ -4677,10 +4677,10 @@ msgstr "" ...@@ -4677,10 +4677,10 @@ msgstr ""
msgid "Milestones" msgid "Milestones"
msgstr "" msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered." msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
msgstr "" msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project. %{milestoneTitle} is not currently used in any issues or merge requests." msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
msgstr "" msgstr ""
msgid "Milestones|<p>%{milestonePromotion}</p> %{finalWarning}" msgid "Milestones|<p>%{milestonePromotion}</p> %{finalWarning}"
...@@ -6974,6 +6974,9 @@ msgstr "" ...@@ -6974,6 +6974,9 @@ msgstr ""
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr "" msgstr ""
msgid "The deployment of this job to %{environmentLink} did not succeed."
msgstr ""
msgid "The fork relationship has been removed." msgid "The fork relationship has been removed."
msgstr "" msgstr ""
...@@ -7187,6 +7190,18 @@ msgstr "" ...@@ -7187,6 +7190,18 @@ msgstr ""
msgid "This job has not started yet" msgid "This job has not started yet"
msgstr "" msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink}."
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink}."
msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner" msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr "" msgstr ""
...@@ -7196,6 +7211,9 @@ msgstr "" ...@@ -7196,6 +7211,9 @@ msgstr ""
msgid "This job is stuck, because you don't have any active runners that can run this job." msgid "This job is stuck, because you don't have any active runners that can run this job."
msgstr "" msgstr ""
msgid "This job is the most recent deployment to %{link}."
msgstr ""
msgid "This job requires a manual action" msgid "This job requires a manual action"
msgstr "" msgstr ""
......
...@@ -141,6 +141,17 @@ describe Groups::MilestonesController do ...@@ -141,6 +141,17 @@ describe Groups::MilestonesController do
end end
end end
describe "#destroy" do
let(:milestone) { create(:milestone, group: group) }
it "removes milestone" do
delete :destroy, group_id: group.to_param, id: milestone.iid, format: :js
expect(response).to be_success
expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
end
end
describe '#ensure_canonical_path' do describe '#ensure_canonical_path' do
before do before do
sign_in(user) sign_in(user)
......
...@@ -34,7 +34,7 @@ FactoryBot.define do ...@@ -34,7 +34,7 @@ FactoryBot.define do
milestone.project_id = evaluator.project_id milestone.project_id = evaluator.project_id
elsif evaluator.parent elsif evaluator.parent
id = evaluator.parent.id id = evaluator.parent.id
evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id evaluator.parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
else else
milestone.project = create(:project) milestone.project = create(:project)
end end
......
...@@ -134,6 +134,7 @@ describe "Admin::Users" do ...@@ -134,6 +134,7 @@ describe "Admin::Users" do
expect(page).to have_content(user.email) expect(page).to have_content(user.email)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_content(user.id)
expect(page).to have_link('Block user', href: block_admin_user_path(user)) expect(page).to have_link('Block user', href: block_admin_user_path(user))
expect(page).to have_button('Delete user') expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions') expect(page).to have_button('Delete user and contributions')
......
require "rails_helper" require "rails_helper"
describe "User deletes milestone", :js do describe "User deletes milestone", :js do
set(:user) { create(:user) } let(:user) { create(:user) }
set(:project) { create(:project) } let(:group) { create(:group) }
set(:milestone) { create(:milestone, project: project) } let(:project) { create(:project, namespace: group) }
before do before do
project.add_developer(user)
sign_in(user) sign_in(user)
end
context "when milestone belongs to project" do
let!(:milestone) { create(:milestone, parent: project, title: "project milestone") }
it "deletes milestone" do
project.add_developer(user)
visit(project_milestones_path(project))
click_link(milestone.title)
click_button("Delete")
click_button("Delete milestone")
expect(page).to have_content("No milestones to show")
visit(activity_project_path(project))
visit(project_milestones_path(project)) expect(page).to have_content("#{user.name} destroyed milestone")
end
end end
it "deletes milestone" do context "when milestone belongs to group" do
click_link(milestone.title) let!(:milestone_to_be_deleted) { create(:milestone, parent: group, title: "group milestone 1") }
click_button("Delete") let!(:milestone) { create(:milestone, parent: group, title: "group milestone 2") }
click_button("Delete milestone")
expect(page).to have_content("No milestones to show") it "deletes milestone" do
group.add_developer(user)
visit(group_milestones_path(group))
visit(activity_project_path(project)) click_link(milestone_to_be_deleted.title)
click_button("Delete")
click_button("Delete milestone")
expect(page).to have_content("#{user.name} destroyed milestone") expect(page).to have_content(milestone.title)
expect(page).not_to have_content(milestone_to_be_deleted)
end
end end
end end
...@@ -130,5 +130,15 @@ describe 'User edit profile' do ...@@ -130,5 +130,15 @@ describe 'User edit profile' do
visit user_path(user) visit user_path(user)
expect(page).not_to have_selector '.cover-status' expect(page).not_to have_selector '.cover-status'
end end
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
visit(profile_path)
fill_in 'js-status-message-field', with: message
within('.js-toggle-emoji-menu') do
expect(page).to have_emoji('speech_balloon')
end
end
end end
end end
...@@ -57,8 +57,11 @@ describe('Flash', () => { ...@@ -57,8 +57,11 @@ describe('Flash', () => {
hideFlash(el); hideFlash(el);
expect( expect(
el.style.transition, el.style['transition-property'],
).toBe('opacity 0.3s'); ).toBe('opacity');
expect(
el.style['transition-duration'],
).toBe('0.3s');
}); });
it('sets opacity style', () => { it('sets opacity style', () => {
......
...@@ -184,7 +184,7 @@ describe('IDE commit module actions', () => { ...@@ -184,7 +184,7 @@ describe('IDE commit module actions', () => {
branch, branch,
}) })
.then(() => { .then(() => {
expect(f.lastCommit.message).toBe(data.message); expect(f.lastCommitSha).toBe(data.id);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -266,19 +266,21 @@ describe('IDE commit module actions', () => { ...@@ -266,19 +266,21 @@ describe('IDE commit module actions', () => {
}); });
describe('success', () => { describe('success', () => {
const COMMIT_RESPONSE = {
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
stats: {
additions: '1',
deletions: '2',
},
};
beforeEach(() => { beforeEach(() => {
spyOn(service, 'commit').and.returnValue( spyOn(service, 'commit').and.returnValue(
Promise.resolve({ Promise.resolve({
data: { data: COMMIT_RESPONSE,
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
stats: {
additions: '1',
deletions: '2',
},
},
}), }),
); );
}); });
...@@ -352,8 +354,8 @@ describe('IDE commit module actions', () => { ...@@ -352,8 +354,8 @@ describe('IDE commit module actions', () => {
store store
.dispatch('commit/commitChanges') .dispatch('commit/commitChanges')
.then(() => { .then(() => {
expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe( expect(store.state.entries[store.state.openFiles[0].path].lastCommitSha).toBe(
'test message', COMMIT_RESPONSE.id,
); );
done(); done();
......
import Vue from 'vue';
import component from '~/jobs/components/environments_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Environments block', () => {
const Component = Vue.extend(component);
let vm;
const icon = {
group: 'success',
icon: 'status_success',
label: 'passed',
text: 'passed',
tooltip: 'passed',
};
const deployment = {
path: 'deployment',
name: 'deployment name',
};
const environment = {
path: '/environment',
name: 'environment',
};
afterEach(() => {
vm.$destroy();
});
describe('with latest deployment', () => {
it('renders info for most recent deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'latest',
icon,
deployment,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is the most recent deployment to environment.',
);
});
});
describe('with out of date deployment', () => {
describe('with last deployment', () => {
it('renders info for out date and most recent', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'out_of_date',
icon,
deployment,
environment: Object.assign({}, environment, {
last_deployment: { name: 'deployment', path: 'last_deployment' },
}),
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is an out-of-date deployment to environment. View the most recent deployment deployment.',
);
});
});
describe('without last deployment', () => {
it('renders info about out of date deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'out_of_date',
icon,
deployment: null,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is an out-of-date deployment to environment.',
);
});
});
});
describe('with failed deployment', () => {
it('renders info about failed deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'failed',
icon,
deployment: null,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'The deployment of this job to environment did not succeed.',
);
});
});
describe('creating deployment', () => {
describe('with last deployment', () => {
it('renders info about creating deployment and overriding lastest deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'creating',
icon,
deployment,
environment: Object.assign({}, environment, {
last_deployment: { name: 'deployment', path: 'last_deployment' },
}),
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is creating a deployment to environment and will overwrite the last deployment.',
);
});
});
describe('without last deployment', () => {
it('renders info about failed deployment', () => {
vm = mountComponent(Component, {
deploymentStatus: {
status: 'creating',
icon,
deployment: null,
environment,
},
});
expect(vm.$el.textContent.trim()).toEqual(
'This job is creating a deployment to environment.',
);
});
});
});
});
...@@ -11,7 +11,7 @@ describe Gitlab::Import::MergeRequestCreator do ...@@ -11,7 +11,7 @@ describe Gitlab::Import::MergeRequestCreator do
context 'merge request already exists' do context 'merge request already exists' do
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) } let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:commits) { merge_request.merge_request_diffs.first.commits } let(:commits) { merge_request.merge_request_diffs.first.commits }
let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes) } let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) }
it 'updates the data' do it 'updates the data' do
commits_count = commits.count commits_count = commits.count
...@@ -28,7 +28,7 @@ describe Gitlab::Import::MergeRequestCreator do ...@@ -28,7 +28,7 @@ describe Gitlab::Import::MergeRequestCreator do
context 'new merge request' do context 'new merge request' do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) } let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes) } let(:attributes) { HashWithIndifferentAccess.new(merge_request.attributes.except("merge_params")) }
it 'creates a new merge request' do it 'creates a new merge request' do
attributes.delete(:id) attributes.delete(:id)
......
...@@ -18,7 +18,7 @@ describe GroupPolicy do ...@@ -18,7 +18,7 @@ describe GroupPolicy do
let(:reporter_permissions) { [:admin_label] } let(:reporter_permissions) { [:admin_label] }
let(:developer_permissions) { [:admin_milestones] } let(:developer_permissions) { [:admin_milestone] }
let(:maintainer_permissions) do let(:maintainer_permissions) do
[ [
......
...@@ -39,19 +39,6 @@ describe API::ProjectMilestones do ...@@ -39,19 +39,6 @@ describe API::ProjectMilestones do
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
it "rejects a member with reporter access from deleting a milestone" do
delete api("/projects/#{project.id}/milestones/#{milestone.id}", reporter)
expect(response).to have_gitlab_http_status(403)
end
it 'deletes the milestone when the user has developer access to the project' do
delete api("/projects/#{project.id}/milestones/#{milestone.id}", user)
expect(project.milestones.find_by_id(milestone.id)).to be_nil
expect(response).to have_gitlab_http_status(204)
end
end end
describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
......
...@@ -35,6 +35,14 @@ describe Groups::DestroyService do ...@@ -35,6 +35,14 @@ describe Groups::DestroyService do
it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) } it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) }
end end
context 'site statistics' do
it 'doesnt trigger project deletion hooks twice' do
expect_any_instance_of(Project).to receive(:untrack_site_statistics).once
destroy_group(group, user, async)
end
end
context 'mattermost team' do context 'mattermost team' do
let!(:chat_team) { create(:chat_team, namespace: group) } let!(:chat_team) { create(:chat_team, namespace: group) }
......
...@@ -4,8 +4,6 @@ describe Milestones::DestroyService do ...@@ -4,8 +4,6 @@ describe Milestones::DestroyService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -23,12 +21,23 @@ describe Milestones::DestroyService do ...@@ -23,12 +21,23 @@ describe Milestones::DestroyService do
end end
it 'deletes milestone id from issuables' do it 'deletes milestone id from issuables' do
issue = create(:issue, project: project, milestone: milestone)
merge_request = create(:merge_request, source_project: project, milestone: milestone)
service.execute(milestone) service.execute(milestone)
expect(issue.reload.milestone).to be_nil expect(issue.reload.milestone).to be_nil
expect(merge_request.reload.milestone).to be_nil expect(merge_request.reload.milestone).to be_nil
end end
it 'logs destroy event' do
service.execute(milestone)
event = Event.where(project_id: milestone.project_id, target_type: 'Milestone')
expect(event.count).to eq(1)
end
context 'group milestones' do context 'group milestones' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:group_milestone) { create(:milestone, group: group) } let(:group_milestone) { create(:milestone, group: group) }
...@@ -38,13 +47,20 @@ describe Milestones::DestroyService do ...@@ -38,13 +47,20 @@ describe Milestones::DestroyService do
group.add_developer(user) group.add_developer(user)
end end
it { expect(service.execute(group_milestone)).to be_nil } it { expect(service.execute(group_milestone)).to eq(group_milestone) }
it 'does not update milestone issuables' do it 'deletes milestone id from issuables' do
expect(MergeRequests::UpdateService).not_to receive(:new) issue = create(:issue, project: project, milestone: group_milestone)
expect(Issues::UpdateService).not_to receive(:new) merge_request = create(:merge_request, source_project: project, milestone: group_milestone)
service.execute(group_milestone) service.execute(group_milestone)
expect(issue.reload.milestone).to be_nil
expect(merge_request.reload.milestone).to be_nil
end
it 'does not log destroy event' do
expect { service.execute(group_milestone) }.not_to change { Event.count }
end end
end end
end end
......
...@@ -196,6 +196,24 @@ shared_examples_for 'group and project milestones' do |route_definition| ...@@ -196,6 +196,24 @@ shared_examples_for 'group and project milestones' do |route_definition|
end end
end end
describe "DELETE #{route_definition}/:milestone_id" do
it "rejects a member with reporter access from deleting a milestone" do
reporter = create(:user)
milestone.parent.add_reporter(reporter)
delete api(resource_route, reporter)
expect(response).to have_gitlab_http_status(403)
end
it 'deletes the milestone when the user has developer access to the project' do
delete api(resource_route, user)
expect(project.milestones.find_by_id(milestone.id)).to be_nil
expect(response).to have_gitlab_http_status(204)
end
end
describe "GET #{route_definition}/:milestone_id/issues" do describe "GET #{route_definition}/:milestone_id/issues" do
let(:issues_route) { "#{route}/#{milestone.id}/issues" } let(:issues_route) { "#{route}/#{milestone.id}/issues" }
......
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