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. ...@@ -12,6 +12,7 @@ entry.
- Trim leading and trailing whitespace on project_path (Linus Thiel) - Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc) - Prevent award emoji via notes for issues/MRs authored by user (barthc)
- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek) - 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) - 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) - Fix Markdown styling inside reference links (Jan Zdráhal)
- Create new issue board list after creating a new label - Create new issue board list after creating a new label
...@@ -73,13 +74,34 @@ entry. ...@@ -73,13 +74,34 @@ entry.
- Updated commit SHA styling on the branches page. - Updated commit SHA styling on the branches page.
- Fix 404 when visit /projects 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) ## 8.13.3 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086 - Removes any symlinks before importing a project export file. CVE-2016-9086
- Fixed Import/Export foreign key issue to do with project members. - Fixed Import/Export foreign key issue to do with project members.
- Fix relative links in Markdown wiki when displayed in "Project" tab !7218 - 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 - Changed build dropdown list length to be 6,5 builds long in the pipeline graph
## 8.13.2 (2016-10-31) ## 8.13.2 (2016-10-31)
...@@ -270,6 +292,10 @@ entry. ...@@ -270,6 +292,10 @@ entry.
- Fix broken Project API docs (Takuya Noguchi) - Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master) - Migrate invalid project members (owner -> master)
## 8.12.9 (2016-11-07)
- Fix XSS issue in Markdown autolinker
## 8.12.8 (2016-11-02) ## 8.12.8 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086 - Removes any symlinks before importing a project export file. CVE-2016-9086
...@@ -534,6 +560,10 @@ entry. ...@@ -534,6 +560,10 @@ entry.
- Fix non-master branch readme display in tree view - Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs - 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) ## 8.11.10 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086 - Removes any symlinks before importing a project export file. CVE-2016-9086
......
...@@ -13,12 +13,12 @@ ...@@ -13,12 +13,12 @@
} }
Activities.prototype.updateTooltips = function() { Activities.prototype.updateTooltips = function() {
return gl.utils.localTimeAgo($('.js-timeago', '.content_list')); gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
}; };
Activities.prototype.reloadActivities = function() { Activities.prototype.reloadActivities = function() {
$(".content_list").html(''); $(".content_list").html('');
return Pager.init(20, true); Pager.init(20, true, false, this.updateTooltips);
}; };
Activities.prototype.toggleFilter = function(sender) { Activities.prototype.toggleFilter = function(sender) {
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
/*= require jquery-ui/sortable */ /*= require jquery-ui/sortable */
/*= require jquery_ujs */ /*= require jquery_ujs */
/*= require jquery.endless-scroll */ /*= require jquery.endless-scroll */
/*= require jquery.timeago */
/*= require jquery.highlight */ /*= require jquery.highlight */
/*= require jquery.waitforimages */ /*= require jquery.waitforimages */
/*= require jquery.atwho */ /*= require jquery.atwho */
...@@ -59,7 +58,6 @@ ...@@ -59,7 +58,6 @@
(function () { (function () {
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
window.addEventListener('hashchange', gl.utils.shiftWindow); window.addEventListener('hashchange', gl.utils.shiftWindow);
$.timeago.settings.allowFuture = true;
window.onload = function () { window.onload = function () {
// Scroll the window to avoid the topnav bar // Scroll the window to avoid the topnav bar
...@@ -199,9 +197,6 @@ ...@@ -199,9 +197,6 @@
warningMessage: warningMessage warningMessage: warningMessage
}); });
}); });
$document.on('click', 'button', function () {
return $(this).blur();
});
$('input[type="search"]').each(function () { $('input[type="search"]').each(function () {
var $this = $(this); var $this = $(this);
$this.attr('value', $this.val()); $this.attr('value', $this.val());
...@@ -243,8 +238,5 @@ ...@@ -243,8 +238,5 @@
// bind sidebar events // bind sidebar events
new gl.Sidebar(); new gl.Sidebar();
// Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago'));
}); });
}).call(this); }).call(this);
...@@ -8,56 +8,55 @@ ...@@ -8,56 +8,55 @@
Build.state = null; Build.state = null;
function Build(options) { function Build(options) {
this.page_url = options.page_url; options = options || $('.js-build-options').data();
this.build_url = options.build_url; this.pageUrl = options.pageUrl;
this.build_status = options.build_status; this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
this.state = options.state1; this.state = options.state1;
this.build_stage = options.build_stage; this.buildStage = options.buildStage;
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
this.updateDropdown = bind(this.updateDropdown, this); this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document); this.$document = $(document);
clearInterval(Build.interval); clearInterval(Build.interval);
// Init breakpoint checker // Init breakpoint checker
this.bp = Breakpoints.get(); this.bp = Breakpoints.get();
this.initSidebar(); this.initSidebar();
this.$buildScroll = $('#js-build-scroll');
this.populateJobs(this.build_stage); this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.build_stage); 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); 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(); this.updateArtifactRemoveDate();
if ($('#build-trace').length) { if ($('#build-trace').length) {
this.getInitialBuildTrace(); 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() { $('#autoscroll-button').on('click', function() {
var state; var state;
state = $(this).data("state"); state = $(this).data("state");
if ("enabled" === state) { if ("enabled" === state) {
$(this).data("state", "disabled"); $(this).data("state", "disabled");
return $(this).text("enable autoscroll"); return $(this).text("Enable autoscroll");
} else { } else {
$(this).data("state", "enabled"); $(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) { 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() { return function() {
if (window.location.href.split("#").first() === _this.page_url) { if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace(); 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); })(this), 4000);
} }
} }
...@@ -72,20 +71,23 @@ ...@@ -72,20 +71,23 @@
top: this.sidebarTranslationLimits.max top: this.sidebarTranslationLimits.max
}); });
this.$sidebar.niceScroll(); this.$sidebar.niceScroll();
this.hideSidebar();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); 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)); 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() { Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
return $.ajax({ return $.ajax({
url: this.build_url, url: this.buildUrl,
dataType: 'json', dataType: 'json',
success: function(build_data) { success: function(buildData) {
$('.js-build-output').html(build_data.trace_html); $('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(build_data.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove(); return $('.js-build-refresh').remove();
} }
} }
...@@ -94,7 +96,7 @@ ...@@ -94,7 +96,7 @@
Build.prototype.getBuildTrace = function() { Build.prototype.getBuildTrace = function() {
return $.ajax({ return $.ajax({
url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)), url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json", dataType: "json",
success: (function(_this) { success: (function(_this) {
return function(log) { return function(log) {
...@@ -108,8 +110,8 @@ ...@@ -108,8 +110,8 @@
$('.js-build-output').html(log.html); $('.js-build-output').html(log.html);
} }
return _this.checkAutoscroll(); return _this.checkAutoscroll();
} else if (log.status !== _this.build_status) { } else if (log.status !== _this.buildStatus) {
return Turbolinks.visit(_this.page_url); return Turbolinks.visit(_this.pageUrl);
} }
}; };
})(this) })(this)
...@@ -122,12 +124,11 @@ ...@@ -122,12 +124,11 @@
} }
}; };
Build.prototype.initScrollButtons = function() { Build.prototype.initScrollButtonAffix = function() {
var $body, $buildScroll, $buildTrace; var $body, $buildTrace;
$buildScroll = $('#js-build-scroll');
$body = $('body'); $body = $('body');
$buildTrace = $('#build-trace'); $buildTrace = $('#build-trace');
return $buildScroll.affix({ return this.$buildScroll.affix({
offset: { offset: {
bottom: function() { bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
...@@ -136,18 +137,12 @@ ...@@ -136,18 +137,12 @@
}); });
}; };
Build.prototype.shouldHideSidebar = function() { Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint; var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize(); bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; 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) { Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop); var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min; if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
...@@ -156,12 +151,20 @@ ...@@ -156,12 +151,20 @@
}); });
}; };
Build.prototype.hideSidebar = function() { Build.prototype.toggleSidebar = function(shouldHide) {
if (this.shouldHideSidebar()) { var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
} else { .toggleClass('sidebar-collapsed', shouldHide);
return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); 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() { Build.prototype.updateArtifactRemoveDate = function() {
...@@ -169,7 +172,7 @@ ...@@ -169,7 +172,7 @@
$date = $('.js-artifacts-remove'); $date = $('.js-artifacts-remove');
if ($date.length) { if ($date.length) {
date = $date.text(); 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 @@ ...@@ -80,7 +80,8 @@
success: function(html) { success: function(html) {
loading.hide(); loading.hide();
$target.html(html); $target.html(html);
return $('.js-timeago', $target).timeago(); var className = '.' + $target[0].className.replace(' ', '.');
gl.utils.localTimeAgo($('.js-timeago', className));
} }
}); });
}; };
......
...@@ -43,10 +43,6 @@ ...@@ -43,10 +43,6 @@
bottom: unfoldBottom, bottom: unfoldBottom,
offset: offset, offset: offset,
unfold: unfold, 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') view: file.data('view')
}; };
return $.get(link, params, function(response) { return $.get(link, params, function(response) {
......
...@@ -29,6 +29,9 @@ ...@@ -29,6 +29,9 @@
case 'projects:boards:index': case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
break; break;
case 'projects:builds:show':
new Build();
break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
Issuable.init(); Issuable.init();
......
...@@ -34,6 +34,8 @@ ...@@ -34,6 +34,8 @@
}, },
DefaultOptions: { DefaultOptions: {
sorter: function(query, items, searchKey) { 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') { if ((items[0].name != null) && items[0].name === 'loading') {
return items; return items;
} }
...@@ -182,6 +184,7 @@ ...@@ -182,6 +184,7 @@
insertTpl: '${atwho-at}"${title}"', insertTpl: '${atwho-at}"${title}"',
data: ['loading'], data: ['loading'],
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter,
beforeSave: function(milestones) { beforeSave: function(milestones) {
return $.map(milestones, function(m) { return $.map(milestones, function(m) {
if (m.title == null) { if (m.title == null) {
...@@ -236,6 +239,7 @@ ...@@ -236,6 +239,7 @@
displayTpl: this.Labels.template, displayTpl: this.Labels.template,
insertTpl: '${atwho-at}${title}', insertTpl: '${atwho-at}${title}',
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter,
beforeSave: function(merges) { beforeSave: function(merges) {
var sanitizeLabelTitle; var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) { sanitizeLabelTitle = function(title) {
......
...@@ -119,31 +119,12 @@ ...@@ -119,31 +119,12 @@
parser.href = url; parser.href = url;
return parser; return parser;
}; };
gl.utils.cleanupBeforeFetch = function() { gl.utils.cleanupBeforeFetch = function() {
// Unbind scroll events // Unbind scroll events
$(document).off('scroll'); $(document).off('scroll');
// Close any open tooltips // Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); $('.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); })(window);
}).call(this); }).call(this);
...@@ -22,51 +22,64 @@ ...@@ -22,51 +22,64 @@
if (setTimeago == null) { if (setTimeago == null) {
setTimeago = true; setTimeago = true;
} }
$timeagoEls.each(function() { $timeagoEls.each(function() {
var $el; var $el = $(this);
$el = $(this); $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
if (setTimeago) {
// Recreate with custom template
$el.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
gl.utils.renderTimeago($el);
}); });
if (setTimeago) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
// Recreate with custom template
return $timeagoEls.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
}; };
w.gl.utils.shortTimeAgo = function($el) { w.gl.utils.getTimeago = function() {
var shortLocale, tmpLocale; var locale = function(number, index) {
shortLocale = { return [
prefixAgo: null, ['less than a minute ago', 'a while'],
prefixFromNow: null, ['less than a minute ago', 'in %s seconds'],
suffixAgo: 'ago', ['about a minute ago', 'in 1 minute'],
suffixFromNow: 'from now', ['%s minutes ago', 'in %s minutes'],
seconds: '1 min', ['about an hour ago', 'in 1 hour'],
minute: '1 min', ['about %s hours ago', 'in %s hours'],
minutes: '%d mins', ['a day ago', 'in 1 day'],
hour: '1 hr', ['%s days ago', 'in %s days'],
hours: '%d hrs', ['a week ago', 'in 1 week'],
day: '1 day', ['%s weeks ago', 'in %s weeks'],
days: '%d days', ['a month ago', 'in 1 month'],
month: '1 month', ['%s months ago', 'in %s months'],
months: '%d months', ['a year ago', 'in 1 year'],
year: '1 year', ['%s years ago', 'in %s years']
years: '%d years', ][index];
wordSeparator: ' ',
numbers: []
}; };
tmpLocale = $.timeago.settings.strings;
$el.each(function(el) { timeago.register('gl_en', locale);
var $el1; return timeago();
$el1 = $(this); };
return $el1.attr('title', gl.utils.formatDate($el.attr('datetime')));
}); w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
$.timeago.settings.strings = shortLocale; var timefor;
$el.timeago(); if (!time) {
$.timeago.settings.strings = tmpLocale; 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) { w.gl.utils.getDayDifference = function(a, b) {
...@@ -75,7 +88,7 @@ ...@@ -75,7 +88,7 @@
var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((date2 - date1) / millisecondsPerDay); return Math.floor((date2 - date1) / millisecondsPerDay);
} };
})(window); })(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 @@ ...@@ -235,7 +235,7 @@
} }
if (environment.deployed_at && environment.deployed_at_formatted) { 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 { } else {
$('.js-environment-timeago', $template).remove(); $('.js-environment-timeago', $template).remove();
environment.name += '.'; environment.name += '.';
......
...@@ -162,7 +162,7 @@ ...@@ -162,7 +162,7 @@
if (data.milestone != null) { if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace; data.milestone.namespace = _this.currentProject.namespace;
data.milestone.path = _this.currentProject.path; 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)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else { } else {
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
&:focus, &:focus,
&:active { &:active {
outline: none;
background-color: $btn-active-gray; background-color: $btn-active-gray;
box-shadow: $gl-btn-active-background; box-shadow: $gl-btn-active-background;
} }
...@@ -267,10 +266,6 @@ ...@@ -267,10 +266,6 @@
outline: none; outline: none;
} }
&:focus {
outline: none;
}
&:active { &:active {
outline: none; outline: none;
} }
......
...@@ -38,7 +38,6 @@ ...@@ -38,7 +38,6 @@
text-align: left; text-align: left;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
outline: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
...@@ -55,6 +54,10 @@ ...@@ -55,6 +54,10 @@
} }
} }
&.no-outline {
outline: 0;
}
&:hover, { &:hover, {
border-color: $dropdown-toggle-hover-border-color; border-color: $dropdown-toggle-hover-border-color;
......
...@@ -100,10 +100,6 @@ header { ...@@ -100,10 +100,6 @@ header {
&:hover { &:hover {
background-color: $btn-gray-hover; background-color: $btn-gray-hover;
} }
&:focus {
outline: none;
}
} }
} }
......
...@@ -58,7 +58,6 @@ ...@@ -58,7 +58,6 @@
&:active, &:active,
&:focus { &:focus {
text-decoration: none; text-decoration: none;
outline: none;
} }
} }
......
...@@ -83,7 +83,6 @@ ...@@ -83,7 +83,6 @@
display: block; display: block;
text-decoration: none; text-decoration: none;
font-weight: normal; font-weight: normal;
outline: none;
&:hover, &:hover,
&:active, &:active,
......
...@@ -14,18 +14,10 @@ ...@@ -14,18 +14,10 @@
} }
} }
.autoscroll-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
}
.scroll-controls { .scroll-controls {
&.affix-top { .scroll-step {
position: absolute; width: 31px;
top: 10px; margin: 0 0 0 auto;
right: 25px;
} }
&.affix-bottom { &.affix-bottom {
...@@ -34,13 +26,13 @@ ...@@ -34,13 +26,13 @@
} }
&.affix { &.affix {
right: 30px; right: 25px;
bottom: 15px; bottom: 15px;
z-index: 1; z-index: 1;
}
@media (min-width: $screen-md-min) { &.sidebar-expanded {
right: 26%; right: #{$gutter_width + ($gl-padding * 2)};
}
} }
a { a {
......
...@@ -92,20 +92,6 @@ ...@@ -92,20 +92,6 @@
&.noteable_line { &.noteable_line {
position: relative; position: relative;
&.old {
&::before {
content: '-';
position: absolute;
}
}
&.new {
&::before {
content: '+';
position: absolute;
}
}
} }
span { span {
...@@ -151,8 +137,9 @@ ...@@ -151,8 +137,9 @@
.line_content { .line_content {
display: block; display: block;
margin: 0; margin: 0;
padding: 0 0.5em; padding: 0 1.5em;
border: none; border: none;
position: relative;
&.parallel { &.parallel {
display: table-cell; display: table-cell;
...@@ -161,6 +148,22 @@ ...@@ -161,6 +148,22 @@
word-break: break-all; 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 { .text-file.diff-wrap-lines table .line_holder td span {
......
...@@ -228,7 +228,6 @@ $colors: ( ...@@ -228,7 +228,6 @@ $colors: (
position: absolute; position: absolute;
right: 10px; right: 10px;
padding: 0; padding: 0;
outline: none;
color: #fff; color: #fff;
width: 75px; // static width to make 2 buttons have same width width: 75px; // static width to make 2 buttons have same width
height: 19px; height: 19px;
......
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
padding-right: 20px; padding-right: 20px;
border: none; border: none;
font-size: 14px; font-size: 14px;
outline: none;
padding: 0; padding: 0;
margin-left: 5px; margin-left: 5px;
line-height: 25px; line-height: 25px;
...@@ -229,6 +228,5 @@ ...@@ -229,6 +228,5 @@
&:hover, &:hover,
&:focus { &:focus {
color: $gl-link-color; color: $gl-link-color;
outline: none;
} }
} }
...@@ -12,7 +12,7 @@ class JwtController < ApplicationController ...@@ -12,7 +12,7 @@ class JwtController < ApplicationController
return head :not_found unless service return head :not_found unless service
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). 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] render json: result, status: result[:http_status]
end end
...@@ -20,7 +20,7 @@ class JwtController < ApplicationController ...@@ -20,7 +20,7 @@ class JwtController < ApplicationController
private private
def authenticate_project_or_user 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| authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
......
...@@ -104,8 +104,7 @@ class UsersController < ApplicationController ...@@ -104,8 +104,7 @@ class UsersController < ApplicationController
end end
def contributions_calendar def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar. @contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
new(contributed_projects, user)
end end
def load_events def load_events
......
...@@ -62,31 +62,26 @@ class IssuableFinder ...@@ -62,31 +62,26 @@ class IssuableFinder
def project def project
return @project if defined?(@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 = project
@project = nil
end
else
@project = nil
end
@project
end end
def projects def projects
return @projects if defined?(@projects) return @projects if defined?(@projects)
return @projects = project if project?
if project? projects =
@projects = project if current_user && params[:authorized_only].presence && !current_user_related?
elsif current_user && params[:authorized_only].presence && !current_user_related? current_user.authorized_projects
@projects = current_user.authorized_projects.reorder(nil) elsif group
elsif group GroupProjectsFinder.new(group).execute(current_user)
@projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil) else
else ProjectsFinder.new.execute(current_user)
@projects = ProjectsFinder.new.execute(current_user).reorder(nil) end
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end end
def search def search
......
...@@ -154,7 +154,6 @@ module ApplicationHelper ...@@ -154,7 +154,6 @@ module ApplicationHelper
# time - Time object # time - Time object
# placement - Tooltip placement String (default: "top") # placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago") # 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 # By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many # initialize the `timeago` jQuery extension. If this method is called many
...@@ -166,22 +165,19 @@ module ApplicationHelper ...@@ -166,22 +165,19 @@ module ApplicationHelper
# `html_class` argument is provided. # `html_class` argument is provided.
# #
# Returns an HTML-safe String # 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 = short_format ? 'js-short-timeago' : 'js-timeago'
css_classes << " #{html_class}" unless html_class.blank? css_classes << " #{html_class}" unless html_class.blank?
css_classes << ' js-timeago-pending' unless skip_js
element = content_tag :time, time.to_s, element = content_tag :time, time.to_s,
class: css_classes, class: css_classes,
datetime: time.to_time.getutc.iso8601,
title: time.to_time.in_time_zone.to_s(:medium), title: time.to_time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' } datetime: time.to_time.getutc.iso8601,
data: {
unless skip_js toggle: 'tooltip',
element << javascript_tag( placement: placement,
"$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" container: 'body'
) }
end
element element
end end
......
...@@ -5,4 +5,14 @@ module BuildsHelper ...@@ -5,4 +5,14 @@ module BuildsHelper
build_class += ' retried' if build.retried? build_class += ' retried' if build.retried?
build_class build_class
end 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 end
...@@ -51,12 +51,11 @@ module DiffHelper ...@@ -51,12 +51,11 @@ module DiffHelper
html.html_safe html.html_safe
end end
def diff_line_content(line, line_type = nil) def diff_line_content(line)
if line.blank? if line.blank?
" &nbsp;".html_safe "&nbsp;".html_safe
else else
line[0] = ' ' if %w[new old].include?(line_type) line.sub(/^[\-+ ]/, '').html_safe
line
end end
end end
......
...@@ -74,4 +74,13 @@ module NotificationsHelper ...@@ -74,4 +74,13 @@ module NotificationsHelper
return unless notification_setting.source_type return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end end
def notification_event_name(event)
case event
when :success_pipeline
'Successful pipeline'
else
event.to_s.humanize
end
end
end end
module Emails module Emails
module Pipelines module Pipelines
def pipeline_success_email(pipeline, to) def pipeline_success_email(pipeline, recipients)
pipeline_mail(pipeline, to, 'succeeded') pipeline_mail(pipeline, recipients, 'succeeded')
end end
def pipeline_failed_email(pipeline, to) def pipeline_failed_email(pipeline, recipients)
pipeline_mail(pipeline, to, 'failed') pipeline_mail(pipeline, recipients, 'failed')
end end
private private
def pipeline_mail(pipeline, to, status) def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project @project = pipeline.project
@pipeline = pipeline @pipeline = pipeline
@merge_request = pipeline.merge_requests.first @merge_request = pipeline.merge_requests.first
add_headers 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.html { render layout: false }
format.text format.text
end end
......
...@@ -81,6 +81,12 @@ module Ci ...@@ -81,6 +81,12 @@ module Ci
PipelineHooksWorker.perform_async(id) PipelineHooksWorker.perform_async(id)
end end
end end
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
end end
# ref can't be HEAD or SHA, can only be branch/tag name # ref can't be HEAD or SHA, can only be branch/tag name
...@@ -109,6 +115,11 @@ module Ci ...@@ -109,6 +115,11 @@ module Ci
project.id project.id
end 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 def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
...@@ -186,6 +186,10 @@ module Issuable ...@@ -186,6 +186,10 @@ module Issuable
grouping_columns grouping_columns
end end
def to_ability_name
model_name.singular
end
end end
def today? def today?
...@@ -247,7 +251,7 @@ module Issuable ...@@ -247,7 +251,7 @@ module Issuable
# issuable.class # => MergeRequest # issuable.class # => MergeRequest
# issuable.to_ability_name # => "merge_request" # issuable.to_ability_name # => "merge_request"
def to_ability_name def to_ability_name
self.class.to_s.underscore self.class.to_ability_name
end end
# Returns a Hash of attributes to be used for Twitter card metadata # Returns a Hash of attributes to be used for Twitter card metadata
......
...@@ -54,6 +54,7 @@ class Event < ActiveRecord::Base ...@@ -54,6 +54,7 @@ class Event < ActiveRecord::Base
update_all(updated_at: Time.now) update_all(updated_at: Time.now)
end end
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions def contributions
where("action = ? OR (target_type in (?) AND action in (?))", where("action = ? OR (target_type in (?) AND action in (?))",
Event::PUSHED, ["MergeRequest", "Issue"], Event::PUSHED, ["MergeRequest", "Issue"],
...@@ -67,7 +68,7 @@ class Event < ActiveRecord::Base ...@@ -67,7 +68,7 @@ class Event < ActiveRecord::Base
def visible_to_user?(user = nil) def visible_to_user?(user = nil)
if push? if push?
true Ability.allowed?(user, :download_code, project)
elsif membership_changed? elsif membership_changed?
true true
elsif created_project? elsif created_project?
......
...@@ -264,29 +264,9 @@ class Issue < ActiveRecord::Base ...@@ -264,29 +264,9 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User # Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user. # or an anonymous user.
def visible_to_user?(user = nil) def visible_to_user?(user = nil)
user ? readable_by?(user) : publicly_visible? return false unless project.feature_available?(:issues, user)
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
# Returns `true` if this Issue is visible to everybody. user ? readable_by?(user) : publicly_visible?
def publicly_visible?
project.public? && !confidential?
end end
def overdue? def overdue?
...@@ -311,4 +291,32 @@ class Issue < ActiveRecord::Base ...@@ -311,4 +291,32 @@ class Issue < ActiveRecord::Base
end end
end 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 end
...@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base ...@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
:reopen_merge_request, :reopen_merge_request,
:close_merge_request, :close_merge_request,
:reassign_merge_request, :reassign_merge_request,
:merge_merge_request :merge_merge_request,
:failed_pipeline,
:success_pipeline
] ]
store :events, accessors: EMAIL_EVENTS, coder: JSON store :events, accessors: EMAIL_EVENTS, coder: JSON
......
...@@ -223,9 +223,39 @@ class Project < ActiveRecord::Base ...@@ -223,9 +223,39 @@ class Project < ActiveRecord::Base
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } 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_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_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
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') } # "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 :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) } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
......
...@@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base ...@@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds repository) 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 # Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete # permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
...@@ -35,9 +44,8 @@ class ProjectFeature < ActiveRecord::Base ...@@ -35,9 +44,8 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user) def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) access_level = public_send(ProjectFeature.access_level_attribute(feature))
get_permission(user, access_level)
get_permission(user, public_send("#{feature}_access_level"))
end end
def builds_enabled? def builds_enabled?
......
class PipelinesEmailService < Service class PipelinesEmailService < Service
prop_accessor :recipients prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_pipelines boolean_accessor :notify_only_broken_pipelines
validates :recipients, validates :recipients, presence: true, if: :activated?
presence: true,
if: ->(s) { s.activated? && !s.add_pusher? }
def initialize_properties def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true } self.properties ||= { notify_only_broken_pipelines: true }
...@@ -34,8 +31,8 @@ class PipelinesEmailService < Service ...@@ -34,8 +31,8 @@ class PipelinesEmailService < Service
return unless all_recipients.any? return unless all_recipients.any?
pipeline = Ci::Pipeline.find(data[:object_attributes][:id]) pipeline_id = data[:object_attributes][:id]
Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients) PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
end end
def can_test? def can_test?
...@@ -57,9 +54,6 @@ class PipelinesEmailService < Service ...@@ -57,9 +54,6 @@ class PipelinesEmailService < Service
{ type: 'textarea', { type: 'textarea',
name: 'recipients', name: 'recipients',
placeholder: 'Emails separated by comma' }, placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
name: 'add_pusher',
label: 'Add pusher to recipients list' },
{ type: 'checkbox', { type: 'checkbox',
name: 'notify_only_broken_pipelines' }, name: 'notify_only_broken_pipelines' },
] ]
...@@ -85,12 +79,6 @@ class PipelinesEmailService < Service ...@@ -85,12 +79,6 @@ class PipelinesEmailService < Service
end end
def retrieve_recipients(data) def retrieve_recipients(data)
all_recipients = recipients.to_s.split(',').reject(&:blank?) recipients.to_s.split(',').reject(&:blank?)
if add_pusher? && data[:user].try(:[], :email)
all_recipients << data[:user][:email]
end
all_recipients
end end
end end
...@@ -5,7 +5,7 @@ module Ci ...@@ -5,7 +5,7 @@ module Ci
# If we can't read build we should also not have that # If we can't read build we should also not have that
# ability when looking at this in context of commit_status # 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" cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end end
end end
......
module Ci
class PipelinePolicy < BuildPolicy
end
end
class IssuePolicy < IssuablePolicy 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 def issue
@subject @subject
end end
......
...@@ -9,8 +9,8 @@ module Auth ...@@ -9,8 +9,8 @@ module Auth
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project unless scope || current_user || project
return error('DENIED', status: 403, message: 'access forbidden') unless scope return error('DENIED', status: 403, message: 'access forbidden')
end end
{ token: authorized_token(scope).encoded } { token: authorized_token(scope).encoded }
...@@ -92,23 +92,23 @@ module Auth ...@@ -92,23 +92,23 @@ module Auth
# Build can: # Build can:
# 1. pull from its own project (for ex. a build) # 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member # 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)) (requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end end
def user_can_pull?(requested_project) 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) can?(current_user, :read_container_image, requested_project)
end end
def build_can_push?(requested_project) def build_can_push?(requested_project)
# Build can push only to the project from which it originates # 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 requested_project == project
end end
def user_can_push?(requested_project) 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) can?(current_user, :create_container_image, requested_project)
end end
...@@ -118,5 +118,9 @@ module Auth ...@@ -118,5 +118,9 @@ module Auth
http_status: status http_status: status
} }
end end
def has_authentication_ability?(capability)
(@authentication_abilities || []).include?(capability)
end
end 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 ...@@ -325,6 +325,22 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end 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 protected
# Get project/group users with CUSTOM notification level # Get project/group users with CUSTOM notification level
...@@ -488,9 +504,14 @@ class NotificationService ...@@ -488,9 +504,14 @@ class NotificationService
end end
def reject_users_without_access(recipients, target) 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| recipients.select do |user|
user.can?(ability, target) user.can?(ability, target)
...@@ -653,6 +674,6 @@ class NotificationService ...@@ -653,6 +674,6 @@ class NotificationService
# Build event key to search on custom notification level # Build event key to search on custom notification level
# Check NotificationSetting::EMAIL_EVENTS # Check NotificationSetting::EMAIL_EVENTS
def build_custom_key(action, object) def build_custom_key(action, object)
"#{action}_#{object.class.name.underscore}".to_sym "#{action}_#{object.class.model_name.name.underscore}".to_sym
end end
end end
- if event.visible_to_user?(current_user) - if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) } .event-item{ class: event_row_class(event) }
.event-item-timestamp .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 = cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40) = author_avatar(event, size: 40)
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.location-badge= label .location-badge= label
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .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-menu.dropdown-select
= dropdown_content do = dropdown_content do
%ul %ul
......
...@@ -8,5 +8,5 @@ ...@@ -8,5 +8,5 @@
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = 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" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot; &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) = commit_author_link(commit, avatar: true, size: 24)
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
.light .light
= commit_author_link(commit, avatar: false) = commit_author_link(commit, avatar: false)
authored authored
#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} #{time_ago_with_tooltip(commit.committed_date)}
%td.line-numbers %td.line-numbers
- line_count = blame_group[:lines].count - line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i| - (current_line...(current_line + line_count)).each do |i|
......
- @no_container = true - @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds" - page_title "#{@build.name} (##{@build.id})", "Builds"
- trace_with_state = @build.trace_with_state
- header_title project_title(@project, "Builds", project_builds_path(@project)) - header_title project_title(@project, "Builds", project_builds_path(@project))
= render "projects/pipelines/head", build_subnav: true = render "projects/pipelines/head", build_subnav: true
...@@ -28,32 +27,27 @@ ...@@ -28,32 +27,27 @@
Runners page Runners page
.prepend-top-default .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? - if @build.erased?
.erased.alert.alert-warning .erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - 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)} Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
#js-build-scroll.scroll-controls #js-build-scroll.scroll-controls
= link_to '#build-trace', class: 'btn' do .scroll-step
%i.fa.fa-angle-up = link_to '#build-trace', class: 'btn' do
= link_to '#down-build-trace', class: 'btn' do %i.fa.fa-angle-up
%i.fa.fa-angle-down = 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 %pre.build-trace#build-trace
%code.bash.js-build-output %code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh") = icon("refresh spin", class: "js-build-refresh")
#down-build-trace #down-build-trace
= render "sidebar" = render "sidebar"
:javascript .js-build-options{ data: javascript_build_options }
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]}"
})
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
- if pipeline.finished_at - if pipeline.finished_at
%p.finished-at %p.finished-at
= icon("calendar") = 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 %td.pipeline-actions.hidden-xs
.controls.pull-right .controls.pull-right
......
...@@ -25,9 +25,9 @@ ...@@ -25,9 +25,9 @@
%a{href: "##{line_code}", data: { linenumber: link_text }} %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) }< %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email - if email
%pre= diff_line_content(line.text, type) %pre= diff_line_content(line.text)
- else - else
= diff_line_content(line.text, type) = diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil) - discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta? - if discussions && !line.meta?
......
...@@ -25,3 +25,6 @@ ...@@ -25,3 +25,6 @@
var url = "#{escape_javascript(@more_log_url)}"; var url = "#{escape_javascript(@more_log_url)}";
ajaxGet(url); ajaxGet(url);
} }
:plain
gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
\ No newline at end of file
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
.col-sm-10.col-sm-offset-2 .col-sm-10.col-sm-offset-2
.checkbox .checkbox
= label_tag 'merge_request[force_remove_source_branch]' do = 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? = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
Remove source branch when merge request is accepted. Remove source branch when merge request is accepted.
......
...@@ -27,5 +27,5 @@ ...@@ -27,5 +27,5 @@
%label{ for: field_id } %label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event]) = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
%strong %strong
= event.to_s.humanize = notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading") = 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 @@ ...@@ -25,6 +25,7 @@
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. - [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. - [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 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 ## Administrator documentation
......
...@@ -40,6 +40,10 @@ of one hour. ...@@ -40,6 +40,10 @@ of one hour.
To enable LDAP integration you need to add your LDAP server settings in To enable LDAP integration you need to add your LDAP server settings in
`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. `/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 >**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
one GitLab server. 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 @@ ...@@ -4,7 +4,7 @@
**Valid notification levels** **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 disabled
...@@ -28,6 +28,8 @@ reopen_merge_request ...@@ -28,6 +28,8 @@ reopen_merge_request
close_merge_request close_merge_request
reassign_merge_request reassign_merge_request
merge_merge_request merge_merge_request
failed_pipeline
success_pipeline
``` ```
## Global notification settings ## Global notification settings
...@@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab ...@@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification | | `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_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 | | `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: Example response:
...@@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab ...@@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification | | `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_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 | | `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: Example responses:
...@@ -161,7 +167,9 @@ Example responses: ...@@ -161,7 +167,9 @@ Example responses:
"reopen_merge_request": false, "reopen_merge_request": false,
"close_merge_request": false, "close_merge_request": false,
"reassign_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 ...@@ -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 ### For admins
``` ```
...@@ -122,6 +134,8 @@ For example: ...@@ -122,6 +134,8 @@ For example:
GET /users?username=jack_smith GET /users?username=jack_smith
``` ```
You can search for users who are external with: `/users?external=true`
## Single user ## Single user
Get a single user. Get a single user.
......
...@@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user. ...@@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
2. Install Docker Engine on server. 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: 3. Add `gitlab-runner` user to `docker` group:
...@@ -122,11 +123,17 @@ In order to do that, follow the steps: ...@@ -122,11 +123,17 @@ In order to do that, follow the steps:
Insecure = false 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 ```yaml
image: docker:latest image: docker:latest
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
DOCKER_DRIVER: overlay
services: services:
- docker:dind - docker:dind
...@@ -140,15 +147,21 @@ In order to do that, follow the steps: ...@@ -140,15 +147,21 @@ In order to do that, follow the steps:
- docker run my-docker-image /script/to/run/tests - 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: Docker-in-Docker works well, and is the recommended configuration, but it is
* By enabling `--docker-privileged`, you are effectively disabling all of not without its own challenges:
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 - By enabling `--docker-privileged`, you are effectively disabling all of
[Runtime privilege and Linux capabilities][docker-cap]. the security mechanisms of containers and exposing your host to privilege
* Using docker-in-docker, each build is in a clean environment without the past escalation which can lead to container breakout. For more information, check
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. out the official Docker documentation on
* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form [Runtime privilege and Linux capabilities][docker-cap].
offered. - 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. 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 ...@@ -221,6 +234,40 @@ work as expected since volume mounting is done in the context of the host
machine, not the build container. machine, not the build container.
e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests` 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 ## Using the GitLab Container Registry
> **Note:** > **Note:**
......
...@@ -24,7 +24,7 @@ namespace you can use the `configure` class method. This method simply yields ...@@ -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 the supplied block while passing `Gitlab::Metrics::Instrumentation` as its
argument. An example: argument. An example:
``` ```ruby
Gitlab::Metrics::Instrumentation.configure do |conf| Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(Foo, :bar) conf.instrument_method(Foo, :bar)
conf.instrument_method(Foo, :baz) conf.instrument_method(Foo, :baz)
...@@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer ...@@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer
Instrumenting a single method: Instrumenting a single method:
``` ```ruby
Gitlab::Metrics::Instrumentation.configure do |conf| Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(User, :find_by) conf.instrument_method(User, :find_by)
end end
...@@ -49,7 +49,7 @@ end ...@@ -49,7 +49,7 @@ end
Instrumenting an entire class hierarchy: Instrumenting an entire class hierarchy:
``` ```ruby
Gitlab::Metrics::Instrumentation.configure do |conf| Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_class_hierarchy(ActiveRecord::Base) conf.instrument_class_hierarchy(ActiveRecord::Base)
end end
...@@ -57,7 +57,7 @@ end ...@@ -57,7 +57,7 @@ end
Instrumenting all public class methods: Instrumenting all public class methods:
``` ```ruby
Gitlab::Metrics::Instrumentation.configure do |conf| Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_methods(User) conf.instrument_methods(User)
end end
...@@ -68,7 +68,7 @@ end ...@@ -68,7 +68,7 @@ end
The easiest way to check if a method has been instrumented is to check its The easiest way to check if a method has been instrumented is to check its
source location. For example: source location. For example:
``` ```ruby
method = Rugged::TagCollection.instance_method(:[]) method = Rugged::TagCollection.instance_method(:[])
method.source_location method.source_location
......
...@@ -60,7 +60,7 @@ migration was tested. ...@@ -60,7 +60,7 @@ migration was tested.
If you need to remove index, please add a condition like in following example: 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) 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 ...@@ -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 the method `disable_ddl_transaction!` in the body of your migration class like
so: so:
``` ```ruby
class MyMigration < ActiveRecord::Migration class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
...@@ -96,7 +96,7 @@ the `up` and `down` methods in your migration class. ...@@ -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 For example, to add the column `foo` to the `projects` table with a default
value of `10` you'd write the following: value of `10` you'd write the following:
``` ```ruby
class MyMigration < ActiveRecord::Migration class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
...@@ -125,7 +125,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to ...@@ -125,7 +125,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to
Rails migration example: Rails migration example:
``` ```ruby
add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8) add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
# or # or
...@@ -145,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin ...@@ -145,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin
Example with Arel: Example with Arel:
``` ```ruby
users = Arel::Table.new(:users) users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5)) 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)) ...@@ -154,7 +154,7 @@ users.group(users[:user_id]).having(users[:id].count.gt(5))
Example with plain SQL and `quote_string` helper: 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| 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"]) tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]} 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 ...@@ -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 standard output of a process instead of a file. The following two commands do
roughly the same: roughly the same:
``` ```ruby
`touch /tmp/pawned-by-backticks` `touch /tmp/pawned-by-backticks`
File.read('|touch /tmp/pawned-by-file-read') 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 ...@@ -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 instance, the following is sufficient to protect against accidentally starting
a shell command with `|`: a shell command with `|`:
``` ```ruby
# we assume repo_path is not controlled by the attacker (user) # we assume repo_path is not controlled by the attacker (user)
path = File.join(repo_path, user_input) path = File.join(repo_path, user_input)
# path cannot start with '|' now. # path cannot start with '|' now.
...@@ -160,7 +160,7 @@ Path traversal is a security where the program (GitLab) tries to restrict user ...@@ -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 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. 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 # Suppose the user gave us a path and they are trying to trick us
user_input = '../other-repo.git/other-file' user_input = '../other-repo.git/other-file'
...@@ -177,7 +177,7 @@ File.open(full_path) do # Oops! ...@@ -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 A good way to protect against this is to compare the full path with its
'absolute path' according to Ruby's `File.absolute_path`. 'absolute path' according to Ruby's `File.absolute_path`.
``` ```ruby
full_path = File.join(repo_path, user_input) full_path = File.join(repo_path, user_input)
if full_path != File.absolute_path(full_path) if full_path != File.absolute_path(full_path)
raise "Invalid path: #{full_path.inspect}" raise "Invalid path: #{full_path.inspect}"
......
# Check Rake Tasks # Check Rake Tasks
## Repository Integrity This document was moved to [administration/raketasks/check](../administration/raketasks/check.md).
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)
...@@ -66,6 +66,7 @@ Below is the table of events users can be notified of: ...@@ -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: In all of the below cases, the notification will be sent to:
- Participants: - Participants:
- the author and assignee of the issue/merge request - the author and assignee of the issue/merge request
- the author of the pipeline
- authors of comments on the issue/merge request - 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 the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request - 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: ...@@ -88,6 +89,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | | | Reopen merge request | |
| Merge merge request | | | Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | | 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 In addition, if the title or description of an Issue or Merge Request is
......
...@@ -10,6 +10,9 @@ module API ...@@ -10,6 +10,9 @@ module API
# GET /users # GET /users
# GET /users?search=Admin # GET /users?search=Admin
# GET /users?username=root # GET /users?username=root
# GET /users?active=true
# GET /users?external=true
# GET /users?blocked=true
get do get do
unless can?(current_user, :read_users_list, nil) unless can?(current_user, :read_users_list, nil)
render_api_error!("Not authorized.", 403) render_api_error!("Not authorized.", 403)
...@@ -20,9 +23,11 @@ module API ...@@ -20,9 +23,11 @@ module API
else else
skip_ldap = params[:skip_ldap].present? && params[:skip_ldap] == 'true' skip_ldap = params[:skip_ldap].present? && params[:skip_ldap] == 'true'
@users = User.all @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.non_ldap if skip_ldap
@users = @users.search(params[:search]) if params[:search].present? @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 @users = paginate @users
end end
......
...@@ -71,6 +71,14 @@ module Banzai ...@@ -71,6 +71,14 @@ module Banzai
@doc = parse_html(rinku) @doc = parse_html(rinku)
end 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 # Autolinks any text matching LINK_PATTERN that Rinku didn't already
# replace # replace
def text_parse def text_parse
...@@ -89,17 +97,27 @@ module Banzai ...@@ -89,17 +97,27 @@ module Banzai
doc doc
end end
def autolink_filter(text) def autolink_match(match)
text.gsub(LINK_PATTERN) do |match| # start by stripping out dangerous links
# Remove any trailing HTML entities and store them for appending begin
# outside the link element. The entity must be marked HTML safe in uri = Addressable::URI.parse(match)
# order to be output literally rather than escaped. return match if contains_unsafe?(uri.scheme)
match.gsub!(/((?:&[\w#]+;)+)\z/, '') rescue Addressable::URI::InvalidURIError
dropped = ($1 || '').html_safe return match
options = link_options.merge(href: match)
content_tag(:a, match, options) + dropped
end end
# Remove any trailing HTML entities and store them for appending
# outside the link element. The entity must be marked HTML safe in
# order to be output literally rather than escaped.
match.gsub!(/((?:&[\w#]+;)+)\z/, '')
dropped = ($1 || '').html_safe
options = link_options.merge(href: match)
content_tag(:a, match, options) + dropped
end
def autolink_filter(text)
text.gsub(LINK_PATTERN) { |match| autolink_match(match) }
end end
def link_options def link_options
......
...@@ -63,12 +63,7 @@ module Banzai ...@@ -63,12 +63,7 @@ module Banzai
nodes.select do |node| nodes.select do |node|
if node.has_attribute?(project_attr) if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i node_id = node.attr(project_attr).to_i
can_read_reference?(user, projects[node_id])
if project && project.id == node_id
true
else
can?(user, :read_project, projects[node_id])
end
else else
true true
end end
...@@ -226,6 +221,15 @@ module Banzai ...@@ -226,6 +221,15 @@ module Banzai
attr_reader :current_user, :project 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) def lazy(&block)
Gitlab::Lazy.new(&block) Gitlab::Lazy.new(&block)
end end
......
...@@ -29,6 +29,12 @@ module Banzai ...@@ -29,6 +29,12 @@ module Banzai
commits commits
end end
private
def can_read_reference?(user, ref_project)
can?(user, :download_code, ref_project)
end
end end
end end
end end
...@@ -33,6 +33,12 @@ module Banzai ...@@ -33,6 +33,12 @@ module Banzai
range.valid_commits? ? range : nil range.valid_commits? ? range : nil
end end
private
def can_read_reference?(user, ref_project)
can?(user, :download_code, ref_project)
end
end end
end end
end end
...@@ -20,6 +20,12 @@ module Banzai ...@@ -20,6 +20,12 @@ module Banzai
def issue_ids_per_project(nodes) def issue_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute) gather_attributes_per_project(nodes, self.class.data_attribute)
end end
private
def can_read_reference?(user, ref_project)
can?(user, :read_issue, ref_project)
end
end end
end end
end end
...@@ -6,6 +6,12 @@ module Banzai ...@@ -6,6 +6,12 @@ module Banzai
def references_relation def references_relation
Label Label
end end
private
def can_read_reference?(user, ref_project)
can?(user, :read_label, ref_project)
end
end end
end end
end end
...@@ -6,6 +6,12 @@ module Banzai ...@@ -6,6 +6,12 @@ module Banzai
def references_relation def references_relation
MergeRequest.includes(:author, :assignee, :target_project) MergeRequest.includes(:author, :assignee, :target_project)
end end
private
def can_read_reference?(user, ref_project)
can?(user, :read_merge_request, ref_project)
end
end end
end end
end end
...@@ -6,6 +6,12 @@ module Banzai ...@@ -6,6 +6,12 @@ module Banzai
def references_relation def references_relation
Milestone Milestone
end end
private
def can_read_reference?(user, ref_project)
can?(user, :read_milestone, ref_project)
end
end end
end end
end end
...@@ -6,6 +6,12 @@ module Banzai ...@@ -6,6 +6,12 @@ module Banzai
def references_relation def references_relation
Snippet Snippet
end end
private
def can_read_reference?(user, ref_project)
can?(user, :read_project_snippet, ref_project)
end
end end
end end
end end
...@@ -30,22 +30,36 @@ module Banzai ...@@ -30,22 +30,36 @@ module Banzai
nodes.each do |node| nodes.each do |node|
if node.has_attribute?(group_attr) if node.has_attribute?(group_attr)
node_group = groups[node.attr(group_attr).to_i] next unless can_read_group_reference?(node, user, groups)
visible << node
if node_group && elsif can_read_project_reference?(node)
can?(user, :read_group, node_group) visible << node
visible << node
end
# Remaining nodes will be processed by the parent class'
# implementation of this method.
else else
remaining << node remaining << node
end end
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) visible + super(current_user, remaining)
end 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) def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project' project_attr = 'data-project'
author_attr = 'data-author' author_attr = 'data-author'
...@@ -88,6 +102,10 @@ module Banzai ...@@ -88,6 +102,10 @@ module Banzai
collection_objects_for_ids(Project, ids). collection_objects_for_ids(Project, ids).
flat_map { |p| p.team.members.to_a } flat_map { |p| p.team.members.to_a }
end end
def can_read_reference?(user, ref_project)
can?(user, :read_project, ref_project)
end
end end
end end
end end
module Gitlab module Gitlab
class ContributionsCalendar class ContributionsCalendar
attr_reader :activity_dates, :projects, :user attr_reader :contributor
attr_reader :current_user
attr_reader :projects
def initialize(projects, user) def initialize(contributor, current_user = nil)
@projects = projects @contributor = contributor
@user = user @current_user = current_user
@projects = ContributedProjectsFinder.new(contributor).execute(current_user)
end end
def activity_dates def activity_dates
return @activity_dates if @activity_dates.present? 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 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). union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
where("created_at > ?", date_from).where(project_id: projects). events = Event.find_by_sql(union.to_sql).map(&:attributes)
group('date(created_at)').
select('date(created_at) as date, count(id) as total_amount').
map(&:attributes)
activity_dates = (1.year.ago.to_date..Date.today).to_a @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
activities[event["date"]] += event["total_amount"]
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
end end
@activity_dates
end end
def events_by_date(date) def events_by_date(date)
events = Event.contributions.where(author_id: user.id). events = Event.contributions.where(author_id: contributor.id).
where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day). where(created_at: date.beginning_of_day..date.end_of_day).
where(project_id: projects) where(project_id: projects)
events.select do |event| # Use visible_to_user? instead of the complicated logic in activity_dates
event.push? || event.issue? || event.merge_request? # because we're only viewing the events for a single day.
end events.select {|event| event.visible_to_user?(current_user) }
end end
def starting_year def starting_year
...@@ -49,5 +48,30 @@ module Gitlab ...@@ -49,5 +48,30 @@ module Gitlab
def starting_month def starting_month
Date.today.month Date.today.month
end 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
end end
...@@ -5,11 +5,6 @@ module Gitlab ...@@ -5,11 +5,6 @@ module Gitlab
include PathLocksHelper include PathLocksHelper
UnauthorizedError = Class.new(StandardError) 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 = { ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.', upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.', download: 'You are not allowed to download code from this project.',
...@@ -17,6 +12,11 @@ module Gitlab ...@@ -17,6 +12,11 @@ module Gitlab
no_repo: 'A repository for this project does not exist yet.' 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 attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
def initialize(actor, project, protocol, authentication_abilities:) def initialize(actor, project, protocol, authentication_abilities:)
......
...@@ -105,6 +105,10 @@ module Gitlab ...@@ -105,6 +105,10 @@ module Gitlab
options['external_groups'] options['external_groups']
end end
def has_auth?
options['password'] || options['bind_dn']
end
protected protected
def base_config def base_config
...@@ -135,10 +139,6 @@ module Gitlab ...@@ -135,10 +139,6 @@ module Gitlab
} }
} }
end end
def has_auth?
options['password'] || options['bind_dn']
end
end end
end end
end end
...@@ -761,7 +761,7 @@ namespace :gitlab do ...@@ -761,7 +761,7 @@ namespace :gitlab do
end end
namespace :ldap do 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. # Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script. # This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100) args.with_defaults(limit: 100)
...@@ -769,7 +769,7 @@ namespace :gitlab do ...@@ -769,7 +769,7 @@ namespace :gitlab do
start_checking "LDAP" start_checking "LDAP"
if Gitlab::LDAP::Config.enabled? if Gitlab::LDAP::Config.enabled?
print_users(args.limit) check_ldap(args.limit)
else else
puts 'LDAP is disabled in config/gitlab.yml' puts 'LDAP is disabled in config/gitlab.yml'
end end
...@@ -777,21 +777,42 @@ namespace :gitlab do ...@@ -777,21 +777,42 @@ namespace :gitlab do
finished_checking "LDAP" finished_checking "LDAP"
end end
def print_users(limit) def check_ldap(limit)
puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
servers = Gitlab::LDAP::Config.providers servers = Gitlab::LDAP::Config.providers
servers.each do |server| servers.each do |server|
puts "Server: #{server}" puts "Server: #{server}"
Gitlab::LDAP::Adapter.open(server) do |adapter|
users = adapter.users(adapter.config.uid, '*', limit) begin
users.each do |user| Gitlab::LDAP::Adapter.open(server) do |adapter|
puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" 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 end
rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
puts "Could not connect to the LDAP server: #{e.message}".color(:red)
end end
end 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 end
namespace :repo do namespace :repo do
......
require 'spec_helper'
feature 'Group issues page', feature: true do
let(:path) { issues_group_path(group) }
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
include_examples 'project features apply to issuables', Issue
end
require 'spec_helper'
feature 'Group merge requests page', feature: true do
let(:path) { merge_requests_group_path(group) }
let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")}
include_examples 'project features apply to issuables', MergeRequest
end
...@@ -22,6 +22,7 @@ feature 'Start new branch from an issue', feature: true do ...@@ -22,6 +22,7 @@ feature 'Start new branch from an issue', feature: true do
create(:note, :on_issue, :system, project: project, noteable: issue, create(:note, :on_issue, :system, project: project, noteable: issue,
note: "Mentioned in !#{referenced_mr.iid}") note: "Mentioned in !#{referenced_mr.iid}")
end end
let(:referenced_mr) do let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project, create(:merge_request, :simple, source_project: project, target_project: project,
description: "Fixes ##{issue.iid}", author: user) description: "Fixes ##{issue.iid}", author: user)
......
...@@ -218,42 +218,24 @@ describe ApplicationHelper do ...@@ -218,42 +218,24 @@ describe ApplicationHelper do
end end
it 'includes a default js-timeago class' do 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 end
it 'accepts a custom html_class' do it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')). expect(element(html_class: 'custom_class').attr('class')).
to eq 'js-timeago custom_class js-timeago-pending' to eq 'js-timeago custom_class'
end end
it 'accepts a custom tooltip placement' do it 'accepts a custom tooltip placement' do
expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom' expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom'
end 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 it 'converts to Time' do
expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
end 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') 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.attr('class')).to eq 'js-short-timeago'
expect(timeago_element.next_element).to eq nil expect(timeago_element.next_element).to eq nil
end end
......
...@@ -61,7 +61,7 @@ describe DiffHelper do ...@@ -61,7 +61,7 @@ describe DiffHelper do
describe '#diff_line_content' do describe '#diff_line_content' do
it 'returns non breaking space when line is empty' 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 end
it 'returns the line itself' do 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 */ /* eslint-disable */
/*= require merge_request_widget */ /*= require merge_request_widget */
/*= require jquery.timeago.js */ /*= require lib/utils/timeago.js */
(function() { (function() {
describe('MergeRequestWidget', function() { describe('MergeRequestWidget', function() {
......
...@@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do ...@@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(doc.at_css('a')['href']).to eq link expect(doc.at_css('a')['href']).to eq link
end 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 it 'does not include trailing punctuation' do
doc = filter("See #{link}.") doc = filter("See #{link}.")
expect(doc.at_css('a').text).to eq link expect(doc.at_css('a').text).to eq link
......
...@@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do ...@@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do
and_return(parser_class) and_return(parser_class)
end end
it 'removes unpermitted Project references' do context 'valid projects' do
user = create(:user) before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) }
project = create(:empty_project)
link = reference_link(project: project.id, reference_type: 'test') it 'allows permitted Project references' do
doc = filter(link, current_user: user) 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 end
it 'allows permitted Project references' do context 'invalid projects' do
user = create(:user) before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) }
project = create(:empty_project)
project.team << [user, :master]
link = reference_link(project: project.id, reference_type: 'test') it 'removes unpermitted references' do
doc = filter(link, current_user: user) user = create(:user)
project = create(:empty_project)
expect(doc.css('a').length).to eq 1 link = reference_link(project: project.id, reference_type: 'test')
end doc = filter(link, current_user: user)
it 'handles invalid Project references' do expect(doc.css('a').length).to eq 0
link = reference_link(project: 12345, reference_type: 'test') end
it 'handles invalid references' do
link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error expect { filter(link) }.not_to raise_error
end
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment