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 ...@@ -6,6 +6,7 @@ app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/helpers/system_note_helper.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb app/workers/stuck_merge_jobs_worker.rb
......
# 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: ...@@ -70,14 +70,15 @@ linters:
enabled: false enabled: false
RuboCop: RuboCop:
enabled: false enabled: true
# These cops are incredibly noisy when it comes to HAML templates, so we # These cops are incredibly noisy when it comes to HAML templates, so we
# ignore them. # ignore them.
ignored_cops: ignored_cops:
- Lint/BlockAlignment - Layout/BlockAlignment
- Lint/EndAlignment - Layout/EndAlignment
- Lint/Void - Lint/Void
- Metrics/LineLength - Metrics/LineLength
- Naming/FileName
- Style/AlignParameters - Style/AlignParameters
- Style/BlockNesting - Style/BlockNesting
- Style/ElseAlignment - Style/ElseAlignment
...@@ -91,6 +92,52 @@ linters: ...@@ -91,6 +92,52 @@ linters:
- Style/TrailingWhitespace - Style/TrailingWhitespace
- Style/WhileUntilModifier - 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: RubyComments:
enabled: true enabled: true
......
...@@ -314,7 +314,7 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has ...@@ -314,7 +314,7 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
## Definition of done ## 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 ## Style guides
......
...@@ -126,7 +126,7 @@ GEM ...@@ -126,7 +126,7 @@ GEM
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
citrus (3.0.2) citrus (3.0.2)
coderay (1.1.1) coderay (1.1.2)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
commonmarker (0.17.8) commonmarker (0.17.8)
...@@ -495,7 +495,7 @@ GEM ...@@ -495,7 +495,7 @@ GEM
memoist (0.16.0) memoist (0.16.0)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.9.0)
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
...@@ -638,12 +638,11 @@ GEM ...@@ -638,12 +638,11 @@ GEM
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.4) prometheus-client-mmap (0.9.4)
pry (0.10.4) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.9.0)
slop (~> 3.4) pry-byebug (3.4.3)
pry-byebug (3.4.2) byebug (>= 9.0, < 9.1)
byebug (~> 9.0)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.5)
pry (>= 0.9.10) pry (>= 0.9.10)
...@@ -751,7 +750,7 @@ GEM ...@@ -751,7 +750,7 @@ GEM
retriable (3.1.2) retriable (3.1.2)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.2.0) rouge (3.2.1)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -817,7 +816,7 @@ GEM ...@@ -817,7 +816,7 @@ GEM
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.2) rugged (0.27.4)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.6) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
...@@ -878,7 +877,6 @@ GEM ...@@ -878,7 +877,6 @@ GEM
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.5.1) slack-notifier (1.5.1)
slop (3.6.0)
spring (2.0.1) spring (2.0.1)
activesupport (>= 4.2) activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
......
...@@ -146,13 +146,7 @@ export default { ...@@ -146,13 +146,7 @@ export default {
staged: false, staged: false,
prevPath: '', prevPath: '',
moved: false, moved: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, { lastCommitSha: lastCommit.commit.id,
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
}),
}); });
if (prevPath) { if (prevPath) {
......
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
export default {
components: {
CiIcon,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'latest':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
},
false,
);
} else {
environmentText = sprintf(
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
case 'failed':
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
{ environmentLink: this.environmentLink },
false,
);
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
environmentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.deploymentStatus.environment.path}">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
},
deploymentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeployment.path}">`,
name: _.escape(this.lastDeployment.name),
endLink: '</a>',
},
false,
);
},
hasLastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">
<div class="environment-information">
<ci-icon :status="deploymentStatus.icon" />
<p v-html="environment"></p>
</div>
</div>
</template>
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
import Milestone from '~/milestone'; import Milestone from '~/milestone';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow(); initMilestonesShow();
initDeleteMilestoneModal();
Milestone.initDeprecationMessage(); Milestone.initDeprecationMessage();
}); });
...@@ -40,8 +40,8 @@ ...@@ -40,8 +40,8 @@
if (this.issueCount === 0 && this.mergeRequestCount === 0) { if (this.issueCount === 0 && this.mergeRequestCount === 0) {
return sprintf( return sprintf(
s__(`Milestones| s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project. You’re about to permanently delete the milestone %{milestoneTitle}.
%{milestoneTitle} is not currently used in any issues or merge requests.`), This milestone is not currently used in any issues or merge requests.`),
{ {
milestoneTitle, milestoneTitle,
}, },
...@@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p ...@@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p
return sprintf( return sprintf(
s__(`Milestones| s__(`Milestones|
You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}.
Once deleted, it cannot be undone or recovered.`), Once deleted, it cannot be undone or recovered.`),
{ {
milestoneTitle, milestoneTitle,
......
...@@ -3,15 +3,22 @@ import createFlash from '~/flash'; ...@@ -3,15 +3,22 @@ import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete'; import GfmAutoComplete from '~/gfm_auto_complete';
import EmojiMenu from './emoji_menu'; import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu'; const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector); const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field'); const statusMessageField = document.getElementById('js-status-message-field');
const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder');
const toggleNoEmojiPlaceholder = (isVisible) => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
const removeStatusEmoji = () => { const removeStatusEmoji = () => {
const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji'); const statusEmoji = findStatusEmoji();
if (statusEmoji) { if (statusEmoji) {
statusEmoji.remove(); statusEmoji.remove();
} }
...@@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => {
const selectEmojiCallback = (emoji, emojiTag) => { const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji; statusEmojiField.value = emoji;
findNoEmojiPlaceholder().classList.add('hidden'); toggleNoEmojiPlaceholder(false);
removeStatusEmoji(); removeStatusEmoji();
toggleEmojiMenuButton.innerHTML += emojiTag; toggleEmojiMenuButton.innerHTML += emojiTag;
}; };
...@@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => {
statusEmojiField.value = ''; statusEmojiField.value = '';
statusMessageField.value = ''; statusMessageField.value = '';
removeStatusEmoji(); removeStatusEmoji();
findNoEmojiPlaceholder().classList.remove('hidden'); toggleNoEmojiPlaceholder(true);
}); });
const emojiAutocomplete = new GfmAutoComplete(); const emojiAutocomplete = new GfmAutoComplete();
...@@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => {
selectEmojiCallback, selectEmojiCallback,
); );
emojiMenu.bindEvents(); emojiMenu.bindEvents();
const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
statusMessageField.addEventListener('input', () => {
const hasStatusMessage = statusMessageField.value.trim() !== '';
const statusEmoji = findStatusEmoji();
if (hasStatusMessage && statusEmoji) {
return;
}
if (hasStatusMessage) {
toggleNoEmojiPlaceholder(false);
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
toggleNoEmojiPlaceholder(true);
removeStatusEmoji();
}
});
}) })
.catch(() => createFlash('Failed to load emoji list!')); .catch(() => createFlash('Failed to load emoji list!'));
}); });
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; 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 timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
MemoryUsage, MemoryUsage,
StatusIcon, StatusIcon,
Icon, Icon,
TooltipOnTruncate,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -88,14 +90,20 @@ export default { ...@@ -88,14 +90,20 @@ export default {
<span> <span>
Deployed to Deployed to
</span> </span>
<tooltip-on-truncate
:title="deployment.name"
truncate-target="child"
class="deploy-link label-truncate"
>
<a <a
:href="deployment.url" :href="deployment.url"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-meta" class="js-deploy-meta"
> >
{{ deployment.name }} {{ deployment.name }}
</a> </a>
</tooltip-on-truncate>
</template> </template>
<span <span
v-tooltip v-tooltip
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import _ from 'underscore';
import { n__ } from '~/locale'; import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: { components: {
Icon, Icon,
clipboardButton, clipboardButton,
TooltipOnTruncate,
}, },
props: { props: {
mr: { mr: {
...@@ -24,8 +23,12 @@ export default { ...@@ -24,8 +23,12 @@ export default {
shouldShowCommitsBehindText() { shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0; return this.mr.divergedCommitsCount > 0;
}, },
commitsText() { commitsBehindText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); 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() { branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that // This supports code in app/assets/javascripts/copy_to_clipboard.js that
...@@ -36,12 +39,6 @@ export default { ...@@ -36,12 +39,6 @@ export default {
gfm: `\`${this.mr.sourceBranch}\``, gfm: `\`${this.mr.sourceBranch}\``,
}); });
}, },
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() { webIdePath() {
return mergeUrlParams({ return mergeUrlParams({
target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ?
...@@ -49,11 +46,6 @@ export default { ...@@ -49,11 +46,6 @@ export default {
}, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`));
}, },
}, },
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
}; };
</script> </script>
<template> <template>
...@@ -65,30 +57,21 @@ export default { ...@@ -65,30 +57,21 @@ export default {
<div class="normal"> <div class="normal">
<strong> <strong>
{{ s__("mrWidget|Request to merge") }} {{ s__("mrWidget|Request to merge") }}
<span <tooltip-on-truncate
:class="{ 'label-truncated': isSourceBranchLong }" :title="mr.sourceBranch"
:title="isSourceBranchLong ? mr.sourceBranch : ''" truncate-target="child"
:v-tooltip="isSourceBranchLong" class="label-branch label-truncate js-source-branch"
class="label-branch js-source-branch"
data-placement="bottom"
v-html="mr.sourceBranchLink" v-html="mr.sourceBranchLink"
> /><clipboard-button
</span>
<clipboard-button
:text="branchNameClipboardData" :text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')" :title="__('Copy branch name to clipboard')"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
{{ s__("mrWidget|into") }} {{ s__("mrWidget|into") }}
<tooltip-on-truncate
<span :title="mr.targetBranch"
:v-tooltip="isTargetBranchLong" truncate-target="child"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }" class="label-branch label-truncate"
:title="isTargetBranchLong ? mr.targetBranch : ''"
class="label-branch"
data-placement="bottom"
> >
<a <a
:href="mr.targetBranchTreePath" :href="mr.targetBranchTreePath"
...@@ -96,15 +79,13 @@ export default { ...@@ -96,15 +79,13 @@ export default {
> >
{{ mr.targetBranch }} {{ mr.targetBranch }}
</a> </a>
</span> </tooltip-on-truncate>
</strong> </strong>
<div <div
v-if="shouldShowCommitsBehindText" v-if="shouldShowCommitsBehindText"
class="diverged-commits-count" class="diverged-commits-count"
v-html="commitsBehindText"
> >
<span class="monospace">{{ mr.sourceBranch }}</span>
is {{ commitsText }}
<span class="monospace">{{ mr.targetBranch }}</span>
</div> </div>
</div> </div>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import PipelineStage from '~/pipelines/components/stage.vue'; import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
PipelineStage, PipelineStage,
CiIcon, CiIcon,
Icon, Icon,
TooltipOnTruncate,
}, },
props: { props: {
pipeline: { pipeline: {
...@@ -30,6 +32,10 @@ export default { ...@@ -30,6 +32,10 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
sourceBranch: {
type: String,
required: false,
},
}, },
computed: { computed: {
hasPipeline() { hasPipeline() {
...@@ -107,11 +113,12 @@ export default { ...@@ -107,11 +113,12 @@ export default {
> >
{{ pipeline.commit.short_id }}</a> {{ pipeline.commit.short_id }}</a>
on on
<span <tooltip-on-truncate
class="label-branch" :title="sourceBranch"
truncate-target="child"
class="label-branch label-truncate"
v-html="sourceBranchLink" v-html="sourceBranchLink"
> />
</span>
</template> </template>
</div> </div>
<div <div
......
...@@ -254,6 +254,7 @@ export default { ...@@ -254,6 +254,7 @@ export default {
:pipeline="mr.pipeline" :pipeline="mr.pipeline"
:ci-status="mr.ciStatus" :ci-status="mr.ciStatus"
:has-ci="mr.hasCI" :has-ci="mr.hasCI"
:source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink" :source-branch-link="mr.sourceBranchLink"
/> />
<deployment <deployment
......
...@@ -71,7 +71,11 @@ export default { ...@@ -71,7 +71,11 @@ export default {
}, },
methods: { methods: {
getPercent(count) { 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) { barStyle(percent) {
return `width: ${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 @@ ...@@ -195,6 +195,7 @@
.ci-widget-content { .ci-widget-content {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1;
} }
} }
...@@ -222,6 +223,7 @@ ...@@ -222,6 +223,7 @@
.normal { .normal {
flex: 1; flex: 1;
flex-basis: auto;
} }
.capitalize { .capitalize {
...@@ -235,22 +237,23 @@ ...@@ -235,22 +237,23 @@
font-weight: normal; font-weight: normal;
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
}
&.label-truncated { .deploy-link,
position: relative; .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; display: inline-block;
width: 250px; max-width: 12.5em;
margin-bottom: -3px; margin-bottom: -3px;
white-space: nowrap; white-space: nowrap;
text-overflow: clip; text-overflow: ellipsis;
line-height: 14px; line-height: 14px;
overflow: hidden;
&::after {
position: absolute;
content: '...';
right: 0;
font-family: $regular-font;
background-color: $gray-light;
} }
} }
} }
...@@ -582,7 +585,7 @@ ...@@ -582,7 +585,7 @@
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: stretch;
.branch-actions { .branch-actions {
margin-top: 16px; margin-top: 16px;
...@@ -593,13 +596,13 @@ ...@@ -593,13 +596,13 @@
.branch-actions { .branch-actions {
align-self: center; align-self: center;
margin-left: $gl-padding; margin-left: $gl-padding;
white-space: nowrap;
} }
} }
} }
.diverged-commits-count { .diverged-commits-count {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 12px;
} }
} }
...@@ -918,7 +921,7 @@ ...@@ -918,7 +921,7 @@
flex: 1; flex: 1;
flex-direction: row; flex-direction: row;
@include media-breakpoint-down(md) { @include media-breakpoint-down(sm) {
flex-direction: column; flex-direction: column;
.stage-cell .stage-container { .stage-cell .stage-container {
......
...@@ -2,8 +2,8 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -2,8 +2,8 @@ class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions include MilestoneActions
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels] before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update] before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
def index def index
respond_to do |format| respond_to do |format|
...@@ -56,10 +56,21 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -56,10 +56,21 @@ class Groups::MilestonesController < Groups::ApplicationController
redirect_to milestone_path redirect_to milestone_path
end end
def destroy
return render_404 if @milestone.legacy_group_milestone?
Milestones::DestroyService.new(group, current_user).execute(@milestone)
respond_to do |format|
format.html { redirect_to group_milestones_path(group), status: :see_other }
format.js { head :ok }
end
end
private private
def authorize_admin_milestones! def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestones, group) return render_404 unless can?(current_user, :admin_milestone, group)
end end
def milestone_params def milestone_params
......
...@@ -5,6 +5,10 @@ module ImportHelper ...@@ -5,6 +5,10 @@ module ImportHelper
false false
end end
def sanitize_project_name(name)
name.gsub(/[^\w\-]/, '-')
end
def import_project_target(owner, name) def import_project_target(owner, name)
namespace = current_user.can_create_group? ? owner : current_user.namespace_path namespace = current_user.can_create_group? ? owner : current_user.namespace_path
"#{namespace}/#{name}" "#{namespace}/#{name}"
......
...@@ -447,7 +447,7 @@ module ProjectsHelper ...@@ -447,7 +447,7 @@ module ProjectsHelper
end end
def project_permissions_panel_data(project) def project_permissions_panel_data(project)
data = { {
currentSettings: project_permissions_settings(project), currentSettings: project_permissions_settings(project),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project), allowedVisibilityOptions: project_allowed_visibility_levels(project),
...@@ -457,8 +457,10 @@ module ProjectsHelper ...@@ -457,8 +457,10 @@ module ProjectsHelper
lfsAvailable: Gitlab.config.lfs.enabled, lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') 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 end
def project_allowed_visibility_levels(project) def project_allowed_visibility_levels(project)
......
...@@ -649,8 +649,7 @@ module Ci ...@@ -649,8 +649,7 @@ module Ci
def keep_around_commits def keep_around_commits
return unless project return unless project
project.repository.keep_around(self.sha) project.repository.keep_around(self.sha, self.before_sha)
project.repository.keep_around(self.before_sha)
end end
def valid_source def valid_source
......
...@@ -191,14 +191,18 @@ class DiffNote < Note ...@@ -191,14 +191,18 @@ class DiffNote < Note
end end
def keep_around_commits def keep_around_commits
project.repository.keep_around(self.original_position.base_sha) shas = [
project.repository.keep_around(self.original_position.start_sha) self.original_position.base_sha,
project.repository.keep_around(self.original_position.head_sha) self.original_position.start_sha,
self.original_position.head_sha
]
if self.position != self.original_position if self.position != self.original_position
project.repository.keep_around(self.position.base_sha) shas << self.position.base_sha
project.repository.keep_around(self.position.start_sha) shas << self.position.start_sha
project.repository.keep_around(self.position.head_sha) shas << self.position.head_sha
end end
project.repository.keep_around(*shas)
end end
end end
...@@ -314,9 +314,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -314,9 +314,7 @@ class MergeRequestDiff < ActiveRecord::Base
def keep_around_commits def keep_around_commits
[repository, merge_request.source_project.repository].uniq.each do |repo| [repository, merge_request.source_project.repository].uniq.each do |repo|
repo.keep_around(start_commit_sha) repo.keep_around(start_commit_sha, head_commit_sha, base_commit_sha)
repo.keep_around(head_commit_sha)
repo.keep_around(base_commit_sha)
end end
end end
end end
...@@ -85,8 +85,7 @@ class Project < ActiveRecord::Base ...@@ -85,8 +85,7 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) } after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! } before_destroy :untrack_site_statistics
after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
after_create :create_ci_cd_settings, after_create :create_ci_cd_settings,
unless: :ci_cd_settings, unless: :ci_cd_settings,
...@@ -2072,13 +2071,19 @@ class Project < ActiveRecord::Base ...@@ -2072,13 +2071,19 @@ class Project < ActiveRecord::Base
private private
def rename_or_migrate_repository! 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 ::Projects::HashedStorageMigrationService.new(self, full_path_was).execute
else else
storage.rename_repo storage.rename_repo
end end
end end
def storage_upgradable?
storage_version != LATEST_STORAGE_VERSION
end
def after_rename_repository(full_path_before, path_before) def after_rename_repository(full_path_before, path_before)
execute_rename_repository_hooks!(full_path_before) execute_rename_repository_hooks!(full_path_before)
...@@ -2093,6 +2098,11 @@ class Project < ActiveRecord::Base ...@@ -2093,6 +2098,11 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path) Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path)
end end
def untrack_site_statistics
SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
self.project_feature.untrack_statistics_for_deletion!
end
def execute_rename_repository_hooks!(full_path_before) def execute_rename_repository_hooks!(full_path_before)
# When we import a project overwriting the original project, there # When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions. # is a move operation. In that case we don't want to send the instructions.
......
...@@ -4,6 +4,8 @@ class ProtectedTag < ActiveRecord::Base ...@@ -4,6 +4,8 @@ class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef include ProtectedRef
validates :name, uniqueness: { scope: :project_id }
protected_ref_access_levels :create protected_ref_access_levels :create
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
......
...@@ -247,16 +247,23 @@ class Repository ...@@ -247,16 +247,23 @@ class Repository
# Git GC will delete commits from the repository that are no longer in any # 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 # branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds. # 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) # 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) raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
rescue Gitlab::Git::CommandError => ex rescue Gitlab::Git::CommandError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end end
end
end
def kept_around?(sha) def kept_around?(sha)
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
......
...@@ -53,7 +53,7 @@ class GroupPolicy < BasePolicy ...@@ -53,7 +53,7 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestones rule { developer }.enable :admin_milestone
rule { reporter }.policy do rule { reporter }.policy do
enable :admin_label enable :admin_label
...@@ -72,6 +72,8 @@ class GroupPolicy < BasePolicy ...@@ -72,6 +72,8 @@ class GroupPolicy < BasePolicy
enable :admin_namespace enable :admin_namespace
enable :admin_group_member enable :admin_group_member
enable :change_visibility_level enable :change_visibility_level
enable :set_note_created_at
end end
rule { can?(:read_nested_project_resources) }.policy do rule { can?(:read_nested_project_resources) }.policy do
......
...@@ -143,6 +143,10 @@ class ProjectPolicy < BasePolicy ...@@ -143,6 +143,10 @@ class ProjectPolicy < BasePolicy
enable :destroy_merge_request enable :destroy_merge_request
enable :destroy_issue enable :destroy_issue
enable :remove_pages enable :remove_pages
enable :set_issue_iid
enable :set_issue_created_at
enable :set_note_created_at
end end
rule { can?(:guest_access) }.policy do rule { can?(:guest_access) }.policy do
......
...@@ -12,12 +12,15 @@ module Groups ...@@ -12,12 +12,15 @@ module Groups
def execute def execute
group.prepare_for_destroy group.prepare_for_destroy
group.projects.each do |project| group.projects.includes(:project_feature).each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup. # Execute the destruction of the models immediately to ensure atomic cleanup.
success = ::Projects::DestroyService.new(project, current_user).execute success = ::Projects::DestroyService.new(project, current_user).execute
raise DestroyError, "Project #{project.id} can't be deleted" unless success raise DestroyError, "Project #{project.id} can't be deleted" unless success
end end
# reload the relation to prevent triggering destroy hooks on the projects again
group.projects.reload
group.children.each do |group| group.children.each do |group|
# This needs to be synchronous since the namespace gets destroyed below # This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute DestroyService.new(group, current_user).execute
......
...@@ -128,8 +128,7 @@ module MergeRequests ...@@ -128,8 +128,7 @@ module MergeRequests
# #
def assign_title_and_description def assign_title_and_description
assign_title_and_description_from_single_commit 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 ||= source_branch.titleize.humanize
merge_request.title = wip_title if compare_commits.empty? merge_request.title = wip_title if compare_commits.empty?
...@@ -159,20 +158,18 @@ module MergeRequests ...@@ -159,20 +158,18 @@ module MergeRequests
merge_request.description ||= commit.description.try(:strip) merge_request.description ||= commit.description.try(:strip)
end end
def assign_title_from_issue def title_from_issue
return unless 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}"] title_parts = ["Resolve #{issue.to_reference}"]
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
title_parts << "\"#{branch_title}\"" if branch_title.present? title_parts << "\"#{branch_title}\"" if branch_title.present?
merge_request.title = title_parts.join(' ') title_parts.join(' ')
end
end end
def issue_iid def issue_iid
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
module Milestones module Milestones
class DestroyService < Milestones::BaseService class DestroyService < Milestones::BaseService
def execute(milestone) def execute(milestone)
return unless milestone.project_milestone?
Milestone.transaction do Milestone.transaction do
update_params = { milestone: nil } update_params = { milestone: nil }
...@@ -16,15 +14,21 @@ module Milestones ...@@ -16,15 +14,21 @@ module Milestones
MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request) MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request)
end end
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_service.destroy_milestone(milestone, current_user)
Event.for_milestone_id(milestone.id).each do |event| Event.for_milestone_id(milestone.id).each do |event|
event.target_id = nil event.target_id = nil
event.save event.save
end end
milestone.destroy
end
end end
end end
end end
...@@ -39,6 +39,10 @@ ...@@ -39,6 +39,10 @@
%strong= email.email %strong= email.email
= link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
%i.fa.fa-times %i.fa.fa-times
%li
%span.light ID:
%strong
= @user.id
%li.two-factor-status %li.two-factor-status
%span.light Two-factor Authentication: %span.light Two-factor Authentication:
......
- link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank' - link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank'
.append-bottom-10 .append-bottom-10
%h4= _("Setup a #{type} Runner manually") %h4= _("Setup a %{type} Runner manually") % { type: type }
%ol %ol
%li %li
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.nav-controls .nav-controls
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestones, @group) - if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
.milestones .milestones
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend %span.input-group-prepend
.input-group-text / .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 %td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do = button_tag class: "btn btn-import js-add-to-import" do
= has_ci_cd_only_params? ? _('Connect') : _('Import') = has_ci_cd_only_params? ? _('Connect') : _('Import')
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend %span.input-group-prepend
.input-group-text / .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 %td.import-actions.job-status
= button_tag class: 'btn btn-import js-add-to-import' do = button_tag class: 'btn btn-import js-add-to-import' do
= _('Import') = _('Import')
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend %span.input-group-prepend
.input-group-text / .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 %td.import-actions.job-status
= button_tag class: 'btn btn-import js-add-to-import' do = button_tag class: 'btn btn-import js-add-to-import' do
Import Import
......
...@@ -19,9 +19,16 @@ ...@@ -19,9 +19,16 @@
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true = text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append .input-group-append
= clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block') = 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', = mail_to email, class: 'btn btn-clipboard btn-transparent',
subject: _("Enter the #{name} title"), subject: enter_title_text,
body: _("Enter the #{name} description"), body: enter_description_text,
title: _('Send email'), title: _('Send email'),
data: { toggle: 'tooltip', placement: 'bottom' } do data: { toggle: 'tooltip', placement: 'bottom' } do
= sprite_icon('mail') = sprite_icon('mail')
......
...@@ -78,7 +78,7 @@ ...@@ -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| = 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' } %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
-# haml-lint:disable InlineJavaScript -# 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 .js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
......
#modal_merge_info.modal{ tabindex: '-1' } #modal_merge_info.modal{ tabindex: '-1' }
.modal-dialog .modal-dialog.modal-lg
.modal-content .modal-content
.modal-header .modal-header
%h3.modal-title Check out, review, and merge locally %h3.modal-title Check out, review, and merge locally
......
...@@ -43,18 +43,7 @@ ...@@ -43,18 +43,7 @@
- else - else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal', = render 'shared/milestones/delete_button'
target: '#delete-milestone-modal',
milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title),
milestone_url: project_milestone_path(@project, @milestone),
milestone_issue_count: @milestone.issues.count,
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
= icon('spin spinner', class: 'js-loading-icon hidden' )
#delete-milestone-modal
%a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left') = icon('angle-double-left')
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
= _('The repository must be accessible over <code>http://</code>, = _('The repository must be accessible over <code>http://</code>,
<code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe <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= _('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= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
%li %li
= _('This user will be the author of all events in the activity feed that are the result of an update, = _('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 @@ ...@@ -16,6 +16,9 @@
= milestone_date_range(milestone) = milestone_date_range(milestone)
%div %div
= render('shared/milestone_expired', milestone: milestone) = 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? - if milestone.legacy_group_milestone?
.projects .projects
- milestone.milestones.each do |milestone| - milestone.milestones.each do |milestone|
...@@ -49,7 +52,7 @@ ...@@ -49,7 +52,7 @@
- unless milestone.active? - unless milestone.active?
= link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- if @group - if @group
- if can?(current_user, :admin_milestones, @group) - if can?(current_user, :admin_milestone, @group)
- if milestone.closed? - if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else - else
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= milestone_date_range(milestone) = milestone_date_range(milestone)
- if group - if group
.float-right .float-right
- if can?(current_user, :admin_milestones, group) - if can?(current_user, :admin_milestone, group)
- if milestone.group_milestone? - if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit Edit
...@@ -32,6 +32,9 @@ ...@@ -32,6 +32,9 @@
- else - else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- unless is_dynamic_milestone
= render 'shared/milestones/delete_button'
= render 'shared/milestones/deprecation_message' if is_dynamic_milestone = render 'shared/milestones/deprecation_message' if is_dynamic_milestone
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
......
---
title: Increase width of checkout branch modal box
merge_request:
author:
type: fixed
---
title: 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 ...@@ -37,7 +37,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member post :toggle_subscription, on: :member
end end
resources :milestones, constraints: { id: %r{[^/]+} }, only: [:index, :show, :edit, :update, :new, :create] do resources :milestones, constraints: { id: %r{[^/]+} } do
member do member do
get :merge_requests get :merge_requests
get :participants get :participants
......
...@@ -53,9 +53,11 @@ end ...@@ -53,9 +53,11 @@ end
changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } 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") if git.modified_files.include?("CHANGELOG.md")
fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" + 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 end
if changelog_needed if changelog_needed
...@@ -63,6 +65,6 @@ if changelog_needed ...@@ -63,6 +65,6 @@ if changelog_needed
check_changelog(changelog_found) check_changelog(changelog_found)
else else
warn "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).**\n\n" + 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
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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180816193530) do ActiveRecord::Schema.define(version: 20180826111825) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1741,6 +1741,7 @@ ActiveRecord::Schema.define(version: 20180816193530) do ...@@ -1741,6 +1741,7 @@ ActiveRecord::Schema.define(version: 20180816193530) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end 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 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| create_table "push_event_payloads", id: false, force: :cascade do |t|
......
...@@ -53,6 +53,7 @@ following locations: ...@@ -53,6 +53,7 @@ following locations:
- [Project Members](members.md) - [Project Members](members.md)
- [Project Snippets](project_snippets.md) - [Project Snippets](project_snippets.md)
- [Protected Branches](protected_branches.md) - [Protected Branches](protected_branches.md)
- [Protected Tags](protected_tags.md)
- [Repositories](repositories.md) - [Repositories](repositories.md)
- [Repository Files](repository_files.md) - [Repository Files](repository_files.md)
- [Runners](runners.md) - [Runners](runners.md)
......
...@@ -136,7 +136,7 @@ Parameters: ...@@ -136,7 +136,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `issue_iid` | integer | yes | The IID of an issue | | `issue_iid` | integer | yes | The IID of an issue |
| `body` | string | yes | The content of a discussion | | `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/discussions?body=comment 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: ...@@ -159,7 +159,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion | | `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note | | `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion | | `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment 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: ...@@ -342,7 +342,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `snippet_id` | integer | yes | The ID of an snippet | | `snippet_id` | integer | yes | The ID of an snippet |
| `body` | string | yes | The content of a discussion | | `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions?body=comment 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: ...@@ -365,7 +365,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion | | `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note | | `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion | | `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment 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: ...@@ -601,7 +601,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `merge_request_iid` | integer | yes | The IID of a merge request | | `merge_request_iid` | integer | yes | The IID of a merge request |
| `body` | string | yes | The content of a discussion | | `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` | hash | no | Position when creating a diff note |
| `position[base_sha]` | string | yes | Base commit SHA in the source branch | | `position[base_sha]` | string | yes | Base commit SHA in the source branch |
| `position[start_sha]` | string | yes | SHA referencing commit in target branch | | `position[start_sha]` | string | yes | SHA referencing commit in target branch |
...@@ -659,7 +659,7 @@ Parameters: ...@@ -659,7 +659,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion | | `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note | | `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion | | `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment 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: ...@@ -894,7 +894,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `commit_id` | integer | yes | The ID of a commit | | `commit_id` | integer | yes | The ID of a commit |
| `body` | string | yes | The content of a discussion | | `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` | hash | no | Position when creating a diff note |
| `position[base_sha]` | string | yes | Base commit SHA in the source branch | | `position[base_sha]` | string | yes | Base commit SHA in the source branch |
| `position[start_sha]` | string | yes | SHA referencing commit in target branch | | `position[start_sha]` | string | yes | SHA referencing commit in target branch |
...@@ -930,7 +930,7 @@ Parameters: ...@@ -930,7 +930,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion | | `discussion_id` | integer | yes | The ID of a discussion |
| `note_id` | integer | yes | The ID of a discussion note | | `note_id` | integer | yes | The ID of a discussion note |
| `body` | string | yes | The content of a discussion | | `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment 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: ...@@ -96,6 +96,19 @@ Parameters:
- `start_date` (optional) - The start date of the milestone - `start_date` (optional) - The start date of the milestone
- `state_event` (optional) - The state event of the milestone (close|activate) - `state_event` (optional) - The state event of the milestone (close|activate)
## Delete group milestone
Only for user with developer access to the group.
```
DELETE /groups/:id/milestones/:milestone_id
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of the group's milestone
## Get all issues assigned to a single milestone ## Get all issues assigned to a single milestone
Gets all issues assigned to a single group milestone. Gets all issues assigned to a single group milestone.
......
...@@ -470,7 +470,7 @@ POST /projects/:id/issues ...@@ -470,7 +470,7 @@ POST /projects/:id/issues
| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue | | `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 | | `milestone_id` | integer | no | The global ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue | | `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project/group owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.| | `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. | | `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
......
...@@ -100,7 +100,7 @@ Parameters: ...@@ -100,7 +100,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `issue_id` (required) - The IID of an issue - `issue_id` (required) - The IID of an issue
- `body` (required) - The content of a note - `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 ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note 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