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 @@ ...@@ -13,9 +13,11 @@
}, },
"plugins": [ "plugins": [
"filenames", "filenames",
"import" "import",
"html"
], ],
"settings": { "settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"],
"import/resolver": { "import/resolver": {
"webpack": { "webpack": {
"config": "./config/webpack.config.js" "config": "./config/webpack.config.js"
......
...@@ -367,3 +367,5 @@ gem 'sys-filesystem', '~> 1.1.6' ...@@ -367,3 +367,5 @@ gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.5.0' gem 'gitaly', '~> 0.5.0'
gem 'toml-rb', '~> 0.3.15', require: false
...@@ -125,6 +125,7 @@ GEM ...@@ -125,6 +125,7 @@ GEM
chronic_duration (0.10.6) chronic_duration (0.10.6)
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
citrus (3.0.2)
cliver (0.3.2) cliver (0.3.2)
coderay (1.1.1) coderay (1.1.1)
coercible (1.0.0) coercible (1.0.0)
...@@ -820,6 +821,8 @@ GEM ...@@ -820,6 +821,8 @@ GEM
tilt (2.0.6) tilt (2.0.6)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
tool (0.2.3) tool (0.2.3)
truncato (0.7.8) truncato (0.7.8)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
...@@ -1060,6 +1063,7 @@ DEPENDENCIES ...@@ -1060,6 +1063,7 @@ DEPENDENCIES
test_after_commit (~> 1.1) test_after_commit (~> 1.1)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
toml-rb (~> 0.3.15)
truncato (~> 0.7.8) truncato (~> 0.7.8)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
......
...@@ -73,11 +73,20 @@ These types of merge requests need special consideration: ...@@ -73,11 +73,20 @@ These types of merge requests need special consideration:
and a dedicated team with front-end, back-end, and UX. and a dedicated team with front-end, back-end, and UX.
* **Small features**: any other feature request. * **Small features**: any other feature request.
**Large features** must be with a maintainer **by the 1st**. It's OK if they **Large features** must be with a maintainer **by the 1st**. This means that:
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 * There is a merge request (even if it's WIP).
doesn't think it will make it, they should inform the developers working on it * The person (or people, if it needs a frontend and backend maintainer) who will
and the Product Manager responsible for the feature. 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 **Small features** must be with a reviewer (not necessarily maintainer) **by the
3rd**. 3rd**.
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */ import autosize from 'vendor/autosize';
/* global autosize */
var autosize = require('vendor/autosize'); $(() => {
const $fields = $('.js-autosize');
(function() { $fields.on('autosize:resized', function resized() {
$(function() { const $field = $(this);
var $fields; $field.data('height', $field.outerHeight());
$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; $fields.on('resize.autosize', function resize() {
$field = $(this); const $field = $(this);
if ($field.data('height') !== $field.outerHeight()) { if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight()); $field.data('height', $field.outerHeight());
autosize.destroy($field); autosize.destroy($field);
return $field.css('max-height', window.outerHeight); $field.css('max-height', window.outerHeight);
} }
}); });
autosize($fields); autosize($fields);
autosize.update($fields); autosize.update($fields);
return $fields.css('resize', 'vertical'); $fields.css('resize', 'vertical');
}); });
}).call(window);
/* 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 target() {
$("body").on("click", ".js-details-target", function() { $(this).closest('.js-details-container').toggleClass('open');
var container;
container = $(this).closest(".js-details-container");
return container.toggleClass("open");
}); });
// Show details content. Hides link after click. // Show details content. Hides link after click.
// //
// %div // %div
// %a.js-details-expand // %a.js-details-expand
// %div.js-details-content // %div.js-details-content
// //
return $("body").on("click", ".js-details-expand", function(e) { $('body').on('click', '.js-details-expand', function expand(e) {
$(this).next('.js-details-content').removeClass("hide"); e.preventDefault();
$(this).next('.js-details-content').removeClass('hide');
$(this).hide(); $(this).hide();
var truncatedItem = $(this).siblings('.js-details-short'); const truncatedItem = $(this).siblings('.js-details-short');
if (truncatedItem.length) { if (truncatedItem.length) {
truncatedItem.addClass("hide"); truncatedItem.addClass('hide');
} }
return e.preventDefault();
});
}); });
}).call(window); });
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 // Quick Submit behavior
// //
// When a child field of a form with a `js-quick-submit` class receives a // 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 // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted. // is submitted.
//
import '../commons/bootstrap';
// //
// ### Example Markup // ### Example Markup
// //
...@@ -17,61 +14,59 @@ import '../commons/bootstrap'; ...@@ -17,61 +14,59 @@ import '../commons/bootstrap';
// <input type="submit" value="Submit" /> // <input type="submit" value="Submit" />
// </form> // </form>
// //
(function() {
var isMac, keyCodeIs;
isMac = function() { function isMac() {
return navigator.userAgent.match(/Macintosh/); return navigator.userAgent.match(/Macintosh/);
}; }
keyCodeIs = function(e, keyCode) { function keyCodeIs(e, keyCode) {
if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) { if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) {
return false; return false;
} }
return e.keyCode === keyCode; return e.keyCode === keyCode;
}; }
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) { $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
var $form, $submit_button;
// Enter // Enter
if (!keyCodeIs(e, 13)) { if (!keyCodeIs(e, 13)) {
return; return;
} }
if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) {
const onlyMeta = e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey;
const onlyCtrl = e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey;
if (!onlyMeta && !onlyCtrl) {
return; return;
} }
e.preventDefault(); e.preventDefault();
$form = $(e.target).closest('form'); const $form = $(e.target).closest('form');
$submit_button = $form.find('input[type=submit], button[type=submit]'); const $submitButton = $form.find('input[type=submit], button[type=submit]');
if ($submit_button.attr('disabled')) {
return; if (!$submitButton.attr('disabled')) {
$submitButton.disable();
$form.submit();
} }
$submit_button.disable(); });
return $form.submit();
});
// If the user tabs to a submit button on a `js-quick-submit` form, display a // 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 // 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) { $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function displayTooltip(e) {
var $this, title;
// Tab // Tab
if (!keyCodeIs(e, 9)) { if (!keyCodeIs(e, 9)) {
return; return;
} }
if (isMac()) {
title = "You can also press &#8984;-Enter"; const $this = $(this);
} else { const title = isMac() ?
title = "You can also press Ctrl-Enter"; 'You can also press &#8984;-Enter' :
} 'You can also press Ctrl-Enter';
$this = $(this);
return $this.tooltip({ $this.tooltip({
container: 'body', container: 'body',
html: 'true', html: 'true',
placement: 'auto top', placement: 'auto top',
title: title, title,
trigger: 'manual' trigger: 'manual',
}).tooltip('show').one('blur', function() {
return $this.tooltip('hide');
});
}); });
}).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 // Requires Input behavior
// //
// When called on a form with input fields with the `required` attribute, the // 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. // form's submit button will be disabled until all required fields have values.
//
import '../commons/bootstrap';
// //
// ### Example Markup // ### Example Markup
// //
...@@ -14,49 +12,43 @@ import '../commons/bootstrap'; ...@@ -14,49 +12,43 @@ import '../commons/bootstrap';
// <input type="submit" value="Submit"> // <input type="submit" value="Submit">
// </form> // </form>
// //
(function() {
$.fn.requiresInput = function() { $.fn.requiresInput = function requiresInput() {
var $button, $form, fieldSelector, requireInput, required; const $form = $(this);
$form = $(this); const $button = $('button[type=submit], input[type=submit]', $form);
$button = $('button[type=submit], input[type=submit]', $form); const fieldSelector = 'input[required=required], select[required=required], textarea[required=required]';
required = '[required=required]';
fieldSelector = "input" + required + ", select" + required + ", textarea" + required; function requireInput() {
requireInput = function() {
var values;
values = _.map($(fieldSelector, $form), function(field) {
// Collect the input values of *all* required fields // Collect the input values of *all* required fields
return field.value; const values = _.map($(fieldSelector, $form), field => field.value);
});
// Disable the button if any required fields are empty // Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) { if (values.length && _.any(values, _.isEmpty)) {
return $button.disable(); $button.disable();
} else { } else {
return $button.enable(); $button.enable();
}
} }
};
// Set initial button state // Set initial button state
requireInput(); requireInput();
return $form.on('change input', fieldSelector, requireInput); $form.on('change input', fieldSelector, requireInput);
}; };
$(function() { // Hide or Show the help block when creating a new project
var $form, hideOrShowHelpBlock; // based on the option selected
$form = $('form.js-requires-input'); function hideOrShowHelpBlock(form) {
$form.requiresInput(); const selected = $('.js-select-namespace option:selected');
// 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') { if (selected.length && selected.data('options-parent') === 'groups') {
return form.find('.help-block').hide(); form.find('.help-block').hide();
} else if (selected.length) { } else if (selected.length) {
return form.find('.help-block').show(); form.find('.help-block').show();
} }
}; }
$(() => {
const $form = $('form.js-requires-input');
$form.requiresInput();
hideOrShowHelpBlock($form); hideOrShowHelpBlock($form);
return $('.select2.js-select-namespace').change(function() { $('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form));
return hideOrShowHelpBlock($form); });
});
});
}).call(window);
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) { // Toggle button. Show/hide content inside parent container.
$(function() { // Button does not change visibility. If button has icon - it changes chevron style.
var toggleContainer = function(container, /* optional */toggleState) { //
var $container = $(container); // %div.js-toggle-container
// %button.js-toggle-button
// %div.js-toggle-content
//
$(() => {
function toggleContainer(container, toggleState) {
const $container = $(container);
$container $container
.find('.js-toggle-button .fa') .find('.js-toggle-button .fa')
...@@ -12,16 +19,9 @@ ...@@ -12,16 +19,9 @@
$container $container
.find('.js-toggle-content') .find('.js-toggle-content')
.toggle(toggleState); .toggle(toggleState);
}; }
// Toggle button. Show/hide content inside parent container. $('body').on('click', '.js-toggle-button', function toggleButton(e) {
// 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')); toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase(); const targetTag = e.currentTarget.tagName.toLowerCase();
...@@ -32,13 +32,12 @@ ...@@ -32,13 +32,12 @@
// If we're accessing a permalink, ensure it is not inside a // If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container! // closed js-toggle-container!
var hash = w.gl.utils.getLocationHash(); const hash = window.gl.utils.getLocationHash();
var anchor = hash && document.getElementById(hash); const anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container'); const container = anchor && $(anchor).closest('.js-toggle-container');
if (container) { if (container) {
toggleContainer(container, true); toggleContainer(container, true);
anchor.scrollIntoView(); anchor.scrollIntoView();
} }
}); });
})(window);
...@@ -42,6 +42,10 @@ $(() => { ...@@ -42,6 +42,10 @@ $(() => {
Store.create(); 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({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
......
...@@ -90,6 +90,8 @@ window.Build = (function () { ...@@ -90,6 +90,8 @@ window.Build = (function () {
success: ((log) => { success: ((log) => {
const $buildContainer = $('.js-build-output'); const $buildContainer = $('.js-build-output');
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
} }
......
...@@ -12,20 +12,18 @@ Vue.use(VueResource); ...@@ -12,20 +12,18 @@ Vue.use(VueResource);
* Renders Pipelines table in pipelines tab in the commits show view. * 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 = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {}; 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'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); 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 ...@@ -4,8 +4,8 @@ import PipelinesTableComponent from '../../vue_shared/components/pipelines_table
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub'; import eventHub from '../../vue_pipelines_index/event_hub';
import EmptyState from '../../vue_pipelines_index/components/empty_state'; import EmptyState from '../../vue_pipelines_index/components/empty_state.vue';
import ErrorState from '../../vue_pipelines_index/components/error_state'; import ErrorState from '../../vue_pipelines_index/components/error_state.vue';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
......
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
import Issue from './issue'; import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import Group from './group';
import GroupName from './group_name'; import GroupName from './group_name';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
...@@ -285,8 +286,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -285,8 +286,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'groups:create': case 'groups:create':
case 'admin:groups:create': case 'admin:groups:create':
BindInOut.initAll(); BindInOut.initAll();
case 'groups:new': new Group();
case 'admin:groups:new': new GroupAvatar();
break;
case 'groups:edit': case 'groups:edit':
case 'admin:groups:edit': case 'admin:groups:edit':
new GroupAvatar(); 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'; ...@@ -37,14 +37,7 @@ import './shortcuts_issuable';
import './shortcuts_network'; import './shortcuts_network';
// behaviors // behaviors
import './behaviors/autosize'; import './behaviors/';
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();
// blob // blob
import './blob/create_branch_dropdown'; import './blob/create_branch_dropdown';
......
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import CommitPipelinesTable from './commit/pipelines/pipelines_table';
import './breakpoints'; import './breakpoints';
import './flash'; import './flash';
...@@ -234,7 +231,7 @@ import './flash'; ...@@ -234,7 +231,7 @@ import './flash';
} }
mountPipelinesView() { 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 // $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 // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
document.querySelector('#commit-pipeline-table-view') document.querySelector('#commit-pipeline-table-view')
......
...@@ -85,7 +85,7 @@ Vue.component('approvals-body', { ...@@ -85,7 +85,7 @@ Vue.component('approvals-body', {
:disabled='approving' :disabled='approving'
@click='approveMergeRequest' @click='approveMergeRequest'
class='btn btn-primary approve-btn'> class='btn btn-primary approve-btn'>
Approve Merge Request Approve merge request
</button> </button>
</div> </div>
</div> </div>
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
/* global Issuable */ /* global Issuable */
/* global ListMilestone */ /* global ListMilestone */
import Vue from 'vue';
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) { function MilestoneSelect(currentProject, els) {
...@@ -173,12 +171,12 @@ import Vue from 'vue'; ...@@ -173,12 +171,12 @@ import Vue from 'vue';
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1) { if (selected.id !== -1) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({ gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id, id: selected.id,
title: selected.name title: selected.name
})); }));
} else { } else {
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone'); gl.issueBoards.boardStoreIssueDelete('milestone');
} }
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
......
import Vue from 'vue';
(() => { (() => {
class Subscription { class Subscription {
constructor(containerElm) { constructor(containerElm) {
...@@ -29,8 +27,7 @@ import Vue from 'vue'; ...@@ -29,8 +27,7 @@ import Vue from 'vue';
// hack to allow this to work with the issue boards Vue object // hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) { if (document.querySelector('html').classList.contains('issue-boards-page')) {
Vue.set( gl.issueBoards.boardStoreIssueSet(
gl.issueBoards.BoardsStore.detail.issue,
'subscribed', 'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed, !gl.issueBoards.BoardsStore.detail.issue.subscribed,
); );
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
/* global Issuable */ /* global Issuable */
/* global ListUser */ /* global ListUser */
import Vue from 'vue';
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice; slice = [].slice;
...@@ -74,7 +72,7 @@ import Vue from 'vue'; ...@@ -74,7 +72,7 @@ import Vue from 'vue';
e.preventDefault(); e.preventDefault();
if ($dropdown.hasClass('js-issue-board-sidebar')) { 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, id: _this.currentUser.id,
username: _this.currentUser.username, username: _this.currentUser.username,
name: _this.currentUser.name, name: _this.currentUser.name,
...@@ -225,14 +223,14 @@ import Vue from 'vue'; ...@@ -225,14 +223,14 @@ import Vue from 'vue';
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) { if (user.id) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id, id: user.id,
username: user.username, username: user.username,
name: user.name, name: user.name,
avatar_url: user.avatar_url avatar_url: user.avatar_url
})); }));
} else { } else {
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee'); gl.issueBoards.boardStoreIssueDelete('assignee');
} }
updateIssueBoardsIssue(); updateIssueBoardsIssue();
......
<script>
/* eslint-disable no-new, no-alert */ /* eslint-disable no-new, no-alert */
/* global Flash */ /* global Flash */
import '~/flash'; import '~/flash';
...@@ -75,8 +76,10 @@ export default { ...@@ -75,8 +76,10 @@ export default {
}); });
}, },
}, },
};
</script>
template: ` <template>
<button <button
type="button" type="button"
@click="onClick" @click="onClick"
...@@ -85,9 +88,9 @@ export default { ...@@ -85,9 +88,9 @@ export default {
:aria-label="title" :aria-label="title"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
:disabled="isLoading"> :disabled="isLoading"
<i :class="iconClass" aria-hidden="true"/> >
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" /> <i :class="iconClass" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading"></i>
</button> </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'; ...@@ -4,8 +4,8 @@ import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination'; import TablePaginationComponent from '../vue_shared/components/table_pagination';
import EmptyState from './components/empty_state'; import EmptyState from './components/empty_state.vue';
import ErrorState from './components/error_state'; import ErrorState from './components/error_state.vue';
import NavigationTabs from './components/navigation_tabs'; import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls'; import NavigationControls from './components/nav_controls';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
......
/* eslint-disable no-param-reassign */ /* 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 PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../vue_pipelines_index/components/status'; import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
svg { svg {
width: 20px; width: 20px;
height: auto; height: 20px;
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
......
...@@ -19,7 +19,7 @@ ul.notes { ...@@ -19,7 +19,7 @@ ul.notes {
svg { svg {
width: 18px; width: 18px;
height: auto; height: 18px;
fill: $gray-darkest; fill: $gray-darkest;
position: absolute; position: absolute;
left: 30px; left: 30px;
......
...@@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController
else else
@builds @builds
end end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30) @builds = @builds.page(params[:page]).per(30)
end end
......
...@@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController
end end
def create 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? if @trigger.valid?
flash[:notice] = 'Trigger was created successfully.' flash[:notice] = 'Trigger was created successfully.'
...@@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController
end end
def update 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.' redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
else else
render action: "edit" render action: "edit"
...@@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger ||= project.triggers.find(params[:id]) || render_404 @trigger ||= project.triggers.find(params[:id]) || render_404
end end
def create_params def trigger_params
params.require(:trigger).permit(:description) params.require(:trigger).permit(
end :description,
trigger_schedule_attributes: [:id, :active, :cron, :cron_timezone, :ref]
def update_params )
params.require(:trigger).permit(:description)
end end
end end
...@@ -7,7 +7,7 @@ module SystemNoteHelper ...@@ -7,7 +7,7 @@ module SystemNoteHelper
'closed' => 'icon_status_closed', 'closed' => 'icon_status_closed',
'time_tracking' => 'icon_stopwatch', 'time_tracking' => 'icon_stopwatch',
'assignee' => 'icon_user', 'assignee' => 'icon_user',
'title' => 'icon_pencil', 'title' => 'icon_edit',
'task' => 'icon_check_square_o', 'task' => 'icon_check_square_o',
'label' => 'icon_tags', 'label' => 'icon_tags',
'cross_reference' => 'icon_random', 'cross_reference' => 'icon_random',
......
...@@ -31,7 +31,6 @@ module Ci ...@@ -31,7 +31,6 @@ module Ci
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
after_create :refresh_build_status_cache
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
...@@ -351,7 +350,6 @@ module Ci ...@@ -351,7 +350,6 @@ module Ci
when 'manual' then block when 'manual' then block
end end
end end
refresh_build_status_cache
end end
def predefined_variables def predefined_variables
...@@ -393,10 +391,6 @@ module Ci ...@@ -393,10 +391,6 @@ module Ci
.fabricate! .fabricate!
end end
def refresh_build_status_cache
Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
end
private private
def pipeline_data 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 ...@@ -14,6 +14,8 @@ module Ci
before_validation :set_default_values before_validation :set_default_values
accepts_nested_attributes_for :trigger_schedule
def set_default_values def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank? self.token = SecureRandom.hex(15) if self.token.blank?
end end
...@@ -37,5 +39,9 @@ module Ci ...@@ -37,5 +39,9 @@ module Ci
def can_access_project? def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end end
def trigger_schedule
super || build_trigger_schedule(project: project)
end
end end
end end
...@@ -8,15 +8,19 @@ module Ci ...@@ -8,15 +8,19 @@ module Ci
belongs_to :project belongs_to :project
belongs_to :trigger belongs_to :trigger
delegate :ref, to: :trigger
validates :trigger, presence: { unless: :importing? } validates :trigger, presence: { unless: :importing? }
validates :cron, cron: true, 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? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
validates :ref, presence: { unless: :importing? } validates :ref, presence: { unless: :importing_or_inactive? }
before_save :set_next_run_at before_save :set_next_run_at
scope :active, -> { where(active: true) }
def importing_or_inactive?
importing? || !active?
end
def set_next_run_at def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end end
...@@ -26,5 +30,12 @@ module Ci ...@@ -26,5 +30,12 @@ module Ci
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
update_attribute(:next_run_at, nil) # update without validation update_attribute(:next_run_at, nil) # update without validation
end 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
end end
...@@ -68,7 +68,7 @@ module HasStatus ...@@ -68,7 +68,7 @@ module HasStatus
end end
scope :created, -> { where(status: 'created') } scope :created, -> { where(status: 'created') }
scope :relevant, -> { where.not(status: 'created') } scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) }
scope :running, -> { where(status: 'running') } scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') } scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') } scope :success, -> { where(status: 'success') }
......
...@@ -1387,7 +1387,7 @@ class Project < ActiveRecord::Base ...@@ -1387,7 +1387,7 @@ class Project < ActiveRecord::Base
end end
def pipeline_status def pipeline_status
@pipeline_status ||= Ci::PipelineStatus.load_for_project(self) @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end end
def mark_import_as_failed(error_message) def mark_import_as_failed(error_message)
......
...@@ -414,8 +414,6 @@ class Repository ...@@ -414,8 +414,6 @@ class Repository
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_content_cache expire_content_cache
expire_tags_cache
expire_branches_cache
end end
# Runs code after a new commit has been pushed. # Runs code after a new commit has been pushed.
......
...@@ -10,6 +10,8 @@ module Ci ...@@ -10,6 +10,8 @@ module Ci
store.touch(commit_pipelines_path) if pipeline.commit store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path) store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) } merge_requests_pipelines_paths.each { |path| store.touch(path) }
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end end
private private
......
...@@ -35,6 +35,14 @@ module Projects ...@@ -35,6 +35,14 @@ module Projects
unless remove_legacy_registry_tags unless remove_legacy_registry_tags
raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end 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 end
log_info("Project \"#{project.path_with_namespace}\" was removed") log_info("Project \"#{project.path_with_namespace}\" was removed")
......
...@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader ...@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader
File.join(dynamic_path_segment, @secret) File.join(dynamic_path_segment, @secret)
end end
def cache_dir
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
def model def model
project project
end end
......
...@@ -662,6 +662,7 @@ ...@@ -662,6 +662,7 @@
The multiplier can also have a decimal value. The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling. installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
- if Gitlab::Geo.license_allows? - if Gitlab::Geo.license_allows?
%fieldset %fieldset
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
= custom_icon("icon_code_fork") = custom_icon("icon_code_fork")
.event-title .event-title
%span.author_name= link_to_author event
%span{ class: event.action_name } %span{ class: event.action_name }
- if event.target - if event.target
= event.action_name = event.action_name
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
= custom_icon("icon_status_open") = custom_icon("icon_status_open")
.event-title .event-title
%span.author_name= link_to_author event
%span{ class: event.action_name } %span{ class: event.action_name }
= event_action_name(event) = event_action_name(event)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
= custom_icon("icon_comment_o") = custom_icon("icon_comment_o")
.event-title .event-title
%span.author_name= link_to_author event
= event.action_name = event.action_name
= event_note_title_html(event) = event_note_title_html(event)
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
= custom_icon("icon_commit") = custom_icon("icon_commit")
.event-title .event-title
%span.author_name= link_to_author event
%span.pushed #{event.action_name} #{event.ref_type} %span.pushed #{event.action_name} #{event.ref_type}
%strong %strong
- commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name) - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Project #{@project.name} was exported successfully. Project #{@project.name} was exported successfully.
%p %p
The project export can be downloaded from: 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" = @project.name_with_namespace + " export"
%p %p
The download link will expire in 24 hours. The download link will expire in 24 hours.
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= render "home_panel" = render "home_panel"
- if current_user && can?(current_user, :download_code, @project) - 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 %ul.nav
%li %li
= link_to project_files_path(@project) do = link_to project_files_path(@project) do
...@@ -77,11 +77,11 @@ ...@@ -77,11 +77,11 @@
Set up auto deploy Set up auto deploy
- if @repository.commit - if @repository.commit
.limit-container-width{ class: container_class } %div{ class: container_class }
.project-last-commit .project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project = 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? - if @project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
......
...@@ -8,4 +8,26 @@ ...@@ -8,4 +8,26 @@
.form-group .form-group
= f.label :key, "Description", class: "label-light" = f.label :key, "Description", class: "label-light"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" = 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" = f.submit btn_text, class: "btn btn-save"
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
%th %th
%strong Last used %strong Last used
%th %th
%strong Next run at
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else - else
%p.settings-message.text-center.append-bottom-default %p.settings-message.text-center.append-bottom-default
......
...@@ -29,6 +29,12 @@ ...@@ -29,6 +29,12 @@
- else - else
Never Never
%td
- if trigger.trigger_schedule&.active?
= trigger.trigger_schedule.real_next_run
- else
Never
%td.text-right.trigger-actions %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?" - 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?" - 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) - parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id)
- group_path = root_url - group_path = root_url
- group_path << parent.full_path + '/' if parent - 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 .form-group
= f.label :path, class: 'control-label' do = f.label :path, class: 'control-label' do
...@@ -20,7 +16,7 @@ ...@@ -20,7 +16,7 @@
= f.text_field :path, placeholder: 'open-source', class: 'form-control', = f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true, autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, 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}" "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if parent - if parent
= f.hidden_field :parent_id, value: parent.id = f.hidden_field :parent_id, value: parent.id
...@@ -33,6 +29,14 @@ ...@@ -33,6 +29,14 @@
%li It will change web url for access group and group projects. %li It will change web url for access group and group projects.
%li It will change the git path to repositories under this group. %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 .form-group.group-description-holder
= f.label :description, class: 'control-label' = f.label :description, class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -3,7 +3,7 @@ class TriggerScheduleWorker ...@@ -3,7 +3,7 @@ class TriggerScheduleWorker
include CronjobQueue include CronjobQueue
def perform 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 begin
Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project, Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project,
trigger_schedule.trigger, 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: Disable invalid service templates title: Optimise builds endpoint
merge_request: merge_request:
author: author:
---
title: Add spec for schema.rb
merge_request: 10580
author: blackst0ne
...@@ -344,3 +344,57 @@ ...@@ -344,3 +344,57 @@
:why: https://github.com/nodeca/pako/blob/master/LICENSE :why: https://github.com/nodeca/pako/blob/master/LICENSE
:versions: [] :versions: []
:when: 2017-04-05 10:43:45.897720000 Z :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 ...@@ -166,6 +166,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :push_access_levels, only: [:destroy] resources :push_access_levels, only: [:destroy]
end end
end end
resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :variables, only: [:index, :show, :update, :create, :destroy]
......
...@@ -6,6 +6,7 @@ var webpack = require('webpack'); ...@@ -6,6 +6,7 @@ var webpack = require('webpack');
var StatsPlugin = require('stats-webpack-plugin'); var StatsPlugin = require('stats-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin'); var CompressionPlugin = require('compression-webpack-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
var ROOT_PATH = path.resolve(__dirname, '..'); var ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production'; var IS_PRODUCTION = process.env.NODE_ENV === 'production';
...@@ -52,6 +53,7 @@ var config = { ...@@ -52,6 +53,7 @@ var config = {
users: './users/users_bundle.js', users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js', vue_pipelines: './vue_pipelines_index/index.js',
issue_show: './issue_show/index.js', issue_show: './issue_show/index.js',
group: './group.js',
}, },
output: { output: {
...@@ -67,13 +69,18 @@ var config = { ...@@ -67,13 +69,18 @@ var config = {
{ {
test: /\.js$/, test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/, exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader' loader: 'babel-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
}, },
{ {
test: /\.svg$/, test: /\.svg$/,
use: 'raw-loader' loader: 'raw-loader',
}, { },
test: /\.(worker.js|pdf)$/, {
test: /\.(worker\.js|pdf)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: 'file-loader', loader: 'file-loader',
}, },
...@@ -186,6 +193,10 @@ if (IS_DEV_SERVER) { ...@@ -186,6 +193,10 @@ if (IS_DEV_SERVER) {
inline: DEV_SERVER_LIVERELOAD inline: DEV_SERVER_LIVERELOAD
}; };
config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; 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) { 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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -359,8 +359,11 @@ ActiveRecord::Schema.define(version: 20170407140450) do ...@@ -359,8 +359,11 @@ ActiveRecord::Schema.define(version: 20170407140450) do
t.string "cron" t.string "cron"
t.string "cron_timezone" t.string "cron_timezone"
t.datetime "next_run_at" t.datetime "next_run_at"
t.string "ref"
t.boolean "active"
end 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", ["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 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: ...@@ -49,6 +49,10 @@ All technical content published by GitLab lives in the documentation, including:
- [Email](tools/email.md) Email GitLab users from GitLab - [Email](tools/email.md) Email GitLab users from GitLab
- [Push Rules](push_rules/push_rules.md) Advanced push rules for your project. - [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. - [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. - [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. - [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 - [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: ...@@ -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. - [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. - [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. - [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. - [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. - [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. - [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 ...@@ -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. 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. 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 Use that as your polling interval. This way it is [easy for system administrators to change the
polling rate. polling rate](../../administration/polling.md).
A `Poll-Interval: -1` means you should disable polling, and this must be implemented. 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. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling. 1. Use a common library for polling.
......
...@@ -51,5 +51,6 @@ request path. By doing this we avoid query parameter ordering problems and make ...@@ -51,5 +51,6 @@ request path. By doing this we avoid query parameter ordering problems and make
route matching easier. route matching easier.
For more information see: For more information see:
- [`Poll-Interval` header](fe_guide/performance.md#realtime-components)
- [RFC 7232](https://tools.ietf.org/html/rfc7232) - [RFC 7232](https://tools.ietf.org/html/rfc7232)
- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926) - [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926)
...@@ -25,6 +25,8 @@ To create a group: ...@@ -25,6 +25,8 @@ To create a group:
1. Set the "Group path" which will be the namespace under which your projects 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 will be hosted (path can contain only letters, digits, underscores, dashes
and dots; it cannot start with dashes or end in dot). 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 1. Optionally, you can add a description so that others can briefly understand
what this group is about. what this group is about.
1. Optionally, choose and avatar for your project. 1. Optionally, choose and avatar for your project.
......
...@@ -20,8 +20,8 @@ the hardware requirements. ...@@ -20,8 +20,8 @@ the hardware requirements.
- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker. - [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
- [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install - [Installation on Google Cloud Platform](google_cloud_platform/index.md) - Install
GitLab on Google Cloud Platform using our official image. GitLab on Google Cloud Platform using our official image.
- [Digital Ocean and Docker](digitaloceandocker.md) - Install GitLab quickly - Testing only! [DigitalOcean and Docker Machine](digitaloceandocker.md) -
on DigitalOcean using Docker. Quickly test any version of GitLab on DigitalOcean using Docker Machine.
## Database ## 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 ## Initial setup
......
...@@ -143,7 +143,7 @@ into the password field. ...@@ -143,7 +143,7 @@ into the password field.
To disable two-factor authentication on your account (for example, if you To disable two-factor authentication on your account (for example, if you
have lost your code generation device) you can: have lost your code generation device) you can:
* [Use a saved recovery code](#use-a-saved-recovery-code) * [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) * [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 ### Use a saved recovery code
......
...@@ -323,6 +323,5 @@ Merging only when needed prevents creating merge commits in your feature branch ...@@ -323,6 +323,5 @@ Merging only when needed prevents creating merge commits in your feature branch
## References ## 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/) - [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/) - [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 ...@@ -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) ![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. 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. ...@@ -111,7 +111,7 @@ There are four kinds of filters you can use on your Todos dashboard.
| Type | Filter by issue or merge request | | Type | Filter by issue or merge request |
| Action | Filter by the action that triggered the Todo | | 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-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926 [ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926
...@@ -611,7 +611,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -611,7 +611,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click link "Approve"' do step 'I click link "Approve"' do
page.within '.mr-state-widget' do page.within '.mr-state-widget' do
wait_for_ajax wait_for_ajax
click_button 'Approve Merge Request' click_button 'Approve merge request'
end end
end end
...@@ -623,7 +623,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -623,7 +623,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should not see merge button' do step 'I should not see merge button' do
page.within '.mr-state-widget' 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
end end
......
class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
include LoginHelpers include LoginHelpers
include GitlabRoutingHelper include GitlabRoutingHelper
include WaitForAjax
step 'I am on the Merge Request detail page' do step 'I am on the Merge Request detail page' do
visit merge_request_path(@merge_request) visit merge_request_path(@merge_request)
...@@ -20,10 +21,18 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps ...@@ -20,10 +21,18 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'I should see the Remove Source Branch button' do step 'I should see the Remove Source Branch button' do
expect(page).to have_link('Remove source branch') 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 end
step 'I should not see the Remove Source Branch button' do step 'I should not see the Remove Source Branch button' do
expect(page).not_to have_link('Remove source branch') 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 end
step 'There is an open Merge Request' do step 'There is an open Merge Request' do
......
...@@ -97,7 +97,7 @@ module SharedProject ...@@ -97,7 +97,7 @@ module SharedProject
step 'I should see project "Shop" activity feed' do step 'I should see project "Shop" activity feed' do
project = Project.find_by(name: "Shop") 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 end
step 'I should see project settings' do step 'I should see project settings' do
...@@ -251,7 +251,8 @@ module SharedProject ...@@ -251,7 +251,8 @@ module SharedProject
step 'project "Shop" has CI build' do step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop") 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 end
step 'I should see last commit with CI status' do 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 ...@@ -7,7 +7,7 @@ module Banzai
# #
class RedactorFilter < HTML::Pipeline::Filter class RedactorFilter < HTML::Pipeline::Filter
def call def call
Redactor.new(project, current_user).redact([doc]) Redactor.new(project, current_user).redact([doc]) unless context[:skip_redaction]
doc doc
end 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 ...@@ -31,7 +31,8 @@ module Banzai
# #
# Returns the same input objects. # Returns the same input objects.
def render(objects, attribute) 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) redacted = redact_documents(documents)
objects.each_with_index do |object, index| objects.each_with_index do |object, index|
...@@ -41,9 +42,24 @@ module Banzai ...@@ -41,9 +42,24 @@ module Banzai
end end
end end
# Renders the attribute of every given object. private
def render_objects(objects, attribute)
render_attributes(objects, attribute) 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 end
# Redacts the list of documents. # Redacts the list of documents.
...@@ -57,25 +73,15 @@ module Banzai ...@@ -57,25 +73,15 @@ module Banzai
# Returns a Banzai context for the given object and attribute. # Returns a Banzai context for the given object and attribute.
def context_for(object, attribute) def context_for(object, attribute)
context = base_context.dup base_context.merge(object.banzai_render_context(attribute))
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
end end
def base_context 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 end
end end
...@@ -4,6 +4,7 @@ module Banzai ...@@ -4,6 +4,7 @@ module Banzai
def self.filters def self.filters
FilterArray[ FilterArray[
Filter::RelativeLinkFilter, Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
Filter::RedactorFilter Filter::RedactorFilter
] ]
end end
......
...@@ -62,8 +62,7 @@ module Banzai ...@@ -62,8 +62,7 @@ module Banzai
nodes.select do |node| nodes.select do |node|
if node.has_attribute?(project_attr) if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i can_read_reference?(user, projects[node])
can_read_reference?(user, projects[node_id])
else else
true true
end end
...@@ -112,12 +111,12 @@ module Banzai ...@@ -112,12 +111,12 @@ module Banzai
per_project per_project
end end
# Returns a Hash containing objects for an attribute grouped per their # Returns a Hash containing objects for an attribute grouped per the
# IDs. # nodes that reference them.
# #
# The returned Hash uses the following format: # The returned Hash uses the following format:
# #
# { id value => row } # { node => row }
# #
# nodes - An Array of HTML nodes to process. # nodes - An Array of HTML nodes to process.
# #
...@@ -132,9 +131,14 @@ module Banzai ...@@ -132,9 +131,14 @@ module Banzai
return {} if nodes.empty? return {} if nodes.empty?
ids = unique_attribute_values(nodes, attribute) 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 end
# Returns an Array containing all unique values of an attribute of the # Returns an Array containing all unique values of an attribute of the
...@@ -201,7 +205,7 @@ module Banzai ...@@ -201,7 +205,7 @@ module Banzai
# #
# The returned Hash uses the following format: # The returned Hash uses the following format:
# #
# { project ID => project } # { node => project }
# #
def projects_for_nodes(nodes) def projects_for_nodes(nodes)
@projects_for_nodes ||= @projects_for_nodes ||=
......
...@@ -13,14 +13,14 @@ module Banzai ...@@ -13,14 +13,14 @@ module Banzai
issues_readable_by_user(issues.values, user).to_set issues_readable_by_user(issues.values, user).to_set
nodes.select do |node| nodes.select do |node|
readable_issues.include?(issue_for_node(issues, node)) readable_issues.include?(issues[node])
end end
end end
def referenced_by(nodes) def referenced_by(nodes)
issues = issues_for_nodes(nodes) issues = issues_for_nodes(nodes)
nodes.map { |node| issue_for_node(issues, node) }.uniq nodes.map { |node| issues[node] }.compact.uniq
end end
def issues_for_nodes(nodes) def issues_for_nodes(nodes)
...@@ -44,12 +44,6 @@ module Banzai ...@@ -44,12 +44,6 @@ module Banzai
self.class.data_attribute self.class.data_attribute
) )
end end
private
def issue_for_node(issues, node)
issues[node.attr(self.class.data_attribute).to_i]
end
end end
end end
end end
...@@ -3,14 +3,41 @@ module Banzai ...@@ -3,14 +3,41 @@ module Banzai
class MergeRequestParser < BaseParser class MergeRequestParser < BaseParser
self.reference_type = :merge_request self.reference_type = :merge_request
def references_relation def nodes_visible_to_user(user, nodes)
MergeRequest.includes(:author, :assignee, :target_project) 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 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) def merge_requests_for_nodes(nodes)
can?(user, :read_merge_request, ref_project) @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 end
end end
......
...@@ -49,7 +49,7 @@ module Banzai ...@@ -49,7 +49,7 @@ module Banzai
# Check if project belongs to a group which # Check if project belongs to a group which
# user can read. # user can read.
def can_read_group_reference?(node, user, groups) 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) node_group && can?(user, :read_group, node_group)
end end
...@@ -74,8 +74,8 @@ module Banzai ...@@ -74,8 +74,8 @@ module Banzai
if project && project_id && project.id == project_id.to_i if project && project_id && project.id == project_id.to_i
true true
elsif project_id && user_id elsif project_id && user_id
project = projects[project_id.to_i] project = projects[node]
user = users[user_id.to_i] user = users[node]
project && user ? project.team.member?(user) : false project && user ? project.team.member?(user) : false
else 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