Commit e5c43209 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce-to-ee-tue' into 'master'

CE upstream to EE master (Tue)

Closes #1369 and gitaly#173

See merge request !1628
parents 83c0e74e f03734f2
......@@ -13,9 +13,11 @@
},
"plugins": [
"filenames",
"import"
"import",
"html"
],
"settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"],
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
......
......@@ -367,3 +367,5 @@ gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.5.0'
gem 'toml-rb', '~> 0.3.15', require: false
......@@ -125,6 +125,7 @@ GEM
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
cliver (0.3.2)
coderay (1.1.1)
coercible (1.0.0)
......@@ -820,6 +821,8 @@ GEM
tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
tool (0.2.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
......@@ -1060,6 +1063,7 @@ DEPENDENCIES
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
toml-rb (~> 0.3.15)
truncato (~> 0.7.8)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
......
......@@ -73,11 +73,20 @@ These types of merge requests need special consideration:
and a dedicated team with front-end, back-end, and UX.
* **Small features**: any other feature request.
**Large features** must be with a maintainer **by the 1st**. It's OK if they
aren't completely done, but this allows the maintainer enough time to make the
decision about whether this can make it in before the freeze. If the maintainer
doesn't think it will make it, they should inform the developers working on it
and the Product Manager responsible for the feature.
**Large features** must be with a maintainer **by the 1st**. This means that:
* There is a merge request (even if it's WIP).
* The person (or people, if it needs a frontend and backend maintainer) who will
ultimately be responsible for merging this have been pinged on the MR.
It's OK if merge request isn't completely done, but this allows the maintainer
enough time to make the decision about whether this can make it in before the
freeze. If the maintainer doesn't think it will make it, they should inform the
developers working on it and the Product Manager responsible for the feature.
The maintainer can also choose to assign a reviewer to perform an initial
review, but this way the maintainer is unlikely to be surprised by receiving an
MR later in the cycle.
**Small features** must be with a reviewer (not necessarily maintainer) **by the
3rd**.
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */
/* global autosize */
import autosize from 'vendor/autosize';
var autosize = require('vendor/autosize');
$(() => {
const $fields = $('.js-autosize');
(function() {
$(function() {
var $fields;
$fields = $('.js-autosize');
$fields.on('autosize:resized', function() {
var $field;
$field = $(this);
return $field.data('height', $field.outerHeight());
});
$fields.on('resize.autosize', function() {
var $field;
$field = $(this);
if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight());
autosize.destroy($field);
return $field.css('max-height', window.outerHeight);
}
});
autosize($fields);
autosize.update($fields);
return $fields.css('resize', 'vertical');
$fields.on('autosize:resized', function resized() {
const $field = $(this);
$field.data('height', $field.outerHeight());
});
}).call(window);
$fields.on('resize.autosize', function resize() {
const $field = $(this);
if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight());
autosize.destroy($field);
$field.css('max-height', window.outerHeight);
}
});
autosize($fields);
autosize.update($fields);
$fields.css('resize', 'vertical');
});
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
var container;
container = $(this).closest(".js-details-container");
return container.toggleClass("open");
});
// Show details content. Hides link after click.
//
// %div
// %a.js-details-expand
// %div.js-details-content
//
return $("body").on("click", ".js-details-expand", function(e) {
$(this).next('.js-details-content').removeClass("hide");
$(this).hide();
var truncatedItem = $(this).siblings('.js-details-short');
if (truncatedItem.length) {
truncatedItem.addClass("hide");
}
return e.preventDefault();
});
$(() => {
$('body').on('click', '.js-details-target', function target() {
$(this).closest('.js-details-container').toggleClass('open');
});
}).call(window);
// Show details content. Hides link after click.
//
// %div
// %a.js-details-expand
// %div.js-details-content
//
$('body').on('click', '.js-details-expand', function expand(e) {
e.preventDefault();
$(this).next('.js-details-content').removeClass('hide');
$(this).hide();
const truncatedItem = $(this).siblings('.js-details-short');
if (truncatedItem.length) {
truncatedItem.addClass('hide');
}
});
});
import './autosize';
import './bind_in_out';
import './details_behavior';
import { installGlEmojiElement } from './gl_emoji';
import './quick_submit';
import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, max-len */
import '../commons/bootstrap';
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted.
//
import '../commons/bootstrap';
//
// ### Example Markup
//
......@@ -17,61 +14,59 @@ import '../commons/bootstrap';
// <input type="submit" value="Submit" />
// </form>
//
(function() {
var isMac, keyCodeIs;
isMac = function() {
return navigator.userAgent.match(/Macintosh/);
};
function isMac() {
return navigator.userAgent.match(/Macintosh/);
}
keyCodeIs = function(e, keyCode) {
if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) {
return false;
}
return e.keyCode === keyCode;
};
function keyCodeIs(e, keyCode) {
if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) {
return false;
}
return e.keyCode === keyCode;
}
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
var $form, $submit_button;
// Enter
if (!keyCodeIs(e, 13)) {
return;
}
if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) {
return;
}
e.preventDefault();
$form = $(e.target).closest('form');
$submit_button = $form.find('input[type=submit], button[type=submit]');
if ($submit_button.attr('disabled')) {
return;
}
$submit_button.disable();
return $form.submit();
});
$(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
// Enter
if (!keyCodeIs(e, 13)) {
return;
}
const onlyMeta = e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey;
const onlyCtrl = e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey;
if (!onlyMeta && !onlyCtrl) {
return;
}
e.preventDefault();
const $form = $(e.target).closest('form');
const $submitButton = $form.find('input[type=submit], button[type=submit]');
if (!$submitButton.attr('disabled')) {
$submitButton.disable();
$form.submit();
}
});
// If the user tabs to a submit button on a `js-quick-submit` form, display a
// tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function displayTooltip(e) {
// Tab
if (!keyCodeIs(e, 9)) {
return;
}
const $this = $(this);
const title = isMac() ?
'You can also press &#8984;-Enter' :
'You can also press Ctrl-Enter';
// If the user tabs to a submit button on a `js-quick-submit` form, display a
// tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
var $this, title;
// Tab
if (!keyCodeIs(e, 9)) {
return;
}
if (isMac()) {
title = "You can also press &#8984;-Enter";
} else {
title = "You can also press Ctrl-Enter";
}
$this = $(this);
return $this.tooltip({
container: 'body',
html: 'true',
placement: 'auto top',
title: title,
trigger: 'manual'
}).tooltip('show').one('blur', function() {
return $this.tooltip('hide');
});
$this.tooltip({
container: 'body',
html: 'true',
placement: 'auto top',
title,
trigger: 'manual',
});
}).call(window);
$this.tooltip('show').one('blur', () => $this.tooltip('hide'));
});
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, max-len */
import '../commons/bootstrap';
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values.
//
import '../commons/bootstrap';
//
// ### Example Markup
//
......@@ -14,49 +12,43 @@ import '../commons/bootstrap';
// <input type="submit" value="Submit">
// </form>
//
(function() {
$.fn.requiresInput = function() {
var $button, $form, fieldSelector, requireInput, required;
$form = $(this);
$button = $('button[type=submit], input[type=submit]', $form);
required = '[required=required]';
fieldSelector = "input" + required + ", select" + required + ", textarea" + required;
requireInput = function() {
var values;
values = _.map($(fieldSelector, $form), function(field) {
// Collect the input values of *all* required fields
return field.value;
});
// Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) {
return $button.disable();
} else {
return $button.enable();
}
};
// Set initial button state
requireInput();
return $form.on('change input', fieldSelector, requireInput);
};
$(function() {
var $form, hideOrShowHelpBlock;
$form = $('form.js-requires-input');
$form.requiresInput();
// Hide or Show the help block when creating a new project
// based on the option selected
hideOrShowHelpBlock = function(form) {
var selected;
selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('options-parent') === 'groups') {
return form.find('.help-block').hide();
} else if (selected.length) {
return form.find('.help-block').show();
}
};
hideOrShowHelpBlock($form);
return $('.select2.js-select-namespace').change(function() {
return hideOrShowHelpBlock($form);
});
});
}).call(window);
$.fn.requiresInput = function requiresInput() {
const $form = $(this);
const $button = $('button[type=submit], input[type=submit]', $form);
const fieldSelector = 'input[required=required], select[required=required], textarea[required=required]';
function requireInput() {
// Collect the input values of *all* required fields
const values = _.map($(fieldSelector, $form), field => field.value);
// Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) {
$button.disable();
} else {
$button.enable();
}
}
// Set initial button state
requireInput();
$form.on('change input', fieldSelector, requireInput);
};
// Hide or Show the help block when creating a new project
// based on the option selected
function hideOrShowHelpBlock(form) {
const selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('options-parent') === 'groups') {
form.find('.help-block').hide();
} else if (selected.length) {
form.find('.help-block').show();
}
}
$(() => {
const $form = $('form.js-requires-input');
$form.requiresInput();
hideOrShowHelpBlock($form);
$('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form));
});
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
var toggleContainer = function(container, /* optional */toggleState) {
var $container = $(container);
$container
.find('.js-toggle-button .fa')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
$container
.find('.js-toggle-content')
.toggle(toggleState);
};
// Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
// %button.js-toggle-button
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault();
}
});
// If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container!
var hash = w.gl.utils.getLocationHash();
var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container');
if (container) {
toggleContainer(container, true);
anchor.scrollIntoView();
// Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
// %button.js-toggle-button
// %div.js-toggle-content
//
$(() => {
function toggleContainer(container, toggleState) {
const $container = $(container);
$container
.find('.js-toggle-button .fa')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
$container
.find('.js-toggle-content')
.toggle(toggleState);
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault();
}
});
})(window);
// If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container!
const hash = window.gl.utils.getLocationHash();
const anchor = hash && document.getElementById(hash);
const container = anchor && $(anchor).closest('.js-toggle-container');
if (container) {
toggleContainer(container, true);
anchor.scrollIntoView();
}
});
......@@ -42,6 +42,10 @@ $(() => {
Store.create();
// hack to allow sidebar scripts like milestone_select manipulate the BoardsStore
gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args);
gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args);
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
......
......@@ -90,6 +90,8 @@ window.Build = (function () {
success: ((log) => {
const $buildContainer = $('.js-build-output');
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) {
this.state = log.state;
}
......
......@@ -12,20 +12,18 @@ Vue.use(VueResource);
* Renders Pipelines table in pipelines tab in the commits show view.
*/
// export for use in merge_request_tabs.js (TODO: remove this hack)
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
$(() => {
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
if (gl.commits.PipelinesTableBundle) {
document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el);
gl.commits.PipelinesTableBundle.$destroy(true);
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
}
});
......@@ -4,8 +4,8 @@ import PipelinesTableComponent from '../../vue_shared/components/pipelines_table
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub';
import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state';
import EmptyState from '../../vue_pipelines_index/components/empty_state.vue';
import ErrorState from '../../vue_pipelines_index/components/error_state.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
......
......@@ -39,6 +39,7 @@
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import Group from './group';
import GroupName from './group_name';
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
......@@ -285,8 +286,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'groups:create':
case 'admin:groups:create':
BindInOut.initAll();
case 'groups:new':
case 'admin:groups:new':
new Group();
new GroupAvatar();
break;
case 'groups:edit':
case 'admin:groups:edit':
new GroupAvatar();
......
export default class Group {
constructor() {
this.groupPath = $('#group_path');
this.groupName = $('#group_name');
this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this);
if (this.groupName.val() === '') {
this.groupPath.on('keyup', this.updateHandler);
this.groupName.on('keydown', this.resetHandler);
}
}
update() {
this.groupName.val(this.groupPath.val());
}
reset() {
this.groupPath.off('keyup', this.updateHandler);
this.groupName.off('keydown', this.resetHandler);
}
}
......@@ -37,14 +37,7 @@ import './shortcuts_issuable';
import './shortcuts_network';
// behaviors
import './behaviors/autosize';
import './behaviors/details_behavior';
import './behaviors/quick_submit';
import './behaviors/requires_input';
import './behaviors/toggler_behavior';
import './behaviors/bind_in_out';
import { installGlEmojiElement } from './behaviors/gl_emoji';
installGlEmojiElement();
import './behaviors/';
// blob
import './blob/create_branch_dropdown';
......
......@@ -3,9 +3,6 @@
/* global Flash */
import Cookies from 'js-cookie';
import CommitPipelinesTable from './commit/pipelines/pipelines_table';
import './breakpoints';
import './flash';
......@@ -234,7 +231,7 @@ import './flash';
}
mountPipelinesView() {
this.commitPipelinesTable = new CommitPipelinesTable().$mount();
this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount();
// $mount(el) replaces the el with the new rendered component. We need it in order to mount
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
document.querySelector('#commit-pipeline-table-view')
......
......@@ -85,7 +85,7 @@ Vue.component('approvals-body', {
:disabled='approving'
@click='approveMergeRequest'
class='btn btn-primary approve-btn'>
Approve Merge Request
Approve merge request
</button>
</div>
</div>
......
......@@ -2,8 +2,6 @@
/* global Issuable */
/* global ListMilestone */
import Vue from 'vue';
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) {
......@@ -173,12 +171,12 @@ import Vue from 'vue';
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
}));
} else {
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
gl.issueBoards.boardStoreIssueDelete('milestone');
}
$dropdown.trigger('loading.gl.dropdown');
......
import Vue from 'vue';
(() => {
class Subscription {
constructor(containerElm) {
......@@ -29,8 +27,7 @@ import Vue from 'vue';
// hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) {
Vue.set(
gl.issueBoards.BoardsStore.detail.issue,
gl.issueBoards.boardStoreIssueSet(
'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed,
);
......
......@@ -2,8 +2,6 @@
/* global Issuable */
/* global ListUser */
import Vue from 'vue';
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
......@@ -74,7 +72,7 @@ import Vue from 'vue';
e.preventDefault();
if ($dropdown.hasClass('js-issue-board-sidebar')) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: _this.currentUser.id,
username: _this.currentUser.username,
name: _this.currentUser.name,
......@@ -225,14 +223,14 @@ import Vue from 'vue';
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id,
username: user.username,
name: user.name,
avatar_url: user.avatar_url
}));
} else {
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
gl.issueBoards.boardStoreIssueDelete('assignee');
}
updateIssueBoardsIssue();
......
<script>
/* eslint-disable no-new, no-alert */
/* global Flash */
import '~/flash';
......@@ -65,29 +66,31 @@ export default {
this.isLoading = true;
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<button
type="button"
@click="onClick"
:class="buttonClass"
:title="title"
:aria-label="title"
data-container="body"
data-placement="top"
:disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/>
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
</button>
`,
};
</script>
<template>
<button
type="button"
@click="onClick"
:class="buttonClass"
:title="title"
:aria-label="title"
data-container="body"
data-placement="top"
:disabled="isLoading"
>
<i :class="iconClass" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i>
</button>
</template>
import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
export default {
props: {
helpPagePath: {
type: String,
required: true,
},
},
template: `
<div class="row empty-state">
<div class="col-xs-12">
<div class="svg-content">
${pipelinesEmptyStateSVG}
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>Build with confidence</h4>
<p>
Continous Integration can help catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.
</p>
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
</a>
</div>
</div>
</div>
`,
};
<script>
import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
export default {
props: {
helpPagePath: {
type: String,
required: true,
},
},
data: () => ({ pipelinesEmptyStateSVG }),
};
</script>
<template>
<div class="row empty-state">
<div class="col-xs-12">
<div class="svg-content" v-html="pipelinesEmptyStateSVG" />
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>Build with confidence</h4>
<p>
Continous Integration can help catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.
</p>
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
</a>
</div>
</div>
</div>
</template>
import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
export default {
template: `
<div class="row empty-state js-pipelines-error-state">
<div class="col-xs-12">
<div class="svg-content">
${pipelinesErrorStateSVG}
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
</div>
</div>
</div>
`,
};
<script>
import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
export default {
data: () => ({ pipelinesErrorStateSVG }),
};
</script>
<template>
<div class="row empty-state js-pipelines-error-state">
<div class="col-xs-12">
<div class="svg-content" v-html="pipelinesErrorStateSVG" />
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
</div>
</div>
</div>
</template>
......@@ -4,8 +4,8 @@ import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
import EmptyState from './components/empty_state';
import ErrorState from './components/error_state';
import EmptyState from './components/empty_state.vue';
import ErrorState from './components/error_state.vue';
import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls';
import Poll from '../lib/utils/poll';
......
/* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button.vue';
import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
......
......@@ -31,7 +31,7 @@
svg {
width: 20px;
height: auto;
height: 20px;
fill: $gl-text-color-secondary;
}
......
......@@ -19,7 +19,7 @@ ul.notes {
svg {
width: 18px;
height: auto;
height: 18px;
fill: $gray-darkest;
position: absolute;
left: 30px;
......
......@@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController
else
@builds
end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30)
end
......
......@@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController
end
def create
@trigger = project.triggers.create(create_params.merge(owner: current_user))
@trigger = project.triggers.create(trigger_params.merge(owner: current_user))
if @trigger.valid?
flash[:notice] = 'Trigger was created successfully.'
......@@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController
end
def update
if trigger.update(update_params)
if trigger.update(trigger_params)
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
else
render action: "edit"
......@@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger ||= project.triggers.find(params[:id]) || render_404
end
def create_params
params.require(:trigger).permit(:description)
end
def update_params
params.require(:trigger).permit(:description)
def trigger_params
params.require(:trigger).permit(
:description,
trigger_schedule_attributes: [:id, :active, :cron, :cron_timezone, :ref]
)
end
end
......@@ -7,7 +7,7 @@ module SystemNoteHelper
'closed' => 'icon_status_closed',
'time_tracking' => 'icon_stopwatch',
'assignee' => 'icon_user',
'title' => 'icon_pencil',
'title' => 'icon_edit',
'task' => 'icon_check_square_o',
'label' => 'icon_tags',
'cross_reference' => 'icon_random',
......
......@@ -31,7 +31,6 @@ module Ci
validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing?
after_create :refresh_build_status_cache
state_machine :status, initial: :created do
event :enqueue do
......@@ -351,7 +350,6 @@ module Ci
when 'manual' then block
end
end
refresh_build_status_cache
end
def predefined_variables
......@@ -393,10 +391,6 @@ module Ci
.fabricate!
end
def refresh_build_status_cache
Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
end
private
def pipeline_data
......
# This class is not backed by a table in the main database.
# It loads the latest Pipeline for the HEAD of a repository, and caches that
# in Redis.
module Ci
class PipelineStatus
attr_accessor :sha, :status, :project, :loaded
delegate :commit, to: :project
def self.load_for_project(project)
new(project).tap do |status|
status.load_status
end
end
def initialize(project, sha: nil, status: nil)
@project = project
@sha = sha
@status = status
end
def has_status?
loaded? && sha.present? && status.present?
end
def load_status
return if loaded?
if has_cache?
load_from_cache
else
load_from_commit
store_in_cache
end
self.loaded = true
end
def load_from_commit
return unless commit
self.sha = commit.sha
self.status = commit.status
end
# We only cache the status for the HEAD commit of a project
# This status is rendered in project lists
def store_in_cache_if_needed
return unless sha
return delete_from_cache unless commit
store_in_cache if commit.sha == self.sha
end
def load_from_cache
Gitlab::Redis.with do |redis|
self.sha, self.status = redis.hmget(cache_key, :sha, :status)
end
end
def store_in_cache
Gitlab::Redis.with do |redis|
redis.mapped_hmset(cache_key, { sha: sha, status: status })
end
end
def delete_from_cache
Gitlab::Redis.with do |redis|
redis.del(cache_key)
end
end
def has_cache?
Gitlab::Redis.with do |redis|
redis.exists(cache_key)
end
end
def loaded?
self.loaded
end
def cache_key
"projects/#{project.id}/build_status"
end
end
end
......@@ -14,6 +14,8 @@ module Ci
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
......@@ -37,5 +39,9 @@ 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
......@@ -8,15 +8,19 @@ module Ci
belongs_to :project
belongs_to :trigger
delegate :ref, to: :trigger
validates :trigger, presence: { unless: :importing? }
validates :cron, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, 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? }
before_save :set_next_run_at
scope :active, -> { where(active: true) }
def importing_or_inactive?
importing? || !active?
end
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
......@@ -26,5 +30,12 @@ module Ci
rescue ActiveRecord::RecordInvalid
update_attribute(:next_run_at, nil) # update without validation
end
def real_next_run(
worker_cron: Settings.cron_jobs['trigger_schedule_worker']['cron'],
worker_time_zone: Time.zone.name)
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at)
end
end
end
......@@ -68,7 +68,7 @@ module HasStatus
end
scope :created, -> { where(status: 'created') }
scope :relevant, -> { where.not(status: 'created') }
scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) }
scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
......
......@@ -1387,7 +1387,7 @@ class Project < ActiveRecord::Base
end
def pipeline_status
@pipeline_status ||= Ci::PipelineStatus.load_for_project(self)
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
def mark_import_as_failed(error_message)
......
......@@ -414,8 +414,6 @@ class Repository
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
expire_tags_cache
expire_branches_cache
end
# Runs code after a new commit has been pushed.
......
......@@ -101,8 +101,8 @@ class User < ActiveRecord::Base
has_many :award_emoji, dependent: :destroy
has_many :path_locks, dependent: :destroy
has_many :approvals, dependent: :destroy
has_many :approvers, dependent: :destroy
has_many :approvals, dependent: :destroy
has_many :approvers, dependent: :destroy
# Protected Branch Access
has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel
......
......@@ -10,6 +10,8 @@ module Ci
store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) }
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end
private
......
......@@ -35,6 +35,14 @@ module Projects
unless remove_legacy_registry_tags
raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end
unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator.')
end
unless remove_repository(wiki_path)
raise_error('Failed to remove wiki repository. Please try again or contact administrator.')
end
end
log_info("Project \"#{project.path_with_namespace}\" was removed")
......
......@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader
File.join(dynamic_path_segment, @secret)
end
def cache_dir
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
def model
project
end
......
......@@ -662,6 +662,7 @@
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
- if Gitlab::Geo.license_allows?
%fieldset
......
......@@ -10,6 +10,7 @@
= custom_icon("icon_code_fork")
.event-title
%span.author_name= link_to_author event
%span{ class: event.action_name }
- if event.target
= event.action_name
......
......@@ -2,6 +2,7 @@
= custom_icon("icon_status_open")
.event-title
%span.author_name= link_to_author event
%span{ class: event.action_name }
= event_action_name(event)
......
......@@ -2,6 +2,7 @@
= custom_icon("icon_comment_o")
.event-title
%span.author_name= link_to_author event
= event.action_name
= event_note_title_html(event)
......
......@@ -7,6 +7,7 @@
= custom_icon("icon_commit")
.event-title
%span.author_name= link_to_author event
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
......
......@@ -2,7 +2,7 @@
Project #{@project.name} was exported successfully.
%p
The project export can be downloaded from:
= link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do
= link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '' do
= @project.name_with_namespace + " export"
%p
The download link will expire in 24 hours.
......@@ -16,7 +16,7 @@
= render "home_panel"
- if current_user && can?(current_user, :download_code, @project)
%nav.project-stats.limit-container-width{ class: container_class }
%nav.project-stats{ class: container_class }
%ul.nav
%li
= link_to project_files_path(@project) do
......@@ -77,11 +77,11 @@
Set up auto deploy
- if @repository.commit
.limit-container-width{ class: container_class }
%div{ class: container_class }
.project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
.limit-container-width{ class: container_class }
%div{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
......
......@@ -8,4 +8,26 @@
.form-group
= f.label :key, "Description", class: "label-light"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
- if @trigger.persisted?
%hr
= f.fields_for :trigger_schedule do |schedule_fields|
= schedule_fields.hidden_field :id
.form-group
.checkbox
= schedule_fields.label :active do
= schedule_fields.check_box :active
%strong Schedule trigger (experimental)
.help-block
If checked, this trigger will be executed periodically according to cron and timezone.
= link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule')
.form-group
= schedule_fields.label :cron, "Cron", class: "label-light"
= schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *"
.form-group
= schedule_fields.label :cron, "Timezone", class: "label-light"
= schedule_fields.text_field :cron_timezone, class: "form-control", title: 'Timezone is required.', placeholder: "UTC"
.form-group
= schedule_fields.label :ref, "Branch or tag", class: "label-light"
= schedule_fields.text_field :ref, class: "form-control", title: 'Branch or tag is required.', placeholder: "master"
.help-block Existing branch name, tag
= f.submit btn_text, class: "btn btn-save"
......@@ -22,6 +22,8 @@
%th
%strong Last used
%th
%strong Next run at
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.append-bottom-default
......
......@@ -29,6 +29,12 @@
- else
Never
%td
- if trigger.trigger_schedule&.active?
= trigger.trigger_schedule.real_next_run
- else
Never
%td.text-right.trigger-actions
- take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?"
- revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
......
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('group')
- parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id)
- group_path = root_url
- group_path << parent.full_path + '/' if parent
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
Group name
.col-sm-10
= f.text_field :name, placeholder: 'open-source', class: 'form-control'
.form-group
= f.label :path, class: 'control-label' do
......@@ -20,7 +16,7 @@
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
title: 'Please choose a group name with no special characters.',
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent
= f.hidden_field :parent_id, value: parent.id
......@@ -33,6 +29,14 @@
%li It will change web url for access group and group projects.
%li It will change the git path to repositories under this group.
.form-group.group-name-holder
= f.label :name, class: 'control-label' do
Group name
.col-sm-10
= f.text_field :name, class: 'form-control',
required: true,
title: 'You can choose a descriptive name different from the path.'
.form-group.group-description-holder
= f.label :description, class: 'control-label'
.col-sm-10
......
......@@ -3,7 +3,7 @@ class TriggerScheduleWorker
include CronjobQueue
def perform
Ci::TriggerSchedule.where("next_run_at < ?", Time.now).find_each do |trigger_schedule|
Ci::TriggerSchedule.active.where("next_run_at < ?", Time.now).find_each do |trigger_schedule|
begin
Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project,
trigger_schedule.trigger,
......
---
title: Upgrade webpack to v2.3.3 and webpack-dev-server to v2.4.2
merge_request: 10552
author:
---
title: Keep webpack-dev-server process functional across branch changes
merge_request: 10581
author:
---
title: Add indication for closed or merged issuables in GFM
merge_request: 9462
author: Adam Buckland
---
title: Add a name field to the group form
merge_request: 9891
author: Douglas Lovell
---
title: Add UI for Trigger Schedule
merge_request: 10533
author: dosuken123
---
title: add support for .vue templates
merge_request: 10517
author:
---
title: Periodically clean up temporary upload files to recover storage space
merge_request: 9466
author: blackst0ne
---
title: Fix redundant cache expiration in Repository
merge_request: 10575
author: blackst0ne
---
title: Add spec for schema.rb
merge_request: 10580
author: blackst0ne
......@@ -344,3 +344,57 @@
:why: https://github.com/nodeca/pako/blob/master/LICENSE
:versions: []
:when: 2017-04-05 10:43:45.897720000 Z
- - :approve
- caniuse-db
- :who: Mike Greiling
:why: https://github.com/Fyrd/caniuse/blob/master/LICENSE
:versions: []
:when: 2017-04-07 16:05:14.185549000 Z
- - :approve
- domelementtype
- :who: Mike Greiling
:why: https://github.com/fb55/domelementtype/blob/master/LICENSE
:versions: []
:when: 2017-04-07 16:19:17.992640000 Z
- - :approve
- domhandler
- :who: Mike Greiling
:why: https://github.com/fb55/domhandler/blob/master/LICENSE
:versions: []
:when: 2017-04-07 16:19:19.628953000 Z
- - :approve
- domutils
- :who: Mike Greiling
:why: https://github.com/fb55/domutils/blob/master/LICENSE
:versions: []
:when: 2017-04-07 16:19:21.159356000 Z
- - :approve
- entities
- :who: Mike Greiling
:why: https://github.com/fb55/entities/blob/master/LICENSE
:versions: []
:when: 2017-04-07 16:19:23.900571000 Z
- - :approve
- ansi-html
- :who: Mike Greiling
:why: https://github.com/Tjatse/ansi-html/blob/master/LICENSE
:versions: []
:when: 2017-04-10 05:42:12.898178000 Z
- - :approve
- map-stream
- :who: Mike Greiling
:why: https://github.com/dominictarr/map-stream/blob/master/LICENCE
:versions: []
:when: 2017-04-10 06:27:52.269085000 Z
- - :approve
- pause-stream
- :who: Mike Greiling
:why: https://github.com/dominictarr/pause-stream/blob/master/LICENSE
:versions: []
:when: 2017-04-10 06:28:39.825894000 Z
- - :approve
- undefsafe
- :who: Mike Greiling
:why: https://github.com/remy/undefsafe/blob/master/LICENSE
:versions: []
:when: 2017-04-10 06:30:00.002555000 Z
......@@ -166,6 +166,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :push_access_levels, only: [:destroy]
end
end
resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy]
......
......@@ -6,6 +6,7 @@ var webpack = require('webpack');
var StatsPlugin = require('stats-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
var ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production';
......@@ -52,6 +53,7 @@ var config = {
users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
issue_show: './issue_show/index.js',
group: './group.js',
},
output: {
......@@ -67,13 +69,18 @@ var config = {
{
test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader'
loader: 'babel-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.svg$/,
use: 'raw-loader'
}, {
test: /\.(worker.js|pdf)$/,
loader: 'raw-loader',
},
{
test: /\.(worker\.js|pdf)$/,
exclude: /node_modules/,
loader: 'file-loader',
},
......@@ -186,6 +193,10 @@ if (IS_DEV_SERVER) {
inline: DEV_SERVER_LIVERELOAD
};
config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
);
}
if (WEBPACK_REPORT) {
......
class AddRefToCiTriggerSchedule < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_trigger_schedules, :ref, :string
end
end
class AddActiveToCiTriggerSchedule < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_trigger_schedules, :active, :boolean
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToNextRunAtAndActive < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_trigger_schedules, [:active, :next_run_at]
end
def down
remove_concurrent_index :ci_trigger_schedules, [:active, :next_run_at]
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# Remove all files from old custom carrierwave's cache directories.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9466
class RemoveOldCacheDirectories < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# FileUploader cache.
FileUtils.rm_rf(Dir[Rails.root.join('public', 'uploads', 'tmp', '*')])
end
def down
# Old cache is not supposed to be recoverable.
# So the down method is empty.
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170407140450) do
ActiveRecord::Schema.define(version: 20170408033905) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -359,8 +359,11 @@ ActiveRecord::Schema.define(version: 20170407140450) do
t.string "cron"
t.string "cron_timezone"
t.datetime "next_run_at"
t.string "ref"
t.boolean "active"
end
add_index "ci_trigger_schedules", ["active", "next_run_at"], name: "index_ci_trigger_schedules_on_active_and_next_run_at", using: :btree
add_index "ci_trigger_schedules", ["next_run_at"], name: "index_ci_trigger_schedules_on_next_run_at", using: :btree
add_index "ci_trigger_schedules", ["project_id"], name: "index_ci_trigger_schedules_on_project_id", using: :btree
......
......@@ -49,6 +49,10 @@ All technical content published by GitLab lives in the documentation, including:
- [Email](tools/email.md) Email GitLab users from GitLab
- [Push Rules](push_rules/push_rules.md) Advanced push rules for your project.
- [Help message](customization/help_message.md) Set information about administrators of your GitLab instance.
- [Changing the appearance of the login page](customization/branded_login_page.md) Make the login page branded for your GitLab instance.
- [Email](tools/email.md) Email GitLab users from GitLab
- [Push Rules](push_rules/push_rules.md) Advanced push rules for your project.
- [Help message](customization/help_message.md) Set information about administrators of your GitLab instance.
- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
- [Custom Git hooks](administration/custom_hooks.md) Custom Git hooks (on the filesystem) for when webhooks aren't enough.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
......@@ -69,6 +73,7 @@ All technical content published by GitLab lives in the documentation, including:
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Operations](administration/operations.md) Keeping GitLab up and running.
- [Polling](administration/polling.md) Configure how often the GitLab UI polls for updates
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
- [Reply by email](administration/reply_by_email.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
......
# Polling configuration
The GitLab UI polls for updates for different resources (issue notes, issue
titles, pipeline statuses, etc.) on a schedule appropriate to the resource.
In "Application settings -> Real-time features" you can configure "Polling
interval multiplier". This multiplier is applied to all resources at once,
and decimal values are supported. For the sake of the examples below, we will
say that issue notes poll every 2 seconds, and issue titles poll every 5
seconds; these are _not_ the actual values.
- 1 is the default, and recommended for most installations. (Issue notes poll
every 2 seconds, and issue titles poll every 5 seconds.)
- 0 will disable UI polling completely. (On the next poll, clients will stop
polling for updates.)
- A value greater than 1 will slow polling down. If you see issues with
database load from lots of clients polling for updates, increasing the
multiplier from 1 can be a good compromise, rather than disabling polling
completely. (For example: If this is set to 2, then issue notes poll every 4
seconds, and issue titles poll every 10 seconds.)
- A value between 0 and 1 will make the UI poll more frequently (so updates
will show in other sessions faster), but is **not recommended**. 1 should be
fast enough. (For example, if this is set to 0.5, then issue notes poll every
1 second, and issue titles poll every 2.5 seconds.)
......@@ -12,8 +12,8 @@ Thus, we must strike a balance between sending requests and the feeling of realt
Use the following rules when creating realtime solutions.
1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
Use that as your polling interval. This way it is easy for system administrators to change the
polling rate.
Use that as your polling interval. This way it is [easy for system administrators to change the
polling rate](../../administration/polling.md).
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
......
......@@ -51,5 +51,6 @@ request path. By doing this we avoid query parameter ordering problems and make
route matching easier.
For more information see:
- [`Poll-Interval` header](fe_guide/performance.md#realtime-components)
- [RFC 7232](https://tools.ietf.org/html/rfc7232)
- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926)
......@@ -25,6 +25,8 @@ To create a group:
1. Set the "Group path" which will be the namespace under which your projects
will be hosted (path can contain only letters, digits, underscores, dashes
and dots; it cannot start with dashes or end in dot).
1. The "Group name" will populate with the path. Optionally, you can change
it. This is the name that will display in the group views.
1. Optionally, you can add a description so that others can briefly understand
what this group is about.
1. Optionally, choose and avatar for your project.
......
......@@ -20,8 +20,8 @@ the hardware requirements.
- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install
GitLab on Google Cloud Platform using our official image.
- [Digital Ocean and Docker](digitaloceandocker.md) - Install GitLab quickly
on DigitalOcean using Docker.
- Testing only! [DigitalOcean and Docker Machine](digitaloceandocker.md) -
Quickly test any version of GitLab on DigitalOcean using Docker Machine.
## Database
......
# Digital Ocean and Docker
# Digital Ocean and Docker Machine test environment
## Warning. This guide is for quickly testing different versions of GitLab and
## not recommended for ease of future upgrades or keeping the data you create.
## Initial setup
......
......@@ -143,7 +143,7 @@ into the password field.
To disable two-factor authentication on your account (for example, if you
have lost your code generation device) you can:
* [Use a saved recovery code](#use-a-saved-recovery-code)
* [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-SSH)
* [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-ssh)
* [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account)
### Use a saved recovery code
......
......@@ -323,6 +323,5 @@ Merging only when needed prevents creating merge commits in your feature branch
## References
- [Sketch file](https://www.dropbox.com/s/58dvsj5votbwrzv/git_flows.sketch?dl=0) with vectors of images in this article
- [Git Flow by Vincent Driessen](http://nvie.com/posts/a-successful-git-branching-model/)
- [Blog post with responses](https://about.gitlab.com/2014/09/29/gitlab-flow/)
\ No newline at end of file
......@@ -11,9 +11,9 @@ You can create a group by going to the 'Groups' tab of the GitLab dashboard and
![Click the 'New group' button in the 'Groups' tab](groups/new_group_button.png)
Next, enter the name (required) and the optional description and group avatar.
Next, enter the path and name (required) and the optional description and group avatar.
![Fill in the name for your new group](groups/new_group_form.png)
![Fill in the path for your new group](groups/new_group_form.png)
When your group has been created you are presented with the group dashboard feed, which will be empty.
......
......@@ -111,7 +111,7 @@ There are four kinds of filters you can use on your Todos dashboard.
| Type | Filter by issue or merge request |
| Action | Filter by the action that triggered the Todo |
You can also filter by more than one of these at the same time.
You can also filter by more than one of these at the same time. The possible Actions are `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-todo).
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926
......@@ -611,7 +611,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click link "Approve"' do
page.within '.mr-state-widget' do
wait_for_ajax
click_button 'Approve Merge Request'
click_button 'Approve merge request'
end
end
......@@ -623,7 +623,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should not see merge button' do
page.within '.mr-state-widget' do
expect(page).not_to have_button('Accept merge request')
expect(page).not_to have_button('Accept merge mequest')
end
end
......
class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
include LoginHelpers
include GitlabRoutingHelper
include WaitForAjax
step 'I am on the Merge Request detail page' do
visit merge_request_path(@merge_request)
......@@ -20,10 +21,18 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'I should see the Remove Source Branch button' do
expect(page).to have_link('Remove source branch')
# Wait for AJAX requests to complete so they don't blow up if they are
# only handled after `DatabaseCleaner` has already run
wait_for_ajax
end
step 'I should not see the Remove Source Branch button' do
expect(page).not_to have_link('Remove source branch')
# Wait for AJAX requests to complete so they don't blow up if they are
# only handled after `DatabaseCleaner` has already run
wait_for_ajax
end
step 'There is an open Merge Request' do
......
......@@ -97,7 +97,7 @@ module SharedProject
step 'I should see project "Shop" activity feed' do
project = Project.find_by(name: "Shop")
expect(page).to have_content "pushed new branch fix at #{project.name_with_namespace}"
expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}"
end
step 'I should see project settings' do
......@@ -251,7 +251,8 @@ module SharedProject
step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop")
create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped'
pipeline = create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master'
pipeline.skip
end
step 'I should see last commit with CI status' do
......
module Banzai
module Filter
# HTML filter that appends state information to issuable links.
# Runs as a post-process filter as issuable state might change whilst
# Markdown is in the cache.
#
# This filter supports cross-project references.
class IssuableStateFilter < HTML::Pipeline::Filter
VISIBLE_STATES = %w(closed merged).freeze
def call
extractor = Banzai::IssuableExtractor.new(project, current_user)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
if VISIBLE_STATES.include?(issuable.state)
node.children.last.content += " [#{issuable.state}]"
end
end
doc
end
private
def current_user
context[:current_user]
end
def project
context[:project]
end
end
end
end
......@@ -7,7 +7,7 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
Redactor.new(project, current_user).redact([doc])
Redactor.new(project, current_user).redact([doc]) unless context[:skip_redaction]
doc
end
......
module Banzai
# Extract references to issuables from multiple documents
# This populates RequestStore cache used in Banzai::ReferenceParser::IssueParser
# and Banzai::ReferenceParser::MergeRequestParser
# Populating the cache should happen before processing documents one-by-one
# so we can avoid N+1 queries problem
class IssuableExtractor
QUERY = %q(
descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")]
[@data-reference-type="issue" or @data-reference-type="merge_request"]
).freeze
attr_reader :project, :user
def initialize(project, user)
@project = project
@user = user
end
# Returns Hash in the form { node => issuable_instance }
def extract(documents)
nodes = documents.flat_map do |document|
document.xpath(QUERY)
end
issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user)
merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user)
issue_parser.issues_for_nodes(nodes).merge(
merge_request_parser.merge_requests_for_nodes(nodes)
)
end
end
end
......@@ -31,7 +31,8 @@ module Banzai
#
# Returns the same input objects.
def render(objects, attribute)
documents = render_objects(objects, attribute)
documents = render_documents(objects, attribute)
documents = post_process_documents(documents, objects, attribute)
redacted = redact_documents(documents)
objects.each_with_index do |object, index|
......@@ -41,9 +42,24 @@ module Banzai
end
end
# Renders the attribute of every given object.
def render_objects(objects, attribute)
render_attributes(objects, attribute)
private
def render_documents(objects, attribute)
pipeline = HTML::Pipeline.new([])
objects.map do |object|
pipeline.to_document(Banzai.render_field(object, attribute))
end
end
def post_process_documents(documents, objects, attribute)
# Called here to populate cache, refer to IssuableExtractor docs
IssuableExtractor.new(project, user).extract(documents)
documents.zip(objects).map do |document, object|
context = context_for(object, attribute)
Banzai::Pipeline[:post_process].to_document(document, context)
end
end
# Redacts the list of documents.
......@@ -57,25 +73,15 @@ module Banzai
# Returns a Banzai context for the given object and attribute.
def context_for(object, attribute)
context = base_context.dup
context = context.merge(object.banzai_render_context(attribute))
context
end
# Renders the attributes of a set of objects.
#
# Returns an Array of `Nokogiri::HTML::Document`.
def render_attributes(objects, attribute)
objects.map do |object|
string = Banzai.render_field(object, attribute)
context = context_for(object, attribute)
Banzai::Pipeline[:relative_link].to_document(string, context)
end
base_context.merge(object.banzai_render_context(attribute))
end
def base_context
@base_context ||= @redaction_context.merge(current_user: user, project: project)
@base_context ||= @redaction_context.merge(
current_user: user,
project: project,
skip_redaction: true
)
end
end
end
......@@ -4,6 +4,7 @@ module Banzai
def self.filters
FilterArray[
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
Filter::RedactorFilter
]
end
......
......@@ -62,8 +62,7 @@ module Banzai
nodes.select do |node|
if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i
can_read_reference?(user, projects[node_id])
can_read_reference?(user, projects[node])
else
true
end
......@@ -112,12 +111,12 @@ module Banzai
per_project
end
# Returns a Hash containing objects for an attribute grouped per their
# IDs.
# Returns a Hash containing objects for an attribute grouped per the
# nodes that reference them.
#
# The returned Hash uses the following format:
#
# { id value => row }
# { node => row }
#
# nodes - An Array of HTML nodes to process.
#
......@@ -132,9 +131,14 @@ module Banzai
return {} if nodes.empty?
ids = unique_attribute_values(nodes, attribute)
rows = collection_objects_for_ids(collection, ids)
collection_objects = collection_objects_for_ids(collection, ids)
objects_by_id = collection_objects.index_by(&:id)
rows.index_by(&:id)
nodes.each_with_object({}) do |node, hash|
if node.has_attribute?(attribute)
hash[node] = objects_by_id[node.attr(attribute).to_i]
end
end
end
# Returns an Array containing all unique values of an attribute of the
......@@ -201,7 +205,7 @@ module Banzai
#
# The returned Hash uses the following format:
#
# { project ID => project }
# { node => project }
#
def projects_for_nodes(nodes)
@projects_for_nodes ||=
......
......@@ -13,14 +13,14 @@ module Banzai
issues_readable_by_user(issues.values, user).to_set
nodes.select do |node|
readable_issues.include?(issue_for_node(issues, node))
readable_issues.include?(issues[node])
end
end
def referenced_by(nodes)
issues = issues_for_nodes(nodes)
nodes.map { |node| issue_for_node(issues, node) }.uniq
nodes.map { |node| issues[node] }.compact.uniq
end
def issues_for_nodes(nodes)
......@@ -44,12 +44,6 @@ module Banzai
self.class.data_attribute
)
end
private
def issue_for_node(issues, node)
issues[node.attr(self.class.data_attribute).to_i]
end
end
end
end
......@@ -3,14 +3,41 @@ module Banzai
class MergeRequestParser < BaseParser
self.reference_type = :merge_request
def references_relation
MergeRequest.includes(:author, :assignee, :target_project)
def nodes_visible_to_user(user, nodes)
merge_requests = merge_requests_for_nodes(nodes)
nodes.select do |node|
merge_request = merge_requests[node]
merge_request && can?(user, :read_merge_request, merge_request.project)
end
end
private
def referenced_by(nodes)
merge_requests = merge_requests_for_nodes(nodes)
nodes.map { |node| merge_requests[node] }.compact.uniq
end
def can_read_reference?(user, ref_project)
can?(user, :read_merge_request, ref_project)
def merge_requests_for_nodes(nodes)
@merge_requests_for_nodes ||= grouped_objects_for_nodes(
nodes,
MergeRequest.includes(
:author,
:assignee,
{
# These associations are primarily used for checking permissions.
# Eager loading these ensures we don't end up running dozens of
# queries in this process.
target_project: [
{ namespace: :owner },
{ group: [:owners, :group_members] },
:invited_groups,
:project_members
]
}),
self.class.data_attribute
)
end
end
end
......
......@@ -49,7 +49,7 @@ module Banzai
# Check if project belongs to a group which
# user can read.
def can_read_group_reference?(node, user, groups)
node_group = groups[node.attr('data-group').to_i]
node_group = groups[node]
node_group && can?(user, :read_group, node_group)
end
......@@ -74,8 +74,8 @@ module Banzai
if project && project_id && project.id == project_id.to_i
true
elsif project_id && user_id
project = projects[project_id.to_i]
user = users[user_id.to_i]
project = projects[node]
user = users[node]
project && user ? project.team.member?(user) : false
else
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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