Commit e4003ebd authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-05-10'

See merge request !1866.
parents 238a6d76 a0157215
...@@ -146,6 +146,7 @@ stages: ...@@ -146,6 +146,7 @@ stages:
# Trigger a package build on omnibus-gitlab repository # Trigger a package build on omnibus-gitlab repository
build-package: build-package:
before_script: []
services: [] services: []
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
...@@ -153,17 +154,7 @@ build-package: ...@@ -153,17 +154,7 @@ build-package:
stage: build stage: build
when: manual when: manual
script: script:
# If no branch in omnibus is specified, trigger pipeline against master - scripts/trigger-build
- if [ -z "$OMNIBUS_BRANCH" ] ; then export OMNIBUS_BRANCH=master ;fi
- echo "token=${BUILD_TRIGGER_TOKEN}" > version_details
- echo "ref=${OMNIBUS_BRANCH}" >> version_details
- echo "variables[ALTERNATIVE_SOURCES]=true" >> version_details
- echo "variables[GITLAB_VERSION]=${CI_COMMIT_SHA}" >> version_details
# Collect version details of all components
- for f in *_VERSION; do echo "variables[$f]=$(cat $f)" >> version_details; done
# Trigger the API and pass values collected above as parameters to it
- cat version_details | tr '\n' '&' | curl -X POST https://gitlab.com/api/v4/projects/20699/trigger/pipeline --data-binary @-
- rm version_details
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
knapsack: knapsack:
......
...@@ -377,6 +377,6 @@ gem 'vmstat', '~> 2.3.0' ...@@ -377,6 +377,6 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.5.0' gem 'gitaly', '~> 0.6.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
...@@ -287,7 +287,7 @@ GEM ...@@ -287,7 +287,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.5.0) gitaly (0.6.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -462,7 +462,7 @@ GEM ...@@ -462,7 +462,7 @@ GEM
rugged (~> 0.24) rugged (~> 0.24)
little-plugger (1.1.4) little-plugger (1.1.4)
locale (2.1.2) locale (2.1.2)
logging (2.1.0) logging (2.2.2)
little-plugger (~> 1.1) little-plugger (~> 1.1)
multi_json (~> 1.10) multi_json (~> 1.10)
loofah (2.0.3) loofah (2.0.3)
...@@ -955,7 +955,7 @@ DEPENDENCIES ...@@ -955,7 +955,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.5.0) gitaly (~> 0.6.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
const MODAL_SELECTOR = '#modal-delete-branch';
class DeleteModal {
constructor() {
this.$modal = $(MODAL_SELECTOR);
this.$toggleBtns = $(`[data-target="${MODAL_SELECTOR}"]`);
this.$branchName = $('.js-branch-name', this.$modal);
this.$confirmInput = $('.js-delete-branch-input', this.$modal);
this.$deleteBtn = $('.js-delete-branch', this.$modal);
this.bindEvents();
}
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
}
setModalData(e) {
this.branchName = e.currentTarget.dataset.branchName || '';
this.deletePath = e.currentTarget.dataset.deletePath || '';
this.updateModal();
}
setDeleteDisabled(e) {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');
this.$deleteBtn.attr('href', this.deletePath);
this.$deleteBtn.attr('disabled', true);
}
}
export default DeleteModal;
import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
import CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
const StatusIconEntityMap = {
icon_status_canceled: CANCELED_SVG,
icon_status_created: CREATED_SVG,
icon_status_failed: FAILED_SVG,
icon_status_manual: MANUAL_SVG,
icon_status_pending: PENDING_SVG,
icon_status_running: RUNNING_SVG,
icon_status_skipped: SKIPPED_SVG,
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
export {
CANCELED_SVG,
CREATED_SVG,
FAILED_SVG,
MANUAL_SVG,
PENDING_SVG,
RUNNING_SVG,
SKIPPED_SVG,
SUCCESS_SVG,
WARNING_SVG,
StatusIconEntityMap as default,
};
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
import Issue from './issue'; import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal';
import Group from './group'; import Group from './group';
import GroupName from './group_name'; import GroupName from './group_name';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
...@@ -51,6 +52,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -51,6 +52,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki'; import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index'; import BlobViewer from './blob/viewer/index';
import GeoNodes from './geo_nodes'; import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root'; import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
...@@ -186,6 +188,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -186,6 +188,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break; break;
case 'projects:branches:index': case 'projects:branches:index':
gl.AjaxLoadingSpinner.init(); gl.AjaxLoadingSpinner.init();
new DeleteModal();
break; break;
case 'projects:issues:new': case 'projects:issues:new':
case 'projects:issues:edit': case 'projects:issues:edit':
...@@ -267,7 +270,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -267,7 +270,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({ new Pipelines({
initTabs: true, initTabs: true,
pipelineStatusUrl, pipelineStatusUrl,
tabsOptions: { tabsOptions: {
......
...@@ -65,6 +65,7 @@ class GlFieldError { ...@@ -65,6 +65,7 @@ class GlFieldError {
this.state = { this.state = {
valid: false, valid: false,
empty: true, empty: true,
submitted: false,
}; };
this.initFieldValidation(); this.initFieldValidation();
...@@ -108,9 +109,10 @@ class GlFieldError { ...@@ -108,9 +109,10 @@ class GlFieldError {
const currentValue = this.accessCurrentValue(); const currentValue = this.accessCurrentValue();
this.state.valid = false; this.state.valid = false;
this.state.empty = currentValue === ''; this.state.empty = currentValue === '';
this.state.submitted = true;
this.renderValidity(); this.renderValidity();
this.form.focusOnFirstInvalid.apply(this.form); this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup // For UX, wait til after first invalid submission to check each keyup
this.inputElement.off('keyup.fieldValidator') this.inputElement.off('keyup.fieldValidator')
.on('keyup.fieldValidator', this.updateValidity.bind(this)); .on('keyup.fieldValidator', this.updateValidity.bind(this));
......
...@@ -37,6 +37,15 @@ class GlFieldErrors { ...@@ -37,6 +37,15 @@ class GlFieldErrors {
} }
} }
/* Public method for triggering validity updates manually */
updateFormValidityState() {
this.state.inputs.forEach((field) => {
if (field.state.submitted) {
field.updateValidity();
}
});
}
focusOnFirstInvalid () { focusOnFirstInvalid () {
const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
firstInvalid.inputElement.focus(); firstInvalid.inputElement.focus();
......
...@@ -31,82 +31,78 @@ ...@@ -31,82 +31,78 @@
* *
* ### How to use * ### How to use
* *
* new window.gl.LinkedTabs({ * new LinkedTabs({
* action: "#{controller.action_name}", * action: "#{controller.action_name}",
* defaultAction: 'tab1', * defaultAction: 'tab1',
* parentEl: '.tab-links' * parentEl: '.tab-links'
* }); * });
*/ */
(() => { export default class LinkedTabs {
window.gl = window.gl || {}; /**
* Binds the events and activates de default tab.
*
* @param {Object} options
*/
constructor(options = {}) {
this.options = options;
window.gl.LinkedTabs = class LinkedTabs { this.defaultAction = this.options.defaultAction;
/** this.action = this.options.action || this.defaultAction;
* Binds the events and activates de default tab.
*
* @param {Object} options
*/
constructor(options) {
this.options = options || {};
this.defaultAction = this.options.defaultAction; if (this.action === 'show') {
this.action = this.options.action || this.defaultAction; this.action = this.defaultAction;
}
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.currentLocation = window.location; this.currentLocation = window.location;
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`; const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
// since this is a custom event we need jQuery :( // since this is a custom event we need jQuery :(
$(document) $(document)
.off('shown.bs.tab', tabSelector) .off('shown.bs.tab', tabSelector)
.on('shown.bs.tab', tabSelector, e => this.tabShown(e)); .on('shown.bs.tab', tabSelector, e => this.tabShown(e));
this.activateTab(this.action); this.activateTab(this.action);
} }
/** /**
* Handles the `shown.bs.tab` event to set the currect url action. * Handles the `shown.bs.tab` event to set the currect url action.
* *
* @param {type} evt * @param {type} evt
* @return {Function} * @return {Function}
*/ */
tabShown(evt) { tabShown(evt) {
const source = evt.target.getAttribute('href'); const source = evt.target.getAttribute('href');
return this.setCurrentAction(source); return this.setCurrentAction(source);
} }
/** /**
* Updates the URL with the path that matched the given action. * Updates the URL with the path that matched the given action.
* *
* @param {String} source * @param {String} source
* @return {String} * @return {String}
*/ */
setCurrentAction(source) { setCurrentAction(source) {
const copySource = source; const copySource = source;
copySource.replace(/\/+$/, ''); copySource.replace(/\/+$/, '');
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
history.replaceState({ history.replaceState({
url: newState, url: newState,
}, document.title, newState); }, document.title, newState);
return newState; return newState;
} }
/** /**
* Given the current action activates the correct tab. * Given the current action activates the correct tab.
* http://getbootstrap.com/javascript/#tab-show * http://getbootstrap.com/javascript/#tab-show
* Note: Will trigger `shown.bs.tab` * Note: Will trigger `shown.bs.tab`
*/ */
activateTab() { activateTab() {
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show'); return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
} }
}; }
})();
This diff is collapsed.
...@@ -172,7 +172,7 @@ const normalizeNewlines = function(str) { ...@@ -172,7 +172,7 @@ const normalizeNewlines = function(str) {
if ($textarea.val() !== '') { if ($textarea.val() !== '') {
return; return;
} }
myLastNote = $("li.note[data-author-id='" + gon.current_user_id + "'][data-editable]:last"); myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes'));
if (myLastNote.length) { if (myLastNote.length) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit'); myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]); return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
...@@ -1132,8 +1132,8 @@ const normalizeNewlines = function(str) { ...@@ -1132,8 +1132,8 @@ const normalizeNewlines = function(str) {
} }
}; };
Notes.animateAppendNote = function(noteHTML, $notesList) { Notes.animateAppendNote = function(noteHtml, $notesList) {
const $note = window.$(noteHTML); const $note = $(noteHtml);
$note.addClass('fade-in-full').renderGFM(); $note.addClass('fade-in-full').renderGFM();
$notesList.append($note); $notesList.append($note);
......
import Vue from 'vue';
const inputNameAttribute = 'schedule[cron]';
export default {
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
},
data() {
return {
inputNameAttribute,
cronInterval: this.initialCronInterval,
cronIntervalPresets: {
everyDay: '0 4 * * *',
everyWeek: '0 4 * * 0',
everyMonth: '0 4 1 * *',
},
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
customInputEnabled: false,
};
},
computed: {
showUnsetWarning() {
return this.cronInterval === '';
},
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
return !!(this.customInputEnabled || !this.intervalIsPreset);
},
},
methods: {
toggleCustomInput(shouldEnable) {
this.customInputEnabled = shouldEnable;
if (shouldEnable) {
// We need to change the value so other radios don't remain selected
// because the model (cronInterval) hasn't changed. The server trims it.
this.cronInterval = `${this.cronInterval} `;
}
},
},
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
Vue.nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
template: `
<div class="interval-pattern-form-group">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<label for="custom">
Custom
</label>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>)
</span>
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-day">
Every day (at 4:00am)
</label>
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-week">
Every week (Sundays at 4:00am)
</label>
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-month">
Every month (on the 1st at 4:00am)
</label>
<div class="cron-interval-input-wrapper col-md-6">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
type="text"
placeholder="Define a custom pattern with cron syntax"
required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
/>
</div>
<span class="cron-unset-status col-md-3" v-if="showUnsetWarning">
Schedule not yet set
</span>
</div>
`,
};
import Cookies from 'js-cookie';
import illustrationSvg from '../icons/intro_illustration.svg';
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
data() {
return {
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
template: `
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i class="fa fa-times"></i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>Scheduling Pipelines</h4>
<p>
The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags.
Those scheduled pipelines will inherit limited project access based on their associated user.
</p>
<p> Learn more in the
<!-- FIXME -->
<a href="random.com">pipeline schedules documentation</a>.
</p>
</div>
</div>
</div>
`,
};
export default class TargetBranchDropdown {
constructor() {
this.$dropdown = $('.js-target-branch-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_ref');
this.initialValue = this.$input.val();
this.initDropdown();
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.formatBranchesList(),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: cfg => this.updateInputValue(cfg),
text: item => item.name,
});
this.setDropdownToggle();
}
formatBranchesList() {
return this.$dropdown.data('data')
.map(val => ({ name: val }));
}
setDropdownToggle() {
if (this.initialValue) {
this.$dropdownToggle.text(this.initialValue);
}
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.name);
gl.pipelineScheduleFieldErrors.updateFormValidityState();
}
}
/* eslint-disable class-methods-use-this */
export default class TimezoneDropdown {
constructor() {
this.$dropdown = $('.js-timezone-dropdown');
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $('#schedule_cron_timezone');
this.timezoneData = this.$dropdown.data('data');
this.initialValue = this.$input.val();
this.initDropdown();
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.timezoneData,
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: cfg => this.updateInputValue(cfg),
text: item => this.formatTimezone(item),
});
this.setDropdownToggle();
}
formatUtcOffset(offset) {
let prefix = '';
if (offset > 0) {
prefix = '+';
} else if (offset < 0) {
prefix = '-';
}
return `${prefix} ${Math.abs(offset / 3600)}`;
}
formatTimezone(item) {
return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`;
}
setDropdownToggle() {
if (this.initialValue) {
this.$dropdownToggle.text(this.initialValue);
}
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
gl.pipelineScheduleFieldErrors.updateFormValidityState();
}
}
<svg width="140" height="102" viewBox="0 0 140 102" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>illustration</title><defs><rect id="a" width="12.033" height="40.197" rx="3"/><rect id="b" width="12.033" height="40.197" rx="3"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(-.446)"><path d="M91.747 35.675v-6.039a2.996 2.996 0 0 0-2.999-3.005H54.635a2.997 2.997 0 0 0-2.999 3.005v6.039H40.092a3.007 3.007 0 0 0-2.996 3.005v34.187a2.995 2.995 0 0 0 2.996 3.005h11.544V79.9a2.996 2.996 0 0 0 2.999 3.005h34.113a2.997 2.997 0 0 0 2.999-3.005v-4.03h11.544a3.007 3.007 0 0 0 2.996-3.004V38.68a2.995 2.995 0 0 0-2.996-3.005H91.747z" stroke="#B5A7DD" stroke-width="2"/><rect stroke="#E5E5E5" stroke-width="2" fill="#FFF" x="21.556" y="38.69" width="98.27" height="34.167" rx="3"/><path d="M121.325 38.19c.55 0 .995.444.995 1.002 0 .554-.453 1.003-.995 1.003h-4.039a1.004 1.004 0 0 1 0-2.006h4.039zm9.044 0c.55 0 .996.444.996 1.002 0 .554-.454 1.003-.996 1.003h-4.038a1.004 1.004 0 0 1 0-2.006h4.038zm9.044 0c.55 0 .996.444.996 1.002 0 .554-.453 1.003-.996 1.003h-4.038a1.004 1.004 0 0 1 0-2.006h4.038zM121.325 71.854a1.004 1.004 0 0 1 0 2.006h-4.039a1.004 1.004 0 0 1 0-2.006h4.039zm9.044 0a1.004 1.004 0 0 1 0 2.006h-4.038a1.004 1.004 0 0 1 0-2.006h4.038zm9.044 0a1.004 1.004 0 0 1 0 2.006h-4.038a1.004 1.004 0 0 1 0-2.006h4.038z" fill="#E5E5E5"/><g transform="translate(110.3 35.675)"><use fill="#FFF" xlink:href="#a"/><rect stroke="#FDE5D8" stroke-width="2" x="1" y="1" width="10.033" height="38.197" rx="3"/><ellipse fill="#FC8A51" cx="6.017" cy="9.547" rx="1.504" ry="1.507"/><ellipse fill="#FC8A51" cx="6.017" cy="20.099" rx="1.504" ry="1.507"/><ellipse fill="#FC8A51" cx="6.017" cy="30.65" rx="1.504" ry="1.507"/></g><path d="M6.008 38.19c.55 0 .996.444.996 1.002 0 .554-.454 1.003-.996 1.003H1.97a1.004 1.004 0 0 1 0-2.006h4.038zm9.044 0c.55 0 .996.444.996 1.002 0 .554-.453 1.003-.996 1.003h-4.038a1.004 1.004 0 0 1 0-2.006h4.038zm9.045 0c.55 0 .995.444.995 1.002 0 .554-.453 1.003-.995 1.003h-4.039a1.004 1.004 0 0 1 0-2.006h4.039zM6.008 71.854a1.004 1.004 0 0 1 0 2.006H1.97a1.004 1.004 0 0 1 0-2.006h4.038zm9.044 0a1.004 1.004 0 0 1 0 2.006h-4.038a1.004 1.004 0 0 1 0-2.006h4.038zm9.045 0a1.004 1.004 0 0 1 0 2.006h-4.039a1.004 1.004 0 0 1 0-2.006h4.039z" fill="#E5E5E5"/><g transform="translate(19.05 35.675)"><use fill="#FFF" xlink:href="#b"/><rect stroke="#FDE5D8" stroke-width="2" x="1" y="1" width="10.033" height="38.197" rx="3"/><ellipse fill="#FC8A51" cx="6.017" cy="10.049" rx="1.504" ry="1.507"/><ellipse fill="#FC8A51" cx="6.017" cy="20.601" rx="1.504" ry="1.507"/><ellipse fill="#FC8A51" cx="6.017" cy="31.153" rx="1.504" ry="1.507"/></g><g transform="translate(47.096)"><g transform="translate(7.05)"><ellipse fill="#FC8A51" cx="17.548" cy="5.025" rx="4.512" ry="4.522"/><rect stroke="#B5A7DD" stroke-width="2" fill="#FFF" x="13.036" y="4.02" width="9.025" height="20.099" rx="1.5"/><rect stroke="#FDE5D8" stroke-width="2" fill="#FFF" y="4.02" width="35.096" height="4.02" rx="2.01"/><rect stroke="#6B4FBB" stroke-width="2" fill="#FFF" x="4.512" y="18.089" width="26.072" height="17.084" rx="1.5"/></g><rect stroke="#6B4FBB" stroke-width="2" fill="#FFF" transform="rotate(-45 43.117 35.117)" x="38.168" y="31.416" width="9.899" height="7.403" rx="3.702"/><ellipse stroke="#6B4FBB" stroke-width="2" fill="#FFF" cx="25" cy="55" rx="25" ry="25"/><ellipse stroke="#6B4FBB" stroke-width="2" fill="#FFF" cx="25" cy="55" rx="21" ry="21"/><rect stroke="#6B4FBB" stroke-width="2" fill="#FFF" x="43.05" y="53.281" width="2.95" height="1.538" rx=".769"/><rect stroke="#6B4FBB" stroke-width="2" fill="#FFF" x="4.305" y="53.281" width="2.95" height="1.538" rx=".769"/><rect stroke="#6B4FBB" stroke-width="2" fill="#FFF" transform="rotate(90 25.153 74.422)" x="23.677" y="73.653" width="2.95" height="1.538" rx=".769"/><rect stroke="#6B4FBB" stroke-width="2" fill="#FFF" transform="rotate(90 25.153 35.51)" x="23.844" y="34.742" width="2.616" height="1.538" rx=".769"/><path d="M13.362 42.502c-.124-.543.198-.854.74-.69l2.321.704c.533.161.643.592.235.972l-.22.206 7.06 7.572a1.002 1.002 0 1 1-1.467 1.368l-7.06-7.573-.118.11c-.402.375-.826.248-.952-.304l-.54-2.365zM21.606 67.576c-.408.38-.84.255-.968-.295l-.551-2.363c-.127-.542.191-.852.725-.69l.288.089 3.027-9.901a1.002 1.002 0 1 1 1.918.586l-3.027 9.901.154.047c.525.16.627.592.213.977l-1.779 1.65z" fill="#FC8A51"/><ellipse stroke="#6B4FBB" stroke-width="2" fill="#FFF" cx="25.099" cy="54.768" rx="2.507" ry="2.512"/></g></g><path d="M52.697 96.966a1.004 1.004 0 0 1 2.006 0v4.038a1.004 1.004 0 0 1-2.006 0v-4.038zm0-9.044a1.004 1.004 0 0 1 2.006 0v4.038a1.004 1.004 0 0 1-2.006 0v-4.038zM86.29 96.966c0-.55.444-.996 1.002-.996.554 0 1.003.454 1.003.996v4.038a1.004 1.004 0 0 1-2.006 0v-4.038zm0-9.044c0-.55.444-.996 1.002-.996.554 0 1.003.453 1.003.996v4.038a1.004 1.004 0 0 1-2.006 0v-4.038z" fill="#E5E5E5"/></g></svg>
\ No newline at end of file
import Vue from 'vue';
import IntervalPatternInput from './components/interval_pattern_input';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
document.addEventListener('DOMContentLoaded', () => {
const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : '';
new IntervalPatternInputComponent({
propsData: {
initialCronInterval,
},
}).$mount(intervalPatternMount);
const formElement = document.getElementById('new-pipeline-schedule-form');
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
});
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
document.addEventListener('DOMContentLoaded', () => {
new PipelineSchedulesCalloutComponent()
.$mount('#scheduling-pipelines-callout');
});
/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */ import LinkedTabs from './lib/utils/bootstrap_linked_tabs';
require('./lib/utils/bootstrap_linked_tabs'); export default class Pipelines {
constructor(options = {}) {
((global) => { if (options.initTabs && options.tabsOptions) {
class Pipelines { // eslint-disable-next-line no-new
constructor(options = {}) { new LinkedTabs(options.tabsOptions);
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
}
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns();
} }
addMarginToBuildColumns() { if (options.pipelineStatusUrl) {
this.pipelineGraph = document.querySelector('.js-pipeline-graph'); gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
const multiBuildColumn = buildNode.closest('.stage-column');
const previousColumn = multiBuildColumn.previousElementSibling;
if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
multiBuildColumn.classList.add('left-margin');
firstChildBuildNode.classList.add('left-connector');
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
this.pipelineGraph.classList.remove('hidden');
} }
} }
}
global.Pipelines = Pipelines;
})(window.gl || (window.gl = {}));
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/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 {
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
mixins: [
tooltipMixin,
],
computed: {
actionIconSvg() {
return getActionIcon(this.actionIcon);
},
cssClass() {
return `js-${gl.text.dasherize(this.actionIcon)}`;
},
},
};
</script>
<template>
<a
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
class="ci-action-icon-container"
data-toggle="tooltip"
data-container="body">
<i
class="ci-action-icon-wrapper"
:class="cssClass"
v-html="actionIconSvg"
aria-hidden="true"
/>
</a>
</template>
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/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 {
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
mixins: [
tooltipMixin,
],
computed: {
actionIconSvg() {
return getActionIcon(this.actionIcon);
},
},
};
</script>
<template>
<a
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-toggle="tooltip"
data-container="body"
v-html="actionIconSvg"
aria-label="Job's action">
</a>
</template>
<script>
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/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": "icon_action_retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
props: {
job: {
type: Object,
required: true,
},
},
mixins: [
tooltipMixin,
],
components: {
jobComponent,
jobNameComponent,
},
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
},
};
</script>
<template>
<div>
<button
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
ref="tooltip">
<job-name-component
:name="job.name"
:status="job.status" />
<span class="dropdown-counter-badge">
{{job.size}}
</span>
</button>
<ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
<li class="scrollable-menu">
<ul>
<li v-for="item in job.jobs">
<job-component
:job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
/>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../../lib/utils/poll';
import PipelineService from '../../services/pipeline_service';
import PipelineStore from '../../stores/pipeline_store';
import stageColumnComponent from './stage_column_component.vue';
import '../../../flash';
export default {
components: {
stageColumnComponent,
},
data() {
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
const store = new PipelineStore();
return {
isLoading: false,
endpoint: DOMdata.endpoint,
store,
state: store.state,
};
},
created() {
this.service = new PipelineService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipeline',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
},
methods: {
successCallback(response) {
const data = response.json();
this.isLoading = false;
this.store.storeGraph(data.details.stages);
},
errorCallback() {
this.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
},
capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1);
},
isFirstColumn(index) {
return index === 0;
},
stageConnectorClass(index, stage) {
let className;
// If it's the first stage column and only has one job
if (index === 0 && stage.groups.length === 1) {
className = 'no-margin';
} else if (index > 0) {
// If it is not the first column
className = 'left-margin';
}
return className;
},
},
};
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph">
<div class="text-center">
<i
v-if="isLoading"
class="loading-icon fa fa-spin fa-spinner fa-3x"
aria-label="Loading"
aria-hidden="true" />
</div>
<ul
v-if="!isLoading"
class="stage-column-list">
<stage-column-component
v-for="(stage, index) in state.graph"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"/>
</ul>
</div>
</div>
</template>
<script>
import actionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
*
* 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": "icon_action_retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
props: {
job: {
type: Object,
required: true,
},
cssClassJobName: {
type: String,
required: false,
default: '',
},
isDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
actionComponent,
dropdownActionComponent,
jobNameComponent,
},
mixins: [
tooltipMixin,
],
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
/**
* Verifies if the provided job has an action path
*
* @return {Boolean}
*/
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
};
</script>
<template>
<div>
<a
v-if="job.status.details_path"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
:name="job.name"
:status="job.status"
/>
</a>
<div
v-else
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
:name="job.name"
:status="job.status"
/>
</div>
<action-component
v-if="hasAction && !isDropdown"
:tooltip-text="job.status.action.title"
:link="job.status.action.path"
:action-icon="job.status.action.icon"
:action-method="job.status.action.method"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="job.status.action.title"
:link="job.status.action.path"
:action-icon="job.status.action.icon"
:action-method="job.status.action.method"
/>
</div>
</template>
<script>
import ciIcon from '../../../vue_shared/components/ci_icon.vue';
/**
* Component that renders both the CI icon status and the job name.
* Used in
* - Badge component
* - Dropdown badge components
*/
export default {
props: {
name: {
type: String,
required: true,
},
status: {
type: Object,
required: true,
},
},
components: {
ciIcon,
},
};
</script>
<template>
<span>
<ci-icon
:status="status" />
<span class="ci-status-text">
{{name}}
</span>
</span>
</template>
<script>
import jobComponent from './job_component.vue';
import dropdownJobComponent from './dropdown_job_component.vue';
export default {
props: {
title: {
type: String,
required: true,
},
jobs: {
type: Array,
required: true,
},
isFirstColumn: {
type: Boolean,
required: false,
default: false,
},
stageConnectorClass: {
type: String,
required: false,
default: '',
},
},
components: {
jobComponent,
dropdownJobComponent,
},
methods: {
firstJob(list) {
return list[0];
},
jobId(job) {
return `ci-badge-${job.name}`;
},
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
},
};
</script>
<template>
<li
class="stage-column"
:class="stageConnectorClass">
<div class="stage-name">
{{title}}
</div>
<div class="builds-container">
<ul>
<li
v-for="(job, index) in jobs"
:key="job.id"
class="build"
:class="buildConnnectorClass(index)"
:id="jobId(job)">
<div class="curve"></div>
<job-component
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
/>
</li>
</ul>
</div>
</li>
</template>
import Vue from 'vue';
import pipelineGraph from './components/graph/graph_component.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-pipeline-graph-vue',
components: {
pipelineGraph,
},
render: createElement => createElement('pipeline-graph'),
}));
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class PipelineService {
constructor(endpoint) {
this.pipeline = Vue.resource(endpoint);
}
getPipeline() {
return this.pipeline.get();
}
}
export default class PipelineStore {
constructor() {
this.state = {};
this.state.graph = [];
}
storeGraph(graph = []) {
this.state.graph = graph;
}
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
(function() { (function() {
this.ProjectNew = (function() { this.ProjectNew = (function() {
function ProjectNew() { function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select'); this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select'); this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$enableApprovers = $('.js-require-approvals-toggle'); this.$enableApprovers = $('.js-require-approvals-toggle');
......
import Vue from 'vue'; import Vue from 'vue';
export default new Vue(); const eventHub = new Vue();
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = (...args) => eventHub.$emit(...args);
export default eventHub;
...@@ -166,15 +166,23 @@ import d3 from 'd3'; ...@@ -166,15 +166,23 @@ import d3 from 'd3';
}; };
Calendar.prototype.renderKey = function() { Calendar.prototype.renderKey = function() {
var keyColors; const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return 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', (function(_this) {
return function(color, i) { this.svg.append('g')
return _this.daySizeWithSpace * i; .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
}; .selectAll('rect')
})(this)).attr('y', 0).attr('fill', function(color) { .data(keyColors)
return color; .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');
}; };
Calendar.prototype.initColor = function() { Calendar.prototype.initColor = function() {
......
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
/* global Issuable */ /* global Issuable */
/* global emitSidebarEvent */
import eventHub from './sidebar/event_hub'; // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
(function() { (function() {
const slice = [].slice; const slice = [].slice;
...@@ -109,7 +111,7 @@ import eventHub from './sidebar/event_hub'; ...@@ -109,7 +111,7 @@ import eventHub from './sidebar/event_hub';
.find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`); .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`);
firstSelected.remove(); firstSelected.remove();
eventHub.$emit('sidebar.removeAssignee', { emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId, id: firstSelectedId,
}); });
} }
...@@ -329,7 +331,7 @@ import eventHub from './sidebar/event_hub'; ...@@ -329,7 +331,7 @@ import eventHub from './sidebar/event_hub';
defaultLabel: defaultLabel, defaultLabel: defaultLabel,
hidden: function(e) { hidden: function(e) {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
eventHub.$emit('sidebar.saveAssignees'); emitSidebarEvent('sidebar.saveAssignees');
} }
if (!$dropdown.data('always-show-selectbox')) { if (!$dropdown.data('always-show-selectbox')) {
...@@ -363,10 +365,10 @@ import eventHub from './sidebar/event_hub'; ...@@ -363,10 +365,10 @@ import eventHub from './sidebar/event_hub';
const id = parseInt(element.value, 10); const id = parseInt(element.value, 10);
element.remove(); element.remove();
}); });
eventHub.$emit('sidebar.removeAllAssignees'); emitSidebarEvent('sidebar.removeAllAssignees');
} else if (isActive) { } else if (isActive) {
// user selected // user selected
eventHub.$emit('sidebar.addAssignee', user); emitSidebarEvent('sidebar.addAssignee', user);
// Remove unassigned selection (if it was previously selected) // Remove unassigned selection (if it was previously selected)
const unassignedSelected = $dropdown.closest('.selectbox') const unassignedSelected = $dropdown.closest('.selectbox')
...@@ -382,7 +384,7 @@ import eventHub from './sidebar/event_hub'; ...@@ -382,7 +384,7 @@ import eventHub from './sidebar/event_hub';
} }
// User unselected // User unselected
eventHub.$emit('sidebar.removeAssignee', user); emitSidebarEvent('sidebar.removeAssignee', user);
} }
if (getSelected().find(u => u === gon.current_user_id)) { if (getSelected().find(u => u === gon.current_user_id)) {
......
...@@ -108,8 +108,6 @@ export default { ...@@ -108,8 +108,6 @@ export default {
</div> </div>
<mr-widget-memory-usage <mr-widget-memory-usage
v-if="deployment.metrics_url" v-if="deployment.metrics_url"
:mr="mr"
:service="service"
:metricsUrl="deployment.metrics_url" :metricsUrl="deployment.metrics_url"
/> />
</div> </div>
......
...@@ -5,8 +5,6 @@ import MRWidgetService from '../services/mr_widget_service'; ...@@ -5,8 +5,6 @@ import MRWidgetService from '../services/mr_widget_service';
export default { export default {
name: 'MemoryUsage', name: 'MemoryUsage',
props: { props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
metricsUrl: { type: String, required: true }, metricsUrl: { type: String, required: true },
}, },
data() { data() {
...@@ -14,6 +12,7 @@ export default { ...@@ -14,6 +12,7 @@ export default {
// memoryFrom: 0, // memoryFrom: 0,
// memoryTo: 0, // memoryTo: 0,
memoryMetrics: [], memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false, hasMetrics: false,
loadFailed: false, loadFailed: false,
loadingMetrics: true, loadingMetrics: true,
...@@ -23,8 +22,22 @@ export default { ...@@ -23,8 +22,22 @@ export default {
components: { components: {
'mr-memory-graph': MemoryGraph, 'mr-memory-graph': MemoryGraph,
}, },
computed: {
shouldShowLoading() {
return this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
shouldShowMemoryGraph() {
return !this.loadingMetrics && this.hasMetrics && !this.loadFailed;
},
shouldShowLoadFailure() {
return !this.loadingMetrics && !this.hasMetrics && this.loadFailed;
},
shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
},
methods: { methods: {
computeGraphData(metrics) { computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false; this.loadingMetrics = false;
const { memory_values } = metrics; const { memory_values } = metrics;
// if (memory_previous.length > 0) { // if (memory_previous.length > 0) {
...@@ -38,70 +51,73 @@ export default { ...@@ -38,70 +51,73 @@ export default {
if (memory_values.length > 0) { if (memory_values.length > 0) {
this.hasMetrics = true; this.hasMetrics = true;
this.memoryMetrics = memory_values[0].values; this.memoryMetrics = memory_values[0].values;
this.deploymentTime = deploymentTime;
} }
}, },
}, loadMetrics() {
mounted() { gl.utils.backOff((next, stop) => {
this.$props.loadingMetrics = true; MRWidgetService.fetchMetrics(this.metricsUrl)
gl.utils.backOff((next, stop) => { .then((res) => {
MRWidgetService.fetchMetrics(this.$props.metricsUrl) if (res.status === statusCodes.NO_CONTENT) {
.then((res) => { this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (res.status === statusCodes.NO_CONTENT) { /* eslint-disable no-unused-expressions */
this.backOffRequestCounter = this.backOffRequestCounter += 1; this.backOffRequestCounter < 3 ? next() : stop(res);
if (this.backOffRequestCounter < 3) {
next();
} else { } else {
stop(res); stop(res);
} }
} else { })
stop(res); .catch(stop);
})
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
return res;
} }
})
.catch(stop);
})
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
return res;
}
return res.json(); return res.json();
}) })
.then((res) => { .then((res) => {
this.computeGraphData(res.metrics); this.computeGraphData(res.metrics, res.deployment_time);
return res; return res;
}) })
.catch(() => { .catch(() => {
this.$props.loadFailed = true; this.loadFailed = true;
}); this.loadingMetrics = false;
});
},
},
mounted() {
this.loadingMetrics = true;
this.loadMetrics();
}, },
template: ` template: `
<div class="mr-info-list mr-memory-usage"> <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
<div class="legend"></div> <div class="legend"></div>
<p <p
v-if="loadingMetrics" v-if="shouldShowLoading"
class="usage-info usage-info-loading"> class="usage-info js-usage-info usage-info-loading">
<i <i
class="fa fa-spinner fa-spin usage-info-load-spinner" class="fa fa-spinner fa-spin usage-info-load-spinner"
aria-hidden="true" />Loading deployment statistics. aria-hidden="true" />Loading deployment statistics.
</p> </p>
<p <p
v-if="!hasMetrics && !loadingMetrics" v-if="shouldShowMemoryGraph"
class="usage-info usage-info-loading"> class="usage-info js-usage-info">
Deployment statistics are not available currently.
</p>
<p
v-if="hasMetrics"
class="usage-info">
Deployment memory usage: Deployment memory usage:
</p> </p>
<p <p
v-if="loadFailed" v-if="shouldShowLoadFailure"
class="usage-info"> class="usage-info js-usage-info usage-info-failed">
Failed to load deployment statistics. Failed to load deployment statistics.
</p> </p>
<p
v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable">
Deployment statistics are not available currently.
</p>
<mr-memory-graph <mr-memory-graph
v-if="hasMetrics" v-if="shouldShowMemoryGraph"
:metrics="memoryMetrics" :metrics="memoryMetrics"
:deploymentTime="deploymentTime"
height="25" height="25"
width="100" /> width="100" />
</div> </div>
......
/**
* This file is the centerpiece of an attempt to reduce potential conflicts
* between the CE and EE versions of the MR widget. EE additions to the MR widget should
* be contained in the ./vue_merge_request_widget/ee directory, and should **extend**
* rather than mutate CE MR Widget code.
*
* This file should be the only source of conflicts between EE and CE. EE-only components should
* imported directly where they are needed, and import paths for EE extensions of CE components
* should overwrite import paths **without** changing the order of dependencies listed here.
*/
export { default as Vue } from 'vue'; export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval'; export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header'; export { default as WidgetHeader } from './components/mr_widget_header';
......
import cancelSVG from 'icons/_icon_action_cancel.svg';
import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.svg';
import stopSVG from 'icons/_icon_action_stop.svg';
export default function getActionIcon(action) {
let icon;
switch (action) {
case 'icon_action_cancel':
icon = cancelSVG;
break;
case 'icon_action_retry':
icon = retrySVG;
break;
case 'icon_action_play':
icon = playSVG;
break;
case 'icon_action_stop':
icon = stopSVG;
break;
default:
icon = '';
}
return icon;
}
...@@ -2,6 +2,7 @@ export default { ...@@ -2,6 +2,7 @@ export default {
name: 'MemoryGraph', name: 'MemoryGraph',
props: { props: {
metrics: { type: Array, required: true }, metrics: { type: Array, required: true },
deploymentTime: { type: Number, required: true },
width: { type: String, required: true }, width: { type: String, required: true },
height: { type: String, required: true }, height: { type: String, required: true },
}, },
...@@ -9,27 +10,105 @@ export default { ...@@ -9,27 +10,105 @@ export default {
return { return {
pathD: '', pathD: '',
pathViewBox: '', pathViewBox: '',
// dotX: '', dotX: '',
// dotY: '', dotY: '',
}; };
}, },
computed: {
getFormattedMedian() {
const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000);
return `Deployed ${deployedSince}`;
},
},
methods: {
/**
* Returns metric value index in metrics array
* with timestamp closest to matching median
*/
getMedianMetricIndex(median, metrics) {
let matchIndex = 0;
let timestampDiff = 0;
let smallestDiff = 0;
const metricTimestamps = metrics.map(v => v[0]);
// Find metric timestamp which is closest to deploymentTime
timestampDiff = Math.abs(metricTimestamps[0] - median);
metricTimestamps.forEach((timestamp, index) => {
if (index === 0) { // Skip first element
return;
}
smallestDiff = Math.abs(timestamp - median);
if (smallestDiff < timestampDiff) {
matchIndex = index;
timestampDiff = smallestDiff;
}
});
return matchIndex;
},
/**
* Get Graph Plotting values to render Line and Dot
*/
getGraphPlotValues(median, metrics) {
const renderData = metrics.map(v => v[1]);
const medianMetricIndex = this.getMedianMetricIndex(median, metrics);
let cx = 0;
let cy = 0;
// Find Maximum and Minimum values from `renderData` array
const maxMemory = Math.max.apply(null, renderData);
const minMemory = Math.min.apply(null, renderData);
// Find difference between extreme ends
const diff = maxMemory - minMemory;
const lineWidth = renderData.length;
// Iterate over metrics values and perform following
// 1. Find x & y co-ords for deploymentTime's memory value
// 2. Return line path against maxMemory
const linePath = renderData.map((y, x) => {
if (medianMetricIndex === x) {
cx = x;
cy = maxMemory - y;
}
return `${x} ${maxMemory - y}`;
});
return {
pathD: linePath,
pathViewBox: {
lineWidth,
diff,
},
dotX: cx,
dotY: cy,
};
},
/**
* Render Graph based on provided median and metrics values
*/
renderGraph(median, metrics) {
const { pathD, pathViewBox, dotX, dotY } = this.getGraphPlotValues(median, metrics);
// Set props and update graph on UI.
this.pathD = `M ${pathD}`;
this.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`;
this.dotX = dotX;
this.dotY = dotY;
},
},
mounted() { mounted() {
const renderData = this.$props.metrics.map(v => v[1]); this.renderGraph(this.deploymentTime, this.metrics);
const maxMemory = Math.max.apply(null, renderData);
const minMemory = Math.min.apply(null, renderData);
const diff = maxMemory - minMemory;
// const cx = 0;
// const cy = 0;
const lineWidth = renderData.length;
const linePath = renderData.map((y, x) => `${x} ${maxMemory - y}`);
this.pathD = `M ${linePath}`;
this.pathViewBox = `0 0 ${lineWidth} ${diff}`;
}, },
template: ` template: `
<div class="memory-graph-container"> <div class="memory-graph-container">
<svg :width="width" :height="height" xmlns="http://www.w3.org/2000/svg"> <svg class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" xmlns="http://www.w3.org/2000/svg">
<path :d="pathD" :viewBox="pathViewBox" /> <path :d="pathD" :viewBox="pathViewBox" />
<!--<circle r="0.8" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> --> <circle r="1.5" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" />
</svg> </svg>
</div> </div>
`, `,
......
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
};
...@@ -4,13 +4,14 @@ ...@@ -4,13 +4,14 @@
*/ */
.file-holder { .file-holder {
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default;
&.file-holder-no-border { &.file-holder-no-border {
border: 0; border: 0;
} }
&.readme-holder { &.readme-holder {
margin: $gl-padding-top 0; margin: $gl-padding 0;
} }
table { table {
...@@ -25,7 +26,7 @@ ...@@ -25,7 +26,7 @@
text-align: left; text-align: left;
padding: 10px $gl-padding; padding: 10px $gl-padding;
word-wrap: break-word; word-wrap: break-word;
border-radius: 3px 3px 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear { &.file-title-clear {
padding-left: 0; padding-left: 0;
...@@ -94,9 +95,16 @@ ...@@ -94,9 +95,16 @@
tr { tr {
border-bottom: 1px solid $blame-border; border-bottom: 1px solid $blame-border;
&:last-child {
border-bottom: none;
}
} }
td { td {
border-top: none;
border-bottom: none;
&:first-child { &:first-child {
border-left: none; border-left: none;
} }
...@@ -107,7 +115,7 @@ ...@@ -107,7 +115,7 @@
} }
td.blame-commit { td.blame-commit {
padding: 0 10px; padding: 5px 10px;
min-width: 400px; min-width: 400px;
background: $gray-light; background: $gray-light;
} }
...@@ -246,7 +254,7 @@ span.idiff { ...@@ -246,7 +254,7 @@ span.idiff {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
padding: 5px $gl-padding; padding: 5px $gl-padding;
margin: 0; margin: 0;
border-radius: 3px 3px 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
.file-header-content { .file-header-content {
white-space: nowrap; white-space: nowrap;
......
...@@ -158,6 +158,7 @@ ul.content-list { ...@@ -158,6 +158,7 @@ ul.content-list {
} }
} }
&.has-tooltip,
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
......
.memory-graph-container { .memory-graph-container {
svg { svg {
background: $white-light; background: $white-light;
cursor: pointer;
&:hover {
box-shadow: 0 0 4px $gray-darkest inset;
}
} }
path { path {
fill: none; fill: none;
stroke: $blue-500; stroke: $blue-500;
stroke-width: 1px; stroke-width: 2px;
} }
circle { circle {
stroke: $blue-700; stroke: $blue-700;
fill: $blue-700; fill: $blue-700;
stroke-width: 4px;
} }
} }
...@@ -163,7 +163,7 @@ $fixed-layout-width: 1280px; ...@@ -163,7 +163,7 @@ $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$error-exclamation-point: $red-500; $error-exclamation-point: $red-500;
$border-radius-default: 2px; $border-radius-default: 3px;
$settings-icon-size: 18px; $settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500; $provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500; $link-underline-blue: $blue-500;
......
...@@ -163,7 +163,6 @@ ...@@ -163,7 +163,6 @@
.avatar-cell { .avatar-cell {
width: 46px; width: 46px;
padding-left: 10px;
img { img {
margin-right: 0; margin-right: 0;
...@@ -175,7 +174,6 @@ ...@@ -175,7 +174,6 @@
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
flex-grow: 1; flex-grow: 1;
padding-left: 10px;
.merge-request-branches & { .merge-request-branches & {
flex-direction: column; flex-direction: column;
......
// Common // Common
.diff-file { .diff-file {
border: 1px solid $border-color;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
border-radius: 3px;
.commit-short-id { .commit-short-id {
font-family: $regular_font; font-family: $regular_font;
font-weight: 400; font-weight: 400;
} }
.diff-header {
position: relative;
background: $gray-light;
border-bottom: 1px solid $border-color;
padding: 10px 16px;
color: $gl-text-color;
z-index: 10;
border-radius: 3px 3px 0 0;
.diff-title {
font-family: $monospace_font;
word-break: break-all;
display: block;
.file-mode {
color: $file-mode-changed;
}
}
.commit-short-id {
font-family: $monospace_font;
font-size: smaller;
}
}
.file-title, .file-title,
.file-title-flex-parent { .file-title-flex-parent {
cursor: pointer; cursor: pointer;
......
...@@ -132,12 +132,6 @@ ...@@ -132,12 +132,6 @@
line-height: 16px; line-height: 16px;
} }
@media (min-width: $screen-sm-min) {
.stage-cell {
padding: 0 4px;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
order: 1; order: 1;
margin-top: $gl-padding-top; margin-top: $gl-padding-top;
...@@ -183,8 +177,7 @@ ...@@ -183,8 +177,7 @@
} }
&.mr-memory-usage { &.mr-memory-usage {
margin-top: 10px; margin: 5px 0 10px 25px;
margin-bottom: 10px;
} }
} }
...@@ -512,7 +505,12 @@ ...@@ -512,7 +505,12 @@
.mr-info-list.mr-memory-usage { .mr-info-list.mr-memory-usage {
.legend { .legend {
height: 75%; height: 65%;
top: 0;
@media (max-width: $screen-xs-max) {
height: 20px;
}
} }
p { p {
...@@ -825,13 +823,15 @@ ...@@ -825,13 +823,15 @@
} }
.mr-memory-usage { .mr-memory-usage {
p.usage-info-loading { p.usage-info-loading,
margin-bottom: 6px; p.usage-info-unavailable,
p.usage-info-failed {
margin-bottom: 5px;
}
.usage-info-load-spinner { p.usage-info-loading .usage-info-load-spinner {
margin-right: 10px; margin-right: 10px;
font-size: 16px; font-size: 16px;
}
} }
@media (max-width: $screen-md-min) { @media (max-width: $screen-md-min) {
......
...@@ -284,10 +284,6 @@ ul.notes { ...@@ -284,10 +284,6 @@ ul.notes {
} }
} }
.diff-header > span {
margin-right: 10px;
}
.line_content { .line_content {
white-space: pre-wrap; white-space: pre-wrap;
} }
......
.js-pipeline-schedule-form {
.dropdown-select,
.dropdown-menu-toggle {
width: 100%!important;
}
.gl-field-error {
margin: 10px 0 0;
}
}
.interval-pattern-form-group {
label {
margin-right: 10px;
font-size: 12px;
&[for='custom'] {
margin-right: 0;
}
}
.cron-interval-input-wrapper {
padding-left: 0;
}
.cron-interval-input {
margin: 10px 10px 0 0;
}
.cron-syntax-link-wrap {
margin-right: 10px;
font-size: 12px;
}
.cron-unset-status {
padding-top: 16px;
margin-left: -16px;
color: $gl-text-color-secondary;
font-size: 12px;
font-weight: 600;
}
}
.pipeline-schedule-table-row {
.branch-name-cell {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.next-run-cell {
color: $gl-text-color-secondary;
}
a {
color: $text-color;
}
}
.pipeline-schedules-user-callout {
.bordered-box.content-block {
border: 1px solid $border-color;
background-color: transparent;
padding: 16px;
}
#dismiss-callout-btn {
color: $gl-text-color;
}
}
...@@ -261,7 +261,7 @@ ...@@ -261,7 +261,7 @@
.stage-cell { .stage-cell {
font-size: 0; font-size: 0;
padding: 10px 4px; padding: 0 4px;
> .stage-container > div > button > span > svg, > .stage-container > div > button > span > svg,
> .stage-container > button > svg { > .stage-container > button > svg {
...@@ -561,34 +561,6 @@ ...@@ -561,34 +561,6 @@
} }
.arrow {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 18px;
}
&::before {
left: -5px;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
left: -4px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: $white-light;
}
}
// Connect first build in each stage with right horizontal line // Connect first build in each stage with right horizontal line
&:first-child { &:first-child {
&::after { &::after {
...@@ -863,7 +835,8 @@ ...@@ -863,7 +835,8 @@
border-radius: 3px; border-radius: 3px;
// build name // build name
.ci-build-text { .ci-build-text,
.ci-status-text {
font-weight: 200; font-weight: 200;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
...@@ -915,6 +888,38 @@ ...@@ -915,6 +888,38 @@
} }
} }
/**
* Top arrow in the dropdown in the big pipeline graph
*/
.big-pipeline-graph-dropdown-menu {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 18px;
}
&::before {
left: -5px;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
left: -4px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: $white-light;
}
}
/** /**
* Top arrow in the dropdown in the mini pipeline graph * Top arrow in the dropdown in the mini pipeline graph
*/ */
......
...@@ -138,11 +138,12 @@ ...@@ -138,11 +138,12 @@
.blob-commit-info { .blob-commit-info {
list-style: none; list-style: none;
background: $gray-light;
padding: 16px 16px 16px 6px;
border: 1px solid $border-color;
border-bottom: none;
margin: 0; margin: 0;
padding: 0;
}
.blob-content-holder {
margin-top: $gl-padding;
} }
.blob-upload-dropzone-previews { .blob-upload-dropzone-previews {
......
...@@ -47,7 +47,7 @@ module IssuableCollections ...@@ -47,7 +47,7 @@ module IssuableCollections
end end
def merge_requests_collection def merge_requests_collection
merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace) merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, :head_pipeline, target_project: :namespace)
end end
def issues_finder def issues_finder
......
...@@ -40,13 +40,15 @@ class Projects::ApplicationController < ApplicationController ...@@ -40,13 +40,15 @@ class Projects::ApplicationController < ApplicationController
(current_user && current_user.already_forked?(project)) (current_user && current_user.already_forked?(project))
end end
def authorize_project!(action) def authorize_action!(action)
return access_denied! unless can?(current_user, action, project) unless can?(current_user, action, project)
return access_denied!
end
end end
def method_missing(method_sym, *arguments, &block) def method_missing(method_sym, *arguments, &block)
if method_sym.to_s =~ /\Aauthorize_(.*)!\z/ if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
authorize_project!($1.to_sym) authorize_action!($1.to_sym)
else else
super super
end end
......
...@@ -73,15 +73,18 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -73,15 +73,18 @@ class Projects::BranchesController < Projects::ApplicationController
def destroy def destroy
@branch_name = Addressable::URI.unescape(params[:id]) @branch_name = Addressable::URI.unescape(params[:id])
status = DeleteBranchService.new(project, current_user).execute(@branch_name) result = DeleteBranchService.new(project, current_user).execute(@branch_name)
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to namespace_project_branches_path(@project.namespace, flash_type = result[:status] == :error ? :alert : :notice
@project), status: 303 flash[flash_type] = result[:message]
redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303
end end
format.js { render nothing: true, status: status[:return_code] } format.js { render nothing: true, status: result[:return_code] }
format.json { render json: { message: status[:message] }, status: status[:return_code] } format.json { render json: { message: result[:message] }, status: result[:return_code] }
end end
end end
......
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace] before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
layout 'project' layout 'project'
def index def index
...@@ -28,7 +32,12 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -28,7 +32,12 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def cancel_all def cancel_all
@project.builds.running_or_pending.each(&:cancel) return access_denied! unless can?(current_user, :update_build, project)
@project.builds.running_or_pending.each do |build|
build.cancel if can?(current_user, :update_build, build)
end
redirect_to namespace_project_builds_path(project.namespace, project) redirect_to namespace_project_builds_path(project.namespace, project)
end end
...@@ -107,8 +116,13 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -107,8 +116,13 @@ class Projects::BuildsController < Projects::ApplicationController
private private
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, build)
end
def build def build
@build ||= project.builds.find_by!(id: params[:id]).present(current_user: current_user) @build ||= project.builds.find(params[:id])
.present(current_user: current_user)
end end
def build_path(build) def build_path(build)
......
...@@ -235,7 +235,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -235,7 +235,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue def issue
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
...@@ -274,21 +274,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -274,21 +274,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
# Since iids are implemented only in 6.1
# user may navigate to issue page using old global ids.
#
# To prevent 404 errors we provide a redirect to correct iids until 7.0 release
#
def redirect_old
issue = @project.issues.find_by(id: params[:id])
if issue
redirect_to issue_path(issue)
else
raise ActiveRecord::RecordNotFound.new
end
end
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(
:title, :position, :description, :confidential, :weight, :title, :position, :description, :confidential, :weight,
......
...@@ -432,7 +432,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -432,7 +432,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
metrics_url = metrics_url =
if can?(current_user, :read_environment, environment) && environment.has_metrics? if can?(current_user, :read_environment, environment) && environment.has_metrics?
metrics_namespace_project_environment_path(environment.project.namespace, metrics_namespace_project_environment_deployment_path(environment.project.namespace,
environment.project, environment.project,
environment, environment,
deployment) deployment)
......
class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create, :edit, :take_ownership, :update]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action :schedule, only: [:edit, :update, :destroy, :take_ownership]
def index
@scope = params[:scope]
@all_schedules = PipelineSchedulesFinder.new(@project).execute
@schedules = PipelineSchedulesFinder.new(@project).execute(scope: params[:scope])
.includes(:last_pipeline)
end
def new
@schedule = project.pipeline_schedules.new
end
def create
@schedule = Ci::CreatePipelineScheduleService
.new(@project, current_user, schedule_params)
.execute
if @schedule.persisted?
redirect_to pipeline_schedules_path(@project)
else
render :new
end
end
def edit
end
def update
if schedule.update(schedule_params)
redirect_to namespace_project_pipeline_schedules_path(@project.namespace.becomes(Namespace), @project)
else
render :edit
end
end
def take_ownership
if schedule.update(owner: current_user)
redirect_to pipeline_schedules_path(@project)
else
redirect_to pipeline_schedules_path(@project), alert: "Failed to change the owner"
end
end
def destroy
if schedule.destroy
redirect_to pipeline_schedules_path(@project)
else
redirect_to pipeline_schedules_path(@project), alert: "Failed to remove the pipeline schedule"
end
end
private
def schedule
@schedule ||= project.pipeline_schedules.find(params[:id])
end
def schedule_params
params.require(:schedule)
.permit(:description, :cron, :cron_timezone, :ref, :active)
end
end
...@@ -8,6 +8,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -8,6 +8,8 @@ class Projects::PipelinesController < Projects::ApplicationController
wrap_parameters Ci::Pipeline wrap_parameters Ci::Pipeline
POLLING_INTERVAL = 10_000
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
...@@ -31,7 +33,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -31,7 +33,7 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
...@@ -57,15 +59,25 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -57,15 +59,25 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipeline = Ci::CreatePipelineService @pipeline = Ci::CreatePipelineService
.new(project, current_user, create_params) .new(project, current_user, create_params)
.execute(ignore_skip_ci: true, save_on_errors: false) .execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
if @pipeline.persisted?
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
else
render 'new' render 'new'
return
end end
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
end end
def show def show
respond_to do |format|
format.html
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
render json: PipelineSerializer
.new(project: @project, current_user: @current_user)
.represent(@pipeline, grouped: true)
end
end
end end
def builds def builds
......
...@@ -48,7 +48,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -48,7 +48,7 @@ class Projects::TagsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
if result[:status] == :success if result[:status] == :success
format.html do format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project) redirect_to namespace_project_tags_path(@project.namespace, @project), status: 303
end end
format.js format.js
...@@ -57,7 +57,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -57,7 +57,7 @@ class Projects::TagsController < Projects::ApplicationController
format.html do format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project), redirect_to namespace_project_tags_path(@project.namespace, @project),
alert: @error alert: @error, status: 303
end end
format.js do format.js do
......
class PipelineSchedulesFinder
attr_reader :project, :pipeline_schedules
def initialize(project)
@project = project
@pipeline_schedules = project.pipeline_schedules
end
def execute(scope: nil)
scoped_schedules =
case scope
when 'active'
pipeline_schedules.active
when 'inactive'
pipeline_schedules.inactive
else
pipeline_schedules
end
scoped_schedules.order(id: :desc)
end
end
...@@ -18,7 +18,7 @@ module BlobHelper ...@@ -18,7 +18,7 @@ module BlobHelper
blob = options.delete(:blob) blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil blob ||= project.repository.blob_at(ref, path) rescue nil
return unless blob return unless blob && blob.readable_text?
common_classes = "btn js-edit-blob #{options[:extra_class]}" common_classes = "btn js-edit-blob #{options[:extra_class]}"
......
module BranchesHelper module BranchesHelper
def can_remove_branch?(project, branch_name)
if ProtectedBranch.protected?(project, branch_name)
false
elsif branch_name == project.repository.root_ref
false
else
can?(current_user, :push_code, project)
end
end
def filter_branches_path(options = {}) def filter_branches_path(options = {})
exist_opts = { exist_opts = {
search: params[:search], search: params[:search],
......
...@@ -100,17 +100,15 @@ module CommitsHelper ...@@ -100,17 +100,15 @@ module CommitsHelper
end end
def link_to_browse_code(project, commit) def link_to_browse_code(project, commit)
return unless current_controller?(:projects, :commits)
if @path.blank? if @path.blank?
return link_to( return link_to(
"Browse Files", "Browse Files",
namespace_project_tree_path(project.namespace, project, commit), namespace_project_tree_path(project.namespace, project, commit),
class: "btn btn-default" class: "btn btn-default"
) )
end elsif @repo.blob_at(commit.id, @path)
return unless current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to( return link_to(
"Browse File", "Browse File",
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
......
...@@ -221,6 +221,26 @@ module GitlabRoutingHelper ...@@ -221,6 +221,26 @@ module GitlabRoutingHelper
end end
end end
# Pipeline Schedules
def pipeline_schedules_path(project, *args)
namespace_project_pipeline_schedules_path(project.namespace, project, *args)
end
def pipeline_schedule_path(schedule, *args)
project = schedule.project
namespace_project_pipeline_schedule_path(project.namespace, project, schedule, *args)
end
def edit_pipeline_schedule_path(schedule)
project = schedule.project
edit_namespace_project_pipeline_schedule_path(project.namespace, project, schedule)
end
def take_ownership_pipeline_schedule_path(schedule, *args)
project = schedule.project
take_ownership_namespace_project_pipeline_schedule_path(project.namespace, project, schedule, *args)
end
# Settings # Settings
def project_settings_integrations_path(project, *args) def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args) namespace_project_settings_integrations_path(project.namespace, project, *args)
......
module PipelineSchedulesHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
name: timezone.name,
offset: timezone.utc_offset,
identifier: timezone.tzinfo.identifier
}
end
end
end
...@@ -109,7 +109,7 @@ module TreeHelper ...@@ -109,7 +109,7 @@ module TreeHelper
end end
def lock_file_link(project = @project, path = @path, html_options: {}) def lock_file_link(project = @project, path = @path, html_options: {})
return unless license_allows_file_locks? return unless license_allows_file_locks? && current_user
return if path.blank? return if path.blank?
path_lock = project.find_path_lock(path, downstream: true) path_lock = project.find_path_lock(path, downstream: true)
...@@ -139,12 +139,16 @@ module TreeHelper ...@@ -139,12 +139,16 @@ module TreeHelper
end end
end end
else else
if can?(current_user, :push_code, project) _lock_link(current_user, project, html_options: html_options)
html_options[:data] = { state: :lock } end
enabled_lock_link("Lock", '', html_options) end
else
disabled_lock_link("Lock", "You do not have permission to lock this", html_options) def _lock_link(user, project, html_options: {})
end if can?(current_user, :push_code, project)
html_options[:data] = { state: :lock }
enabled_lock_link("Lock", '', html_options)
else
disabled_lock_link("Lock", "You do not have permission to lock this", html_options)
end end
end end
......
...@@ -112,14 +112,9 @@ module Ci ...@@ -112,14 +112,9 @@ module Ci
end end
def play(current_user) def play(current_user)
# Try to queue a current build Ci::PlayBuildService
if self.enqueue .new(project, current_user)
self.update(user: current_user) .execute(self)
self
else
# Otherwise we need to create a duplicate
Ci::Build.retry(self, current_user)
end
end end
def cancelable? def cancelable?
......
module Ci
##
# This domain model is a representation of a group of jobs that are related
# to each other, like `rspec 0 1`, `rspec 0 2`.
#
# It is not persisted in the database.
#
class Group
include StaticModel
attr_reader :stage, :name, :jobs
delegate :size, to: :jobs
def initialize(stage, name:, jobs:)
@stage = stage
@name = name
@jobs = jobs
end
def status
@status ||= commit_statuses.status
end
def detailed_status(current_user)
if jobs.one?
jobs.first.detailed_status(current_user)
else
Gitlab::Ci::Status::Group::Factory
.new(self, current_user).fabricate!
end
end
private
def commit_statuses
@commit_statuses ||= CommitStatus.where(id: jobs.map(&:id))
end
end
end
...@@ -9,6 +9,7 @@ module Ci ...@@ -9,6 +9,7 @@ module Ci
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
...@@ -17,6 +18,10 @@ module Ci ...@@ -17,6 +18,10 @@ module Ci
has_many :builds, foreign_key: :commit_id has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
has_many :merge_requests, foreign_key: "head_pipeline_id"
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
...@@ -380,14 +385,6 @@ module Ci ...@@ -380,14 +385,6 @@ module Ci
project.execute_services(data, :pipeline_hooks) project.execute_services(data, :pipeline_hooks)
end end
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
def merge_requests
@merge_requests ||= project.merge_requests
.where(source_branch: self.ref)
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end
# All the merge requests for which the current pipeline runs/ran against # All the merge requests for which the current pipeline runs/ran against
def all_merge_requests def all_merge_requests
@all_merge_requests ||= project.merge_requests.where(source_branch: ref) @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
......
module Ci module Ci
class TriggerSchedule < ActiveRecord::Base class PipelineSchedule < ActiveRecord::Base
extend Ci::Model extend Ci::Model
include Importable include Importable
acts_as_paranoid acts_as_paranoid
belongs_to :project belongs_to :project
belongs_to :trigger belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
validates :trigger, presence: { unless: :importing? }
validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? } validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
validates :ref, presence: { unless: :importing_or_inactive? } validates :ref, presence: { unless: :importing_or_inactive? }
validates :description, presence: true
before_save :set_next_run_at before_save :set_next_run_at
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
def owned_by?(current_user)
owner == current_user
end
def inactive?
!active?
end
def importing_or_inactive? def importing_or_inactive?
importing? || !active? importing? || inactive?
end end
def set_next_run_at def set_next_run_at
...@@ -32,7 +43,7 @@ module Ci ...@@ -32,7 +43,7 @@ module Ci
end end
def real_next_run( def real_next_run(
worker_cron: Settings.cron_jobs['trigger_schedule_worker']['cron'], worker_cron: Settings.cron_jobs['pipeline_schedule_worker']['cron'],
worker_time_zone: Time.zone.name) worker_time_zone: Time.zone.name)
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at) .next_time_from(next_run_at)
......
...@@ -15,6 +15,14 @@ module Ci ...@@ -15,6 +15,14 @@ module Ci
@warnings = warnings @warnings = warnings
end end
def groups
@groups ||= statuses.ordered.latest
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
end
end
def to_param def to_param
name name
end end
......
...@@ -8,14 +8,11 @@ module Ci ...@@ -8,14 +8,11 @@ module Ci
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
has_many :trigger_requests has_many :trigger_requests
has_one :trigger_schedule, dependent: :destroy
validates :token, presence: true, uniqueness: true validates :token, presence: true, uniqueness: true
before_validation :set_default_values before_validation :set_default_values
accepts_nested_attributes_for :trigger_schedule
def set_default_values def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank? self.token = SecureRandom.hex(15) if self.token.blank?
end end
...@@ -39,9 +36,5 @@ module Ci ...@@ -39,9 +36,5 @@ module Ci
def can_access_project? def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end end
def trigger_schedule
super || build_trigger_schedule(project: project)
end
end end
end end
...@@ -326,13 +326,14 @@ class Commit ...@@ -326,13 +326,14 @@ class Commit
end end
def raw_diffs(*args) def raw_diffs(*args)
# NOTE: This feature is intentionally disabled until use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
# https://gitlab.com/gitlab-org/gitaly/issues/178 is resolved deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
# if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
# Gitlab::GitalyClient::Commit.diff_from_parent(self, *args) if use_gitaly && !deltas_only
# else Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
raw.diffs(*args) else
# end raw.diffs(*args)
end
end end
delegate :deltas, to: :raw, prefix: :raw delegate :deltas, to: :raw, prefix: :raw
......
...@@ -142,12 +142,6 @@ class CommitStatus < ActiveRecord::Base ...@@ -142,12 +142,6 @@ class CommitStatus < ActiveRecord::Base
canceled? && auto_canceled_by_id? canceled? && auto_canceled_by_id?
end end
# Added in 9.0 to keep backward compatibility for projects exported in 8.17
# and prior.
def gl_project_id
'dummy'
end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Factory Gitlab::Ci::Status::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -78,6 +78,8 @@ module Mentionable ...@@ -78,6 +78,8 @@ module Mentionable
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author) def referenced_mentionables(current_user = self.author)
return [] unless matches_cross_reference_regex?
refs = all_references(current_user) refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits) refs = (refs.issues + refs.merge_requests + refs.commits)
...@@ -87,6 +89,20 @@ module Mentionable ...@@ -87,6 +89,20 @@ module Mentionable
refs.reject { |ref| ref == local_reference } refs.reject { |ref| ref == local_reference }
end end
# Uses regex to quickly determine if mentionables might be referenced
# Allows heavy processing to be skipped
def matches_cross_reference_regex?
reference_pattern = if !project || project.default_issues_tracker?
ReferenceRegexes::DEFAULT_PATTERN
else
ReferenceRegexes::EXTERNAL_PATTERN
end
self.class.mentionable_attrs.any? do |attr, _|
__send__(attr) =~ reference_pattern
end
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
def create_cross_references!(author = self.author, without = []) def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author) refs = referenced_mentionables(author)
......
module Mentionable
module ReferenceRegexes
def self.reference_pattern(link_patterns, issue_pattern)
Regexp.union(link_patterns,
issue_pattern,
Commit.reference_pattern,
MergeRequest.reference_pattern)
end
DEFAULT_PATTERN = begin
issue_pattern = Issue.reference_pattern
link_patterns = Regexp.union([Issue, Commit, MergeRequest].map(&:link_reference_pattern))
reference_pattern(link_patterns, issue_pattern)
end
EXTERNAL_PATTERN = begin
issue_pattern = ExternalIssue.reference_pattern
link_patterns = URI.regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern)
end
end
end
...@@ -85,8 +85,8 @@ class Deployment < ActiveRecord::Base ...@@ -85,8 +85,8 @@ class Deployment < ActiveRecord::Base
end end
def stop_action def stop_action
return nil unless on_stop.present? return unless on_stop.present?
return nil unless manual_actions return unless manual_actions
@stop_action ||= manual_actions.find_by(name: on_stop) @stop_action ||= manual_actions.find_by(name: on_stop)
end end
......
...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
has_one :merge_request_diff, has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') } -> { order('merge_request_diffs.id DESC') }
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
has_many :events, as: :target, dependent: :destroy has_many :events, as: :target, dependent: :destroy
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
...@@ -887,12 +889,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -887,12 +889,6 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0 diverged_commits_count > 0
end end
def head_pipeline
return unless diff_head_sha && source_project
@head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines def all_pipelines
return Ci::Pipeline.none unless source_project return Ci::Pipeline.none unless source_project
......
...@@ -184,6 +184,8 @@ class Project < ActiveRecord::Base ...@@ -184,6 +184,8 @@ class Project < ActiveRecord::Base
has_many :remote_mirrors, inverse_of: :project, dependent: :destroy has_many :remote_mirrors, inverse_of: :project, dependent: :destroy
has_many :environments, dependent: :destroy has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy has_many :deployments, dependent: :destroy
has_many :pipeline_schedules, dependent: :destroy, class_name: 'Ci::PipelineSchedule'
has_many :path_locks, dependent: :destroy has_many :path_locks, dependent: :destroy
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
......
...@@ -150,7 +150,7 @@ class ChatNotificationService < Service ...@@ -150,7 +150,7 @@ class ChatNotificationService < Service
def notify_for_ref?(data) def notify_for_ref?(data)
return true if data[:object_attributes][:tag] return true if data[:object_attributes][:tag]
return true unless notify_only_default_branch return true unless notify_only_default_branch?
data[:object_attributes][:ref] == project.default_branch data[:object_attributes][:ref] == project.default_branch
end end
......
...@@ -42,6 +42,17 @@ class User < ActiveRecord::Base ...@@ -42,6 +42,17 @@ class User < ActiveRecord::Base
devise :lockable, :recoverable, :rememberable, :trackable, devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable :validatable, :omniauthable, :confirmable, :registerable
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
def update_tracked_fields!(request)
update_tracked_fields(request)
lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain
save(validate: false)
end
attr_accessor :force_random_password attr_accessor :force_random_password
# Virtual attribute for authenticating by either username or email # Virtual attribute for authenticating by either username or email
......
...@@ -97,6 +97,10 @@ class BasePolicy ...@@ -97,6 +97,10 @@ class BasePolicy
rules rules
end end
def rules
raise NotImplementedError
end
def delegate!(new_subject) def delegate!(new_subject)
@rule_set.merge(Ability.allowed(@user, new_subject)) @rule_set.merge(Ability.allowed(@user, new_subject))
end end
......
module Ci module Ci
class BuildPolicy < CommitStatusPolicy class BuildPolicy < CommitStatusPolicy
alias_method :build, :subject
def rules def rules
super super
...@@ -8,6 +10,20 @@ module Ci ...@@ -8,6 +10,20 @@ module Ci
%w[read create update admin].each do |rule| %w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end end
if can?(:update_build) && protected_action?
cannot! :update_build
end
end
private
def protected_action?
return false unless build.action?
!::Gitlab::UserAccess
.new(user, project: build.project)
.can_push_to_branch?(build.ref)
end end
end end
end end
module Ci module Ci
class PipelinePolicy < BuildPolicy class PipelinePolicy < BasePolicy
def rules
delegate! @subject.project
end
end end
end end
module Ci
class PipelineSchedulePolicy < PipelinePolicy
end
end
class EnvironmentPolicy < BasePolicy class EnvironmentPolicy < BasePolicy
alias_method :environment, :subject
def rules def rules
delegate! @subject.project delegate! environment.project
if can?(:create_deployment) && environment.stop_action?
can! :stop_environment if can_play_stop_action?
end
end
private
def can_play_stop_action?
Ability.allowed?(user, :update_build, environment.stop_action)
end end
end end
...@@ -52,6 +52,7 @@ class ProjectPolicy < BasePolicy ...@@ -52,6 +52,7 @@ class ProjectPolicy < BasePolicy
if project.public_builds? if project.public_builds?
can! :read_pipeline can! :read_pipeline
can! :read_pipeline_schedule
can! :read_build can! :read_build
end end
end end
...@@ -70,6 +71,7 @@ class ProjectPolicy < BasePolicy ...@@ -70,6 +71,7 @@ class ProjectPolicy < BasePolicy
can! :read_build can! :read_build
can! :read_container_image can! :read_container_image
can! :read_pipeline can! :read_pipeline
can! :read_pipeline_schedule
can! :read_environment can! :read_environment
can! :read_deployment can! :read_deployment
can! :read_merge_request can! :read_merge_request
...@@ -94,6 +96,8 @@ class ProjectPolicy < BasePolicy ...@@ -94,6 +96,8 @@ class ProjectPolicy < BasePolicy
can! :update_build can! :update_build
can! :create_pipeline can! :create_pipeline
can! :update_pipeline can! :update_pipeline
can! :create_pipeline_schedule
can! :update_pipeline_schedule
can! :create_merge_request can! :create_merge_request
can! :create_wiki can! :create_wiki
can! :push_code can! :push_code
...@@ -107,6 +111,7 @@ class ProjectPolicy < BasePolicy ...@@ -107,6 +111,7 @@ class ProjectPolicy < BasePolicy
def master_access! def master_access!
can! :push_code_to_protected_branches can! :push_code_to_protected_branches
can! :delete_protected_branch
can! :update_project_snippet can! :update_project_snippet
can! :update_environment can! :update_environment
can! :update_deployment can! :update_deployment
...@@ -120,6 +125,7 @@ class ProjectPolicy < BasePolicy ...@@ -120,6 +125,7 @@ class ProjectPolicy < BasePolicy
can! :admin_build can! :admin_build
can! :admin_container_image can! :admin_container_image
can! :admin_pipeline can! :admin_pipeline
can! :admin_pipeline_schedule
can! :admin_environment can! :admin_environment
can! :admin_deployment can! :admin_deployment
can! :admin_pages can! :admin_pages
...@@ -135,6 +141,7 @@ class ProjectPolicy < BasePolicy ...@@ -135,6 +141,7 @@ class ProjectPolicy < BasePolicy
can! :fork_project can! :fork_project
can! :read_commit_status can! :read_commit_status
can! :read_pipeline can! :read_pipeline
can! :read_pipeline_schedule
can! :read_container_image can! :read_container_image
can! :build_download_code can! :build_download_code
can! :build_read_container_image can! :build_read_container_image
...@@ -183,6 +190,7 @@ class ProjectPolicy < BasePolicy ...@@ -183,6 +190,7 @@ class ProjectPolicy < BasePolicy
cannot! :create_merge_request cannot! :create_merge_request
cannot! :push_code cannot! :push_code
cannot! :push_code_to_protected_branches cannot! :push_code_to_protected_branches
cannot! :delete_protected_branch
cannot! :update_merge_request cannot! :update_merge_request
cannot! :admin_merge_request cannot! :admin_merge_request
end end
...@@ -223,6 +231,7 @@ class ProjectPolicy < BasePolicy ...@@ -223,6 +231,7 @@ class ProjectPolicy < BasePolicy
unless project.feature_available?(:builds, user) && repository_enabled unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build)) cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:pipeline_schedule))
cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment)) cannot!(*named_abilities(:deployment))
end end
...@@ -230,6 +239,7 @@ class ProjectPolicy < BasePolicy ...@@ -230,6 +239,7 @@ class ProjectPolicy < BasePolicy
unless repository_enabled unless repository_enabled
cannot! :push_code cannot! :push_code
cannot! :push_code_to_protected_branches cannot! :push_code_to_protected_branches
cannot! :delete_protected_branch
cannot! :download_code cannot! :download_code
cannot! :fork_project cannot! :fork_project
cannot! :read_commit_status cannot! :read_commit_status
...@@ -302,6 +312,7 @@ class ProjectPolicy < BasePolicy ...@@ -302,6 +312,7 @@ class ProjectPolicy < BasePolicy
can! :read_merge_request can! :read_merge_request
can! :read_note can! :read_note
can! :read_pipeline can! :read_pipeline
can! :read_pipeline_schedule
can! :read_commit_status can! :read_commit_status
can! :read_container_image can! :read_container_image
can! :download_code can! :download_code
......
...@@ -13,4 +13,12 @@ class BuildActionEntity < Grape::Entity ...@@ -13,4 +13,12 @@ class BuildActionEntity < Grape::Entity
end end
expose :playable?, as: :playable expose :playable?, as: :playable
private
alias_method :build, :object
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
end end
...@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity ...@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity
path_to(:retry_namespace_project_build, build) path_to(:retry_namespace_project_build, build)
end end
expose :play_path, if: ->(build, _) { build.playable? } do |build| expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_build, build) path_to(:play_namespace_project_build, build)
end end
...@@ -25,11 +25,15 @@ class BuildEntity < Grape::Entity ...@@ -25,11 +25,15 @@ class BuildEntity < Grape::Entity
alias_method :build, :object alias_method :build, :object
def path_to(route, build) def playable?
send("#{route}_path", build.project.namespace, build.project, build) build.playable? && can?(request.current_user, :update_build, build)
end end
def detailed_status def detailed_status
build.detailed_status(request.current_user) build.detailed_status(request.current_user)
end end
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
end end
class JobGroupEntity < Grape::Entity
include RequestAwareEntity
expose :name
expose :size
expose :detailed_status, as: :status, with: StatusEntity
expose :jobs, with: BuildEntity
private
alias_method :group, :object
def detailed_status
group.detailed_status(request.current_user)
end
end
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
expose :approvals_before_merge
expose :assignee_id expose :assignee_id
include RequestAwareEntity include RequestAwareEntity
......
...@@ -50,15 +50,15 @@ class PipelineEntity < Grape::Entity ...@@ -50,15 +50,15 @@ class PipelineEntity < Grape::Entity
end end
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? } expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
expose :retry_path, if: proc { can_retry? } do |pipeline| expose :retry_path, if: -> (*) { can_retry? } do |pipeline|
retry_namespace_project_pipeline_path(pipeline.project.namespace, retry_namespace_project_pipeline_path(pipeline.project.namespace,
pipeline.project, pipeline.project,
pipeline.id) pipeline.id)
end end
expose :cancel_path, if: proc { can_cancel? } do |pipeline| expose :cancel_path, if: -> (*) { can_cancel? } do |pipeline|
cancel_namespace_project_pipeline_path(pipeline.project.namespace, cancel_namespace_project_pipeline_path(pipeline.project.namespace,
pipeline.project, pipeline.project,
pipeline.id) pipeline.id)
......
...@@ -7,9 +7,11 @@ class StageEntity < Grape::Entity ...@@ -7,9 +7,11 @@ class StageEntity < Grape::Entity
"#{stage.name}: #{detailed_status.label}" "#{stage.name}: #{detailed_status.label}"
end end
expose :detailed_status, expose :groups,
as: :status, if: -> (_, opts) { opts[:grouped] },
with: StatusEntity with: JobGroupEntity
expose :detailed_status, as: :status, with: StatusEntity
expose :path do |stage| expose :path do |stage|
namespace_project_pipeline_path( namespace_project_pipeline_path(
......
...@@ -12,4 +12,11 @@ class StatusEntity < Grape::Entity ...@@ -12,4 +12,11 @@ class StatusEntity < Grape::Entity
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end end
expose :action, if: -> (status, _) { status.has_action? } do
expose :action_icon, as: :icon
expose :action_title, as: :title
expose :action_path, as: :path
expose :action_method, as: :method
end
end end
module Ci
class CreatePipelineScheduleService < BaseService
def execute
project.pipeline_schedules.create(pipeline_schedule_params)
end
private
def pipeline_schedule_params
params.merge(owner: current_user)
end
end
end
...@@ -2,7 +2,7 @@ module Ci ...@@ -2,7 +2,7 @@ module Ci
class CreatePipelineService < BaseService class CreatePipelineService < BaseService
attr_reader :pipeline attr_reader :pipeline
def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, mirror_update: false) def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, mirror_update: false)
@pipeline = Ci::Pipeline.new( @pipeline = Ci::Pipeline.new(
project: project, project: project,
ref: ref, ref: ref,
...@@ -10,7 +10,8 @@ module Ci ...@@ -10,7 +10,8 @@ module Ci
before_sha: before_sha, before_sha: before_sha,
tag: tag?, tag: tag?,
trigger_requests: Array(trigger_request), trigger_requests: Array(trigger_request),
user: current_user user: current_user,
pipeline_schedule: schedule
) )
unless project.builds_enabled? unless project.builds_enabled?
...@@ -49,8 +50,14 @@ module Ci ...@@ -49,8 +50,14 @@ module Ci
return error('No builds for this pipeline.') return error('No builds for this pipeline.')
end end
_create_pipeline
end
private
def _create_pipeline
Ci::Pipeline.transaction do Ci::Pipeline.transaction do
pipeline.save update_merge_requests_head_pipeline if pipeline.save
Ci::CreatePipelineBuildsService Ci::CreatePipelineBuildsService
.new(project, current_user) .new(project, current_user)
...@@ -62,8 +69,6 @@ module Ci ...@@ -62,8 +69,6 @@ module Ci
pipeline.tap(&:process!) pipeline.tap(&:process!)
end end
private
def skip_ci? def skip_ci?
return false unless pipeline.git_commit_message return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
...@@ -121,6 +126,11 @@ module Ci ...@@ -121,6 +126,11 @@ module Ci
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
end end
def update_merge_requests_head_pipeline
MergeRequest.where(source_branch: @pipeline.ref, source_project: @pipeline.project).
update_all(head_pipeline_id: @pipeline.id)
end
def error(message, save: false) def error(message, save: false)
pipeline.errors.add(:base, message) pipeline.errors.add(:base, message)
pipeline.drop if save pipeline.drop if save
......
...@@ -5,9 +5,8 @@ module Ci ...@@ -5,9 +5,8 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref). pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref).
execute(ignore_skip_ci: true, trigger_request: trigger_request) execute(ignore_skip_ci: true, trigger_request: trigger_request)
if pipeline.persisted?
trigger_request trigger_request if pipeline.persisted?
end
end end
end end
end end
module Ci
class PlayBuildService < ::BaseService
def execute(build)
unless can?(current_user, :update_build, build)
raise Gitlab::Access::AccessDeniedError
end
# Try to enqueue the build, otherwise create a duplicate.
#
if build.enqueue
build.tap { |action| action.update(user: current_user) }
else
Ci::Build.retry(build, current_user)
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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