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:
# Trigger a package build on omnibus-gitlab repository
build-package:
before_script: []
services: []
variables:
SETUP_DB: "false"
......@@ -153,17 +154,7 @@ build-package:
stage: build
when: manual
script:
# If no branch in omnibus is specified, trigger pipeline against master
- 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
- scripts/trigger-build
# Prepare and merge knapsack tests
knapsack:
......
......@@ -377,6 +377,6 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.5.0'
gem 'gitaly', '~> 0.6.0'
gem 'toml-rb', '~> 0.3.15', require: false
......@@ -287,7 +287,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.5.0)
gitaly (0.6.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -462,7 +462,7 @@ GEM
rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2)
logging (2.1.0)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.0.3)
......@@ -955,7 +955,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.5.0)
gitaly (~> 0.6.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
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 @@
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal';
import Group from './group';
import GroupName from './group_name';
import GroupsList from './groups_list';
......@@ -51,6 +52,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
......@@ -186,6 +188,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break;
case 'projects:branches:index':
gl.AjaxLoadingSpinner.init();
new DeleteModal();
break;
case 'projects:issues:new':
case 'projects:issues:edit':
......@@ -267,7 +270,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({
new Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
......
......@@ -65,6 +65,7 @@ class GlFieldError {
this.state = {
valid: false,
empty: true,
submitted: false,
};
this.initFieldValidation();
......@@ -108,9 +109,10 @@ class GlFieldError {
const currentValue = this.accessCurrentValue();
this.state.valid = false;
this.state.empty = currentValue === '';
this.state.submitted = true;
this.renderValidity();
this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup
this.inputElement.off('keyup.fieldValidator')
.on('keyup.fieldValidator', this.updateValidity.bind(this));
......
......@@ -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 () {
const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
firstInvalid.inputElement.focus();
......
......@@ -31,24 +31,21 @@
*
* ### How to use
*
* new window.gl.LinkedTabs({
* new LinkedTabs({
* action: "#{controller.action_name}",
* defaultAction: 'tab1',
* parentEl: '.tab-links'
* });
*/
(() => {
window.gl = window.gl || {};
window.gl.LinkedTabs = class LinkedTabs {
export default class LinkedTabs {
/**
* Binds the events and activates de default tab.
*
* @param {Object} options
*/
constructor(options) {
this.options = options || {};
constructor(options = {}) {
this.options = options;
this.defaultAction = this.options.defaultAction;
this.action = this.options.action || this.defaultAction;
......@@ -108,5 +105,4 @@
activateTab() {
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
}
};
})();
}
This diff is collapsed.
......@@ -172,7 +172,7 @@ const normalizeNewlines = function(str) {
if ($textarea.val() !== '') {
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) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
......@@ -1132,8 +1132,8 @@ const normalizeNewlines = function(str) {
}
};
Notes.animateAppendNote = function(noteHTML, $notesList) {
const $note = window.$(noteHTML);
Notes.animateAppendNote = function(noteHtml, $notesList) {
const $note = $(noteHtml);
$note.addClass('fade-in-full').renderGFM();
$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');
((global) => {
class Pipelines {
export default class Pipelines {
constructor(options = {}) {
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
// eslint-disable-next-line no-new
new LinkedTabs(options.tabsOptions);
}
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns();
}
addMarginToBuildColumns() {
this.pipelineGraph = document.querySelector('.js-pipeline-graph');
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 @@
(function() {
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$enableApprovers = $('.js-require-approvals-toggle');
......
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';
};
Calendar.prototype.renderKey = function() {
var keyColors;
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) {
return _this.daySizeWithSpace * i;
};
})(this)).attr('y', 0).attr('fill', function(color) {
return color;
});
const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
this.svg.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0)
.attr('fill', color => color)
.attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body');
};
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 */
/* global Issuable */
/* global emitSidebarEvent */
import eventHub from './sidebar/event_hub';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
(function() {
const slice = [].slice;
......@@ -109,7 +111,7 @@ import eventHub from './sidebar/event_hub';
.find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`);
firstSelected.remove();
eventHub.$emit('sidebar.removeAssignee', {
emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId,
});
}
......@@ -329,7 +331,7 @@ import eventHub from './sidebar/event_hub';
defaultLabel: defaultLabel,
hidden: function(e) {
if ($dropdown.hasClass('js-multiselect')) {
eventHub.$emit('sidebar.saveAssignees');
emitSidebarEvent('sidebar.saveAssignees');
}
if (!$dropdown.data('always-show-selectbox')) {
......@@ -363,10 +365,10 @@ import eventHub from './sidebar/event_hub';
const id = parseInt(element.value, 10);
element.remove();
});
eventHub.$emit('sidebar.removeAllAssignees');
emitSidebarEvent('sidebar.removeAllAssignees');
} else if (isActive) {
// user selected
eventHub.$emit('sidebar.addAssignee', user);
emitSidebarEvent('sidebar.addAssignee', user);
// Remove unassigned selection (if it was previously selected)
const unassignedSelected = $dropdown.closest('.selectbox')
......@@ -382,7 +384,7 @@ import eventHub from './sidebar/event_hub';
}
// User unselected
eventHub.$emit('sidebar.removeAssignee', user);
emitSidebarEvent('sidebar.removeAssignee', user);
}
if (getSelected().find(u => u === gon.current_user_id)) {
......
......@@ -108,8 +108,6 @@ export default {
</div>
<mr-widget-memory-usage
v-if="deployment.metrics_url"
:mr="mr"
:service="service"
:metricsUrl="deployment.metrics_url"
/>
</div>
......
......@@ -5,8 +5,6 @@ import MRWidgetService from '../services/mr_widget_service';
export default {
name: 'MemoryUsage',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
metricsUrl: { type: String, required: true },
},
data() {
......@@ -14,6 +12,7 @@ export default {
// memoryFrom: 0,
// memoryTo: 0,
memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false,
loadFailed: false,
loadingMetrics: true,
......@@ -23,8 +22,22 @@ export default {
components: {
'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: {
computeGraphData(metrics) {
computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false;
const { memory_values } = metrics;
// if (memory_previous.length > 0) {
......@@ -38,21 +51,17 @@ export default {
if (memory_values.length > 0) {
this.hasMetrics = true;
this.memoryMetrics = memory_values[0].values;
this.deploymentTime = deploymentTime;
}
},
},
mounted() {
this.$props.loadingMetrics = true;
loadMetrics() {
gl.utils.backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.$props.metricsUrl)
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
} else {
stop(res);
}
......@@ -67,41 +76,48 @@ export default {
return res.json();
})
.then((res) => {
this.computeGraphData(res.metrics);
this.computeGraphData(res.metrics, res.deployment_time);
return res;
})
.catch(() => {
this.$props.loadFailed = true;
this.loadFailed = true;
this.loadingMetrics = false;
});
},
},
mounted() {
this.loadingMetrics = true;
this.loadMetrics();
},
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>
<p
v-if="loadingMetrics"
class="usage-info usage-info-loading">
v-if="shouldShowLoading"
class="usage-info js-usage-info usage-info-loading">
<i
class="fa fa-spinner fa-spin usage-info-load-spinner"
aria-hidden="true" />Loading deployment statistics.
</p>
<p
v-if="!hasMetrics && !loadingMetrics"
class="usage-info usage-info-loading">
Deployment statistics are not available currently.
</p>
<p
v-if="hasMetrics"
class="usage-info">
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
Deployment memory usage:
</p>
<p
v-if="loadFailed"
class="usage-info">
v-if="shouldShowLoadFailure"
class="usage-info js-usage-info usage-info-failed">
Failed to load deployment statistics.
</p>
<p
v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable">
Deployment statistics are not available currently.
</p>
<mr-memory-graph
v-if="hasMetrics"
v-if="shouldShowMemoryGraph"
:metrics="memoryMetrics"
:deploymentTime="deploymentTime"
height="25"
width="100" />
</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 SmartInterval } from '~/smart_interval';
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 {
name: 'MemoryGraph',
props: {
metrics: { type: Array, required: true },
deploymentTime: { type: Number, required: true },
width: { type: String, required: true },
height: { type: String, required: true },
},
......@@ -9,27 +10,105 @@ export default {
return {
pathD: '',
pathViewBox: '',
// dotX: '',
// dotY: '',
dotX: '',
dotY: '',
};
},
mounted() {
const renderData = this.$props.metrics.map(v => v[1]);
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 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}`;
// 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() {
this.renderGraph(this.deploymentTime, this.metrics);
},
template: `
<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" />
<!--<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>
</div>
`,
......
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
};
......@@ -4,13 +4,14 @@
*/
.file-holder {
border: 1px solid $border-color;
border-radius: $border-radius-default;
&.file-holder-no-border {
border: 0;
}
&.readme-holder {
margin: $gl-padding-top 0;
margin: $gl-padding 0;
}
table {
......@@ -25,7 +26,7 @@
text-align: left;
padding: 10px $gl-padding;
word-wrap: break-word;
border-radius: 3px 3px 0 0;
border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear {
padding-left: 0;
......@@ -94,9 +95,16 @@
tr {
border-bottom: 1px solid $blame-border;
&:last-child {
border-bottom: none;
}
}
td {
border-top: none;
border-bottom: none;
&:first-child {
border-left: none;
}
......@@ -107,7 +115,7 @@
}
td.blame-commit {
padding: 0 10px;
padding: 5px 10px;
min-width: 400px;
background: $gray-light;
}
......@@ -246,7 +254,7 @@ span.idiff {
border-bottom: 1px solid $border-color;
padding: 5px $gl-padding;
margin: 0;
border-radius: 3px 3px 0 0;
border-radius: $border-radius-default $border-radius-default 0 0;
.file-header-content {
white-space: nowrap;
......
......@@ -158,6 +158,7 @@ ul.content-list {
}
}
&.has-tooltip,
&:last-child {
margin-right: 0;
......
.memory-graph-container {
svg {
background: $white-light;
cursor: pointer;
&:hover {
box-shadow: 0 0 4px $gray-darkest inset;
}
}
path {
fill: none;
stroke: $blue-500;
stroke-width: 1px;
stroke-width: 2px;
}
circle {
stroke: $blue-700;
fill: $blue-700;
stroke-width: 4px;
}
}
......@@ -163,7 +163,7 @@ $fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
$border-radius-default: 2px;
$border-radius-default: 3px;
$settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500;
......
......@@ -163,7 +163,6 @@
.avatar-cell {
width: 46px;
padding-left: 10px;
img {
margin-right: 0;
......@@ -175,7 +174,6 @@
justify-content: space-between;
align-items: flex-start;
flex-grow: 1;
padding-left: 10px;
.merge-request-branches & {
flex-direction: column;
......
// Common
.diff-file {
border: 1px solid $border-color;
margin-bottom: $gl-padding;
border-radius: 3px;
.commit-short-id {
font-family: $regular_font;
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-flex-parent {
cursor: pointer;
......
......@@ -132,12 +132,6 @@
line-height: 16px;
}
@media (min-width: $screen-sm-min) {
.stage-cell {
padding: 0 4px;
}
}
@media (max-width: $screen-xs-max) {
order: 1;
margin-top: $gl-padding-top;
......@@ -183,8 +177,7 @@
}
&.mr-memory-usage {
margin-top: 10px;
margin-bottom: 10px;
margin: 5px 0 10px 25px;
}
}
......@@ -512,7 +505,12 @@
.mr-info-list.mr-memory-usage {
.legend {
height: 75%;
height: 65%;
top: 0;
@media (max-width: $screen-xs-max) {
height: 20px;
}
}
p {
......@@ -825,14 +823,16 @@
}
.mr-memory-usage {
p.usage-info-loading {
margin-bottom: 6px;
p.usage-info-loading,
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;
font-size: 16px;
}
}
@media (max-width: $screen-md-min) {
.mr-info-list.mr-memory-usage .legend {
......
......@@ -284,10 +284,6 @@ ul.notes {
}
}
.diff-header > span {
margin-right: 10px;
}
.line_content {
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 @@
.stage-cell {
font-size: 0;
padding: 10px 4px;
padding: 0 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
......@@ -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
&:first-child {
&::after {
......@@ -863,7 +835,8 @@
border-radius: 3px;
// build name
.ci-build-text {
.ci-build-text,
.ci-status-text {
font-weight: 200;
overflow: hidden;
white-space: nowrap;
......@@ -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
*/
......
......@@ -138,11 +138,12 @@
.blob-commit-info {
list-style: none;
background: $gray-light;
padding: 16px 16px 16px 6px;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
padding: 0;
}
.blob-content-holder {
margin-top: $gl-padding;
}
.blob-upload-dropzone-previews {
......
......@@ -47,7 +47,7 @@ module IssuableCollections
end
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
def issues_finder
......
......@@ -40,13 +40,15 @@ class Projects::ApplicationController < ApplicationController
(current_user && current_user.already_forked?(project))
end
def authorize_project!(action)
return access_denied! unless can?(current_user, action, project)
def authorize_action!(action)
unless can?(current_user, action, project)
return access_denied!
end
end
def method_missing(method_sym, *arguments, &block)
if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
authorize_project!($1.to_sym)
authorize_action!($1.to_sym)
else
super
end
......
......@@ -73,15 +73,18 @@ class Projects::BranchesController < Projects::ApplicationController
def destroy
@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|
format.html do
redirect_to namespace_project_branches_path(@project.namespace,
@project), status: 303
flash_type = result[:status] == :error ? :alert : :notice
flash[flash_type] = result[:message]
redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303
end
format.js { render nothing: true, status: status[:return_code] }
format.json { render json: { message: status[:message] }, status: status[:return_code] }
format.js { render nothing: true, status: result[:return_code] }
format.json { render json: { message: result[:message] }, status: result[:return_code] }
end
end
......
class Projects::BuildsController < Projects::ApplicationController
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'
def index
......@@ -28,7 +32,12 @@ class Projects::BuildsController < Projects::ApplicationController
end
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)
end
......@@ -107,8 +116,13 @@ class Projects::BuildsController < Projects::ApplicationController
private
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, build)
end
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
def build_path(build)
......
......@@ -235,7 +235,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
# 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
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
......@@ -274,21 +274,6 @@ class Projects::IssuesController < Projects::ApplicationController
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
params.require(:issue).permit(
:title, :position, :description, :confidential, :weight,
......
......@@ -432,7 +432,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
metrics_url =
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,
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
wrap_parameters Ci::Pipeline
POLLING_INTERVAL = 10_000
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
......@@ -31,7 +33,7 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
render json: {
pipelines: PipelineSerializer
......@@ -57,15 +59,25 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipeline = Ci::CreatePipelineService
.new(project, current_user, create_params)
.execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
render 'new'
return
end
if @pipeline.persisted?
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
else
render 'new'
end
end
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
def builds
......
......@@ -48,7 +48,7 @@ class Projects::TagsController < Projects::ApplicationController
respond_to do |format|
if result[:status] == :success
format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project)
redirect_to namespace_project_tags_path(@project.namespace, @project), status: 303
end
format.js
......@@ -57,7 +57,7 @@ class Projects::TagsController < Projects::ApplicationController
format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project),
alert: @error
alert: @error, status: 303
end
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
blob = options.delete(:blob)
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]}"
......
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 = {})
exist_opts = {
search: params[:search],
......
......@@ -100,17 +100,15 @@ module CommitsHelper
end
def link_to_browse_code(project, commit)
return unless current_controller?(:projects, :commits)
if @path.blank?
return link_to(
"Browse Files",
namespace_project_tree_path(project.namespace, project, commit),
class: "btn btn-default"
)
end
return unless current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
elsif @repo.blob_at(commit.id, @path)
return link_to(
"Browse File",
namespace_project_blob_path(project.namespace, project,
......
......@@ -221,6 +221,26 @@ module GitlabRoutingHelper
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
def project_settings_integrations_path(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
end
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?
path_lock = project.find_path_lock(path, downstream: true)
......@@ -139,6 +139,11 @@ module TreeHelper
end
end
else
_lock_link(current_user, project, html_options: html_options)
end
end
def _lock_link(user, project, html_options: {})
if can?(current_user, :push_code, project)
html_options[:data] = { state: :lock }
enabled_lock_link("Lock", '', html_options)
......@@ -146,7 +151,6 @@ module TreeHelper
disabled_lock_link("Lock", "You do not have permission to lock this", html_options)
end
end
end
def disabled_lock_link(label, title, html_options)
html_options['data-toggle'] = 'tooltip'
......
......@@ -112,14 +112,9 @@ module Ci
end
def play(current_user)
# Try to queue a current build
if self.enqueue
self.update(user: current_user)
self
else
# Otherwise we need to create a duplicate
Ci::Build.retry(self, current_user)
end
Ci::PlayBuildService
.new(project, current_user)
.execute(self)
end
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
belongs_to :project
belongs_to :user
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_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
......@@ -17,6 +18,10 @@ module Ci
has_many :builds, 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 :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'
......@@ -380,14 +385,6 @@ module Ci
project.execute_services(data, :pipeline_hooks)
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
def all_merge_requests
@all_merge_requests ||= project.merge_requests.where(source_branch: ref)
......
module Ci
class TriggerSchedule < ActiveRecord::Base
class PipelineSchedule < ActiveRecord::Base
extend Ci::Model
include Importable
acts_as_paranoid
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_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
validates :ref, presence: { unless: :importing_or_inactive? }
validates :description, presence: true
before_save :set_next_run_at
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?
importing? || !active?
importing? || inactive?
end
def set_next_run_at
......@@ -32,7 +43,7 @@ module Ci
end
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)
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at)
......
......@@ -15,6 +15,14 @@ module Ci
@warnings = warnings
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
name
end
......
......@@ -8,14 +8,11 @@ module Ci
belongs_to :owner, class_name: "User"
has_many :trigger_requests
has_one :trigger_schedule, dependent: :destroy
validates :token, presence: true, uniqueness: true
before_validation :set_default_values
accepts_nested_attributes_for :trigger_schedule
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
......@@ -39,9 +36,5 @@ module Ci
def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end
def trigger_schedule
super || build_trigger_schedule(project: project)
end
end
end
......@@ -326,13 +326,14 @@ class Commit
end
def raw_diffs(*args)
# NOTE: This feature is intentionally disabled until
# https://gitlab.com/gitlab-org/gitaly/issues/178 is resolved
# if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
# Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
# else
use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
if use_gitaly && !deltas_only
Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
else
raw.diffs(*args)
# end
end
end
delegate :deltas, to: :raw, prefix: :raw
......
......@@ -142,12 +142,6 @@ class CommitStatus < ActiveRecord::Base
canceled? && auto_canceled_by_id?
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)
Gitlab::Ci::Status::Factory
.new(self, current_user)
......
......@@ -78,6 +78,8 @@ module Mentionable
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author)
return [] unless matches_cross_reference_regex?
refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits)
......@@ -87,6 +89,20 @@ module Mentionable
refs.reject { |ref| ref == local_reference }
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+.
def create_cross_references!(author = self.author, without = [])
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
end
def stop_action
return nil unless on_stop.present?
return nil unless manual_actions
return unless on_stop.present?
return unless manual_actions
@stop_action ||= manual_actions.find_by(name: on_stop)
end
......
......@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
has_one :merge_request_diff,
-> { 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 :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
......@@ -887,12 +889,6 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
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
return Ci::Pipeline.none unless source_project
......
......@@ -184,6 +184,8 @@ class Project < ActiveRecord::Base
has_many :remote_mirrors, inverse_of: :project, dependent: :destroy
has_many :environments, 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 :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
......
......@@ -150,7 +150,7 @@ class ChatNotificationService < Service
def notify_for_ref?(data)
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
end
......
......@@ -42,6 +42,17 @@ class User < ActiveRecord::Base
devise :lockable, :recoverable, :rememberable, :trackable,
: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
# Virtual attribute for authenticating by either username or email
......
......@@ -97,6 +97,10 @@ class BasePolicy
rules
end
def rules
raise NotImplementedError
end
def delegate!(new_subject)
@rule_set.merge(Ability.allowed(@user, new_subject))
end
......
module Ci
class BuildPolicy < CommitStatusPolicy
alias_method :build, :subject
def rules
super
......@@ -8,6 +10,20 @@ module Ci
%w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
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
module Ci
class PipelinePolicy < BuildPolicy
class PipelinePolicy < BasePolicy
def rules
delegate! @subject.project
end
end
end
module Ci
class PipelineSchedulePolicy < PipelinePolicy
end
end
class EnvironmentPolicy < BasePolicy
alias_method :environment, :subject
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
......@@ -52,6 +52,7 @@ class ProjectPolicy < BasePolicy
if project.public_builds?
can! :read_pipeline
can! :read_pipeline_schedule
can! :read_build
end
end
......@@ -70,6 +71,7 @@ class ProjectPolicy < BasePolicy
can! :read_build
can! :read_container_image
can! :read_pipeline
can! :read_pipeline_schedule
can! :read_environment
can! :read_deployment
can! :read_merge_request
......@@ -94,6 +96,8 @@ class ProjectPolicy < BasePolicy
can! :update_build
can! :create_pipeline
can! :update_pipeline
can! :create_pipeline_schedule
can! :update_pipeline_schedule
can! :create_merge_request
can! :create_wiki
can! :push_code
......@@ -107,6 +111,7 @@ class ProjectPolicy < BasePolicy
def master_access!
can! :push_code_to_protected_branches
can! :delete_protected_branch
can! :update_project_snippet
can! :update_environment
can! :update_deployment
......@@ -120,6 +125,7 @@ class ProjectPolicy < BasePolicy
can! :admin_build
can! :admin_container_image
can! :admin_pipeline
can! :admin_pipeline_schedule
can! :admin_environment
can! :admin_deployment
can! :admin_pages
......@@ -135,6 +141,7 @@ class ProjectPolicy < BasePolicy
can! :fork_project
can! :read_commit_status
can! :read_pipeline
can! :read_pipeline_schedule
can! :read_container_image
can! :build_download_code
can! :build_read_container_image
......@@ -183,6 +190,7 @@ class ProjectPolicy < BasePolicy
cannot! :create_merge_request
cannot! :push_code
cannot! :push_code_to_protected_branches
cannot! :delete_protected_branch
cannot! :update_merge_request
cannot! :admin_merge_request
end
......@@ -223,6 +231,7 @@ class ProjectPolicy < BasePolicy
unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:pipeline_schedule))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
end
......@@ -230,6 +239,7 @@ class ProjectPolicy < BasePolicy
unless repository_enabled
cannot! :push_code
cannot! :push_code_to_protected_branches
cannot! :delete_protected_branch
cannot! :download_code
cannot! :fork_project
cannot! :read_commit_status
......@@ -302,6 +312,7 @@ class ProjectPolicy < BasePolicy
can! :read_merge_request
can! :read_note
can! :read_pipeline
can! :read_pipeline_schedule
can! :read_commit_status
can! :read_container_image
can! :download_code
......
......@@ -13,4 +13,12 @@ class BuildActionEntity < Grape::Entity
end
expose :playable?, as: :playable
private
alias_method :build, :object
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
end
......@@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity
path_to(:retry_namespace_project_build, build)
end
expose :play_path, if: ->(build, _) { build.playable? } do |build|
expose :play_path, if: -> (*) { playable? } do |build|
path_to(:play_namespace_project_build, build)
end
......@@ -25,11 +25,15 @@ class BuildEntity < Grape::Entity
alias_method :build, :object
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
def detailed_status
build.detailed_status(request.current_user)
end
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
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
expose :approvals_before_merge
expose :assignee_id
include RequestAwareEntity
......
......@@ -50,15 +50,15 @@ class PipelineEntity < Grape::Entity
end
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,
pipeline.project,
pipeline.id)
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,
pipeline.project,
pipeline.id)
......
......@@ -7,9 +7,11 @@ class StageEntity < Grape::Entity
"#{stage.name}: #{detailed_status.label}"
end
expose :detailed_status,
as: :status,
with: StatusEntity
expose :groups,
if: -> (_, opts) { opts[:grouped] },
with: JobGroupEntity
expose :detailed_status, as: :status, with: StatusEntity
expose :path do |stage|
namespace_project_pipeline_path(
......
......@@ -12,4 +12,11 @@ class StatusEntity < Grape::Entity
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
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
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
class CreatePipelineService < BaseService
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(
project: project,
ref: ref,
......@@ -10,7 +10,8 @@ module Ci
before_sha: before_sha,
tag: tag?,
trigger_requests: Array(trigger_request),
user: current_user
user: current_user,
pipeline_schedule: schedule
)
unless project.builds_enabled?
......@@ -49,8 +50,14 @@ module Ci
return error('No builds for this pipeline.')
end
_create_pipeline
end
private
def _create_pipeline
Ci::Pipeline.transaction do
pipeline.save
update_merge_requests_head_pipeline if pipeline.save
Ci::CreatePipelineBuildsService
.new(project, current_user)
......@@ -62,8 +69,6 @@ module Ci
pipeline.tap(&:process!)
end
private
def skip_ci?
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
......@@ -121,6 +126,11 @@ module Ci
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
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)
pipeline.errors.add(:base, message)
pipeline.drop if save
......
......@@ -5,9 +5,8 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref).
execute(ignore_skip_ci: true, trigger_request: trigger_request)
if pipeline.persisted?
trigger_request
end
trigger_request if pipeline.persisted?
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