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) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
// Recreate with custom template
return $timeagoEls.tooltip({
$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);
});
};
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)
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
@projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
GroupProjectsFinder.new(group).execute(current_user)
else
@projects = ProjectsFinder.new.execute(current_user).reorder(nil)
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,19 +27,21 @@
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
.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")
......@@ -49,11 +50,4 @@
= 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,8 +97,15 @@ module Banzai
doc
end
def autolink_filter(text)
text.gsub(LINK_PATTERN) do |match|
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.
......@@ -100,6 +115,9 @@ module Banzai
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)
next unless can_read_group_reference?(node, user, groups)
visible << node
elsif can_read_project_reference?(node)
visible << node
end
# Remaining nodes will be processed by the parent class'
# implementation of this method.
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,23 +777,44 @@ 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}"
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
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment 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,33 +28,41 @@ describe Banzai::Filter::RedactorFilter, lib: true do
and_return(parser_class)
end
it 'removes unpermitted Project references' do
context 'valid projects' do
before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) }
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
context 'invalid projects' do
before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) }
it 'removes unpermitted 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 1
expect(doc.css('a').length).to eq 0
end
it 'handles invalid Project references' do
it 'handles invalid references' do
link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error
end
end
end
context 'with data-issue' do
context 'for confidential issues' do
......
......@@ -27,41 +27,12 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
let(:link) { empty_html_link }
context 'when the link has a data-project attribute' do
it 'returns the nodes if the attribute value equals the current project ID' do
it 'checks if user can read the resource' do
link['data-project'] = project.id.to_s
expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns the nodes if the user can read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array when the attribute value is empty' do
link['data-project'] = ''
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
it 'returns an empty Array when the user can not read the project' do
other_project = create(:empty_project, :public)
link['data-project'] = other_project.id.to_s
expect(Ability).to receive(:allowed?).
with(user, :read_project, other_project).
and_return(false)
expect(subject).to receive(:can_read_reference?).with(user, project)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
subject.nodes_visible_to_user(user, [link])
end
end
......
......@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before { link['data-commit'] = 123 }
it_behaves_like "referenced feature visibility", "repository"
end
end
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
......
......@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before { link['data-commit-range'] = '123..456' }
it_behaves_like "referenced feature visibility", "repository"
end
end
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
......
......@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before { link['data-external-issue'] = 123 }
it_behaves_like "referenced feature visibility", "issues"
end
end
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
......
......@@ -6,8 +6,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) }
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
subject { described_class.new(project, user) }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
......@@ -15,6 +15,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
link['data-issue'] = issue.id.to_s
end
it_behaves_like "referenced feature visibility", "issues"
it 'returns the nodes when the user can read the issue' do
expect(Ability).to receive(:issues_readable_by_user).
with([issue], user).
......
......@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before { link['data-label'] = label.id.to_s }
it_behaves_like "referenced feature visibility", "issues", "merge_requests"
end
end
describe '#referenced_by' do
describe 'when the link has a data-label attribute' do
context 'using an existing label ID' do
......
......@@ -8,6 +8,19 @@ describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
subject { described_class.new(merge_request.target_project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
let(:project) { merge_request.target_project }
before do
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
link['data-merge-request'] = merge_request.id.to_s
end
it_behaves_like "referenced feature visibility", "merge_requests"
end
end
describe '#referenced_by' do
describe 'when the link has a data-merge-request attribute' do
context 'using an existing merge request ID' do
......
......@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before { link['data-milestone'] = milestone.id.to_s }
it_behaves_like "referenced feature visibility", "issues", "merge_requests"
end
end
describe '#referenced_by' do
describe 'when the link has a data-milestone attribute' do
context 'using an existing milestone ID' do
......
......@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
before { link['data-snippet'] = snippet.id.to_s }
it_behaves_like "referenced feature visibility", "snippets"
end
end
describe '#referenced_by' do
describe 'when the link has a data-snippet attribute' do
context 'using an existing snippet ID' do
......
......@@ -103,6 +103,8 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
# Ensure that we dont call for Ability.allowed?
# When project_id in the node is equal to current project ID
expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
......
......@@ -257,8 +257,9 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
context 'with an external issue tracker reference' do
it 'extracts the referenced issue' do
jira_project = create(:jira_project, name: 'JIRA_EXT1')
jira_project.team << [jira_project.creator, :master]
jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project)
closing_issue_extractor = described_class.new jira_project
closing_issue_extractor = described_class.new(jira_project, jira_project.creator)
message = "Resolve #{jira_issue.to_reference}"
expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue])
......
require 'spec_helper'
describe Gitlab::ContributionsCalendar do
let(:contributor) { create(:user) }
let(:user) { create(:user) }
let(:private_project) do
create(:empty_project, :private) do |project|
create(:project_member, user: contributor, project: project)
end
end
let(:public_project) do
create(:empty_project, :public) do |project|
create(:project_member, user: contributor, project: project)
end
end
let(:feature_project) do
create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
create(:project_member, user: contributor, project: project).project
end
end
let(:today) { Time.now.to_date }
let(:last_week) { today - 7.days }
let(:last_year) { today - 1.year }
before do
travel_to today
end
after do
travel_back
end
def calendar(current_user = nil)
described_class.new(contributor, current_user)
end
def create_event(project, day)
@targets ||= {}
@targets[project] ||= create(:issue, project: project, author: contributor)
Event.create!(
project: project,
action: Event::CREATED,
target: @targets[project],
author: contributor,
created_at: day,
)
end
describe '#activity_dates' do
it "returns a hash of date => count" do
create_event(public_project, last_week)
create_event(public_project, last_week)
create_event(public_project, today)
expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
end
it "only shows private events to authorized users" do
create_event(private_project, today)
create_event(feature_project, today)
expect(calendar.activity_dates[today]).to eq(0)
expect(calendar(user).activity_dates[today]).to eq(0)
expect(calendar(contributor).activity_dates[today]).to eq(2)
end
end
describe '#events_by_date' do
it "returns all events for a given date" do
e1 = create_event(public_project, today)
e2 = create_event(public_project, today)
create_event(public_project, last_week)
expect(calendar.events_by_date(today)).to contain_exactly(e1, e2)
end
it "only shows private events to authorized users" do
e1 = create_event(public_project, today)
e2 = create_event(private_project, today)
e3 = create_event(feature_project, today)
create_event(public_project, last_week)
expect(calendar.events_by_date(today)).to contain_exactly(e1)
expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
end
end
describe '#starting_year' do
it "should be the start of last year" do
expect(calendar.starting_year).to eq(last_year.year)
end
end
describe '#starting_month' do
it "should be the start of this month" do
expect(calendar.starting_month).to eq(today.month)
end
end
end
......@@ -6,7 +6,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) }
before { old_project.team << [user, :guest] }
before { old_project.team << [user, :reporter] }
describe '#rewrite' do
subject do
......
require 'spec_helper'
describe Gitlab::LDAP::Config, lib: true do
let(:config) { Gitlab::LDAP::Config.new provider }
let(:provider) { 'ldapmain' }
include LdapHelpers
let(:config) { Gitlab::LDAP::Config.new('ldapmain') }
describe '#initalize' do
it 'requires a provider' do
expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError
end
it "works" do
it 'works' do
expect(config).to be_a described_class
end
......@@ -17,4 +18,34 @@ describe Gitlab::LDAP::Config, lib: true do
expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(Gitlab::LDAP::Config::InvalidProvider)
end
end
describe '#has_auth?' do
it 'is true when password is set' do
stub_ldap_config(
options: {
'bind_dn' => 'uid=admin,dc=example,dc=com',
'password' => 'super_secret'
}
)
expect(config.has_auth?).to be_truthy
end
it 'is true when bind_dn is set and password is empty' do
stub_ldap_config(
options: {
'bind_dn' => 'uid=admin,dc=example,dc=com',
'password' => ''
}
)
expect(config.has_auth?).to be_truthy
end
it 'is false when password and bind_dn are not set' do
stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil })
expect(config.has_auth?).to be_falsey
end
end
end
......@@ -3,6 +3,8 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
before { project.team << [project.creator, :developer] }
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
it 'accesses valid user objects' do
......@@ -42,7 +44,6 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid issue objects' do
project.team << [project.creator, :developer]
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
......
......@@ -524,4 +524,78 @@ describe Ci::Pipeline, models: true do
expect(pipeline.merge_requests).to be_empty
end
end
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('master').sha,
user: create(:user))
end
before do
reset_delivered_emails!
project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
perform_enqueued_jobs do
pipeline.enqueue
pipeline.run
end
end
shared_examples 'sending a notification' do
it 'sends an email' do
should_only_email(pipeline.user, kind: :bcc)
end
end
shared_examples 'not sending any notification' do
it 'does not send any email' do
should_not_email_anyone
end
end
context 'with success pipeline' do
before do
perform_enqueued_jobs do
pipeline.succeed
end
end
it_behaves_like 'sending a notification'
end
context 'with failed pipeline' do
before do
perform_enqueued_jobs do
pipeline.drop
end
end
it_behaves_like 'sending a notification'
end
context 'with skipped pipeline' do
before do
perform_enqueued_jobs do
pipeline.skip
end
end
it_behaves_like 'not sending any notification'
end
context 'with cancelled pipeline' do
before do
perform_enqueued_jobs do
pipeline.cancel
end
end
it_behaves_like 'not sending any notification'
end
end
end
......@@ -97,6 +97,11 @@ describe Issue, "Issuable" do
end
end
describe '.to_ability_name' do
it { expect(Issue.to_ability_name).to eq("issue") }
it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
end
describe "#today?" do
it "returns true when created today" do
# Avoid timezone differences and just return exactly what we want
......
......@@ -27,13 +27,14 @@ describe Event, models: true do
end
describe "Push event" do
let(:project) { create(:project) }
let(:project) { create(:project, :private) }
let(:user) { project.owner }
let(:event) { create_event(project, user) }
it do
expect(event.push?).to be_truthy
expect(event.visible_to_user?).to be_truthy
expect(event.visible_to_user?(user)).to be_truthy
expect(event.visible_to_user?(nil)).to be_falsey
expect(event.tag?).to be_falsey
expect(event.branch_name).to eq("master")
expect(event.author).to eq(user)
......
......@@ -22,7 +22,7 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
describe 'visible_to_user' do
describe '.visible_to_user' do
let(:user) { create(:user) }
let(:authorized_user) { create(:user) }
let(:project) { create(:project, namespace: authorized_user.namespace) }
......@@ -102,17 +102,17 @@ describe Issue, models: true do
it 'returns the merge request to close this issue' do
mr
expect(issue.closed_by_merge_requests).to eq([mr])
expect(issue.closed_by_merge_requests(mr.author)).to eq([mr])
end
it "returns an empty array when the merge request is closed already" do
closed_mr
expect(issue.closed_by_merge_requests).to eq([])
expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([])
end
it "returns an empty array when the current issue is closed already" do
expect(closed_issue.closed_by_merge_requests).to eq([])
expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([])
end
end
......@@ -218,7 +218,7 @@ describe Issue, models: true do
source_project: subject.project,
source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user)
expect(subject.referenced_merge_requests).not_to be_empty
expect(subject.referenced_merge_requests(user)).not_to be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
......@@ -314,6 +314,22 @@ describe Issue, models: true do
end
describe '#visible_to_user?' do
context 'without a user' do
let(:issue) { build(:issue) }
it 'returns true when the issue is publicly visible' do
expect(issue).to receive(:publicly_visible?).and_return(true)
expect(issue.visible_to_user?).to eq(true)
end
it 'returns false when the issue is not publicly visible' do
expect(issue).to receive(:publicly_visible?).and_return(false)
expect(issue.visible_to_user?).to eq(false)
end
end
context 'with a user' do
let(:user) { build(:user) }
let(:issue) { build(:issue) }
......@@ -329,26 +345,24 @@ describe Issue, models: true do
expect(issue.visible_to_user?(user)).to eq(false)
end
end
context 'without a user' do
let(:issue) { build(:issue) }
it 'returns false when feature is disabled' do
expect(issue).not_to receive(:readable_by?)
it 'returns true when the issue is publicly visible' do
expect(issue).to receive(:publicly_visible?).and_return(true)
issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
expect(issue.visible_to_user?).to eq(true)
expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false when the issue is not publicly visible' do
expect(issue).to receive(:publicly_visible?).and_return(false)
it 'returns false when restricted for members' do
expect(issue).not_to receive(:readable_by?)
expect(issue.visible_to_user?).to eq(false)
end
issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
expect(issue.visible_to_user?(user)).to eq(false)
end
end
describe '#readable_by?' do
describe 'with a regular user that is not a team member' do
let(:user) { create(:user) }
......@@ -358,13 +372,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false for a confidential issue' do
issue = build(:issue, project: project, confidential: true)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(false)
end
end
......@@ -375,13 +389,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(false)
end
end
......@@ -393,13 +407,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(false)
end
end
end
......@@ -410,26 +424,28 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(false)
end
context 'when the user is the project owner' do
before { project.team << [user, :master] }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
......@@ -447,13 +463,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
end
......@@ -467,13 +483,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
end
......@@ -487,13 +503,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
......@@ -505,13 +521,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).to be_readable_by(user)
expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
......@@ -523,13 +539,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
expect(issue).to be_publicly_visible
expect(issue).to be_truthy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_publicly_visible
expect(issue).not_to be_falsy
end
end
......@@ -539,13 +555,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_publicly_visible
expect(issue).not_to be_falsy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_publicly_visible
expect(issue).not_to be_falsy
end
end
......@@ -555,13 +571,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
expect(issue).not_to be_publicly_visible
expect(issue).not_to be_falsy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
expect(issue).not_to be_publicly_visible
expect(issue).not_to be_falsy
end
end
end
......
......@@ -13,7 +13,7 @@ describe PipelinesEmailService do
end
before do
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
describe 'Validations' do
......@@ -23,14 +23,6 @@ describe PipelinesEmailService do
end
it { is_expected.to validate_presence_of(:recipients) }
context 'when pusher is added' do
before do
subject.add_pusher = true
end
it { is_expected.not_to validate_presence_of(:recipients) }
end
end
context 'when service is inactive' do
......@@ -66,8 +58,7 @@ describe PipelinesEmailService do
end
it 'sends email' do
sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
expect(sent_to).to contain_exactly(recipient)
should_only_email(double(notification_email: recipient), kind: :bcc)
end
end
......@@ -79,7 +70,7 @@ describe PipelinesEmailService do
end
it 'does not send email' do
expect(ActionMailer::Base.deliveries).to be_empty
should_not_email_anyone
end
end
......
......@@ -48,6 +48,17 @@ describe API::API, api: true do
end['username']).to eq(username)
end
it "returns an array of blocked users" do
ldap_blocked_user
create(:user, state: 'blocked')
get api("/users?blocked=true", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
end
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response).to have_http_status(200)
......@@ -69,6 +80,16 @@ describe API::API, api: true do
expect(json_response.first.keys).to include 'last_sign_in_at'
expect(json_response.first.keys).to include 'confirmed_at'
end
it "returns an array of external users" do
create(:user, external: true)
get api("/users?external=true", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to all(include('external' => true))
end
end
context "when authenticated and ldap is enabled" do
......
......@@ -20,7 +20,7 @@ describe JwtController do
end
end
context 'when using authorized request' do
context 'when using authenticated request' do
context 'using CI token' do
let(:build) { create(:ci_build, :running) }
let(:project) { build.project }
......@@ -65,7 +65,7 @@ describe JwtController do
let(:access_token) { create(:personal_access_token, user: user) }
let(:headers) { { authorization: credentials(user.username, access_token.token) } }
it 'rejects the authorization attempt' do
it 'accepts the authorization attempt' do
expect(response).to have_http_status(200)
end
end
......@@ -81,6 +81,20 @@ describe JwtController do
end
end
context 'when using unauthenticated request' do
it 'accepts the authorization attempt' do
get '/jwt/auth', parameters
expect(response).to have_http_status(200)
end
it 'allows read access' do
expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities)
get '/jwt/auth', parameters
end
end
context 'unknown service' do
subject! { get '/jwt/auth', service: 'unknown' }
......
require 'spec_helper'
describe Ci::SendPipelineNotificationService, services: true do
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('master').sha,
user: user,
status: status)
end
let(:project) { create(:project) }
let(:user) { create(:user) }
subject{ described_class.new(pipeline) }
describe '#execute' do
before do
reset_delivered_emails!
end
shared_examples 'sending emails' do
it 'sends an email to pipeline user' do
perform_enqueued_jobs do
subject.execute([user.email])
end
email = ActionMailer::Base.deliveries.last
expect(email.subject).to include(email_subject)
expect(email.to).to eq([user.email])
end
end
context 'with success pipeline' do
let(:status) { 'success' }
let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
it_behaves_like 'sending emails'
end
context 'with failed pipeline' do
let(:status) { 'failed' }
let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
it_behaves_like 'sending emails'
end
end
end
......@@ -17,7 +17,7 @@ describe NotificationService, services: true do
it 'sends no emails when no new mentions are present' do
send_notifications
expect(ActionMailer::Base.deliveries).to be_empty
should_not_email_anyone
end
it 'emails new mentions with a watch level higher than participant' do
......@@ -27,7 +27,7 @@ describe NotificationService, services: true do
it 'does not email new mentions with a watch level equal to or less than participant' do
send_notifications(@u_participating, @u_mentioned)
expect(ActionMailer::Base.deliveries).to be_empty
should_not_email_anyone
end
end
......@@ -79,7 +79,7 @@ describe NotificationService, services: true do
# Ensure create SentNotification by noteable = issue 6 times, not noteable = note
expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
notification.new_note(note)
......@@ -111,7 +111,7 @@ describe NotificationService, services: true do
context 'participating' do
context 'by note' do
before do
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
note.author = @u_lazy_participant
note.save
notification.new_note(note)
......@@ -134,7 +134,7 @@ describe NotificationService, services: true do
@u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(note.project.group).global!
update_custom_notification(:new_note, @u_custom_global)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
it do
......@@ -173,7 +173,7 @@ describe NotificationService, services: true do
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
notification.new_note(note)
......@@ -196,7 +196,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
note.project.team << [note.author, :master]
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
describe '#new_note' do
......@@ -238,7 +238,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
note.project.team << [note.author, :master]
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
describe '#new_note' do
......@@ -273,7 +273,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
update_custom_notification(:new_note, @u_guest_custom, project)
update_custom_notification(:new_note, @u_custom_global)
......@@ -348,7 +348,7 @@ describe NotificationService, services: true do
before do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, project)
update_custom_notification(:new_issue, @u_custom_global)
end
......@@ -408,7 +408,7 @@ describe NotificationService, services: true do
label.toggle_subscription(guest)
label.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
notification.new_issue(confidential_issue, @u_disabled)
......@@ -604,7 +604,7 @@ describe NotificationService, services: true do
label_2.toggle_subscription(guest)
label_2.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
......@@ -733,7 +733,7 @@ describe NotificationService, services: true do
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, project)
update_custom_notification(:new_merge_request, @u_custom_global)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
describe '#new_merge_request' do
......@@ -1146,7 +1146,7 @@ describe NotificationService, services: true do
before do
build_team(project)
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
describe '#project_was_moved' do
......
......@@ -49,7 +49,8 @@ module CycleAnalyticsHelpers
end
def merge_merge_requests_closing_issue(issue)
merge_requests = issue.closed_by_merge_requests
merge_requests = issue.closed_by_merge_requests(user)
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end
......
module EmailHelpers
def sent_to_user?(user)
ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
def sent_to_user?(user, recipients = email_recipients)
recipients.include?(user.notification_email)
end
def reset_delivered_emails!
ActionMailer::Base.deliveries.clear
end
def should_only_email(*users)
users.each {|user| should_email(user) }
recipients = ActionMailer::Base.deliveries.flat_map(&:to)
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
users.each { |user| should_email(user, recipients) }
expect(recipients.count).to eq(users.count)
end
def should_email(user)
expect(sent_to_user?(user)).to be_truthy
def should_email(user, recipients = email_recipients)
expect(sent_to_user?(user, recipients)).to be_truthy
end
def should_not_email(user, recipients = email_recipients)
expect(sent_to_user?(user, recipients)).to be_falsey
end
def should_not_email_anyone
expect(ActionMailer::Base.deliveries).to be_empty
end
def should_not_email(user)
expect(sent_to_user?(user)).to be_falsey
def email_recipients(kind: :to)
ActionMailer::Base.deliveries.flat_map(&kind)
end
end
......@@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do
let(:new_user_address) { 'newguy@example.com' }
before do
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
......
shared_examples 'project features apply to issuables' do |klass|
let(:described_class) { klass }
let(:group) { create(:group) }
let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
let(:user_outside_group) { create(:user) }
let(:project) { create(:empty_project, :public, project_args) }
def project_args
feature = "#{described_class.model_name.plural}_access_level".to_sym
args = { group: group }
args[feature] = access_level
args
end
before do
_ = issuable
login_as(user)
visit path
end
context 'public access level' do
let(:access_level) { ProjectFeature::ENABLED }
context 'group member' do
let(:user) { user_in_group }
it { expect(page).to have_content(issuable.title) }
end
context 'non-member' do
let(:user) { user_outside_group }
it { expect(page).to have_content(issuable.title) }
end
end
context 'private access level' do
let(:access_level) { ProjectFeature::PRIVATE }
context 'group member' do
let(:user) { user_in_group }
it { expect(page).to have_content(issuable.title) }
end
context 'non-member' do
let(:user) { user_outside_group }
it { expect(page).not_to have_content(issuable.title) }
end
end
end
RSpec.shared_examples "referenced feature visibility" do |*related_features|
let(:feature_fields) do
related_features.map { |feature| (feature + "_access_level").to_sym }
end
before { link['data-project'] = project.id.to_s }
context "when feature is disabled" do
it "does not create reference" do
set_features_fields_to(ProjectFeature::DISABLED)
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
end
context "when feature is enabled only for team members" do
before { set_features_fields_to(ProjectFeature::PRIVATE) }
it "does not create reference for non member" do
non_member = create(:user)
expect(subject.nodes_visible_to_user(non_member, [link])).to eq([])
end
it "creates reference for member" do
project.team << [user, :developer]
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
context "when feature is enabled" do
# The project is public
it "creates reference" do
set_features_fields_to(ProjectFeature::ENABLED)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
def set_features_fields_to(visibility_level)
feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) }
end
end
require 'rake_helper'
describe 'gitlab:ldap:check rake task' do
include LdapHelpers
before do
Rake.application.rake_require 'tasks/gitlab/check'
stub_warn_user_is_not_gitlab
end
context 'when LDAP is not enabled' do
it 'does not attempt to bind or search for users' do
expect(Gitlab::LDAP::Config).not_to receive(:providers)
expect(Gitlab::LDAP::Adapter).not_to receive(:open)
run_rake_task('gitlab:ldap:check')
end
end
context 'when LDAP is enabled' do
let(:ldap) { double(:ldap) }
let(:adapter) { ldap_adapter('ldapmain', ldap) }
before do
allow(Gitlab::LDAP::Config)
.to receive_messages(
enabled?: true,
providers: ['ldapmain']
)
allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter)
allow(adapter).to receive(:users).and_return([])
end
it 'attempts to bind using credentials' do
stub_ldap_config(has_auth?: true)
expect(ldap).to receive(:bind)
run_rake_task('gitlab:ldap:check')
end
it 'searches for 100 LDAP users' do
stub_ldap_config(uid: 'uid')
expect(adapter).to receive(:users).with('uid', '*', 100)
run_rake_task('gitlab:ldap:check')
end
end
end
......@@ -24,7 +24,7 @@ describe BuildEmailWorker do
end
it "gracefully handles an input SMTP error" do
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
subject.perform(build.id, [user.email], data.stringify_keys)
......
......@@ -87,7 +87,7 @@ describe EmailsOnPushWorker do
context "when there is an SMTP error" do
before do
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
allow(subject).to receive_message_chain(:logger, :info)
perform
......@@ -112,7 +112,7 @@ describe EmailsOnPushWorker do
original.call(Mail.new(mail.encoded))
end
ActionMailer::Base.deliveries.clear
reset_delivered_emails!
end
it "sends the mail to each of the recipients" do
......
require 'spec_helper'
describe PipelineNotificationWorker do
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('master').sha,
user: pusher,
status: status)
end
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:pusher) { user }
let(:watcher) { pusher }
describe '#execute' do
before do
reset_delivered_emails!
pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
end
context 'when watcher has developer access' do
before do
pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER]
end
shared_examples 'sending emails' do
it 'sends emails' do
perform_enqueued_jobs do
subject.perform(pipeline.id)
end
emails = ActionMailer::Base.deliveries
actual = emails.flat_map(&:bcc).sort
expected_receivers = receivers.map(&:email).uniq.sort
expect(actual).to eq(expected_receivers)
expect(emails.size).to eq(1)
expect(emails.last.subject).to include(email_subject)
end
end
context 'with success pipeline' do
let(:status) { 'success' }
let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
let(:receivers) { [pusher, watcher] }
it_behaves_like 'sending emails'
context 'with pipeline from someone else' do
let(:pusher) { create(:user) }
let(:watcher) { user }
context 'with success pipeline notification on' do
before do
watcher.global_notification_setting.
update(level: 'custom', success_pipeline: true)
end
it_behaves_like 'sending emails'
end
context 'with success pipeline notification off' do
let(:receivers) { [pusher] }
before do
watcher.global_notification_setting.
update(level: 'custom', success_pipeline: false)
end
it_behaves_like 'sending emails'
end
end
context 'with failed pipeline' do
let(:status) { 'failed' }
let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
it_behaves_like 'sending emails'
context 'with pipeline from someone else' do
let(:pusher) { create(:user) }
let(:watcher) { user }
context 'with failed pipeline notification on' do
before do
watcher.global_notification_setting.
update(level: 'custom', failed_pipeline: true)
end
it_behaves_like 'sending emails'
end
context 'with failed pipeline notification off' do
let(:receivers) { [pusher] }
before do
watcher.global_notification_setting.
update(level: 'custom', failed_pipeline: false)
end
it_behaves_like 'sending emails'
end
end
end
end
end
context 'when watcher has no read_build access' do
let(:status) { 'failed' }
let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
let(:watcher) { create(:user) }
before do
pipeline.project.team << [watcher, Gitlab::Access::GUEST]
watcher.global_notification_setting.
update(level: 'custom', failed_pipeline: true)
perform_enqueued_jobs do
subject.perform(pipeline.id)
end
end
it 'does not send emails' do
should_only_email(pusher, kind: :bcc)
end
end
end
end
/* eslint-disable */
/**
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* @name timeago
* @version 1.1.0
* @requires jQuery v1.2.3+
* @author Ryan McGeary
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) {
return inWords(timestamp);
} else if (typeof timestamp === "string") {
return inWords($.timeago.parse(timestamp));
} else if (typeof timestamp === "number") {
return inWords(new Date(timestamp));
} else {
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowFuture: false,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "ago",
suffixFromNow: "from now",
seconds: "less than a minute",
minute: "about a minute",
minutes: "%d minutes",
hour: "about an hour",
hours: "about %d hours",
day: "a day",
days: "%d days",
month: "about a month",
months: "%d months",
year: "about a year",
years: "%d years",
wordSeparator: " ",
numbers: []
}
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
}
var seconds = Math.abs(distanceMillis) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 42 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.round(days)) ||
days < 45 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
years < 1.5 && substitute($l.year, 1) ||
substitute($l.years, Math.round(years));
var separator = $l.wordSeparator || "";
if ($l.wordSeparator === undefined) { separator = " "; }
return $.trim([prefix, words, suffix].join(separator));
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
s = s.replace(/T/," ").replace(/Z/," UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
return new Date(s);
},
datetime: function(elem) {
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
},
isTime: function(elem) {
// jQuery's `is()` doesn't play well with HTML5 in IE
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
}
});
// functions that can be called via $(el).timeago('action')
// init is default when no action is given
// functions are called with context of a single element
var functions = {
init: function(){
var refresh_el = $.proxy(refresh, this);
refresh_el();
var $s = $t.settings;
if ($s.refreshMillis > 0) {
setInterval(refresh_el, $s.refreshMillis);
}
},
update: function(time){
$(this).data('timeago', { datetime: $t.parse(time) });
refresh.apply(this);
}
};
$.fn.timeago = function(action, options) {
var fn = action ? functions[action] : functions.init;
if(!fn){
throw new Error("Unknown function name '"+ action +"' for timeago");
}
// each over objects here and call the requested function
this.each(function(){
fn.call(this, options);
});
return this;
};
function refresh() {
var data = prepareData(this);
if (!isNaN(data.datetime)) {
$(this).text(inWords(data.datetime));
}
return this;
}
function prepareData(element) {
element = $(element);
if (!element.data("timeago")) {
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}));
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