Commit a21d91e2 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

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

CE Upstream

See merge request !859
parents add80eb9 178f4499
......@@ -12,6 +12,7 @@ entry.
- Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc)
- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand)
- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
- Fix Markdown styling inside reference links (Jan Zdráhal)
- Create new issue board list after creating a new label
......@@ -73,13 +74,34 @@ entry.
- Updated commit SHA styling on the branches page.
- Fix 404 when visit /projects page
## 8.13.5 (2016-11-08)
- Restore unauthenticated access to public container registries
- Fix showing pipeline status for a given commit from correct branch. !7034
- Only skip group when it's actually a group in the "Share with group" select. !7262
- Introduce round-robin project creation to spread load over multiple shards. !7266
- Ensure merge request's "remove branch" accessors return booleans. !7267
- Ensure external users are not able to clone disabled repositories.
- Fix XSS issue in Markdown autolinker.
- Respect event visibility in Gitlab::ContributionsCalendar.
- Honour issue and merge request visibility in their respective finders.
- Disable reference Markdown for unavailable features.
- Fix lightweight tags not processed correctly by GitTagPushService. !6532
- Allow owners to fetch source code in CI builds. !6943
- Return conflict error in label API when title is taken by group label. !7014
- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123
- Fix builds tab visibility. !7178
- Fix project features default values. !7181
## 8.13.4
- Pulled due to packaging error.
## 8.13.3 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
- Fixed Import/Export foreign key issue to do with project members.
- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project
- Fix project features default values
- Changed build dropdown list length to be 6,5 builds long in the pipeline graph
## 8.13.2 (2016-10-31)
......@@ -270,6 +292,10 @@ entry.
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
## 8.12.9 (2016-11-07)
- Fix XSS issue in Markdown autolinker
## 8.12.8 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
......@@ -534,6 +560,10 @@ entry.
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
## 8.11.11 (2016-11-07)
- Fix XSS issue in Markdown autolinker
## 8.11.10 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
......
......@@ -13,12 +13,12 @@
}
Activities.prototype.updateTooltips = function() {
return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
return Pager.init(20, true);
Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
......
......@@ -13,7 +13,6 @@
/*= require jquery-ui/sortable */
/*= require jquery_ujs */
/*= require jquery.endless-scroll */
/*= require jquery.timeago */
/*= require jquery.highlight */
/*= require jquery.waitforimages */
/*= require jquery.atwho */
......@@ -59,7 +58,6 @@
(function () {
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
window.addEventListener('hashchange', gl.utils.shiftWindow);
$.timeago.settings.allowFuture = true;
window.onload = function () {
// Scroll the window to avoid the topnav bar
......@@ -199,9 +197,6 @@
warningMessage: warningMessage
});
});
$document.on('click', 'button', function () {
return $(this).blur();
});
$('input[type="search"]').each(function () {
var $this = $(this);
$this.attr('value', $this.val());
......@@ -243,8 +238,5 @@
// bind sidebar events
new gl.Sidebar();
// Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago'));
});
}).call(this);
......@@ -8,56 +8,55 @@
Build.state = null;
function Build(options) {
this.page_url = options.page_url;
this.build_url = options.build_url;
this.build_status = options.build_status;
options = options || $('.js-build-options').data();
this.pageUrl = options.pageUrl;
this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
this.state = options.state1;
this.build_stage = options.build_stage;
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar();
this.$buildScroll = $('#js-build-scroll');
this.populateJobs(this.build_stage);
this.updateStageDropdownText(this.build_stage);
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
$(window).off('resize.build').on('resize.build', this.hideSidebar);
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
$('#js-build-scroll > a').off('click').on('click', this.stepTrace);
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
this.initScrollButtons();
this.initScrollButtonAffix();
}
if (this.build_status === "running" || this.build_status === "pending") {
if (this.buildStatus === "running" || this.buildStatus === "pending") {
// Bind autoscroll button to follow build output
$('#autoscroll-button').on('click', function() {
var state;
state = $(this).data("state");
if ("enabled" === state) {
$(this).data("state", "disabled");
return $(this).text("enable autoscroll");
return $(this).text("Enable autoscroll");
} else {
$(this).data("state", "enabled");
return $(this).text("disable autoscroll");
return $(this).text("Disable autoscroll");
}
//
// Bind autoscroll button to follow build output
//
});
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
return function() {
if (window.location.href.split("#").first() === _this.page_url) {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
//
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
//
})(this), 4000);
}
}
......@@ -72,20 +71,23 @@
top: this.sidebarTranslationLimits.max
});
this.$sidebar.niceScroll();
this.hideSidebar();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
};
Build.prototype.location = function() {
return window.location.href.split("#")[0];
};
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
return $.ajax({
url: this.build_url,
url: this.buildUrl,
dataType: 'json',
success: function(build_data) {
$('.js-build-output').html(build_data.trace_html);
if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
......@@ -94,7 +96,7 @@
Build.prototype.getBuildTrace = function() {
return $.ajax({
url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
......@@ -108,8 +110,8 @@
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
} else if (log.status !== _this.build_status) {
return Turbolinks.visit(_this.page_url);
} else if (log.status !== _this.buildStatus) {
return Turbolinks.visit(_this.pageUrl);
}
};
})(this)
......@@ -122,12 +124,11 @@
}
};
Build.prototype.initScrollButtons = function() {
var $body, $buildScroll, $buildTrace;
$buildScroll = $('#js-build-scroll');
Build.prototype.initScrollButtonAffix = function() {
var $body, $buildTrace;
$body = $('body');
$buildTrace = $('#build-trace');
return $buildScroll.affix({
return this.$buildScroll.affix({
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
......@@ -136,18 +137,12 @@
});
};
Build.prototype.shouldHideSidebar = function() {
Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
Build.prototype.toggleSidebar = function() {
if (this.shouldHideSidebar()) {
return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
}
};
Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
......@@ -156,12 +151,20 @@
});
};
Build.prototype.hideSidebar = function() {
if (this.shouldHideSidebar()) {
return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else {
return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
};
Build.prototype.sidebarOnResize = function() {
this.toggleSidebar(this.shouldHideSidebarForViewport());
};
Build.prototype.sidebarOnClick = function() {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
......@@ -169,7 +172,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
return $date.text(gl.utils.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
......
......@@ -80,7 +80,8 @@
success: function(html) {
loading.hide();
$target.html(html);
return $('.js-timeago', $target).timeago();
var className = '.' + $target[0].className.replace(' ', '.');
gl.utils.localTimeAgo($('.js-timeago', className));
}
});
};
......
......@@ -43,10 +43,6 @@
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
// indent is used to compensate for single space indent to fit
// '+' and '-' prepended to diff lines,
// see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
indent: 1,
view: file.data('view')
};
return $.get(link, params, function(response) {
......
......@@ -29,6 +29,9 @@
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:builds:show':
new Build();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
......
......@@ -34,6 +34,8 @@
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
// Highlight first item only if at least one char was typed
this.setting.highlightFirst = query.length > 0;
if ((items[0].name != null) && items[0].name === 'loading') {
return items;
}
......@@ -182,6 +184,7 @@
insertTpl: '${atwho-at}"${title}"',
data: ['loading'],
callbacks: {
sorter: this.DefaultOptions.sorter,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
......@@ -236,6 +239,7 @@
displayTpl: this.Labels.template,
insertTpl: '${atwho-at}${title}',
callbacks: {
sorter: this.DefaultOptions.sorter,
beforeSave: function(merges) {
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
......
......@@ -119,31 +119,12 @@
parser.href = url;
return parser;
};
gl.utils.cleanupBeforeFetch = function() {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
};
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
return '';
}
suffix || (suffix = 'remaining');
expiredLabel || (expiredLabel = 'Past due');
jQuery.timeago.settings.allowFuture = true;
suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow;
jQuery.timeago.settings.strings.suffixFromNow = suffix;
timefor = $.timeago(time);
if (timefor.indexOf('ago') > -1) {
timefor = expiredLabel;
}
jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow;
return timefor;
};
})(window);
}).call(this);
......@@ -22,51 +22,64 @@
if (setTimeago == null) {
setTimeago = true;
}
$timeagoEls.each(function() {
var $el;
$el = $(this);
return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
if (setTimeago) {
// Recreate with custom template
$el.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
gl.utils.renderTimeago($el);
});
if (setTimeago) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
// Recreate with custom template
return $timeagoEls.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
};
w.gl.utils.shortTimeAgo = function($el) {
var shortLocale, tmpLocale;
shortLocale = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
seconds: '1 min',
minute: '1 min',
minutes: '%d mins',
hour: '1 hr',
hours: '%d hrs',
day: '1 day',
days: '%d days',
month: '1 month',
months: '%d months',
year: '1 year',
years: '%d years',
wordSeparator: ' ',
numbers: []
w.gl.utils.getTimeago = function() {
var locale = function(number, index) {
return [
['less than a minute ago', 'a while'],
['less than a minute ago', 'in %s seconds'],
['about a minute ago', 'in 1 minute'],
['%s minutes ago', 'in %s minutes'],
['about an hour ago', 'in 1 hour'],
['about %s hours ago', 'in %s hours'],
['a day ago', 'in 1 day'],
['%s days ago', 'in %s days'],
['a week ago', 'in 1 week'],
['%s weeks ago', 'in %s weeks'],
['a month ago', 'in 1 month'],
['%s months ago', 'in %s months'],
['a year ago', 'in 1 year'],
['%s years ago', 'in %s years']
][index];
};
tmpLocale = $.timeago.settings.strings;
$el.each(function(el) {
var $el1;
$el1 = $(this);
return $el1.attr('title', gl.utils.formatDate($el.attr('datetime')));
});
$.timeago.settings.strings = shortLocale;
$el.timeago();
$.timeago.settings.strings = tmpLocale;
timeago.register('gl_en', locale);
return timeago();
};
w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
var timefor;
if (!time) {
return '';
}
suffix || (suffix = 'remaining');
expiredLabel || (expiredLabel = 'Past due');
timefor = gl.utils.getTimeago().format(time).replace('in', '');
if (timefor.indexOf('ago') > -1) {
timefor = expiredLabel;
} else {
timefor = timefor.trim() + ' ' + suffix;
}
return timefor;
};
w.gl.utils.renderTimeago = function($element) {
var timeagoInstance = gl.utils.getTimeago();
timeagoInstance.render($element, 'gl_en');
};
w.gl.utils.getDayDifference = function(a, b) {
......@@ -75,7 +88,7 @@
var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((date2 - date1) / millisecondsPerDay);
}
};
})(window);
......
/**
* Copyright (c) 2016 hustcc
* License: MIT
* Version: v2.0.2
* https://github.com/hustcc/timeago.js
* This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js)
**/
/* eslint-disable */
/* jshint expr: true */
!function (root, factory) {
if (typeof module === 'object' && module.exports)
module.exports = factory(root);
else
root.timeago = factory(root);
}(typeof window !== 'undefined' ? window : this,
function () {
var cnt = 0, // the timer counter, for timer key
indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),
// build-in locales: en & zh_CN
locales = {
'en': function(number, index) {
if (index === 0) return ['just now', 'right now'];
var unit = indexMapEn[parseInt(index / 2)];
if (number > 1) unit += 's';
return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];
},
},
// second, minute, hour, day, week, month, year(365 days)
SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
SEC_ARRAY_LEN = 6,
ATTR_DATETIME = 'datetime';
// format Date / string / timestamp to Date instance.
function toDate(input) {
if (input instanceof Date) return input;
if (!isNaN(input)) return new Date(toInt(input));
if (/^\d+$/.test(input)) return new Date(toInt(input, 10));
input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
.replace(/-/, '/').replace(/-/, '/')
.replace(/T/, ' ').replace(/Z/, ' UTC')
.replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
return new Date(input);
}
// change f into int, remove Decimal. just for code compression
function toInt(f) {
return parseInt(f);
}
// format the diff second to *** time ago, with setting locale
function formatDiff(diff, locale, defaultLocale) {
// if locale is not exist, use defaultLocale.
// if defaultLocale is not exist, use build-in `en`.
// be sure of no error when locale is not exist.
locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');
// if (! locales[locale]) locale = defaultLocale;
var i = 0;
agoin = diff < 0 ? 1 : 0; // timein or timeago
diff = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
}
diff = toInt(diff);
i *= 2;
if (diff > (i === 0 ? 9 : 1)) i += 1;
return locales[locale](diff, i)[agoin].replace('%s', diff);
}
// calculate the diff second between date to be formated an now date.
function diffSec(date, nowDate) {
nowDate = nowDate ? toDate(nowDate) : new Date();
return (nowDate - toDate(date)) / 1000;
}
/**
* nextInterval: calculate the next interval time.
* - diff: the diff sec between now and date to be formated.
*
* What's the meaning?
* diff = 61 then return 59
* diff = 3601 (an hour + 1 second), then return 3599
* make the interval with high performace.
**/
function nextInterval(diff) {
var rst = 1, i = 0, d = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
rst *= SEC_ARRAY[i];
}
// return leftSec(d, rst);
d = d % rst;
d = d ? rst - d : rst;
return Math.ceil(d);
}
// get the datetime attribute, jQuery and DOM
function getDateAttr(node) {
if (node.getAttribute) return node.getAttribute(ATTR_DATETIME);
if(node.attr) return node.attr(ATTR_DATETIME);
}
/**
* timeago: the function to get `timeago` instance.
* - nowDate: the relative date, default is new Date().
* - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
*
* How to use it?
* var timeagoLib = require('timeago.js');
* var timeago = timeagoLib(); // all use default.
* var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
* var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
* var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
**/
function Timeago(nowDate, defaultLocale) {
var timers = {}; // real-time render timers
// if do not set the defaultLocale, set it with `en`
if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale
// what the timer will do
function doRender(node, date, locale, cnt) {
var diff = diffSec(date, nowDate);
node.innerHTML = formatDiff(diff, locale, defaultLocale);
// waiting %s seconds, do the next render
timers['k' + cnt] = setTimeout(function() {
doRender(node, date, locale, cnt);
}, nextInterval(diff) * 1000);
}
/**
* nextInterval: calculate the next interval time.
* - diff: the diff sec between now and date to be formated.
*
* What's the meaning?
* diff = 61 then return 59
* diff = 3601 (an hour + 1 second), then return 3599
* make the interval with high performace.
**/
// this.nextInterval = function(diff) { // for dev test
// var rst = 1, i = 0, d = Math.abs(diff);
// for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
// diff /= SEC_ARRAY[i];
// rst *= SEC_ARRAY[i];
// }
// // return leftSec(d, rst);
// d = d % rst;
// d = d ? rst - d : rst;
// return Math.ceil(d);
// }; // for dev test
/**
* format: format the date to *** time ago, with setting or default locale
* - date: the date / string / timestamp to be formated
* - locale: the formated string's locale name, e.g. en / zh_CN
*
* How to use it?
* var timeago = require('timeago.js')();
* timeago.format(new Date(), 'pl'); // Date instance
* timeago.format('2016-09-10', 'fr'); // formated date string
* timeago.format(1473473400269); // timestamp with ms
**/
this.format = function(date, locale) {
return formatDiff(diffSec(date, nowDate), locale, defaultLocale);
};
/**
* render: render the DOM real-time.
* - nodes: which nodes will be rendered.
* - locale: the locale name used to format date.
*
* How to use it?
* var timeago = new require('timeago.js')();
* // 1. javascript selector
* timeago.render(document.querySelectorAll('.need_to_be_rendered'));
* // 2. use jQuery selector
* timeago.render($('.need_to_be_rendered'), 'pl');
*
* Notice: please be sure the dom has attribute `datetime`.
**/
this.render = function(nodes, locale) {
if (nodes.length === undefined) nodes = [nodes];
for (var i = 0; i < nodes.length; i++) {
doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item
}
};
/**
* cancel: cancel all the timers which are doing real-time render.
*
* How to use it?
* var timeago = new require('timeago.js')();
* timeago.render(document.querySelectorAll('.need_to_be_rendered'));
* timeago.cancel(); // will stop all the timer, stop render in real time.
**/
this.cancel = function() {
for (var key in timers) {
clearTimeout(timers[key]);
}
timers = {};
};
/**
* setLocale: set the default locale name.
*
* How to use it?
* var timeago = require('timeago.js');
* timeago = new timeago();
* timeago.setLocale('fr');
**/
this.setLocale = function(locale) {
defaultLocale = locale;
};
return this;
}
/**
* timeago: the function to get `timeago` instance.
* - nowDate: the relative date, default is new Date().
* - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
*
* How to use it?
* var timeagoLib = require('timeago.js');
* var timeago = timeagoLib(); // all use default.
* var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
* var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
* var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
**/
function timeagoFactory(nowDate, defaultLocale) {
return new Timeago(nowDate, defaultLocale);
}
/**
* register: register a new language locale
* - locale: locale name, e.g. en / zh_CN, notice the standard.
* - localeFunc: the locale process function
*
* How to use it?
* var timeagoLib = require('timeago.js');
*
* timeagoLib.register('the locale name', the_locale_func);
* // or
* timeagoLib.register('pl', require('timeago.js/locales/pl'));
**/
timeagoFactory.register = function(locale, localeFunc) {
locales[locale] = localeFunc;
};
return timeagoFactory;
});
\ No newline at end of file
......@@ -235,7 +235,7 @@
}
if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.';
environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.';
} else {
$('.js-environment-timeago', $template).remove();
environment.name += '.';
......
......@@ -162,7 +162,7 @@
if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace;
data.milestone.path = _this.currentProject.path;
data.milestone.remaining = $.timefor(data.milestone.due_date);
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
......
......@@ -6,7 +6,6 @@
&:focus,
&:active {
outline: none;
background-color: $btn-active-gray;
box-shadow: $gl-btn-active-background;
}
......@@ -267,10 +266,6 @@
outline: none;
}
&:focus {
outline: none;
}
&:active {
outline: none;
}
......
......@@ -38,7 +38,6 @@
text-align: left;
border: 1px solid $border-color;
border-radius: $border-radius-base;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
......@@ -55,6 +54,10 @@
}
}
&.no-outline {
outline: 0;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
......
......@@ -100,10 +100,6 @@ header {
&:hover {
background-color: $btn-gray-hover;
}
&:focus {
outline: none;
}
}
}
......
......@@ -58,7 +58,6 @@
&:active,
&:focus {
text-decoration: none;
outline: none;
}
}
......
......@@ -83,7 +83,6 @@
display: block;
text-decoration: none;
font-weight: normal;
outline: none;
&:hover,
&:active,
......
......@@ -14,18 +14,10 @@
}
}
.autoscroll-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
}
.scroll-controls {
&.affix-top {
position: absolute;
top: 10px;
right: 25px;
.scroll-step {
width: 31px;
margin: 0 0 0 auto;
}
&.affix-bottom {
......@@ -34,13 +26,13 @@
}
&.affix {
right: 30px;
right: 25px;
bottom: 15px;
z-index: 1;
}
@media (min-width: $screen-md-min) {
right: 26%;
}
&.sidebar-expanded {
right: #{$gutter_width + ($gl-padding * 2)};
}
a {
......
......@@ -92,20 +92,6 @@
&.noteable_line {
position: relative;
&.old {
&::before {
content: '-';
position: absolute;
}
}
&.new {
&::before {
content: '+';
position: absolute;
}
}
}
span {
......@@ -151,8 +137,9 @@
.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
padding: 0 1.5em;
border: none;
position: relative;
&.parallel {
display: table-cell;
......@@ -161,6 +148,22 @@
word-break: break-all;
}
}
&.old {
&::before {
content: '-';
position: absolute;
left: 0.5em;
}
}
&.new {
&::before {
content: '+';
position: absolute;
left: 0.5em;
}
}
}
.text-file.diff-wrap-lines table .line_holder td span {
......
......@@ -228,7 +228,6 @@ $colors: (
position: absolute;
right: 10px;
padding: 0;
outline: none;
color: #fff;
width: 75px; // static width to make 2 buttons have same width
height: 19px;
......
......@@ -31,7 +31,6 @@
padding-right: 20px;
border: none;
font-size: 14px;
outline: none;
padding: 0;
margin-left: 5px;
line-height: 25px;
......@@ -229,6 +228,5 @@
&:hover,
&:focus {
color: $gl-link-color;
outline: none;
}
}
......@@ -12,7 +12,7 @@ class JwtController < ApplicationController
return head :not_found unless service
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
execute(authentication_abilities: @authentication_result.authentication_abilities || [])
execute(authentication_abilities: @authentication_result.authentication_abilities)
render json: result, status: result[:http_status]
end
......@@ -20,7 +20,7 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
@authentication_result = Gitlab::Auth::Result.new
@authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities)
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
......
......@@ -104,8 +104,7 @@ class UsersController < ApplicationController
end
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.
new(contributed_projects, user)
@contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
end
def load_events
......
......@@ -62,31 +62,26 @@ class IssuableFinder
def project
return @project if defined?(@project)
if project?
@project = Project.find(params[:project_id])
project = Project.find(params[:project_id])
project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
else
@project = nil
end
@project
@project = project
end
def projects
return @projects if defined?(@projects)
return @projects = project if project?
if project?
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
@projects = current_user.authorized_projects.reorder(nil)
elsif group
@projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
else
@projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
GroupProjectsFinder.new(group).execute(current_user)
else
ProjectsFinder.new.execute(current_user)
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end
def search
......
......@@ -154,7 +154,6 @@ module ApplicationHelper
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
# skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
......@@ -166,22 +165,19 @@ module ApplicationHelper
# `html_class` argument is provided.
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false)
def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
css_classes << " #{html_class}" unless html_class.blank?
css_classes << ' js-timeago-pending' unless skip_js
element = content_tag :time, time.to_s,
class: css_classes,
datetime: time.to_time.getutc.iso8601,
title: time.to_time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js
element << javascript_tag(
"$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
)
end
datetime: time.to_time.getutc.iso8601,
data: {
toggle: 'tooltip',
placement: placement,
container: 'body'
}
element
end
......
......@@ -5,4 +5,14 @@ module BuildsHelper
build_class += ' retried' if build.retried?
build_class
end
def javascript_build_options
{
page_url: namespace_project_build_url(@project.namespace, @project, @build),
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
state1: @build.trace_with_state[:state]
}
end
end
......@@ -51,12 +51,11 @@ module DiffHelper
html.html_safe
end
def diff_line_content(line, line_type = nil)
def diff_line_content(line)
if line.blank?
" &nbsp;".html_safe
"&nbsp;".html_safe
else
line[0] = ' ' if %w[new old].include?(line_type)
line
line.sub(/^[\-+ ]/, '').html_safe
end
end
......
......@@ -74,4 +74,13 @@ module NotificationsHelper
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
def notification_event_name(event)
case event
when :success_pipeline
'Successful pipeline'
else
event.to_s.humanize
end
end
end
module Emails
module Pipelines
def pipeline_success_email(pipeline, to)
pipeline_mail(pipeline, to, 'succeeded')
def pipeline_success_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'succeeded')
end
def pipeline_failed_email(pipeline, to)
pipeline_mail(pipeline, to, 'failed')
def pipeline_failed_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'failed')
end
private
def pipeline_mail(pipeline, to, status)
def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project
@pipeline = pipeline
@merge_request = pipeline.merge_requests.first
add_headers
mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
# We use bcc here because we don't want to generate this emails for a
# thousand times. This could be potentially expensive in a loop, and
# recipients would contain all project watchers so it could be a lot.
mail(bcc: recipients,
subject: pipeline_subject(status),
skip_premailer: true) do |format|
format.html { render layout: false }
format.text
end
......
......@@ -81,6 +81,12 @@ module Ci
PipelineHooksWorker.perform_async(id)
end
end
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
end
# ref can't be HEAD or SHA, can only be branch/tag name
......@@ -109,6 +115,11 @@ module Ci
project.id
end
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
end
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
......@@ -186,6 +186,10 @@ module Issuable
grouping_columns
end
def to_ability_name
model_name.singular
end
end
def today?
......@@ -247,7 +251,7 @@ module Issuable
# issuable.class # => MergeRequest
# issuable.to_ability_name # => "merge_request"
def to_ability_name
self.class.to_s.underscore
self.class.to_ability_name
end
# Returns a Hash of attributes to be used for Twitter card metadata
......
......@@ -54,6 +54,7 @@ class Event < ActiveRecord::Base
update_all(updated_at: Time.now)
end
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type in (?) AND action in (?))",
Event::PUSHED, ["MergeRequest", "Issue"],
......@@ -67,7 +68,7 @@ class Event < ActiveRecord::Base
def visible_to_user?(user = nil)
if push?
true
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
elsif created_project?
......
......@@ -264,29 +264,9 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
user ? readable_by?(user) : publicly_visible?
end
# Returns `true` if the given User can read the current Issue.
def readable_by?(user)
if user.admin?
true
elsif project.owner == user
true
elsif confidential?
author == user ||
assignee == user ||
project.team.member?(user, Gitlab::Access::REPORTER)
else
project.public? ||
project.internal? && !user.external? ||
project.team.member?(user)
end
end
return false unless project.feature_available?(:issues, user)
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential?
user ? readable_by?(user) : publicly_visible?
end
def overdue?
......@@ -311,4 +291,32 @@ class Issue < ActiveRecord::Base
end
end
end
private
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
# for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
# Make sure to sync this method with issue_policy.rb
def readable_by?(user)
if user.admin?
true
elsif project.owner == user
true
elsif confidential?
author == user ||
assignee == user ||
project.team.member?(user, Gitlab::Access::REPORTER)
else
project.public? ||
project.internal? && !user.external? ||
project.team.member?(user)
end
end
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential?
end
end
......@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
:merge_merge_request
:merge_merge_request,
:failed_pipeline,
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
......
......@@ -223,9 +223,39 @@ class Project < ActiveRecord::Base
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
scope :with_wiki_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.wiki_access_level IS NULL or project_features.wiki_access_level > 0') }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(feature)
with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
}
# Picks a feature where the level is exactly that given.
scope :with_feature_access_level, ->(feature, level) {
access_level_attribute = ProjectFeature.access_level_attribute(feature)
with_project_feature.where(project_features: { access_level_attribute => level })
}
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_wiki_enabled, -> { with_project_feature(:wiki) }
# project features may be "disabled", "internal" or "enabled". If "internal",
# they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user.
def self.with_feature_available_for_user(feature, user)
return with_feature_enabled(feature) if user.try(:admin?)
unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED])
return unconditional if user.nil?
conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE)
authorized = user.authorized_projects.merge(conditional.reorder(nil))
union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)])
where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql)))
end
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
......
......@@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds repository)
class << self
def access_level_attribute(feature)
feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
"#{feature}_access_level".to_sym
end
end
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
......@@ -35,9 +44,8 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
get_permission(user, public_send("#{feature}_access_level"))
access_level = public_send(ProjectFeature.access_level_attribute(feature))
get_permission(user, access_level)
end
def builds_enabled?
......
class PipelinesEmailService < Service
prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_pipelines
validates :recipients,
presence: true,
if: ->(s) { s.activated? && !s.add_pusher? }
validates :recipients, presence: true, if: :activated?
def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true }
......@@ -34,8 +31,8 @@ class PipelinesEmailService < Service
return unless all_recipients.any?
pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
pipeline_id = data[:object_attributes][:id]
PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
end
def can_test?
......@@ -57,9 +54,6 @@ class PipelinesEmailService < Service
{ type: 'textarea',
name: 'recipients',
placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
name: 'add_pusher',
label: 'Add pusher to recipients list' },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' },
]
......@@ -85,12 +79,6 @@ class PipelinesEmailService < Service
end
def retrieve_recipients(data)
all_recipients = recipients.to_s.split(',').reject(&:blank?)
if add_pusher? && data[:user].try(:[], :email)
all_recipients << data[:user][:email]
end
all_recipients
recipients.to_s.split(',').reject(&:blank?)
end
end
......@@ -5,7 +5,7 @@ module Ci
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
%w(read create update admin).each do |rule|
%w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
end
......
module Ci
class PipelinePolicy < BuildPolicy
end
end
class IssuePolicy < IssuablePolicy
# This class duplicates the same check of Issue#readable_by? for performance reasons
# Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
def issue
@subject
end
......
......@@ -9,8 +9,8 @@ module Auth
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project
return error('DENIED', status: 403, message: 'access forbidden') unless scope
unless scope || current_user || project
return error('DENIED', status: 403, message: 'access forbidden')
end
{ token: authorized_token(scope).encoded }
......@@ -92,23 +92,23 @@ module Auth
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
@authentication_abilities.include?(:build_read_container_image) &&
has_authentication_ability?(:build_read_container_image) &&
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end
def user_can_pull?(requested_project)
@authentication_abilities.include?(:read_container_image) &&
has_authentication_ability?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
end
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
@authentication_abilities.include?(:build_create_container_image) &&
has_authentication_ability?(:build_create_container_image) &&
requested_project == project
end
def user_can_push?(requested_project)
@authentication_abilities.include?(:create_container_image) &&
has_authentication_ability?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
......@@ -118,5 +118,9 @@ module Auth
http_status: status
}
end
def has_authentication_ability?(capability)
(@authentication_abilities || []).include?(capability)
end
end
end
module Ci
class SendPipelineNotificationService
attr_reader :pipeline
def initialize(new_pipeline)
@pipeline = new_pipeline
end
def execute(recipients)
email_template = "pipeline_#{pipeline.status}_email"
return unless Notify.respond_to?(email_template)
recipients.each do |to|
Notify.public_send(email_template, pipeline, to).deliver_later
end
end
end
end
......@@ -325,6 +325,22 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
def pipeline_finished(pipeline, recipients = nil)
email_template = "pipeline_#{pipeline.status}_email"
return unless mailer.respond_to?(email_template)
recipients ||= build_recipients(
pipeline,
pipeline.project,
nil, # The acting user, who won't be added to recipients
action: pipeline.status).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
end
end
protected
# Get project/group users with CUSTOM notification level
......@@ -488,9 +504,14 @@ class NotificationService
end
def reject_users_without_access(recipients, target)
return recipients unless target.is_a?(Issuable)
ability = case target
when Issuable
:"read_#{target.to_ability_name}"
when Ci::Pipeline
:read_build # We have build trace in pipeline emails
end
ability = :"read_#{target.to_ability_name}"
return recipients unless ability
recipients.select do |user|
user.can?(ability, target)
......@@ -653,6 +674,6 @@ class NotificationService
# Build event key to search on custom notification level
# Check NotificationSetting::EMAIL_EVENTS
def build_custom_key(action, object)
"#{action}_#{object.class.name.underscore}".to_sym
"#{action}_#{object.class.model_name.name.underscore}".to_sym
end
end
- if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) }
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at, skip_js: true)}
#{time_ago_with_tooltip(event.created_at)}
= cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40)
......
......@@ -13,7 +13,7 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
= search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
......
......@@ -8,5 +8,5 @@
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot;
#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
#{time_ago_with_tooltip(commit.committed_date)} by
= commit_author_link(commit, avatar: true, size: 24)
......@@ -32,7 +32,7 @@
.light
= commit_author_link(commit, avatar: false)
authored
#{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
#{time_ago_with_tooltip(commit.committed_date)}
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
......
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
- trace_with_state = @build.trace_with_state
- header_title project_title(@project, "Builds", project_builds_path(@project))
= render "projects/pipelines/head", build_subnav: true
......@@ -28,32 +27,27 @@
Runners page
.prepend-top-default
- if @build.active?
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
= link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
.scroll-step
= link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
- if @build.active?
.autoscroll-container
%button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
Enable autoscroll
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
#down-build-trace
#down-build-trace
= render "sidebar"
:javascript
new Build({
page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
build_status: "#{@build.status}",
build_stage: "#{@build.stage}",
state1: "#{trace_with_state[:state]}"
})
.js-build-options{ data: javascript_build_options }
......@@ -59,7 +59,7 @@
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
......
......@@ -25,9 +25,9 @@
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
%pre= diff_line_content(line.text, type)
%pre= diff_line_content(line.text)
- else
= diff_line_content(line.text, type)
= diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta?
......
......@@ -25,3 +25,6 @@
var url = "#{escape_javascript(@more_log_url)}";
ajaxGet(url);
}
:plain
gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
\ No newline at end of file
......@@ -151,7 +151,7 @@
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[force_remove_source_branch]' do
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0'
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
Remove source branch when merge request is accepted.
......
......@@ -27,5 +27,5 @@
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
%strong
= event.to_s.humanize
= notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
class PipelineNotificationWorker
include Sidekiq::Worker
include PipelineQueue
def perform(pipeline_id, recipients = nil)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
NotificationService.new.pipeline_finished(pipeline, recipients)
end
end
---
title: Add query param to filter users by external & blocked type
merge_request: 7109
author: Yatish Mehta
---
title: Clicking "force remove source branch" label now toggles the checkbox again
merge_request:
author:
---
title: Introduce better credential and error checking to `rake gitlab:ldap:check`
merge_request: 6601
author:
---
title: Add CI notifications. Who triggered a pipeline would receive an email after
the pipeline is succeeded or failed. Users could also update notification settings
accordingly
merge_request: 6342
author:
---
title: Remove an extra leading space from diff paste data
merge_request: 7133
author: Hiroyuki Sato
---
title: Replace jQuery.timeago with timeago.js
merge_request: 6274
author: ClemMakesApps
......@@ -25,6 +25,7 @@
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [University](university/README.md) Learn Git and GitLab through videos and courses.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
## Administrator documentation
......
......@@ -40,6 +40,10 @@ of one hour.
To enable LDAP integration you need to add your LDAP server settings in
`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
There is a Rake task to check LDAP configuration. After configuring LDAP
using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check)
for information on the LDAP check Rake task.
>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
one GitLab server.
......
# Check Rake Tasks
## Repository Integrity
Even though Git is very resilient and tries to prevent data integrity issues,
there are times when things go wrong. The following Rake tasks intend to
help GitLab administrators diagnose problem repositories so they can be fixed.
There are 3 things that are checked to determine integrity.
1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
This step verifies the connectivity and validity of objects in the repository.
1. Check for `config.lock` in the repository directory.
1. Check for any branch/references lock files in `refs/heads`.
It's important to note that the existence of `config.lock` or reference locks
alone do not necessarily indicate a problem. Lock files are routinely created
and removed as Git and GitLab perform operations on the repository. They serve
to prevent data integrity issues. However, if a Git operation is interrupted these
locks may not be cleaned up properly.
The following symptoms may indicate a problem with repository integrity. If users
experience these symptoms you may use the rake tasks described below to determine
exactly which repositories are causing the trouble.
- Receiving an error when trying to push code - `remote: error: cannot lock ref`
- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
### Check all GitLab repositories
This task loops through all repositories on the GitLab server and runs the
3 integrity checks described previously.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:repo:check
```
**Source Installation**
```bash
sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production
```
### Check repositories for a specific user
This task checks all repositories that a specific user has access to. This is important
because sometimes you know which user is experiencing trouble but you don't know
which project might be the cause.
If the rake task is executed without brackets at the end, you will be prompted
to enter a username.
**Omnibus Installation**
```bash
sudo gitlab-rake gitlab:user:check_repos
sudo gitlab-rake gitlab:user:check_repos[<username>]
```
**Source Installation**
```bash
sudo -u git -H bundle exec rake gitlab:user:check_repos RAILS_ENV=production
sudo -u git -H bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
```
Example output:
![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png)
## LDAP Check
The LDAP check Rake task will test the bind_dn and password credentials
(if configured) and will list a sample of LDAP users. This task is also
executed as part of the `gitlab:check` task, but can run independently
using the command below.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:ldap:check
```
**Source Installation**
```bash
sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
```
By default, the task will return a sample of 100 LDAP users. Change this
limit by passing a number to the check task:
```bash
rake gitlab:ldap:check[50]
```
......@@ -4,7 +4,7 @@
**Valid notification levels**
The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized:
The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized:
```
disabled
......@@ -28,6 +28,8 @@ reopen_merge_request
close_merge_request
reassign_merge_request
merge_merge_request
failed_pipeline
success_pipeline
```
## Global notification settings
......@@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
Example response:
......@@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
Example responses:
......@@ -161,7 +167,9 @@ Example responses:
"reopen_merge_request": false,
"close_merge_request": false,
"reassign_merge_request": false,
"merge_merge_request": false
"merge_merge_request": false,
"failed_pipeline": false,
"success_pipeline": false
}
}
```
......
......@@ -35,6 +35,18 @@ GET /users
]
```
In addition, you can filter users based on states eg. `blocked`, `active`
This works only to filter users who are `blocked` or `active`.
It does not support `active=false` or `blocked=false`.
```
GET /users?active=true
```
```
GET /users?blocked=true
```
### For admins
```
......@@ -122,6 +134,8 @@ For example:
GET /users?username=jack_smith
```
You can search for users who are external with: `/users?external=true`
## Single user
Get a single user.
......
......@@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
2. Install Docker Engine on server.
For more information how to install Docker Engine on different systems checkout the [Supported installations](https://docs.docker.com/engine/installation/).
For more information how to install Docker Engine on different systems
checkout the [Supported installations](https://docs.docker.com/engine/installation/).
3. Add `gitlab-runner` user to `docker` group:
......@@ -122,11 +123,17 @@ In order to do that, follow the steps:
Insecure = false
```
1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
1. You can now use `docker` in the build script (note the inclusion of the
`docker:dind` service):
```yaml
image: docker:latest
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
DOCKER_DRIVER: overlay
services:
- docker:dind
......@@ -140,15 +147,21 @@ In order to do that, follow the steps:
- docker run my-docker-image /script/to/run/tests
```
Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
* By enabling `--docker-privileged`, you are effectively disabling all of
the security mechanisms of containers and exposing your host to privilege
escalation which can lead to container breakout. For more information, check out the official Docker documentation on
[Runtime privilege and Linux capabilities][docker-cap].
* Using docker-in-docker, each build is in a clean environment without the past
history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
offered.
Docker-in-Docker works well, and is the recommended configuration, but it is
not without its own challenges:
- By enabling `--docker-privileged`, you are effectively disabling all of
the security mechanisms of containers and exposing your host to privilege
escalation which can lead to container breakout. For more information, check
out the official Docker documentation on
[Runtime privilege and Linux capabilities][docker-cap].
- Using docker-in-docker, each build is in a clean environment without the past
history. Concurrent builds work fine because every build gets it's own
instance of Docker engine so they won't conflict with each other. But this
also means builds can be slower because there's no caching of layers.
- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
form offered. To use a different driver, see
[Using the overlayfs driver](#using-the-overlayfs-driver).
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
......@@ -221,6 +234,40 @@ work as expected since volume mounting is done in the context of the host
machine, not the build container.
e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
## Using the OverlayFS driver
By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
copies the filesystem on every run. This is a very disk-intensive operation
which can be avoided if a different driver is used, for example `overlay`.
1. Make sure a recent kernel is used, preferably `>= 4.2`.
1. Check whether the `overlay` module is loaded:
```
sudo lsmod | grep overlay
```
If you see no result, then it isn't loaded. To load it use:
```
sudo modprobe overlay
```
If everything went fine, you need to make sure module is loaded on reboot.
On Ubuntu systems, this is done by editing `/etc/modules`. Just add the
following line into it:
```
overlay
```
1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`:
```
variables:
DOCKER_DRIVER: overlay
```
## Using the GitLab Container Registry
> **Note:**
......
......@@ -24,7 +24,7 @@ namespace you can use the `configure` class method. This method simply yields
the supplied block while passing `Gitlab::Metrics::Instrumentation` as its
argument. An example:
```
```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(Foo, :bar)
conf.instrument_method(Foo, :baz)
......@@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer
Instrumenting a single method:
```
```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(User, :find_by)
end
......@@ -49,7 +49,7 @@ end
Instrumenting an entire class hierarchy:
```
```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_class_hierarchy(ActiveRecord::Base)
end
......@@ -57,7 +57,7 @@ end
Instrumenting all public class methods:
```
```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_methods(User)
end
......@@ -68,7 +68,7 @@ end
The easiest way to check if a method has been instrumented is to check its
source location. For example:
```
```ruby
method = Rugged::TagCollection.instance_method(:[])
method.source_location
......
......@@ -60,7 +60,7 @@ migration was tested.
If you need to remove index, please add a condition like in following example:
```
```ruby
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
```
......@@ -75,7 +75,7 @@ need for downtime. To use this method you must disable transactions by calling
the method `disable_ddl_transaction!` in the body of your migration class like
so:
```
```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......@@ -96,7 +96,7 @@ the `up` and `down` methods in your migration class.
For example, to add the column `foo` to the `projects` table with a default
value of `10` you'd write the following:
```
```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......@@ -125,7 +125,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to
Rails migration example:
```
```ruby
add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
# or
......@@ -145,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin
Example with Arel:
```
```ruby
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
......@@ -154,7 +154,7 @@ users.group(users[:user_id]).having(users[:id].count.gt(5))
Example with plain SQL and `quote_string` helper:
```
```ruby
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
......
......@@ -129,7 +129,7 @@ Various methods for opening and reading files in Ruby can be used to read the
standard output of a process instead of a file. The following two commands do
roughly the same:
```
```ruby
`touch /tmp/pawned-by-backticks`
File.read('|touch /tmp/pawned-by-file-read')
```
......@@ -142,7 +142,7 @@ attacker cannot control the start of the filename string you are opening. For
instance, the following is sufficient to protect against accidentally starting
a shell command with `|`:
```
```ruby
# we assume repo_path is not controlled by the attacker (user)
path = File.join(repo_path, user_input)
# path cannot start with '|' now.
......@@ -160,7 +160,7 @@ Path traversal is a security where the program (GitLab) tries to restrict user
access to a certain directory on disk, but the user manages to open a file
outside that directory by taking advantage of the `../` path notation.
```
```ruby
# Suppose the user gave us a path and they are trying to trick us
user_input = '../other-repo.git/other-file'
......@@ -177,7 +177,7 @@ File.open(full_path) do # Oops!
A good way to protect against this is to compare the full path with its
'absolute path' according to Ruby's `File.absolute_path`.
```
```ruby
full_path = File.join(repo_path, user_input)
if full_path != File.absolute_path(full_path)
raise "Invalid path: #{full_path.inspect}"
......
# Check Rake Tasks
## Repository Integrity
Even though Git is very resilient and tries to prevent data integrity issues,
there are times when things go wrong. The following Rake tasks intend to
help GitLab administrators diagnose problem repositories so they can be fixed.
There are 3 things that are checked to determine integrity.
1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
This step verifies the connectivity and validity of objects in the repository.
1. Check for `config.lock` in the repository directory.
1. Check for any branch/references lock files in `refs/heads`.
It's important to note that the existence of `config.lock` or reference locks
alone do not necessarily indicate a problem. Lock files are routinely created
and removed as Git and GitLab perform operations on the repository. They serve
to prevent data integrity issues. However, if a Git operation is interrupted these
locks may not be cleaned up properly.
The following symptoms may indicate a problem with repository integrity. If users
experience these symptoms you may use the rake tasks described below to determine
exactly which repositories are causing the trouble.
- Receiving an error when trying to push code - `remote: error: cannot lock ref`
- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
### Check all GitLab repositories
This task loops through all repositories on the GitLab server and runs the
3 integrity checks described previously.
```
# omnibus-gitlab
sudo gitlab-rake gitlab:repo:check
# installation from source
bundle exec rake gitlab:repo:check RAILS_ENV=production
```
### Check repositories for a specific user
This task checks all repositories that a specific user has access to. This is important
because sometimes you know which user is experiencing trouble but you don't know
which project might be the cause.
If the rake task is executed without brackets at the end, you will be prompted
to enter a username.
```bash
# omnibus-gitlab
sudo gitlab-rake gitlab:user:check_repos
sudo gitlab-rake gitlab:user:check_repos[<username>]
# installation from source
bundle exec rake gitlab:user:check_repos RAILS_ENV=production
bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
```
Example output:
![gitlab:user:check_repos output](check_repos_output.png)
This document was moved to [administration/raketasks/check](../administration/raketasks/check.md).
......@@ -66,6 +66,7 @@ Below is the table of events users can be notified of:
In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- the author of the pipeline
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
......@@ -88,6 +89,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | |
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The above, plus the author of the pipeline |
| Successful pipeline | The above, plus the author of the pipeline |
In addition, if the title or description of an Issue or Merge Request is
......
......@@ -10,6 +10,9 @@ module API
# GET /users
# GET /users?search=Admin
# GET /users?username=root
# GET /users?active=true
# GET /users?external=true
# GET /users?blocked=true
get do
unless can?(current_user, :read_users_list, nil)
render_api_error!("Not authorized.", 403)
......@@ -20,9 +23,11 @@ module API
else
skip_ldap = params[:skip_ldap].present? && params[:skip_ldap] == 'true'
@users = User.all
@users = @users.active if params[:active].present?
@users = @users.active if to_boolean(params[:active])
@users = @users.non_ldap if skip_ldap
@users = @users.search(params[:search]) if params[:search].present?
@users = @users.blocked if to_boolean(params[:blocked])
@users = @users.external if to_boolean(params[:external]) && current_user.is_admin?
@users = paginate @users
end
......
......@@ -71,6 +71,14 @@ module Banzai
@doc = parse_html(rinku)
end
# Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme
def contains_unsafe?(scheme)
return false unless scheme
scheme = scheme.strip.downcase
Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) }
end
# Autolinks any text matching LINK_PATTERN that Rinku didn't already
# replace
def text_parse
......@@ -89,17 +97,27 @@ module Banzai
doc
end
def autolink_filter(text)
text.gsub(LINK_PATTERN) do |match|
# Remove any trailing HTML entities and store them for appending
# outside the link element. The entity must be marked HTML safe in
# order to be output literally rather than escaped.
match.gsub!(/((?:&[\w#]+;)+)\z/, '')
dropped = ($1 || '').html_safe
options = link_options.merge(href: match)
content_tag(:a, match, options) + dropped
def autolink_match(match)
# start by stripping out dangerous links
begin
uri = Addressable::URI.parse(match)
return match if contains_unsafe?(uri.scheme)
rescue Addressable::URI::InvalidURIError
return match
end
# Remove any trailing HTML entities and store them for appending
# outside the link element. The entity must be marked HTML safe in
# order to be output literally rather than escaped.
match.gsub!(/((?:&[\w#]+;)+)\z/, '')
dropped = ($1 || '').html_safe
options = link_options.merge(href: match)
content_tag(:a, match, options) + dropped
end
def autolink_filter(text)
text.gsub(LINK_PATTERN) { |match| autolink_match(match) }
end
def link_options
......
......@@ -63,12 +63,7 @@ module Banzai
nodes.select do |node|
if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i
if project && project.id == node_id
true
else
can?(user, :read_project, projects[node_id])
end
can_read_reference?(user, projects[node_id])
else
true
end
......@@ -226,6 +221,15 @@ module Banzai
attr_reader :current_user, :project
# When a feature is disabled or visible only for
# team members we should not allow team members
# see reference comments.
# Override this method on subclasses
# to check if user can read resource
def can_read_reference?(user, ref_project)
raise NotImplementedError
end
def lazy(&block)
Gitlab::Lazy.new(&block)
end
......
......@@ -29,6 +29,12 @@ module Banzai
commits
end
private
def can_read_reference?(user, ref_project)
can?(user, :download_code, ref_project)
end
end
end
end
......@@ -33,6 +33,12 @@ module Banzai
range.valid_commits? ? range : nil
end
private
def can_read_reference?(user, ref_project)
can?(user, :download_code, ref_project)
end
end
end
end
......@@ -20,6 +20,12 @@ module Banzai
def issue_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
private
def can_read_reference?(user, ref_project)
can?(user, :read_issue, ref_project)
end
end
end
end
......@@ -6,6 +6,12 @@ module Banzai
def references_relation
Label
end
private
def can_read_reference?(user, ref_project)
can?(user, :read_label, ref_project)
end
end
end
end
......@@ -6,6 +6,12 @@ module Banzai
def references_relation
MergeRequest.includes(:author, :assignee, :target_project)
end
private
def can_read_reference?(user, ref_project)
can?(user, :read_merge_request, ref_project)
end
end
end
end
......@@ -6,6 +6,12 @@ module Banzai
def references_relation
Milestone
end
private
def can_read_reference?(user, ref_project)
can?(user, :read_milestone, ref_project)
end
end
end
end
......@@ -6,6 +6,12 @@ module Banzai
def references_relation
Snippet
end
private
def can_read_reference?(user, ref_project)
can?(user, :read_project_snippet, ref_project)
end
end
end
end
......@@ -30,22 +30,36 @@ module Banzai
nodes.each do |node|
if node.has_attribute?(group_attr)
node_group = groups[node.attr(group_attr).to_i]
if node_group &&
can?(user, :read_group, node_group)
visible << node
end
# Remaining nodes will be processed by the parent class'
# implementation of this method.
next unless can_read_group_reference?(node, user, groups)
visible << node
elsif can_read_project_reference?(node)
visible << node
else
remaining << node
end
end
# If project does not belong to a group
# and does not have the same project id as the current project
# base class will check if user can read the project that contains
# the user reference.
visible + super(current_user, remaining)
end
# 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 && can?(user, :read_group, node_group)
end
def can_read_project_reference?(node)
node_id = node.attr('data-project').to_i
project && project.id == node_id
end
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
......@@ -88,6 +102,10 @@ module Banzai
collection_objects_for_ids(Project, ids).
flat_map { |p| p.team.members.to_a }
end
def can_read_reference?(user, ref_project)
can?(user, :read_project, ref_project)
end
end
end
end
module Gitlab
class ContributionsCalendar
attr_reader :activity_dates, :projects, :user
attr_reader :contributor
attr_reader :current_user
attr_reader :projects
def initialize(projects, user)
@projects = projects
@user = user
def initialize(contributor, current_user = nil)
@contributor = contributor
@current_user = current_user
@projects = ContributedProjectsFinder.new(contributor).execute(current_user)
end
def activity_dates
return @activity_dates if @activity_dates.present?
@activity_dates = {}
# Can't use Event.contributions here because we need to check 3 different
# project_features for the (currently) 3 different contribution types
date_from = 1.year.ago
repo_events = event_counts(date_from, :repository).
having(action: Event::PUSHED)
issue_events = event_counts(date_from, :issues).
having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue")
mr_events = event_counts(date_from, :merge_requests).
having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
events = Event.reorder(nil).contributions.where(author_id: user.id).
where("created_at > ?", date_from).where(project_id: projects).
group('date(created_at)').
select('date(created_at) as date, count(id) as total_amount').
map(&:attributes)
union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
events = Event.find_by_sql(union.to_sql).map(&:attributes)
activity_dates = (1.year.ago.to_date..Date.today).to_a
activity_dates.each do |date|
day_events = events.find { |day_events| day_events["date"] == date }
if day_events
@activity_dates[date] = day_events["total_amount"]
end
@activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
activities[event["date"]] += event["total_amount"]
end
@activity_dates
end
def events_by_date(date)
events = Event.contributions.where(author_id: user.id).
where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day).
events = Event.contributions.where(author_id: contributor.id).
where(created_at: date.beginning_of_day..date.end_of_day).
where(project_id: projects)
events.select do |event|
event.push? || event.issue? || event.merge_request?
end
# Use visible_to_user? instead of the complicated logic in activity_dates
# because we're only viewing the events for a single day.
events.select {|event| event.visible_to_user?(current_user) }
end
def starting_year
......@@ -49,5 +48,30 @@ module Gitlab
def starting_month
Date.today.month
end
private
def event_counts(date_from, feature)
t = Event.arel_table
# re-running the contributed projects query in each union is expensive, so
# use IN(project_ids...) instead. It's the intersection of two users so
# the list will be (relatively) short
@contributed_project_ids ||= projects.uniq.pluck(:id)
authed_projects = Project.where(id: @contributed_project_ids).
with_feature_available_for_user(feature, current_user).
reorder(nil).
select(:id)
conditions = t[:created_at].gteq(date_from.beginning_of_day).
and(t[:created_at].lteq(Date.today.end_of_day)).
and(t[:author_id].eq(contributor.id))
Event.reorder(nil).
select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount').
group(t[:project_id], t[:target_type], t[:action], 'date(created_at)').
where(conditions).
having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql)))
end
end
end
......@@ -5,11 +5,6 @@ module Gitlab
include PathLocksHelper
UnauthorizedError = Class.new(StandardError)
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
GIT_ANNEX_COMMANDS = %w{ git-annex-shell }
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS + GIT_ANNEX_COMMANDS
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.',
......@@ -17,6 +12,11 @@ module Gitlab
no_repo: 'A repository for this project does not exist yet.'
}
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
GIT_ANNEX_COMMANDS = %w{ git-annex-shell }
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS + GIT_ANNEX_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
def initialize(actor, project, protocol, authentication_abilities:)
......
......@@ -105,6 +105,10 @@ module Gitlab
options['external_groups']
end
def has_auth?
options['password'] || options['bind_dn']
end
protected
def base_config
......@@ -135,10 +139,6 @@ module Gitlab
}
}
end
def has_auth?
options['password'] || options['bind_dn']
end
end
end
end
......@@ -761,7 +761,7 @@ namespace :gitlab do
end
namespace :ldap do
task :check, [:limit] => :environment do |t, args|
task :check, [:limit] => :environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100)
......@@ -769,7 +769,7 @@ namespace :gitlab do
start_checking "LDAP"
if Gitlab::LDAP::Config.enabled?
print_users(args.limit)
check_ldap(args.limit)
else
puts 'LDAP is disabled in config/gitlab.yml'
end
......@@ -777,21 +777,42 @@ namespace :gitlab do
finished_checking "LDAP"
end
def print_users(limit)
puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
def check_ldap(limit)
servers = Gitlab::LDAP::Config.providers
servers.each do |server|
puts "Server: #{server}"
Gitlab::LDAP::Adapter.open(server) do |adapter|
users = adapter.users(adapter.config.uid, '*', limit)
users.each do |user|
puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
begin
Gitlab::LDAP::Adapter.open(server) do |adapter|
check_ldap_auth(adapter)
puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
users = adapter.users(adapter.config.uid, '*', limit)
users.each do |user|
puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
end
end
rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
puts "Could not connect to the LDAP server: #{e.message}".color(:red)
end
end
end
def check_ldap_auth(adapter)
auth = adapter.config.has_auth?
if auth && adapter.ldap.bind
message = 'Success'.color(:green)
elsif auth
message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
else
message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
end
puts "LDAP authentication... #{message}"
end
end
namespace :repo do
......
require 'spec_helper'
feature 'Group issues page', feature: true do
let(:path) { issues_group_path(group) }
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
include_examples 'project features apply to issuables', Issue
end
require 'spec_helper'
feature 'Group merge requests page', feature: true do
let(:path) { merge_requests_group_path(group) }
let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")}
include_examples 'project features apply to issuables', MergeRequest
end
......@@ -22,6 +22,7 @@ feature 'Start new branch from an issue', feature: true do
create(:note, :on_issue, :system, project: project, noteable: issue,
note: "Mentioned in !#{referenced_mr.iid}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
description: "Fixes ##{issue.iid}", author: user)
......
......@@ -218,42 +218,24 @@ describe ApplicationHelper do
end
it 'includes a default js-timeago class' do
expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
expect(element.attr('class')).to eq 'js-timeago'
end
it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')).
to eq 'js-timeago custom_class js-timeago-pending'
to eq 'js-timeago custom_class'
end
it 'accepts a custom tooltip placement' do
expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom'
end
it 're-initializes timeago Javascript' do
el = element.next_element
expect(el.name).to eq 'script'
expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
end
it 'allows the script tag to be excluded' do
expect(element(skip_js: true)).not_to include 'script'
end
it 'converts to Time' do
expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
end
it 'add class for the short format and includes inline script' do
it 'add class for the short format' do
timeago_element = element(short_format: 'short')
expect(timeago_element.attr('class')).to eq 'js-short-timeago js-timeago-pending'
script_element = timeago_element.next_element
expect(script_element.name).to eq 'script'
end
it 'add class for the short format and does not include inline script' do
timeago_element = element(short_format: 'short', skip_js: true)
expect(timeago_element.attr('class')).to eq 'js-short-timeago'
expect(timeago_element.next_element).to eq nil
end
......
......@@ -61,7 +61,7 @@ describe DiffHelper do
describe '#diff_line_content' do
it 'returns non breaking space when line is empty' do
expect(diff_line_content(nil)).to eq(' &nbsp;')
expect(diff_line_content(nil)).to eq('&nbsp;')
end
it 'returns the line itself' do
......
{
"plugins": ["jasmine"],
"env": {
"jasmine": true
},
"extends": "plugin:jasmine/recommended",
"rules": {
"prefer-arrow-callback": 0,
"func-names": 0
}
}
/* global Build */
/* eslint-disable no-new */
//= require build
//= require breakpoints
//= require jquery.nicescroll
//= require turbolinks
(() => {
describe('Build', () => {
fixture.preload('build.html');
beforeEach(function () {
fixture.load('build.html');
spyOn($, 'ajax');
});
describe('constructor', () => {
beforeEach(function () {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
describe('setup', function () {
beforeEach(function () {
this.build = new Build();
});
it('copies build options', function () {
expect(this.build.pageUrl).toBe('http://example.com/root/test-build/builds/2');
expect(this.build.buildUrl).toBe('http://example.com/root/test-build/builds/2.json');
expect(this.build.buildStatus).toBe('passed');
expect(this.build.buildStage).toBe('test');
expect(this.build.state).toBe('buildstate');
});
it('only shows the jobs matching the current stage', function () {
expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
});
it('selects the current stage in the build dropdown menu', function () {
expect($('.stage-selection').text()).toBe('test');
});
it('updates the jobs when the build dropdown changes', function () {
$('.stage-item:contains("build")').click();
expect($('.stage-selection').text()).toBe('build');
expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
});
});
describe('initial build trace', function () {
beforeEach(function () {
new Build();
});
it('displays the initial build trace', function () {
expect($.ajax.calls.count()).toBe(1);
const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
expect(url).toBe('http://example.com/root/test-build/builds/2.json');
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
});
it('removes the spinner', function () {
const [{ success, context }] = $.ajax.calls.argsFor(0);
success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0);
});
});
describe('running build', function () {
beforeEach(function () {
$('.js-build-options').data('buildStatus', 'running');
this.build = new Build();
spyOn(this.build, 'location')
.and.returnValue('http://example.com/root/test-build/builds/2');
});
it('updates the build trace on an interval', function () {
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(2);
let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
expect(url).toBe(
'http://example.com/root/test-build/builds/2/trace.json?state=buildstate'
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
success.call(context, {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
expect(this.build.state).toBe('newstate');
jasmine.clock().tick(4001);
expect($.ajax.calls.count()).toBe(3);
[{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
expect(url).toBe(
'http://example.com/root/test-build/builds/2/trace.json?state=newstate'
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
success.call(context, {
html: '<span>More</span>',
status: 'running',
state: 'finalstate',
append: true,
});
expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
expect(this.build.state).toBe('finalstate');
});
it('replaces the entire build trace', function () {
jasmine.clock().tick(4001);
let [{ success, context }] = $.ajax.calls.argsFor(1);
success.call(context, {
html: '<span>Update</span>',
status: 'running',
append: true,
});
expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
jasmine.clock().tick(4001);
[{ success, context }] = $.ajax.calls.argsFor(2);
success.call(context, {
html: '<span>Different</span>',
status: 'running',
append: false,
});
expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
});
it('reloads the page when the build is done', function () {
spyOn(Turbolinks, 'visit');
jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1);
success.call(context, {
html: '<span>Final</span>',
status: 'passed',
append: true,
});
expect(Turbolinks.visit).toHaveBeenCalledWith(
'http://example.com/root/test-build/builds/2'
);
});
});
});
});
})();
.build-page
.prepend-top-default
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
#js-build-scroll.scroll-controls
%a.btn{href: '#build-trace'}
%i.fa.fa-angle-up
%a.btn{href: '#down-build-trace'}
%i.fa.fa-angle-down
%pre.build-trace#build-trace
%code.bash.js-build-output
%i.fa.fa-refresh.fa-spin.js-build-refresh
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Build
%strong #1
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
%i.fa.fa-angle-double-right
.blocks-container
.dropdown.build-dropdown
.title Stage
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.stage-selection More
%i.fa.fa-caret-down
%ul.dropdown-menu
%li
%a.stage-item build
%li
%a.stage-item test
%li
%a.stage-item deploy
.builds-container
.build-job{data: {stage: 'build'}}
%a{href: 'http://example.com/root/test-build/builds/1'}
%i.fa.fa-check
%i.fa.fa-check-circle-o
%span
Setup
.build-job{data: {stage: 'test'}}
%a{href: 'http://example.com/root/test-build/builds/2'}
%i.fa.fa-check
%i.fa.fa-check-circle-o
%span
Tests
.build-job{data: {stage: 'deploy'}}
%a{href: 'http://example.com/root/test-build/builds/3'}
%i.fa.fa-check
%i.fa.fa-check-circle-o
%span
Deploy
.js-build-options{ data: { page_url: 'http://example.com/root/test-build/builds/2',
build_url: 'http://example.com/root/test-build/builds/2.json',
build_status: 'passed',
build_stage: 'test',
state1: 'buildstate' }}
/* eslint-disable */
/*= require merge_request_widget */
/*= require jquery.timeago.js */
/*= require lib/utils/timeago.js */
(function() {
describe('MergeRequestWidget', function() {
......
......@@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(doc.at_css('a')['href']).to eq link
end
it 'autolinks rdar' do
link = 'rdar://localhost.com/blah'
doc = filter("See #{link}")
expect(doc.at_css('a').text).to eq link
expect(doc.at_css('a')['href']).to eq link
end
it 'does not autolink javascript' do
link = 'javascript://alert(document.cookie);'
doc = filter("See #{link}")
expect(doc.at_css('a')).to be_nil
end
it 'does not autolink bad URLs' do
link = 'foo://23423:::asdf'
doc = filter("See #{link}")
expect(doc.to_s).to eq("See #{link}")
end
it 'does not include trailing punctuation' do
doc = filter("See #{link}.")
expect(doc.at_css('a').text).to eq link
......
......@@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do
and_return(parser_class)
end
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
context 'valid projects' do
before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) }
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
it 'allows permitted Project references' do
user = create(:user)
project = create(:empty_project)
project.team << [user, :master]
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
expect(doc.css('a').length).to eq 1
end
end
it 'allows permitted Project references' do
user = create(:user)
project = create(:empty_project)
project.team << [user, :master]
context 'invalid projects' do
before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) }
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
it 'removes unpermitted references' do
user = create(:user)
project = create(:empty_project)
expect(doc.css('a').length).to eq 1
end
link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
it 'handles invalid Project references' do
link = reference_link(project: 12345, reference_type: 'test')
expect(doc.css('a').length).to eq 0
end
it 'handles invalid references' do
link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error
expect { filter(link) }.not_to raise_error
end
end
end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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