summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js8
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue118
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js4
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue6
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue61
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue67
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss39
-rw-r--r--app/controllers/groups/milestones_controller.rb17
-rw-r--r--app/helpers/import_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/models/ci/pipeline.rb3
-rw-r--r--app/models/diff_note.rb16
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/project.rb16
-rw-r--r--app/models/protected_tag.rb2
-rw-r--r--app/models/repository.rb21
-rw-r--r--app/policies/group_policy.rb4
-rw-r--r--app/policies/project_policy.rb4
-rw-r--r--app/services/groups/destroy_service.rb5
-rw-r--r--app/services/merge_requests/build_service.rb19
-rw-r--r--app/services/milestones/destroy_service.rb20
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml2
-rw-r--r--app/views/groups/milestones/index.html.haml2
-rw-r--r--app/views/import/_githubish_status.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/projects/_issuable_by_email.html.haml11
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml13
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml4
-rw-r--r--app/views/shared/milestones/_delete_button.html.haml14
-rw-r--r--app/views/shared/milestones/_milestone.html.haml5
-rw-r--r--app/views/shared/milestones/_top.html.haml5
40 files changed, 434 insertions, 156 deletions
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 56a8d9430c7..0347f803757 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -146,13 +146,7 @@ export default {
staged: false,
prevPath: '',
moved: false,
- lastCommit: Object.assign(state.entries[file.path].lastCommit, {
- id: lastCommit.commit.id,
- url: lastCommit.commit_path,
- message: lastCommit.commit.message,
- author: lastCommit.commit.author_name,
- updatedAt: lastCommit.commit.authored_date,
- }),
+ lastCommitSha: lastCommit.commit.id,
});
if (prevPath) {
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
new file mode 100644
index 00000000000..ca6386595c7
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -0,0 +1,118 @@
+<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>
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 74cc4ba42c1..ebaea5ef3dc 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,8 +1,10 @@
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
+import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
+
import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
-
+ initDeleteMilestoneModal();
Milestone.initDeprecationMessage();
});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 4061c11ba8f..48668562f09 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -40,8 +40,8 @@
if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf(
s__(`Milestones|
-You’re about to permanently delete the milestone %{milestoneTitle} from this project.
-%{milestoneTitle} is not currently used in any issues or merge requests.`),
+You’re about to permanently delete the milestone %{milestoneTitle}.
+This milestone is not currently used in any issues or merge requests.`),
{
milestoneTitle,
},
@@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p
return sprintf(
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.`),
{
milestoneTitle,
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index 949219a0837..aea7b649c20 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -3,15 +3,22 @@ import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
import EmojiMenu from './emoji_menu';
+const defaultStatusEmoji = 'speech_balloon';
+
document.addEventListener('DOMContentLoaded', () => {
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-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 statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji');
+ const statusEmoji = findStatusEmoji();
if (statusEmoji) {
statusEmoji.remove();
}
@@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => {
const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji;
- findNoEmojiPlaceholder().classList.add('hidden');
+ toggleNoEmojiPlaceholder(false);
removeStatusEmoji();
toggleEmojiMenuButton.innerHTML += emojiTag;
};
@@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => {
statusEmojiField.value = '';
statusMessageField.value = '';
removeStatusEmoji();
- findNoEmojiPlaceholder().classList.remove('hidden');
+ toggleNoEmojiPlaceholder(true);
});
const emojiAutocomplete = new GfmAutoComplete();
@@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => {
selectEmojiCallback,
);
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!'));
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 21f21232596..d530ab2767b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -1,5 +1,6 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -16,6 +17,7 @@ export default {
MemoryUsage,
StatusIcon,
Icon,
+ TooltipOnTruncate,
},
directives: {
tooltip,
@@ -88,14 +90,20 @@ export default {
<span>
Deployed to
</span>
- <a
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-meta"
+ <tooltip-on-truncate
+ :title="deployment.name"
+ truncate-target="child"
+ class="deploy-link label-truncate"
>
- {{ deployment.name }}
- </a>
+ <a
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta"
+ >
+ {{ deployment.name }}
+ </a>
+ </tooltip-on-truncate>
</template>
<span
v-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index a4c2289c590..72bd28ae03f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,18 +1,17 @@
<script>
-import tooltip from '~/vue_shared/directives/tooltip';
-import { n__ } from '~/locale';
+import _ from 'underscore';
+import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
name: 'MRWidgetHeader',
- directives: {
- tooltip,
- },
components: {
Icon,
clipboardButton,
+ TooltipOnTruncate,
},
props: {
mr: {
@@ -24,8 +23,12 @@ export default {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
- commitsText() {
- return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
+ commitsBehindText() {
+ return sprintf(s__('mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch'), {
+ commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`,
+ commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount),
+ commitsBehindLinkEnd: '</a>',
+ }, false);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
@@ -36,12 +39,6 @@ export default {
gfm: `\`${this.mr.sourceBranch}\``,
});
},
- isSourceBranchLong() {
- return this.isBranchTitleLong(this.mr.sourceBranch);
- },
- isTargetBranchLong() {
- return this.isBranchTitleLong(this.mr.targetBranch);
- },
webIdePath() {
return mergeUrlParams({
target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
@@ -49,11 +46,6 @@ export default {
}, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
},
},
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
- },
};
</script>
<template>
@@ -65,30 +57,21 @@ export default {
<div class="normal">
<strong>
{{ s__("mrWidget|Request to merge") }}
- <span
- :class="{ 'label-truncated': isSourceBranchLong }"
- :title="isSourceBranchLong ? mr.sourceBranch : ''"
- :v-tooltip="isSourceBranchLong"
- class="label-branch js-source-branch"
- data-placement="bottom"
+ <tooltip-on-truncate
+ :title="mr.sourceBranch"
+ truncate-target="child"
+ class="label-branch label-truncate js-source-branch"
v-html="mr.sourceBranchLink"
- >
- </span>
-
- <clipboard-button
+ /><clipboard-button
:text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')"
css-class="btn-default btn-transparent btn-clipboard"
/>
-
{{ s__("mrWidget|into") }}
-
- <span
- :v-tooltip="isTargetBranchLong"
- :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
- :title="isTargetBranchLong ? mr.targetBranch : ''"
- class="label-branch"
- data-placement="bottom"
+ <tooltip-on-truncate
+ :title="mr.targetBranch"
+ truncate-target="child"
+ class="label-branch label-truncate"
>
<a
:href="mr.targetBranchTreePath"
@@ -96,15 +79,13 @@ export default {
>
{{ mr.targetBranch }}
</a>
- </span>
+ </tooltip-on-truncate>
</strong>
<div
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count"
+ v-html="commitsBehindText"
>
- <span class="monospace">{{ mr.sourceBranch }}</span>
- is {{ commitsText }}
- <span class="monospace">{{ mr.targetBranch }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 4a3fd01fa39..fee41b239e8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -3,6 +3,7 @@
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
name: 'MRWidgetPipeline',
@@ -10,6 +11,7 @@ export default {
PipelineStage,
CiIcon,
Icon,
+ TooltipOnTruncate,
},
props: {
pipeline: {
@@ -30,6 +32,10 @@ export default {
type: String,
required: false,
},
+ sourceBranch: {
+ type: String,
+ required: false,
+ },
},
computed: {
hasPipeline() {
@@ -107,11 +113,12 @@ export default {
>
{{ pipeline.commit.short_id }}</a>
on
- <span
- class="label-branch"
+ <tooltip-on-truncate
+ :title="sourceBranch"
+ truncate-target="child"
+ class="label-branch label-truncate"
v-html="sourceBranchLink"
- >
- </span>
+ />
</template>
</div>
<div
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 80593d1f34a..dc6be025f11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -254,6 +254,7 @@ export default {
:pipeline="mr.pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
+ :source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink"
/>
<deployment
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index f44d361c47e..78fde463507 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -71,7 +71,11 @@ export default {
},
methods: {
getPercent(count) {
- return roundOffFloat((count / this.totalCount) * 100, 1);
+ const percent = roundOffFloat((count / this.totalCount) * 100, 1);
+ if (percent > 0 && percent < 1) {
+ return '< 1';
+ }
+ return percent;
},
barStyle(percent) {
return `width: ${percent}%;`;
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
new file mode 100644
index 00000000000..125826da6c3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -0,0 +1,67 @@
+<script>
+import _ from 'underscore';
+import tooltip from '../directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ placement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ truncateTarget: {
+ type: [String, Function],
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ mounted() {
+ const target = this.selectTarget();
+
+ if (target && target.scrollWidth > target.offsetWidth) {
+ this.showTooltip = true;
+ }
+ },
+ methods: {
+ selectTarget() {
+ if (_.isFunction(this.truncateTarget)) {
+ return this.truncateTarget(this.$el);
+ } else if (this.truncateTarget === 'child') {
+ return this.$el.childNodes[0];
+ }
+
+ return this.$el;
+ },
+ },
+};
+</script>
+
+<template>
+ <span
+ v-tooltip
+ v-if="showTooltip"
+ :title="title"
+ :data-placement="placement"
+ class="js-show-tooltip"
+ >
+ <slot></slot>
+ </span>
+ <span
+ v-else
+ >
+ <slot></slot>
+ </span>
+</template>
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 5631d943984..7b8cad254c7 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -195,6 +195,7 @@
.ci-widget-content {
display: flex;
align-items: center;
+ flex: 1;
}
}
@@ -222,6 +223,7 @@
.normal {
flex: 1;
+ flex-basis: auto;
}
.capitalize {
@@ -235,22 +237,23 @@
font-weight: normal;
overflow: hidden;
word-break: break-all;
+ }
- &.label-truncated {
- position: relative;
- display: inline-block;
- width: 250px;
- margin-bottom: -3px;
- white-space: nowrap;
- text-overflow: clip;
- line-height: 14px;
-
- &::after {
- position: absolute;
- content: '...';
- right: 0;
- font-family: $regular-font;
- background-color: $gray-light;
+ .deploy-link,
+ .label-branch {
+ &.label-truncate {
+ // NOTE: This selector targets its children because some of the HTML comes from
+ // 'source_branch_link'. Once this external HTML is no longer used, we could
+ // simplify this.
+ > a,
+ > span {
+ display: inline-block;
+ max-width: 12.5em;
+ margin-bottom: -3px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ line-height: 14px;
+ overflow: hidden;
}
}
}
@@ -582,7 +585,7 @@
@include media-breakpoint-down(md) {
flex-direction: column;
- align-items: flex-start;
+ align-items: stretch;
.branch-actions {
margin-top: 16px;
@@ -593,13 +596,13 @@
.branch-actions {
align-self: center;
margin-left: $gl-padding;
+ white-space: nowrap;
}
}
}
.diverged-commits-count {
color: $gl-text-color-secondary;
- font-size: 12px;
}
}
@@ -918,7 +921,7 @@
flex: 1;
flex-direction: row;
- @include media-breakpoint-down(md) {
+ @include media-breakpoint-down(sm) {
flex-direction: column;
.stage-cell .stage-container {
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 9bd51de7e97..6bdc0f79ef2 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -2,8 +2,8 @@ class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects
- before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels]
- before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update]
+ before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
+ before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
def index
respond_to do |format|
@@ -56,10 +56,21 @@ class Groups::MilestonesController < Groups::ApplicationController
redirect_to milestone_path
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
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
def milestone_params
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 4664b1728c4..c65f1565425 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -5,6 +5,10 @@ module ImportHelper
false
end
+ def sanitize_project_name(name)
+ name.gsub(/[^\w\-]/, '-')
+ end
+
def import_project_target(owner, name)
namespace = current_user.can_create_group? ? owner : current_user.namespace_path
"#{namespace}/#{name}"
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 6b4079b4113..18b3badda8d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -447,7 +447,7 @@ module ProjectsHelper
end
def project_permissions_panel_data(project)
- data = {
+ {
currentSettings: project_permissions_settings(project),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project),
@@ -457,8 +457,10 @@ module ProjectsHelper
lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
+ end
- data.to_json.html_safe
+ def project_permissions_panel_data_json(project)
+ project_permissions_panel_data(project).to_json.html_safe
end
def project_allowed_visibility_levels(project)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index e4aed76f611..526bf7af99b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -649,8 +649,7 @@ module Ci
def keep_around_commits
return unless project
- project.repository.keep_around(self.sha)
- project.repository.keep_around(self.before_sha)
+ project.repository.keep_around(self.sha, self.before_sha)
end
def valid_source
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 58d949315e0..716cf6574d3 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -191,14 +191,18 @@ class DiffNote < Note
end
def keep_around_commits
- project.repository.keep_around(self.original_position.base_sha)
- project.repository.keep_around(self.original_position.start_sha)
- project.repository.keep_around(self.original_position.head_sha)
+ shas = [
+ self.original_position.base_sha,
+ self.original_position.start_sha,
+ self.original_position.head_sha
+ ]
if self.position != self.original_position
- project.repository.keep_around(self.position.base_sha)
- project.repository.keep_around(self.position.start_sha)
- project.repository.keep_around(self.position.head_sha)
+ shas << self.position.base_sha
+ shas << self.position.start_sha
+ shas << self.position.head_sha
end
+
+ project.repository.keep_around(*shas)
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d9393b4e545..bbe4f6f7969 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -314,9 +314,7 @@ class MergeRequestDiff < ActiveRecord::Base
def keep_around_commits
[repository, merge_request.source_project.repository].uniq.each do |repo|
- repo.keep_around(start_commit_sha)
- repo.keep_around(head_commit_sha)
- repo.keep_around(base_commit_sha)
+ repo.keep_around(start_commit_sha, head_commit_sha, base_commit_sha)
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 8f631d7f0ed..67593c9b2fe 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -85,8 +85,7 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature
after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
- before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! }
- after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
+ before_destroy :untrack_site_statistics
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
@@ -2072,13 +2071,19 @@ class Project < ActiveRecord::Base
private
def rename_or_migrate_repository!
- if Gitlab::CurrentSettings.hashed_storage_enabled? && storage_version != LATEST_STORAGE_VERSION
+ if Gitlab::CurrentSettings.hashed_storage_enabled? &&
+ storage_upgradable? &&
+ Feature.disabled?(:skip_hashed_storage_upgrade) # kill switch in case we need to disable upgrade behavior
::Projects::HashedStorageMigrationService.new(self, full_path_was).execute
else
storage.rename_repo
end
end
+ def storage_upgradable?
+ storage_version != LATEST_STORAGE_VERSION
+ end
+
def after_rename_repository(full_path_before, path_before)
execute_rename_repository_hooks!(full_path_before)
@@ -2093,6 +2098,11 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path)
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)
# 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.
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index a36f0d36262..94746141945 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -4,6 +4,8 @@ class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
+ validates :name, uniqueness: { scope: :project_id }
+
protected_ref_access_levels :create
def self.protected?(project, ref_name)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 69f375dc6f3..cf255c8951f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -247,15 +247,22 @@ class Repository
# Git GC will delete commits from the repository that are no longer in any
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
- def keep_around(sha)
- return unless sha.present? && commit_by(oid: sha)
+ #
+ # For Geo's sake, pass in multiple shas rather than calling it multiple times,
+ # to avoid unnecessary syncing.
+ def keep_around(*shas)
+ shas.each do |sha|
+ begin
+ next unless sha.present? && commit_by(oid: sha)
- return if kept_around?(sha)
+ next if kept_around?(sha)
- # This will still fail if the file is corrupted (e.g. 0 bytes)
- raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
- rescue Gitlab::Git::CommandError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
+ # This will still fail if the file is corrupted (e.g. 0 bytes)
+ raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
+ rescue Gitlab::Git::CommandError => ex
+ Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
+ end
+ end
end
def kept_around?(sha)
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index a8d7a05f509..73c93b22c95 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -53,7 +53,7 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace
- rule { developer }.enable :admin_milestones
+ rule { developer }.enable :admin_milestone
rule { reporter }.policy do
enable :admin_label
@@ -72,6 +72,8 @@ class GroupPolicy < BasePolicy
enable :admin_namespace
enable :admin_group_member
enable :change_visibility_level
+
+ enable :set_note_created_at
end
rule { can?(:read_nested_project_resources) }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 00c58f15013..fd6cc504a3b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -143,6 +143,10 @@ class ProjectPolicy < BasePolicy
enable :destroy_merge_request
enable :destroy_issue
enable :remove_pages
+
+ enable :set_issue_iid
+ enable :set_issue_created_at
+ enable :set_note_created_at
end
rule { can?(:guest_access) }.policy do
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 12aeba4af71..93d84bd8a9c 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -12,12 +12,15 @@ module Groups
def execute
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.
success = ::Projects::DestroyService.new(project, current_user).execute
raise DestroyError, "Project #{project.id} can't be deleted" unless success
end
+ # reload the relation to prevent triggering destroy hooks on the projects again
+ group.projects.reload
+
group.children.each do |group|
# This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bc988eb2a26..55750269bb4 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -128,8 +128,7 @@ module MergeRequests
#
def assign_title_and_description
assign_title_and_description_from_single_commit
- assign_title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
-
+ merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
merge_request.title ||= source_branch.titleize.humanize
merge_request.title = wip_title if compare_commits.empty?
@@ -159,20 +158,18 @@ module MergeRequests
merge_request.description ||= commit.description.try(:strip)
end
- def assign_title_from_issue
+ def title_from_issue
return unless issue
- merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue)
+ return "Resolve \"#{issue.title}\"" if issue.is_a?(Issue)
- return if merge_request.title.present?
+ return if issue_iid.blank?
- if issue_iid.present?
- title_parts = ["Resolve #{issue.to_reference}"]
- branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
+ title_parts = ["Resolve #{issue.to_reference}"]
+ branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
- title_parts << "\"#{branch_title}\"" if branch_title.present?
- merge_request.title = title_parts.join(' ')
- end
+ title_parts << "\"#{branch_title}\"" if branch_title.present?
+ title_parts.join(' ')
end
def issue_iid
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
index 15c04525075..7cda802c120 100644
--- a/app/services/milestones/destroy_service.rb
+++ b/app/services/milestones/destroy_service.rb
@@ -3,8 +3,6 @@
module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
- return unless milestone.project_milestone?
-
Milestone.transaction do
update_params = { milestone: nil }
@@ -16,15 +14,21 @@ module Milestones
MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request)
end
- event_service.destroy_milestone(milestone, current_user)
-
- Event.for_milestone_id(milestone.id).each do |event|
- event.target_id = nil
- event.save
- end
+ log_destroy_event_for(milestone)
milestone.destroy
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
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 029efadd75d..a74e052707f 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -39,6 +39,10 @@
%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
%i.fa.fa-times
+ %li
+ %span.light ID:
+ %strong
+ = @user.id
%li.two-factor-status
%span.light Two-factor Authentication:
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index 13f96b9747c..c26eb873718 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -1,6 +1,6 @@
- link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank'
.append-bottom-10
- %h4= _("Setup a #{type} Runner manually")
+ %h4= _("Setup a %{type} Runner manually") % { type: type }
%ol
%li
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index f5f621507b8..b6424df55cd 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -5,7 +5,7 @@
.nav-controls
= 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"
.milestones
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index f0d1e837317..f4a29ed18dc 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -45,7 +45,7 @@
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend
.input-group-text /
- = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
+ = text_field_tag :path, sanitize_project_name(repo.name), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
= has_ci_cd_only_params? ? _('Connect') : _('Import')
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index a75b7aa9dd2..3b1b5e55302 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -63,7 +63,7 @@
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend
.input-group-text /
- = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
+ = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: 'btn btn-import js-add-to-import' do
= _('Import')
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 3d05a5e696f..ae09e0dfa18 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -61,7 +61,7 @@
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend
.input-group-text /
- = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
+ = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: 'btn btn-import js-add-to-import' do
Import
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index 22adf5b4008..d59191a6f87 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -19,9 +19,16 @@
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block')
+
+ - if issuable_type == 'issue'
+ - enter_title_text = _('Enter the issue title')
+ - enter_description_text = _('Enter the issue description')
+ - else
+ - enter_title_text = _('Enter the merge request title')
+ - enter_description_text = _('Enter the merge request description')
= mail_to email, class: 'btn btn-clipboard btn-transparent',
- subject: _("Enter the #{name} title"),
- body: _("Enter the #{name} description"),
+ subject: enter_title_text,
+ body: enter_description_text,
title: _('Send email'),
data: { toggle: 'tooltip', placement: 'bottom' } do
= sprite_icon('mail')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 30544dde451..e37a444c1c9 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -78,7 +78,7 @@
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
-# haml-lint:disable InlineJavaScript
- %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
+ %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 62dd21ef6e0..d3871453b9f 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -1,5 +1,5 @@
#modal_merge_info.modal{ tabindex: '-1' }
- .modal-dialog
+ .modal-dialog.modal-lg
.modal-content
.modal-header
%h3.modal-title Check out, review, and merge locally
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 2a9e20c2caa..0a684f9016a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -43,18 +43,7 @@
- else
= 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',
- 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
+ = render 'shared/milestones/delete_button'
%a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index e051f9e6331..35a6885318a 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -4,7 +4,9 @@
= _('The repository must be accessible over <code>http://</code>,
<code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe
%li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
- %li= _("The update action will time out after #{import_will_timeout_message(Gitlab.config.gitlab_shell.git_timeout)} minutes. For big repositories, use a clone/push combination.")
+ %li
+ - minutes = Gitlab.config.gitlab_shell.git_timeout / 60
+ = _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
%li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
%li
= _('This user will be the author of all events in the activity feed that are the result of an update,
diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml
new file mode 100644
index 00000000000..e236c24b088
--- /dev/null
+++ b/app/views/shared/milestones/_delete_button.html.haml
@@ -0,0 +1,14 @@
+- 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
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index c559945a9c9..3dd2842be4f 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -16,6 +16,9 @@
= milestone_date_range(milestone)
%div
= render('shared/milestone_expired', milestone: milestone)
+ - if milestone.group_milestone?
+ .label-badge.label-badge-blue.d-inline-block
+ = milestone.group.full_name
- if milestone.legacy_group_milestone?
.projects
- milestone.milestones.each do |milestone|
@@ -49,7 +52,7 @@
- unless milestone.active?
= link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- if @group
- - if can?(current_user, :admin_milestones, @group)
+ - if can?(current_user, :admin_milestone, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 320e3788a0f..0499b04a482 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -23,7 +23,7 @@
= milestone_date_range(milestone)
- if group
.float-right
- - if can?(current_user, :admin_milestones, group)
+ - if can?(current_user, :admin_milestone, group)
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
@@ -32,6 +32,9 @@
- else
= 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
.detail-page-description.milestone-detail