Commit 04845fde authored by Stan Hu's avatar Stan Hu

Merge remote-tracking branch 'origin/master' into dev/master

parents 27580720 e19f2531
......@@ -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/push_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/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
......
# Test Plan
<!-- This issue outlines testing activities related to a particular issue or epic.
[Here is an example test plan](https://gitlab.com/gitlab-org/gitlab-ce/issues/50353)
This and other comments should be removed as you write the plan -->
## Introduction
<!-- Briefly outline what is being tested
Mention the issue(s) this test plan is related to -->
## Scope
<!-- State any limits on aspects of the feature being tested
Outline the types of data to be included
Outline the types of tests to be performed (functional, security, performance,
database, automated, etc) -->
## ACC Matrix
<!-- Use the matrix below as a template to identify the Attributes, Components, and
Capabilities relevant to the scope of this test plan. Add or remove Attributes
and Components as required and list Capabilities in the next section
Attributes (columns) are adverbs or adjectives that describe (at a high level)
the qualities testing is meant to ensure Components have.
Components (rows) are nouns that define major parts of the product being tested.
Capabilities link Attributes and Components. They are what your product needs to
do to make sure a Component fulfills an Attribute
For more information see the [Google Testing Blog article about the 10 minute
test plan](https://testing.googleblog.com/2011/09/10-minute-test-plan.html) and
[this wiki page from an open-source tool that implements the ACC
model](https://code.google.com/archive/p/test-analytics/wikis/AccExplained.wiki). -->
| | Simple | Secure | Responsive | Obvious | Stable |
|------------|:------:|:------:|:----------:|:-------:|:------:|
| Admin | | | | | |
| Groups | | | | | |
| Project | | | | | |
| Repository | | | | | |
| Issues | | | | | |
| MRs | | | | | |
| CI/CD | | | | | |
| Ops | | | | | |
| Registry | | | | | |
| Wiki | | | | | |
| Snippets | | | | | |
| Settings | | | | | |
| Tracking | | | | | |
| API | | | | | |
## Capabilities
<!-- Use the ACC matrix above to help you identify Capabilities at each relevant
intersection of Components and Attributes.
Some features might be simple enough that they only involve one Component, while
more complex features could involve multiple or even all.
Example (from https://gitlab.com/gitlab-org/gitlab-ce/issues/50353):
* Respository is
* Simple
* It's easy to select the desired file template
* It doesn't require unnecessary actions to save the change
* It's easy to undo the change after selecting a template
* Responsive
* The list of templates can be restricted to allow a user to find a specific template among many
* Once a template is selected the file content updates quickly and smoothly
-->
## Test Plan
<!-- If the scope is small enough you may not need to write a list of tests to
perform. It might be enough to use the Capabilities to guide your testing.
If the feature is more complex, especially if it involves multiple Components,
briefly outline a set of tests here. When identifying tests to perform be sure
to consider risk. Note inherent/known levels of risk so that testing can focus
on high risk areas first.
New end-to-end and integration tests (Selenium and API) should be added to the
[Test Coverage sheet](https://docs.google.com/spreadsheets/d/1RlLfXGboJmNVIPP9jgFV5sXIACGfdcFq1tKd7xnlb74/)
Please note if automated tests already exist.
When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ce/development/testing_guide/testing_levels.html)
in mind.
-->
/label ~Quality
\ No newline at end of file
......@@ -70,14 +70,15 @@ linters:
enabled: false
RuboCop:
enabled: false
enabled: true
# These cops are incredibly noisy when it comes to HAML templates, so we
# ignore them.
ignored_cops:
- Lint/BlockAlignment
- Lint/EndAlignment
- Layout/BlockAlignment
- Layout/EndAlignment
- Lint/Void
- Metrics/LineLength
- Naming/FileName
- Style/AlignParameters
- Style/BlockNesting
- Style/ElseAlignment
......@@ -91,6 +92,52 @@ linters:
- Style/TrailingWhitespace
- Style/WhileUntilModifier
# These cops should eventually get enabled
- Cop/LineBreakAfterGuardClauses
- Cop/LineBreakAroundConditionalBlock
- Cop/ProjectPathHelper
- GitlabSecurity/PublicSend
- Layout/LeadingCommentSpace
- Layout/SpaceAfterColon
- Layout/SpaceAfterComma
- Layout/SpaceAroundOperators
- Layout/SpaceBeforeBlockBraces
- Layout/SpaceBeforeComma
- Layout/SpaceBeforeFirstArg
- Layout/SpaceInsideArrayLiteralBrackets
- Layout/SpaceInsideHashLiteralBraces
- Layout/SpaceInsideStringInterpolation
- Layout/TrailingBlankLines
- Lint/BooleanSymbol
- Lint/LiteralInInterpolation
- Lint/ParenthesesAsGroupedExpression
- Lint/RedundantWithIndex
- Lint/Syntax
- Lint/UselessAssignment
- Metrics/BlockNesting
- Naming/VariableName
- Performance/RedundantMatch
- Performance/StringReplacement
- Rails/Presence
- Rails/RequestReferer
- Style/AndOr
- Style/ColonMethodCall
- Style/ConditionalAssignment
- Style/HashSyntax
- Style/IdenticalConditionalBranches
- Style/NegatedIf
- Style/NestedTernaryOperator
- Style/Not
- Style/ParenthesesAroundCondition
- Style/RedundantParentheses
- Style/SelfAssignment
- Style/Semicolon
- Style/TernaryParentheses
- Style/TrailingCommaInHashLiteral
- Style/UnlessElse
- Style/WordArray
- Style/ZeroLengthPredicate
RubyComments:
enabled: true
......
......@@ -314,7 +314,7 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
## Definition of done
This [documentation](doc/development/contributing/merge_request_workflow.md)) has been moved.
This [documentation](doc/development/contributing/merge_request_workflow.md) has been moved.
## Style guides
......
......@@ -126,7 +126,7 @@ GEM
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
coderay (1.1.1)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
commonmarker (0.17.8)
......@@ -495,7 +495,7 @@ GEM
memoist (0.16.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
method_source (0.9.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
......@@ -638,12 +638,11 @@ GEM
unparser
procto (0.0.3)
prometheus-client-mmap (0.9.4)
pry (0.10.4)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-byebug (3.4.2)
byebug (~> 9.0)
method_source (~> 0.9.0)
pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1)
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
......@@ -751,7 +750,7 @@ GEM
retriable (3.1.2)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.2.0)
rouge (3.2.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......@@ -817,7 +816,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.2)
rugged (0.27.4)
safe_yaml (1.0.4)
sanitize (4.6.6)
crass (~> 1.0.2)
......@@ -878,7 +877,6 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.5.1)
slop (3.6.0)
spring (2.0.1)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
......
......@@ -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) {
......
<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 initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
initDeleteMilestoneModal();
Milestone.initDeprecationMessage();
});
......@@ -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,
......
......@@ -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!'));
});
<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
......
<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>
......
......@@ -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
......
......@@ -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
......
......@@ -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}%;`;
......
<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>
......@@ -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 {
......
......@@ -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
......
......@@ -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}"
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......@@ -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
......@@ -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.
......
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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:
......
- 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
......
......@@ -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
......
......@@ -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')
......
......@@ -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')
......
......@@ -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
......
......@@ -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')
......
......@@ -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"
......
#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
......
......@@ -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')
......
......@@ -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,
......
- 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
......@@ -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
......
......@@ -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
......
---
title: Increase width of checkout branch modal box
merge_request:
author:
type: fixed
---
title: Truncate branch names and update "commits behind" text in MR page
merge_request: 21206
author:
type: changed
---
title: Add group name badge under group milestone
merge_request: 21384
author:
type: added
---
title: Creates vue component for environments information in job log view
merge_request:
author:
type: other
---
title: Feature flag to disable Hashed Storage migration when renaming a repository
merge_request: 21291
author:
type: added
---
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: Show '< 1%' when percent value evaluated is less than 1 on Stacked Progress
Bar
merge_request: 21306
author:
type: fixed
---
title: 'API: Protected tags'
merge_request: 14986
author: Robert Schilling
type: added
---
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 date parameters on Issues, Notes, and Discussions API for group owners
merge_request: 21342
author: Florent Dubois
type: fixed
---
title: Fix fallback logic for automatic MR title assignment
merge_request: 20930
author: Franz Liedke
type: fixed
---
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: Rails5 update Gemfile.rails5.lock
merge_request: 21388
author: Jasper Maes
type: other
---
title: 'Bitbucket Server importer: Eliminate most idle-in-transaction issues'
merge_request:
author:
type: performance
---
title: Use slugs for default project path and sanitize names before import
merge_request: 21367
author:
type: fixed
---
title: Display default status emoji if only message is entered
merge_request: 21330
author:
type: changed
......@@ -37,7 +37,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member
end
resources :milestones, constraints: { id: %r{[^/]+} }, only: [:index, :show, :edit, :update, :new, :create] do
resources :milestones, constraints: { id: %r{[^/]+} } do
member do
get :merge_requests
get :participants
......
......@@ -53,9 +53,11 @@ end
changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
mr_title = gitlab.mr_json["title"].gsub(/^WIP: */, '')
if git.modified_files.include?("CHANGELOG.md")
fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" +
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: gitlab.mr_json["title"], labels: presented_no_changelog_labels)
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: mr_title, labels: presented_no_changelog_labels)
end
if changelog_needed
......@@ -63,6 +65,6 @@ if changelog_needed
check_changelog(changelog_found)
else
warn "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).**\n\n" +
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: gitlab.mr_json["title"], labels: presented_no_changelog_labels)
format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: mr_title, labels: presented_no_changelog_labels)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class DropDuplicateProtectedTags < ActiveRecord::Migration
DOWNTIME = false
disable_ddl_transaction!
BATCH_SIZE = 1000
class Project < ActiveRecord::Base
self.table_name = 'projects'
include ::EachBatch
end
class ProtectedTag < ActiveRecord::Base
self.table_name = 'protected_tags'
end
def up
Project.each_batch(of: BATCH_SIZE) do |projects|
ids = ProtectedTag
.where(project_id: projects)
.group(:name, :project_id)
.select('max(id)')
tags = ProtectedTag
.where(project_id: projects)
.where.not(id: ids)
if Gitlab::Database.postgresql?
tags.delete_all
else
# Workaround needed for MySQL
sql = "SELECT id FROM (#{tags.to_sql}) protected_tags"
ProtectedTag.where("id IN (#{sql})").delete_all # rubocop:disable GitlabSecurity/SqlInjection
end
end
end
def down
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddProtectedTagsIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :protected_tags, [:project_id, :name], unique: true
end
def down
remove_concurrent_index :protected_tags, [:project_id, :name]
end
end
# 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 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180816193530) do
ActiveRecord::Schema.define(version: 20180826111825) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1741,6 +1741,7 @@ ActiveRecord::Schema.define(version: 20180816193530) do
t.datetime "updated_at", null: false
end
add_index "protected_tags", ["project_id", "name"], name: "index_protected_tags_on_project_id_and_name", unique: true, using: :btree
add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
create_table "push_event_payloads", id: false, force: :cascade do |t|
......
......@@ -53,6 +53,7 @@ following locations:
- [Project Members](members.md)
- [Project Snippets](project_snippets.md)
- [Protected Branches](protected_branches.md)
- [Protected Tags](protected_tags.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Runners](runners.md)
......
......@@ -136,7 +136,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `issue_iid` | integer | yes | The IID of an issue |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/discussions?body=comment
......@@ -159,7 +159,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
......@@ -342,7 +342,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `snippet_id` | integer | yes | The ID of an snippet |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions?body=comment
......@@ -365,7 +365,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
......@@ -601,7 +601,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `merge_request_iid` | integer | yes | The IID of a merge request |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
| `position` | hash | no | Position when creating a diff note |
| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
......@@ -659,7 +659,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
......@@ -894,7 +894,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `commit_id` | integer | yes | The ID of a commit |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
| `position` | hash | no | Position when creating a diff note |
| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
......@@ -930,7 +930,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
......
......@@ -96,6 +96,19 @@ Parameters:
- `start_date` (optional) - The start date of the milestone
- `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
Gets all issues assigned to a single group milestone.
......
......@@ -470,7 +470,7 @@ POST /projects/:id/issues
| `assignee_ids` | Array[integer] | no | The ID of the users 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 |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project 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` |
| `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`. |
......
......@@ -100,7 +100,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `issue_id` (required) - The IID of an issue
- `body` (required) - The content of a note
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights)
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note
......
# Protected tags API
>**Note:** This feature was introduced in GitLab 11.3
**Valid access levels**
Currently, these levels are recognized:
```
0 => No access
30 => Developer access
40 => Maintainer access
```
## List protected tags
Gets a list of protected tags from a project.
This function takes pagination parameters `page` and `per_page` to restrict the list of protected tags.
```
GET /projects/:id/protected_tags
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags'
```
Example response:
```json
[
{
"name": "release-1-0",
"create_access_levels": [
{
"access_level": 40,
"access_level_description": "Maintainers"
}
]
},
...
]
```
## Get a single protected tag or wildcard protected tag
Gets a single protected tag or wildcard protected tag.
The pagination parameters `page` and `per_page` can be used to restrict the list of protected tags.
```
GET /projects/:id/protected_tags/:name
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the tag or wildcard |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags/release-1-0'
```
Example response:
```json
{
"name": "release-1-0",
"create_access_levels": [
{
"access_level": 40,
"access_level_description": "Maintainers"
}
]
}
```
## Protect repository tags
Protects a single repository tag or several project repository
tags using a wildcard protected tag.
```
POST /projects/:id/protected_tags
```
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags?name=*-stable&create_access_level=30'
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the tag or wildcard |
| `create_access_level` | string | no | Access levels allowed to create (defaults: `40`, maintainer access level) |
Example response:
```json
{
"name": "*-stable",
"create_access_levels": [
{
"access_level": 30,
"access_level_description": "Developers + Maintainers"
}
]
}
```
## Unprotect repository tags
Unprotects the given protected tag or wildcard protected tag.
```
DELETE /projects/:id/protected_tags/:name
```
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_tags/*-stable'
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the tag |
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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