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:
.use-mysql: &use-mysql
services:
- mysql:latest
- mysql:5.7
- redis:alpine
.rails5-variables: &rails5-variables
......
......@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
......@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb'
- 'spec/support/seed_repo.rb'
- 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/helpers/seed_repo.rb'
# Offense count: 112
# Configuration parameters: Blacklist.
......@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_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
# 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._
- [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)
- [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)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
......@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- 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
[labels page][labels-page].
......@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| 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 | |
| ~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 |
......
......@@ -178,7 +178,7 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.1)
doorkeeper (4.3.2)
railties (>= 4.2)
doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3)
......
10.7.0-pre
10.8.0-pre
......@@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
......
......@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
} else {
this.hide();
}
})
.catch(() => {
this.unavailable();
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 {
this.unavailableButton.classList.remove('hide');
}
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
......@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
......@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
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 {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
$container.find('tbody').prepend(lines);
$container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
......
......@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
......@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
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 {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
// 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
......@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
......@@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
return d3.select(container)
const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3
.select(container)
.append('svg')
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
}
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
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) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
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);
if (
lastMonth == null ||
......@@ -133,86 +160,113 @@ export default class ActivityCalendar {
}
}
});
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
.data(stamp => stamp)
.enter()
.append('rect')
.attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day)
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('fill', stamp => (
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
))
.attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body')
.on('click', this.clickDay);
.data(stamp => stamp)
.enter()
.append('rect')
.attr('x', '0')
.attr('y', stamp => this.dayYPos(stamp.day))
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr(
'fill',
stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
)
.attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body')
.on('click', this.clickDay);
}
renderDayTitles() {
const days = [
{
text: 'M',
y: 29 + (this.daySizeWithSpace * 1),
}, {
y: 29 + this.dayYPos(1),
},
{
text: 'W',
y: 29 + (this.daySizeWithSpace * 3),
}, {
y: 29 + this.dayYPos(2),
},
{
text: 'F',
y: 29 + (this.daySizeWithSpace * 5),
y: 29 + this.dayYPos(3),
},
];
this.svg.append('g')
this.svg
.append('g')
.selectAll('text')
.data(days)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 8)
.attr('y', day => day.y)
.text(day => day.text)
.attr('class', 'user-contrib-text');
.data(days)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 8)
.attr('y', day => day.y)
.text(day => day.text)
.attr('class', 'user-contrib-text');
}
renderMonths() {
this.svg.append('g')
this.svg
.append('g')
.attr('direction', 'ltr')
.selectAll('text')
.data(this.months)
.enter()
.append('text')
.attr('x', date => date.x)
.attr('y', 10)
.attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]);
.data(this.months)
.enter()
.append('text')
.attr('x', date => date.x)
.attr('y', 10)
.attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]);
}
renderKey() {
const keyValues = ['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)];
const keyValues = [
'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')
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
this.svg
.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0)
.attr('fill', color => color)
.attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body');
.data(keyColors)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0)
.attr('fill', color => color)
.attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body');
}
initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
const 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) {
......@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
axios.get(this.calendarActivitiesPath, {
params: {
date,
},
responseType: 'text',
})
.then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
axios
.get(this.calendarActivitiesPath, {
params: {
date,
},
responseType: 'text',
})
.then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');
......
......@@ -32,26 +32,38 @@ export default {
required: true,
},
buttonDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
isDisabled() {
return this.buttonDisabled === this.link;
},
watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
},
},
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
},
},
};
......@@ -62,7 +74,8 @@ export default {
@click="onClickAction"
v-tooltip
: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"
data-container="body"
: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>
import $ from 'jquery';
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import $ from 'jquery';
import JobNameComponent from './job_name_component.vue';
import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
*
* The following object should be provided as `job`:
*
* {
* "id": 4256,
* "name": "test",
* "status": {
* "icon": "icon_status_success",
* "text": "passed",
* "label": "passed",
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
* "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
directives: {
tooltip,
},
/**
* Renders the dropdown for the pipeline graph.
*
* The following object should be provided as `job`:
*
* {
* "id": 4256,
* "name": "test",
* "status": {
* "icon": "icon_status_success",
* "text": "passed",
* "label": "passed",
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
* "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
directives: {
tooltip,
},
components: {
jobComponent,
jobNameComponent,
},
components: {
JobComponent,
JobNameComponent,
},
props: {
job: {
type: Object,
required: true,
},
props: {
job: {
type: Object,
required: true,
},
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
mounted() {
this.stopDropdownClickPropagation();
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
},
mounted() {
this.stopDropdownClickPropagation();
},
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed so we stop propagation
* of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
stopDropdownClickPropagation() {
$(
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation();
});
},
};
},
};
</script>
<template>
<div class="ci-job-dropdown-container">
......@@ -101,8 +107,8 @@
:key="i">
<job-component
:job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/>
</li>
</ul>
......
......@@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
props: {
isLoading: {
type: Boolean,
......@@ -17,10 +16,10 @@ export default {
type: Object,
required: true,
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
......@@ -75,7 +74,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled"
:request-finished-for="requestFinishedFor"
/>
</ul>
</div>
......
<script>
import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
......@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
DropdownActionComponent,
JobNameComponent,
},
directives: {
tooltip,
},
......@@ -44,26 +41,17 @@ export default {
type: Object,
required: true,
},
cssClassJobName: {
type: String,
required: false,
default: '',
},
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
......@@ -134,19 +122,11 @@ export default {
</div>
<action-component
v-if="hasAction && !isDropdown"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:action-method="status.action.method"
:request-finished-for="requestFinishedFor"
/>
</div>
</template>
......@@ -29,10 +29,11 @@ export default {
required: false,
default: '',
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
......@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
:action-disabled="actionDisabled"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
:request-finished-for="requestFinishedFor"
/>
</li>
......
......@@ -25,7 +25,7 @@ export default () => {
data() {
return {
mediator,
actionDisabled: null,
requestFinishedFor: null,
};
},
created() {
......@@ -36,15 +36,17 @@ export default () => {
},
methods: {
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(() => {
this.mediator.refreshPipeline();
this.actionDisabled = null;
this.requestFinishedFor = action;
})
.catch(() => {
this.actionDisabled = null;
this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
},
......@@ -54,7 +56,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled,
requestFinishedFor: this.requestFinishedFor,
},
});
},
......@@ -79,7 +81,8 @@ export default () => {
},
methods: {
postAction(action) {
this.mediator.service.postAction(action.path)
this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
......
<script>
import ciIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
export default {
components: {
ciIcon,
export default {
components: {
CiIcon,
},
directives: {
tooltip,
},
props: {
status: {
type: Object,
required: true,
},
directives: {
tooltip,
showText: {
type: Boolean,
required: false,
default: true,
},
props: {
status: {
type: Object,
required: true,
},
showText: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status';
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
};
},
};
</script>
<template>
<a
......
<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.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
export default {
components: {
icon,
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
export default {
components: {
Icon,
},
props: {
status: {
type: Object,
required: true,
},
props: {
status: {
type: Object,
required: true,
},
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
};
},
};
</script>
<template>
<span :class="cssClass">
......
<script>
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
import tooltip from '../directives/tooltip';
/**
* 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';
export default {
name: 'ClipboardButton',
directives: {
tooltip,
export default {
name: 'ClipboardButton',
directives: {
tooltip,
},
props: {
text: {
type: String,
required: true,
},
props: {
text: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: [String, Boolean],
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn-default',
},
title: {
type: String,
required: true,
},
};
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: [String, Boolean],
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn-default',
},
},
};
</script>
<template>
......
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
UserAvatarLink,
Icon,
},
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
required: false,
default: false,
},
components: {
userAvatarLink,
icon,
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
showBranch: {
type: Boolean,
required: false,
default: true,
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
showBranch: {
type: Boolean,
required: false,
default: true,
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
created() {
this.commitIconSvg = commitIconSvg;
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
};
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
},
},
};
</script>
<template>
<div class="branch-commit">
......@@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
<div
v-html="commitIconSvg"
<icon
name="commit"
class="commit-icon js-commit-icon"
>
</div>
/>
<a
class="commit-sha"
......
<script>
import { __ } from '~/locale';
/**
* Port of detail_behavior expand button.
*
* @example
* <expand-button>
* <template slot="expanded">
* Text goes here.
* </template>
* </expand-button>
*/
export default {
name: 'ExpandButton',
data() {
return {
isCollapsed: true,
};
import { __ } from '~/locale';
/**
* Port of detail_behavior expand button.
*
* @example
* <expand-button>
* <template slot="expanded">
* Text goes here.
* </template>
* </expand-button>
*/
export default {
name: 'ExpandButton',
data() {
return {
isCollapsed: true,
};
},
computed: {
ariaLabel() {
return __('Click to expand text');
},
computed: {
ariaLabel() {
return __('Click to expand text');
},
},
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
},
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
},
},
};
},
};
</script>
<template>
<span>
......
<script>
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
import CiIconBadge from './ci_badge_link.vue';
import LoadingIcon from './loading_icon.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarImage,
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
components: {
CiIconBadge,
LoadingIcon,
TimeagoTooltip,
UserAvatarImage,
},
directives: {
tooltip,
},
props: {
status: {
type: Object,
required: true,
},
directives: {
tooltip,
itemName: {
type: String,
required: true,
},
props: {
status: {
type: Object,
required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
},
methods: {
onClickAction(action) {
this.$emit('actionClicked', action);
},
methods: {
onClickAction(action) {
this.$emit('actionClicked', action);
},
};
},
};
</script>
<template>
......
<script>
/* This is a re-usable vue component for rendering a svg sprite
icon
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
Sample configuration:
<icon
name="retry"
:size="32"
css-classes="top"
/>
<icon
name="retry"
:size="32"
css-classes="top"
/>
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default {
props: {
name: {
type: String,
required: true,
},
export default {
props: {
name: {
type: String,
required: true,
},
size: {
type: Number,
required: false,
default: 16,
validator(value) {
return validSizes.includes(value);
},
size: {
type: Number,
required: false,
default: 16,
validator(value) {
return validSizes.includes(value);
},
},
cssClasses: {
type: String,
required: false,
default: '',
},
cssClasses: {
type: String,
required: false,
default: '',
},
width: {
type: Number,
required: false,
default: null,
},
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
},
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
};
},
};
</script>
<template>
......@@ -79,7 +78,8 @@
:width="width"
:height="height"
:x="x"
:y="y">
:y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>
<script>
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
......@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
},
};
</script>
......@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
......@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
<div class="dropdown">
<div
class="dropdown"
ref="dropdown"
>
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
......
......@@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
};
</script>
......@@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
@click="handleClick"
>
<i
aria-hidden="true"
......
......@@ -468,6 +468,14 @@
margin-bottom: 10px;
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
.ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover;
......
......@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch
can_create = current_user &&
can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?(current_user)
@issue.can_be_worked_on?
respond_to do |format|
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
......@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
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
def render_issue_json
......
......@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h)
end
result[:html] = result[:html].presence || 'No job log'
render json: result
end
end
......
......@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users)
users = by_external_identity(users)
users = by_external(users)
users = by_2fa(users)
users = by_created_at(users)
users = by_custom_attributes(users)
......@@ -76,4 +77,15 @@ class UsersFinder
users.external
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
......@@ -81,6 +81,14 @@ module GitlabRoutingHelper
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)
if entity.is_a?(Issue)
toggle_subscription_project_issue_path(entity.project, entity)
......
......@@ -27,6 +27,7 @@ module Ci
has_one :metadata, class_name: 'Ci::BuildMetadata'
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!
......@@ -604,6 +605,7 @@ module Ci
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
.concat(deploy_token_variables)
end
end
......@@ -654,6 +656,15 @@ module Ci
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
options&.dig(:environment, :url) || persisted_environment&.external_url
end
......
......@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do
if read_attribute(column).blank?
scope_attrs = { scope => association(scope).reader }
scope_value = 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
new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
......
......@@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host
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,
# 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
# 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
only_path = false
end
......@@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root
end
url_base + avatar.url
url_base + avatar.local_url
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
# 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.
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
end
......@@ -124,6 +124,11 @@ module Routable
end
end
# Group would override this to check from association
def owned_by?(user)
owner == user
end
private
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
# 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.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def initialize(counter = nil)
@counter = counter
end
def string(base)
@base = base
@counter = nil
increment_counter! while yield(base_string)
base_string
......
......@@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
default_value_for(:expires_at) { Forever.date }
......@@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
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!
update!(revoked: true)
end
......
class Deployment < ActiveRecord::Base
include NonatomicInternalId
include AtomicInternalId
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :user
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 :ref, presence: true
......
......@@ -125,6 +125,10 @@ class Group < Namespace
self[:lfs_enabled]
end
def owned_by?(user)
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users(
self,
......
......@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base
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
......
......@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
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
# ignores merge requests branchs
def has_related_branch?
......@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end
end
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
# Returns `true` if the current issue can be viewed by either a logged in User
......
class MergeRequest < ActiveRecord::Base
include NonatomicInternalId
include AtomicInternalId
include Issuable
include Noteable
include Referable
......@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project"
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_one :merge_request_diff,
......
......@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include NonatomicInternalId
include AtomicInternalId
include Sortable
include Referable
include StripAttribute
......@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base
belongs_to :project
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 :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
......
......@@ -1879,6 +1879,10 @@ class Project < ActiveRecord::Base
[]
end
def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
private
def storage
......
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
prop_accessor :token
validates :token, presence: true, if: :activated?
......@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before],
data[:after],
token: token,
repo: project.repository.path_to_repo,
repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
......
......@@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
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
with_options scope: :subject, score: 0
......@@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
end
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
......
......@@ -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
OpenStruct.new(enabled: auto_devops_enabled?,
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?
OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'),
......
......@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model
end
def local_url
File.join('/', self.class.base_dir, dynamic_segment, filename)
end
private
# Designed to be overridden by child uploaders that have a dynamic path
......
......@@ -21,7 +21,7 @@
.help-block
Manage repository storage paths. Learn more in the
= 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
%h4 Circuit breaker
.form-group
......
......@@ -22,7 +22,7 @@
%hr
%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))
= 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 @@
- unless @repository.gitlab_ci_yml
= 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
.content-list.builds-content-list
......
......@@ -26,7 +26,7 @@
%ul
- pipeline.yaml_errors.split(",").each do |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
.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 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%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
= f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder
......
......@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%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
= 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) }
.settings-header
%h4
......
......@@ -9,7 +9,7 @@
- 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 }
.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',
'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:
author:
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
merge_request: 18026
title: Show new branch/mr button even when branch exists
merge_request: 17712
author: Jacopo Beschi @jacopo-beschi
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