Commit 6a88ffb4 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into matbaj/gitlab-ce-add-inherit-command

parents db377437 a544f6ec
...@@ -75,7 +75,7 @@ stages: ...@@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql .use-mysql: &use-mysql
services: services:
- mysql:latest - mysql:5.7
- redis:alpine - redis:alpine
.rails5-variables: &rails5-variables .rails5-variables: &rails5-variables
......
...@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective: ...@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral: Lint/NestedPercentLiteral:
Exclude: Exclude:
- 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb' - 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
...@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase: ...@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb' - 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb' - 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb' - 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb' - 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/seed_repo.rb' - 'spec/support/helpers/seed_repo.rb'
# Offense count: 112 # Offense count: 112
# Configuration parameters: Blacklist. # Configuration parameters: Blacklist.
...@@ -496,7 +496,7 @@ Style/EmptyLiteral: ...@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb' - 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb' - 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb' - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102 # Offense count: 102
# Cop supports --auto-correct. # Cop supports --auto-correct.
......
This diff is collapsed.
...@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release) - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
...@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following: ...@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release" - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the All labels, their meaning and priority are defined on the
[labels page][labels-page]. [labels page][labels-page].
...@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga ...@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| Label | Meaning | Estimate time to fix | Guidance | | Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------| |-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release | | | ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | | | ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
......
...@@ -178,7 +178,7 @@ GEM ...@@ -178,7 +178,7 @@ GEM
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.1) doorkeeper (4.3.2)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.3.0) doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3) doorkeeper (~> 4.3)
......
10.7.0-pre 10.8.0-pre
...@@ -113,6 +113,8 @@ class List { ...@@ -113,6 +113,8 @@ class List {
issue.id = data.id; issue.id = data.id;
issue.iid = data.iid; issue.iid = data.iid;
issue.project = data.project; issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id; const moveBeforeId = this.issues[1].id;
......
...@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown { ...@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) { if (data.can_create_branch) {
this.available(); this.available();
this.enable(); this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) { if (!this.droplabInitialized) {
this.droplabInitialized = true; this.droplabInitialized = true;
this.initDroplab(); this.initDroplab();
this.bindEvents(); this.bindEvents();
} }
} else if (data.has_related_branch) { } else {
this.hide(); this.hide();
} }
}) })
.catch(() => { .catch(() => {
this.unavailable(); this.unavailable();
this.disable(); this.disable();
Flash('Failed to check if a new branch can be created.'); Flash(__('Failed to check related branches.'));
}); });
} }
...@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown { ...@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide'); this.unavailableButton.classList.remove('hide');
} }
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) { updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for. // target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed. // ref - string - what a user typed.
// result - string - what has been found on backend. // result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed, // If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists. // that means a new branch cannot be created as it already exists.
if (ref === result) { if (ref === result) {
...@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown { ...@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true; this.refIsValid = true;
this.refInput.dataset.value = ref; this.refInput.dataset.value = ref;
this.showAvailableMessage('ref'); this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath, this.updateCreatePaths(target, ref);
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
} }
} else if (target === 'branch') { } else if (target === 'branch') {
this.branchIsValid = true; this.branchIsValid = true;
this.showAvailableMessage('branch'); this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath, this.updateCreatePaths(target, ref);
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
} else { } else {
this.refIsValid = false; this.refIsValid = false;
this.refInput.dataset.value = ref; this.refInput.dataset.value = ref;
...@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown { ...@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction(); this.disableCreateAction();
} }
} }
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
} }
...@@ -1427,7 +1427,7 @@ export default class Notes { ...@@ -1427,7 +1427,7 @@ export default class Notes {
const { discussion_html } = data; const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder'); const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in'); lines.addClass('fade-in');
$container.find('tbody').prepend(lines); $container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder'); const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove(); $container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder); syntaxHighlight(fileHolder);
......
...@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) { ...@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date(); const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset(); const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60; const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes); date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date; return date;
} }
...@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) { ...@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`; return `${contribText}<br />${dateDayName} ${dateText}`;
} }
const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); const initColorKey = () =>
d3
.scaleLinear()
.range(['#acd5f2', '#254e77'])
.domain([0, 3]);
export default class ActivityCalendar { export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) { constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath; this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this); this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = ''; this.currentSelectedDate = '';
this.daySpace = 1; this.daySpace = 1;
this.daySize = 15; this.daySize = 15;
this.daySizeWithSpace = this.daySize + (this.daySpace * 2); this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
this.months = []; this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects // Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are // The group of objects will be grouped based on the day of the week they are
...@@ -70,7 +88,7 @@ export default class ActivityCalendar { ...@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week // Create a new group array if this is the first day of the week
// or if is first object // or if is first object
if ((day === 0 && i !== 0) || i === 0) { if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]); this.timestampsTmp.push([]);
group += 1; group += 1;
} }
...@@ -109,21 +127,30 @@ export default class ActivityCalendar { ...@@ -109,21 +127,30 @@ export default class ActivityCalendar {
} }
renderSvg(container, group) { renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group); const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3.select(container) return d3
.select(container)
.append('svg') .append('svg')
.attr('width', width) .attr('width', width)
.attr('height', 167) .attr('height', 167)
.attr('class', 'contrib-calendar'); .attr('class', 'contrib-calendar');
} }
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() { renderDays() {
this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g') this.svg
.selectAll('g')
.data(this.timestampsTmp)
.enter()
.append('g')
.attr('transform', (group, i) => { .attr('transform', (group, i) => {
_.each(group, (stamp, a) => { _.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) { if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth(); const month = stamp.date.getMonth();
const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace; const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months); const lastMonth = _.last(this.months);
if ( if (
lastMonth == null || lastMonth == null ||
...@@ -133,19 +160,20 @@ export default class ActivityCalendar { ...@@ -133,19 +160,20 @@ export default class ActivityCalendar {
} }
} }
}); });
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`; return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
}) })
.selectAll('rect') .selectAll('rect')
.data(stamp => stamp) .data(stamp => stamp)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', '0') .attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day) .attr('y', stamp => this.dayYPos(stamp.day))
.attr('width', this.daySize) .attr('width', this.daySize)
.attr('height', this.daySize) .attr('height', this.daySize)
.attr('fill', stamp => ( .attr(
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed' 'fill',
)) stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
)
.attr('title', stamp => formatTooltipText(stamp)) .attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip') .attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body') .attr('data-container', 'body')
...@@ -156,16 +184,19 @@ export default class ActivityCalendar { ...@@ -156,16 +184,19 @@ export default class ActivityCalendar {
const days = [ const days = [
{ {
text: 'M', text: 'M',
y: 29 + (this.daySizeWithSpace * 1), y: 29 + this.dayYPos(1),
}, { },
{
text: 'W', text: 'W',
y: 29 + (this.daySizeWithSpace * 3), y: 29 + this.dayYPos(2),
}, { },
{
text: 'F', text: 'F',
y: 29 + (this.daySizeWithSpace * 5), y: 29 + this.dayYPos(3),
}, },
]; ];
this.svg.append('g') this.svg
.append('g')
.selectAll('text') .selectAll('text')
.data(days) .data(days)
.enter() .enter()
...@@ -178,7 +209,8 @@ export default class ActivityCalendar { ...@@ -178,7 +209,8 @@ export default class ActivityCalendar {
} }
renderMonths() { renderMonths() {
this.svg.append('g') this.svg
.append('g')
.attr('direction', 'ltr') .attr('direction', 'ltr')
.selectAll('text') .selectAll('text')
.data(this.months) .data(this.months)
...@@ -191,11 +223,24 @@ export default class ActivityCalendar { ...@@ -191,11 +223,24 @@ export default class ActivityCalendar {
} }
renderKey() { renderKey() {
const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions']; const keyValues = [
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; 'no contributions',
'1-9 contributions',
'10-19 contributions',
'20-29 contributions',
'30+ contributions',
];
const keyColors = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
this.svg.append('g') this.svg
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`) .append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect') .selectAll('rect')
.data(keyColors) .data(keyColors)
.enter() .enter()
...@@ -211,8 +256,17 @@ export default class ActivityCalendar { ...@@ -211,8 +256,17 @@ export default class ActivityCalendar {
} }
initColor() { initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; const colorRange = [
return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange); '#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
return d3
.scaleThreshold()
.domain([0, 10, 20, 30])
.range(colorRange);
} }
clickDay(stamp) { clickDay(stamp) {
...@@ -227,7 +281,8 @@ export default class ActivityCalendar { ...@@ -227,7 +281,8 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML); $('.user-calendar-activities').html(LOADING_HTML);
axios.get(this.calendarActivitiesPath, { axios
.get(this.calendarActivitiesPath, {
params: { params: {
date, date,
}, },
......
...@@ -32,26 +32,38 @@ export default { ...@@ -32,26 +32,38 @@ export default {
required: true, required: true,
}, },
buttonDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
isDisabled() { },
return this.buttonDisabled === this.link; watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
}, },
}, },
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
}, },
}, },
}; };
...@@ -62,7 +74,8 @@ export default { ...@@ -62,7 +74,8 @@ export default {
@click="onClickAction" @click="onClickAction"
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled" :disabled="isDisabled"
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the dropdown for the pipeline graph. * Renders the dropdown for the pipeline graph.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
...@@ -27,14 +27,14 @@ ...@@ -27,14 +27,14 @@
* } * }
* } * }
*/ */
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
jobComponent, JobComponent,
jobNameComponent, JobNameComponent,
}, },
props: { props: {
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
type: Object, type: Object,
required: true, required: true,
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -56,22 +61,23 @@ ...@@ -56,22 +61,23 @@
methods: { methods: {
/** /**
* When the user right clicks or cmd/ctrl + click in the job name * When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed and the link should open in another tab, * the dropdown should not be closed so we stop propagation
* so we stop propagation of the click event inside the dropdown. * of the click event inside the dropdown.
* *
* Since this component is rendered multiple times per page we need to guarantee we only * Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el $(
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
.on('click', (e) => { this.$el,
).on('click', e => {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container"> <div class="ci-job-dropdown-container">
...@@ -101,8 +107,8 @@ ...@@ -101,8 +107,8 @@
:key="i"> :key="i">
<job-component <job-component
:job="item" :job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
</ul> </ul>
......
...@@ -7,7 +7,6 @@ export default { ...@@ -7,7 +7,6 @@ export default {
StageColumnComponent, StageColumnComponent,
LoadingIcon, LoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -17,10 +16,10 @@ export default { ...@@ -17,10 +16,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
actionDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -75,7 +74,7 @@ export default { ...@@ -75,7 +74,7 @@ export default {
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled" :request-finished-for="requestFinishedFor"
/> />
</ul> </ul>
</div> </div>
......
<script> <script>
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
ActionComponent, ActionComponent,
DropdownActionComponent,
JobNameComponent, JobNameComponent,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -44,26 +41,17 @@ export default { ...@@ -44,26 +41,17 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
cssClassJobName: { cssClassJobName: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
computed: { computed: {
status() { status() {
return this.job && this.job.status ? this.job.status : {}; return this.job && this.job.status ? this.job.status : {};
...@@ -134,19 +122,11 @@ export default { ...@@ -134,19 +122,11 @@ export default {
</div> </div>
<action-component <action-component
v-if="hasAction && !isDropdown" v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :request-finished-for="requestFinishedFor"
/> />
</div> </div>
</template> </template>
...@@ -29,10 +29,11 @@ export default { ...@@ -29,10 +29,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
actionDisabled: {
requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -74,12 +75,12 @@ export default { ...@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
......
...@@ -25,7 +25,7 @@ export default () => { ...@@ -25,7 +25,7 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null, requestFinishedFor: null,
}; };
}, },
created() { created() {
...@@ -36,15 +36,17 @@ export default () => { ...@@ -36,15 +36,17 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.actionDisabled = action; // Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action) this.mediator.service
.postAction(action)
.then(() => { .then(() => {
this.mediator.refreshPipeline(); this.mediator.refreshPipeline();
this.actionDisabled = null; this.requestFinishedFor = action;
}) })
.catch(() => { .catch(() => {
this.actionDisabled = null; this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.')); Flash(__('An error occurred while making the request.'));
}); });
}, },
...@@ -54,7 +56,7 @@ export default () => { ...@@ -54,7 +56,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled, requestFinishedFor: this.requestFinishedFor,
}, },
}); });
}, },
...@@ -79,7 +81,8 @@ export default () => { ...@@ -79,7 +81,8 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.mediator.service.postAction(action.path) this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
......
<script> <script>
import ciIcon from './ci_icon.vue'; import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
/** /**
* Renders CI Badge link with CI icon and status text based on * Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used. * API response shared between all places where it is used.
* *
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
* - MR widget * - MR widget
*/ */
export default { export default {
components: { components: {
ciIcon, CiIcon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
return className ? `ci-status ci-${className}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <a
......
<script> <script>
import icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
/** /**
* Renders CI icon based on API response shared between all places where it is used. * Renders CI icon based on API response shared between all places where it is used.
* *
* Receives status object containing: * Receives status object containing:
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
* - Jobs show view header * - Jobs show view header
* - Jobs show view sidebar * - Jobs show view sidebar
*/ */
export default { export default {
components: { components: {
icon, Icon,
}, },
props: { props: {
status: { status: {
...@@ -32,14 +32,13 @@ ...@@ -32,14 +32,13 @@
required: true, required: true,
}, },
}, },
computed: { computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span :class="cssClass"> <span :class="cssClass">
......
<script> <script>
/** /**
* Falls back to the code used in `copy_to_clipboard.js` * Falls back to the code used in `copy_to_clipboard.js`
*
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
* when clicked.
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
*/ */
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
export default { export default {
name: 'ClipboardButton', name: 'ClipboardButton',
directives: { directives: {
tooltip, tooltip,
...@@ -34,7 +44,7 @@ ...@@ -34,7 +44,7 @@
default: 'btn-default', default: 'btn-default',
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import commitIconSvg from 'icons/_icon_commit.svg'; import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip';
import tooltip from '../directives/tooltip'; import Icon from '../../vue_shared/components/icon.vue';
import icon from '../../vue_shared/components/icon.vue';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
userAvatarLink, UserAvatarLink,
icon, Icon,
}, },
props: { props: {
/** /**
...@@ -94,10 +93,7 @@ ...@@ -94,10 +93,7 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
hasAuthor() { hasAuthor() {
return this.author && return this.author && this.author.avatar_url && this.author.path && this.author.username;
this.author.avatar_url &&
this.author.path &&
this.author.username;
}, },
/** /**
* If information about the author is provided will return a string * If information about the author is provided will return a string
...@@ -106,14 +102,10 @@ ...@@ -106,14 +102,10 @@
* @returns {String} * @returns {String}
*/ */
userImageAltDescription() { userImageAltDescription() {
return this.author && return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
this.author.username ? `${this.author.username}'s avatar` : null;
}, },
}, },
created() { };
this.commitIconSvg = commitIconSvg;
},
};
</script> </script>
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
...@@ -141,11 +133,10 @@ ...@@ -141,11 +133,10 @@
{{ commitRef.name }} {{ commitRef.name }}
</a> </a>
</template> </template>
<div <icon
v-html="commitIconSvg" name="commit"
class="commit-icon js-commit-icon" class="commit-icon js-commit-icon"
> />
</div>
<a <a
class="commit-sha" class="commit-sha"
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
/** /**
* Port of detail_behavior expand button. * Port of detail_behavior expand button.
* *
* @example * @example
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
* </template> * </template>
* </expand-button> * </expand-button>
*/ */
export default { export default {
name: 'ExpandButton', name: 'ExpandButton',
data() { data() {
return { return {
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span> <span>
......
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import CiIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue'; import LoadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
components: { components: {
ciIconBadge, CiIconBadge,
loadingIcon, LoadingIcon,
timeagoTooltip, TimeagoTooltip,
userAvatarImage, UserAvatarImage,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
/* This is a re-usable vue component for rendering a svg sprite
/* This is a re-usable vue component for rendering a svg sprite
icon icon
Sample configuration: Sample configuration:
...@@ -11,11 +10,11 @@ ...@@ -11,11 +10,11 @@
css-classes="top" css-classes="top"
/> />
*/ */
// only allow classes in images.scss e.g. s12 // only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default { export default {
props: { props: {
name: { name: {
type: String, type: String,
...@@ -70,7 +69,7 @@ ...@@ -70,7 +69,7 @@
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -79,7 +78,8 @@ ...@@ -79,7 +78,8 @@
:width="width" :width="width"
:height="height" :height="height"
:x="x" :x="x"
:y="y"> :y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" /> <use v-bind="{ 'xlink:href':spriteHref }" />
</svg> </svg>
</template> </template>
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue'; import LoadingIcon from '../../loading_icon.vue';
...@@ -98,11 +99,18 @@ export default { ...@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick, handleClick: this.handleClick,
}); });
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
}, },
methods: { methods: {
handleClick(label) { handleClick(label) {
this.$emit('onLabelClick', label); this.$emit('onLabelClick', label);
}, },
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
}, },
}; };
</script> </script>
...@@ -112,6 +120,7 @@ export default { ...@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="showCreate" v-if="showCreate"
:labels="context.labels" :labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/> />
<dropdown-title <dropdown-title
:can-edit="canEdit" :can-edit="canEdit"
...@@ -133,7 +142,10 @@ export default { ...@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName" :name="hiddenInputName"
:label="label" :label="label"
/> />
<div class="dropdown"> <div
class="dropdown"
ref="dropdown"
>
<dropdown-button <dropdown-button
:ability-name="abilityName" :ability-name="abilityName"
:field-name="hiddenInputName" :field-name="hiddenInputName"
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
return labelsString; return labelsString;
}, },
}, },
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
}; };
</script> </script>
...@@ -36,6 +41,7 @@ export default { ...@@ -36,6 +41,7 @@ export default {
data-placement="left" data-placement="left"
data-container="body" data-container="body"
:title="labelsList" :title="labelsList"
@click="handleClick"
> >
<i <i
aria-hidden="true" aria-hidden="true"
......
...@@ -468,6 +468,14 @@ ...@@ -468,6 +468,14 @@
margin-bottom: 10px; margin-bottom: 10px;
white-space: normal; white-space: normal;
.ci-job-dropdown-container {
// override dropdown.scss
.dropdown-menu li button {
padding: 0;
text-align: center;
}
}
// ensure .build-content has hover style when action-icon is hovered // ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content { .ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover; @extend .build-content:hover;
......
...@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch def can_create_branch
can_create = current_user && can_create = current_user &&
can?(current_user, :push_code, @project) && can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?(current_user) @issue.can_be_worked_on?
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? } render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end end
end end
end end
...@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def authorize_create_merge_request! def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end end
def render_issue_json def render_issue_json
......
...@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h) result.merge!(trace.to_h)
end end
result[:html] = result[:html].presence || 'No job log'
render json: result render json: result
end end
end end
......
...@@ -32,6 +32,7 @@ class UsersFinder ...@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users) users = by_active(users)
users = by_external_identity(users) users = by_external_identity(users)
users = by_external(users) users = by_external(users)
users = by_2fa(users)
users = by_created_at(users) users = by_created_at(users)
users = by_custom_attributes(users) users = by_custom_attributes(users)
...@@ -76,4 +77,15 @@ class UsersFinder ...@@ -76,4 +77,15 @@ class UsersFinder
users.external users.external
end end
def by_2fa(users)
case params[:two_factor]
when 'enabled'
users.with_two_factor
when 'disabled'
users.without_two_factor
else
users
end
end
end end
...@@ -81,6 +81,14 @@ module GitlabRoutingHelper ...@@ -81,6 +81,14 @@ module GitlabRoutingHelper
end end
end end
def edit_milestone_path(entity, *args)
if entity.parent.is_a?(Group)
edit_group_milestone_path(entity.parent, entity, *args)
else
edit_project_milestone_path(entity.parent, entity, *args)
end
end
def toggle_subscription_path(entity, *args) def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue) if entity.is_a?(Issue)
toggle_subscription_project_issue_path(entity.project, entity) toggle_subscription_project_issue_path(entity.project, entity)
......
...@@ -27,6 +27,7 @@ module Ci ...@@ -27,6 +27,7 @@ module Ci
has_one :metadata, class_name: 'Ci::BuildMetadata' has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :gitlab_deploy_token, to: :project
## ##
# The "environment" field for builds is a String, and is the unexpanded name! # The "environment" field for builds is a String, and is the unexpanded name!
...@@ -604,6 +605,7 @@ module Ci ...@@ -604,6 +605,7 @@ module Ci
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER) .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false) .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false) .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
.concat(deploy_token_variables)
end end
end end
...@@ -654,6 +656,15 @@ module Ci ...@@ -654,6 +656,15 @@ module Ci
end end
end end
def deploy_token_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless gitlab_deploy_token
variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name)
variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
end
end
def environment_url def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url options&.dig(:environment, :url) || persisted_environment&.external_url
end end
......
...@@ -27,8 +27,9 @@ module AtomicInternalId ...@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do before_validation(on: :create) do
if read_attribute(column).blank? scope_value = association(scope).reader
scope_attrs = { scope => association(scope).reader } if read_attribute(column).blank? && scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init) new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
......
...@@ -31,12 +31,13 @@ module Avatarable ...@@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present? use_asset_host = asset_host.present?
use_authentication = respond_to?(:public?) && !public?
# Avatars for private and internal groups and projects require authentication to be viewed, # Avatars for private and internal groups and projects require authentication to be viewed,
# which means they can only be served by Rails, on the regular GitLab host. # which means they can only be served by Rails, on the regular GitLab host.
# If an asset host is configured, we need to return the fully qualified URL # If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host. # instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
if use_asset_host && respond_to?(:public?) && !public? if use_asset_host && use_authentication
use_asset_host = false use_asset_host = false
only_path = false only_path = false
end end
...@@ -49,6 +50,6 @@ module Avatarable ...@@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root url_base << gitlab_config.relative_url_root
end end
url_base + avatar.url url_base + avatar.local_url
end end
end end
module NonatomicInternalId
extend ActiveSupport::Concern
included do
validate :set_iid, on: :create
validates :iid, presence: true, numericality: true
end
def set_iid
if iid.blank?
parent = project || group
records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
max_iid = records.maximum(:iid)
self.iid = max_iid.to_i + 1
end
end
def to_param
iid.to_s
end
end
...@@ -102,7 +102,7 @@ module Routable ...@@ -102,7 +102,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances, # the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases. # we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active? return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path RequestStore[full_path_key] ||= uncached_full_path
end end
...@@ -124,6 +124,11 @@ module Routable ...@@ -124,6 +124,11 @@ module Routable
end end
end end
# Group would override this to check from association
def owned_by?(user)
owner == user
end
private private
def set_path_errors def set_path_errors
......
# Uniquify
#
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# You can pass an initial value for the counter, if not given
# counting starts from 1.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
class Uniquify class Uniquify
# Return a version of the given 'base' string that is unique def initialize(counter = nil)
# by appending a counter to it. Uniqueness is determined by @counter = counter
# repeated calls to the passed block. end
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def string(base) def string(base)
@base = base @base = base
@counter = nil
increment_counter! while yield(base_string) increment_counter! while yield(base_string)
base_string base_string
......
...@@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
add_authentication_token_field :token add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
default_value_for(:expires_at) { Forever.date } default_value_for(:expires_at) { Forever.date }
...@@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base ...@@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
scope :active, -> { where("revoked = false AND expires_at >= NOW()") } scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
def self.gitlab_deploy_token
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
end
def revoke! def revoke!
update!(revoked: true) update!(revoked: true)
end end
......
class Deployment < ActiveRecord::Base class Deployment < ActiveRecord::Base
include NonatomicInternalId include AtomicInternalId
belongs_to :project, required: true belongs_to :project, required: true
belongs_to :environment, required: true belongs_to :environment, required: true
belongs_to :user belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
validates :sha, presence: true validates :sha, presence: true
validates :ref, presence: true validates :ref, presence: true
......
...@@ -125,6 +125,10 @@ class Group < Namespace ...@@ -125,6 +125,10 @@ class Group < Namespace
self[:lfs_enabled] self[:lfs_enabled]
end end
def owned_by?(user)
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users( GroupMember.add_users(
self, self,
......
...@@ -12,8 +12,9 @@ ...@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope. # * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base class InternalId < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :namespace
enum usage: { issues: 0 } enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
validates :usage, presence: true validates :usage, presence: true
......
...@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base ...@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request branches_with_iid - branches_with_merge_request
end end
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
# Returns boolean if a related branch exists for the current issue # Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs # ignores merge requests branchs
def has_related_branch? def has_related_branch?
...@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base ...@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end end
end end
def can_be_worked_on?(current_user) def can_be_worked_on?
!self.closed? && !self.closed? && !self.project.forked?
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
end end
# Returns `true` if the current issue can be viewed by either a logged in User # Returns `true` if the current issue can be viewed by either a logged in User
......
class MergeRequest < ActiveRecord::Base class MergeRequest < ActiveRecord::Base
include NonatomicInternalId include AtomicInternalId
include Issuable include Issuable
include Noteable include Noteable
include Referable include Referable
...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
......
...@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base ...@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3) Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField include CacheMarkdownField
include NonatomicInternalId include AtomicInternalId
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
...@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base ...@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :group belongs_to :group
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests has_many :merge_requests
......
...@@ -1879,6 +1879,10 @@ class Project < ActiveRecord::Base ...@@ -1879,6 +1879,10 @@ class Project < ActiveRecord::Base
[] []
end end
def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
private private
def storage def storage
......
require "flowdock-git-hook" require "flowdock-git-hook"
# Flow dock depends on Grit to compute the number of commits between two given
# commits. To make this depend on Gitaly, a monkey patch is applied
module Flowdock
class Git
# pass down a Repository all the way down
def repo
@options[:repo]
end
def config
{}
end
def messages
Git::Builder.new(repo: repo,
ref: @ref,
before: @from,
after: @to,
commit_url: @commit_url,
branch_url: @branch_url,
diff_url: @diff_url,
repo_url: @repo_url,
repo_name: @repo_name,
permanent_refs: @permanent_refs,
tags: tags
).to_hashes
end
class Builder
def commits
@repo.commits_between(@before, @after).map do |commit|
{
url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
id: commit.sha,
message: commit.message,
author: {
name: commit.author_name,
email: commit.author_email
}
}
end
end
end
end
end
class FlowdockService < Service class FlowdockService < Service
prop_accessor :token prop_accessor :token
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
...@@ -34,7 +80,7 @@ class FlowdockService < Service ...@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before], data[:before],
data[:after], data[:after],
token: token, token: token,
repo: project.repository.path_to_repo, repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s", commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
......
...@@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy ...@@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) } condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
end end
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
...@@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy ...@@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
end end
rule { admin } .enable :read_group rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
rule { has_projects }.policy do
enable :read_group
enable :read_label
end
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace
......
...@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?, OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled? elsif auto_devops_enabled?
OpenStruct.new(enabled: true, OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'), label: _('Auto DevOps enabled'),
......
...@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model !!model
end end
def local_url
File.join('/', self.class.base_dir, dynamic_segment, filename)
end
private private
# Designed to be overridden by child uploaders that have a dynamic path # Designed to be overridden by child uploaders that have a dynamic path
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.help-block .help-block
Manage repository storage paths. Learn more in the Manage repository storage paths. Learn more in the
= succeed "." do = succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages") = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section .sub-section
%h4 Circuit breaker %h4 Circuit breaker
.form-group .form-group
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%hr %hr
%p %p
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to project_ci_lint_path(@project), class: 'btn btn-default' do
%span CI lint %span CI lint
.content-list.builds-content-list .content-list.builds-content-list
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
%ul %ul
- pipeline.yaml_errors.split(",").each do |error| - pipeline.yaml_errors.split(",").each do |error|
%li= error %li= error
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning .bs-callout.bs-callout-warning
......
.row.prepend-top-default
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong= s_('CICD|Enable Auto DevOps')
%br
= s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong= s_('CICD|Disable Auto DevOps')
%br
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
%br
= s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
= form.label :domain, class:"prepend-top-10" do
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
...@@ -3,44 +3,6 @@ ...@@ -3,44 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project) = form_errors(@project)
%fieldset.builds-feature %fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
.form-group.append-bottom-default.js-secret-runner-token .form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light' = f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder .form-control.js-secret-value-placeholder
......
...@@ -12,10 +12,22 @@ ...@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout or Auto DevOps. Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content .settings-content
= render 'form' = render 'form'
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps (Beta)')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
= render 'autodevops_form'
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.banner-buttons .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' } 'aria-label' => 'Dismiss Auto DevOps box' }
......
---
title: Enable restore rake task to handle nested storage directories
merge_request: 17516
author: Balasankar C
type: fixed
---
title: Add support for patch link extension for commit links on GitLab Flavored Markdown
merge_request:
author:
type: added
---
title: Include matching branches and tags in protected branches / tags count
merge_request:
author: Jan Beckmann
type: fixed
---
title: Send notification emails when push to a merge request
merge_request: 7610
author: YarNayar
type: feature
---
title: Atomic generation of internal ids for issues.
merge_request: 17580
author:
type: other
---
title: Create Deploy Tokens to allow permanent access to repository and registry
merge_request: 17894
author:
type: added
--- ---
title: Fix bug rendering group icons when forking title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request: merge_request:
author: author:
type: fixed type: fixed
---
title: Drop JSON response in Project Milestone along with avoiding error
merge_request: 17977
author: Takuya Noguchi
type: fixed
---
title: Fix generated URL when listing repoitories for import
merge_request: 17692
author:
type: fixed
---
title: lazy load diffs on merge request discussions
merge_request:
author:
type: performance
---
title: Fixed bug in dropdown selector when selecting the same selection again
merge_request: 14631
author: bitsapien
type: fixed
---
title: Apply NestingDepth (level 5) (framework/dropdowns.scss)
merge_request: 17820
author: Takuya Noguchi
type: other
---
title: 'API: Add parameter merge_method to projects'
merge_request: 18031
author: Jan Beckmann
type: added
---
title: Add object storage support for LFS objects, CI artifacts, and uploads.
merge_request: 17358
author:
type: added
---
title: Increase dropdown width in pipeline graph & center action icon
merge_request: 18089
author:
type: fixed
---
title: 'Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436'
merge_request: 18036
author:
type: added
---
title: Added confirmation modal for changing username
merge_request: 17405
author:
type: added
---
title: Adds the option to the project export API to override the project description and display GitLab export description once imported
merge_request: 17744
author:
type: added
---
title: adds closed by informations in issue api
merge_request: 17042
author: haseebeqx
type: added
---
title: Fix XSS on diff view stored on filenames
merge_request:
author:
type: security
---
title: Long instance urls do not overflow anymore during project creation
merge_request: 17717
author:
type: fixed
---
title: Improved visual styles and consistency for commit hash and possible actions
across commit lists
merge_request: 17406
author:
type: changed
---
title: Improve empty state for canceled job
merge_request: 17646
author:
type: fixed
---
title: Fix hover style of dropdown items in the right sidebar
merge_request: 17519
author:
type: fixed
--- ---
title: Adds cancel btn to new pages domain page title: Show new branch/mr button even when branch exists
merge_request: 18026 merge_request: 17712
author: Jacopo Beschi @jacopo-beschi author: Jacopo Beschi @jacopo-beschi
type: added type: added
---
title: Fix Firefox stealing formatting characters on issue notes
merge_request:
author:
type: fixed
---
title: Fix discussions API setting created_at for notable in a group or notable in
a project in a group with owners
merge_request: 18464
author:
type: fixed
---
title: Improve performance of loading issues with lots of references to merge requests
merge_request: 17986
author:
type: performance
---
title: Polish design for verifying domains
merge_request: 17767
author:
type: changed
---
title: Require at least one filter when listing issues or merge requests on dashboard
page
merge_request:
author:
type: performance
---
title: Use specific names for filtered CI variable controller parameters
merge_request: 17796
author:
type: other
---
title: Create settings section for autodevops
merge_request: 18321
author:
type: changed
---
title: Add empty repo check before running AutoDevOps pipeline
merge_request: 17605
author:
type: changed
---
title: Adds support for OmniAuth JWT provider
merge_request: 17774
author:
type: added
---
title: Limit the number of failed logins when using LDAP for authentication
merge_request: 43525
author:
type: added
---
title: Improves the performance of projects list page
merge_request: 17934
author:
type: performance
---
title: Move ci/lint under project's namespace
merge_request: 17729
author:
type: added
---
title: Update wording to specify create/manage project vs group labels in labels dropdown
merge_request: 17640
author:
type: changed
---
title: Set breadcrumb for admin/runners/show
merge_request: 17431
author: Takuya Noguchi
type: fixed
---
title: Update documentation to reflect current minimum required versions of node and
yarn
merge_request: 17706
author:
type: other
---
title: Store sha256 checksum of artifact metadata
merge_request: 18149
author:
type: added
---
title: Change avatar error message to include allowed file formats
merge_request: 17747
author: Fabian Schneider
type: changed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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