Commit 363059e6 authored by Regis's avatar Regis

Merge branch 'master' into auto-pipelines-vue

parents 8724d587 a01e76ba
......@@ -331,7 +331,7 @@ trigger_docs:
cache: {}
artifacts: {}
script:
- "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds"
- "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
only:
- master@gitlab-org/gitlab-ce
......
......@@ -6,13 +6,16 @@ entry.
- Show correct environment log in admin/logs (@duk3luk3 !7191)
- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
- Diff collapse won't shift when collapsing.
- Backups do not fail anymore when using tar on annex and custom_hooks only. !5814
- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
- Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc)
- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand)
- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
- Fix Markdown styling inside reference links (Jan Zdráhal)
- Create new issue board list after creating a new label
- Fix extra space on Build sidebar on Firefox !7060
- Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087
......@@ -66,8 +69,32 @@ entry.
- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
- Improve search query parameter naming in /admin/users !7115 (YarNayar)
- Fix table pagination to be responsive
- Fix applying GitHub-imported labels when importing job is interrupted
- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
- Updated commit SHA styling on the branches page.
- Fix 404 when visit /projects page
## 8.13.5 (2016-11-08)
- Restore unauthenticated access to public container registries
## 8.13.4 (2016-11-07)
- 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.3 (2016-11-02)
......@@ -266,6 +293,10 @@ entry.
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
## 8.12.9 (2016-11-07)
- Fix XSS issue in Markdown autolinker
## 8.12.8 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
......@@ -530,6 +561,10 @@ entry.
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
## 8.11.11 (2016-11-07)
- Fix XSS issue in Markdown autolinker
## 8.11.10 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
......
......@@ -26,7 +26,7 @@ gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.7.0'
......@@ -100,7 +100,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.0'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
......@@ -152,7 +152,7 @@ gem 'settingslogic', '~> 2.0.9'
gem 'version_sorter', '~> 2.1.0'
# Cache
gem 'redis-rails', '~> 4.0.0'
gem 'redis-rails', '~> 5.0.1'
# Redis
gem 'redis', '~> 3.2'
......
......@@ -159,7 +159,7 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (1.0.5)
deckar01-task_list (1.0.6)
activesupport (~> 4.0)
html-pipeline
rack (~> 1.0)
......@@ -456,7 +456,7 @@ GEM
omniauth-github (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-gitlab (1.0.1)
omniauth-gitlab (1.0.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1)
......@@ -573,23 +573,23 @@ GEM
json
redcarpet (3.3.3)
redis (3.2.2)
redis-actionpack (4.0.1)
actionpack (~> 4)
redis-rack (~> 1.5.0)
redis-store (~> 1.1.0)
redis-activesupport (4.1.5)
activesupport (>= 3, < 5)
redis-store (~> 1.1.0)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.1)
activesupport (>= 3, < 6)
redis-store (~> 1.2.0)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
redis-rack (1.5.0)
redis-rack (1.6.0)
rack (~> 1.5)
redis-store (~> 1.1.0)
redis-rails (4.0.0)
redis-actionpack (~> 4)
redis-activesupport (~> 4)
redis-store (~> 1.1.0)
redis-store (1.1.7)
redis-store (~> 1.2.0)
redis-rails (5.0.1)
redis-actionpack (~> 5.0.0)
redis-activesupport (~> 5.0.0)
redis-store (~> 1.2.0)
redis-store (1.2.0)
redis (>= 2.2)
request_store (1.3.1)
rerun (0.11.0)
......@@ -840,7 +840,7 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 1.0.5)
deckar01-task_list (= 1.0.6)
default_value_for (~> 3.0.0)
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
......@@ -913,7 +913,7 @@ DEPENDENCIES
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.0)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.7.0)
......@@ -938,7 +938,7 @@ DEPENDENCIES
redcarpet (~> 3.3.3)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-rails (~> 4.0.0)
redis-rails (~> 5.0.1)
request_store (~> 1.3)
rerun (~> 0.11.0)
responders (~> 2.0)
......@@ -994,4 +994,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.13.5
1.13.6
......@@ -13,12 +13,12 @@
}
Activities.prototype.updateTooltips = function() {
return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
return Pager.init(20, true);
Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
......
......@@ -13,7 +13,6 @@
/*= require jquery-ui/sortable */
/*= require jquery_ujs */
/*= require jquery.endless-scroll */
/*= require jquery.timeago */
/*= require jquery.highlight */
/*= require jquery.waitforimages */
/*= require jquery.atwho */
......@@ -194,9 +193,6 @@
e.preventDefault();
return new ConfirmDangerModal(form, text);
});
$document.on('click', 'button', function () {
return $(this).blur();
});
$('input[type="search"]').each(function () {
var $this = $(this);
$this.attr('value', $this.val());
......@@ -238,8 +234,5 @@
// bind sidebar events
new gl.Sidebar();
// Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago'));
});
}).call(this);
......@@ -2,6 +2,19 @@
$(() => {
const Store = gl.issueBoards.BoardsStore;
$(document).off('created.label').on('created.label', (e, label) => {
Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color
}
});
});
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
......
......@@ -8,56 +8,55 @@
Build.state = null;
function Build(options) {
this.page_url = options.page_url;
this.build_url = options.build_url;
this.build_status = options.build_status;
options = options || $('.js-build-options').data();
this.pageUrl = options.pageUrl;
this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
this.state = options.state1;
this.build_stage = options.build_stage;
this.hideSidebar = bind(this.hideSidebar, this);
this.toggleSidebar = bind(this.toggleSidebar, this);
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar();
this.$buildScroll = $('#js-build-scroll');
this.populateJobs(this.build_stage);
this.updateStageDropdownText(this.build_stage);
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
$(window).off('resize.build').on('resize.build', this.hideSidebar);
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
$('#js-build-scroll > a').off('click').on('click', this.stepTrace);
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
this.initScrollButtons();
this.initScrollButtonAffix();
}
if (this.build_status === "running" || this.build_status === "pending") {
if (this.buildStatus === "running" || this.buildStatus === "pending") {
// Bind autoscroll button to follow build output
$('#autoscroll-button').on('click', function() {
var state;
state = $(this).data("state");
if ("enabled" === state) {
$(this).data("state", "disabled");
return $(this).text("enable autoscroll");
return $(this).text("Enable autoscroll");
} else {
$(this).data("state", "enabled");
return $(this).text("disable autoscroll");
return $(this).text("Disable autoscroll");
}
//
// Bind autoscroll button to follow build output
//
});
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
return function() {
if (window.location.href.split("#").first() === _this.page_url) {
if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
//
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
//
})(this), 4000);
}
}
......@@ -72,20 +71,23 @@
top: this.sidebarTranslationLimits.max
});
this.$sidebar.niceScroll();
this.hideSidebar();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
};
Build.prototype.location = function() {
return window.location.href.split("#")[0];
};
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
return $.ajax({
url: this.build_url,
url: this.buildUrl,
dataType: 'json',
success: function(build_data) {
$('.js-build-output').html(build_data.trace_html);
if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
......@@ -94,7 +96,7 @@
Build.prototype.getBuildTrace = function() {
return $.ajax({
url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
......@@ -108,8 +110,8 @@
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
} else if (log.status !== _this.build_status) {
return Turbolinks.visit(_this.page_url);
} else if (log.status !== _this.buildStatus) {
return Turbolinks.visit(_this.pageUrl);
}
};
})(this)
......@@ -122,12 +124,11 @@
}
};
Build.prototype.initScrollButtons = function() {
var $body, $buildScroll, $buildTrace;
$buildScroll = $('#js-build-scroll');
Build.prototype.initScrollButtonAffix = function() {
var $body, $buildTrace;
$body = $('body');
$buildTrace = $('#build-trace');
return $buildScroll.affix({
return this.$buildScroll.affix({
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
......@@ -136,18 +137,12 @@
});
};
Build.prototype.shouldHideSidebar = function() {
Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
Build.prototype.toggleSidebar = function() {
if (this.shouldHideSidebar()) {
return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
}
};
Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
......@@ -156,12 +151,20 @@
});
};
Build.prototype.hideSidebar = function() {
if (this.shouldHideSidebar()) {
return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else {
return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide);
};
Build.prototype.sidebarOnResize = function() {
this.toggleSidebar(this.shouldHideSidebarForViewport());
};
Build.prototype.sidebarOnClick = function() {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
......@@ -169,7 +172,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
return $date.text(gl.utils.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
......
......@@ -80,7 +80,8 @@
success: function(html) {
loading.hide();
$target.html(html);
return $('.js-timeago', $target).timeago();
var className = '.' + $target[0].className.replace(' ', '.');
gl.utils.localTimeAgo($('.js-timeago', className));
}
});
};
......
......@@ -115,6 +115,8 @@
.show();
} else {
this.$dropdownBack.trigger('click');
$(document).trigger('created.label', label);
}
});
}
......
......@@ -43,10 +43,6 @@
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
// indent is used to compensate for single space indent to fit
// '+' and '-' prepended to diff lines,
// see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
indent: 1,
view: file.data('view')
};
return $.get(link, params, function(response) {
......
......@@ -29,6 +29,9 @@
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:builds:show':
new Build();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
......
/* eslint-disable */
Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatches;
/* global Element */
/* eslint-disable consistent-return, max-len */
Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
Element.prototype.closest = function closest(selector, selectedElement = this) {
if (!selectedElement) return;
......
......@@ -34,6 +34,8 @@
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
// Highlight first item only if at least one char was typed
this.setting.highlightFirst = query.length > 0;
if ((items[0].name != null) && items[0].name === 'loading') {
return items;
}
......@@ -182,6 +184,7 @@
insertTpl: '${atwho-at}"${title}"',
data: ['loading'],
callbacks: {
sorter: this.DefaultOptions.sorter,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
......@@ -236,6 +239,7 @@
displayTpl: this.Labels.template,
insertTpl: '${atwho-at}${title}',
callbacks: {
sorter: this.DefaultOptions.sorter,
beforeSave: function(merges) {
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
......
......@@ -10,6 +10,7 @@
Issuable.initSearch();
Issuable.initChecks();
Issuable.initResetFilters();
Issuable.resetIncomingEmailToken();
return Issuable.initLabelFilterRemove();
},
initTemplates: function() {
......@@ -154,6 +155,27 @@
this.issuableBulkActions.willUpdateLabels = false;
}
return true;
},
resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success: function(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend: function() {
$('.incoming-email-token-reset').text('resetting...');
},
complete: function() {
$('.incoming-email-token-reset').text('reset it');
}
});
});
}
};
......
......@@ -119,31 +119,12 @@
parser.href = url;
return parser;
};
gl.utils.cleanupBeforeFetch = function() {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
};
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
return '';
}
suffix || (suffix = 'remaining');
expiredLabel || (expiredLabel = 'Past due');
jQuery.timeago.settings.allowFuture = true;
suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow;
jQuery.timeago.settings.strings.suffixFromNow = suffix;
timefor = $.timeago(time);
if (timefor.indexOf('ago') > -1) {
timefor = expiredLabel;
}
jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow;
return timefor;
};
})(window);
}).call(this);
......@@ -22,51 +22,64 @@
if (setTimeago == null) {
setTimeago = true;
}
$timeagoEls.each(function() {
var $el;
$el = $(this);
return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
if (setTimeago) {
// Recreate with custom template
$el.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
gl.utils.renderTimeago($el);
});
if (setTimeago) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
// Recreate with custom template
return $timeagoEls.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
};
w.gl.utils.shortTimeAgo = function($el) {
var shortLocale, tmpLocale;
shortLocale = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
seconds: '1 min',
minute: '1 min',
minutes: '%d mins',
hour: '1 hr',
hours: '%d hrs',
day: '1 day',
days: '%d days',
month: '1 month',
months: '%d months',
year: '1 year',
years: '%d years',
wordSeparator: ' ',
numbers: []
w.gl.utils.getTimeago = function() {
var locale = function(number, index) {
return [
['less than a minute ago', 'a while'],
['less than a minute ago', 'in %s seconds'],
['about a minute ago', 'in 1 minute'],
['%s minutes ago', 'in %s minutes'],
['about an hour ago', 'in 1 hour'],
['about %s hours ago', 'in %s hours'],
['a day ago', 'in 1 day'],
['%s days ago', 'in %s days'],
['a week ago', 'in 1 week'],
['%s weeks ago', 'in %s weeks'],
['a month ago', 'in 1 month'],
['%s months ago', 'in %s months'],
['a year ago', 'in 1 year'],
['%s years ago', 'in %s years']
][index];
};
tmpLocale = $.timeago.settings.strings;
$el.each(function(el) {
var $el1;
$el1 = $(this);
return $el1.attr('title', gl.utils.formatDate($el.attr('datetime')));
});
$.timeago.settings.strings = shortLocale;
$el.timeago();
$.timeago.settings.strings = tmpLocale;
timeago.register('gl_en', locale);
return timeago();
};
w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
var timefor;
if (!time) {
return '';
}
suffix || (suffix = 'remaining');
expiredLabel || (expiredLabel = 'Past due');
timefor = gl.utils.getTimeago().format(time).replace('in', '');
if (timefor.indexOf('ago') > -1) {
timefor = expiredLabel;
} else {
timefor = timefor.trim() + ' ' + suffix;
}
return timefor;
};
w.gl.utils.renderTimeago = function($element) {
var timeagoInstance = gl.utils.getTimeago();
timeagoInstance.render($element, 'gl_en');
};
w.gl.utils.getDayDifference = function(a, b) {
......@@ -75,7 +88,7 @@
var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((date2 - date1) / millisecondsPerDay);
}
};
})(window);
......
/**
* Copyright (c) 2016 hustcc
* License: MIT
* Version: v2.0.2
* https://github.com/hustcc/timeago.js
* This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js)
**/
/* eslint-disable */
/* jshint expr: true */
!function (root, factory) {
if (typeof module === 'object' && module.exports)
module.exports = factory(root);
else
root.timeago = factory(root);
}(typeof window !== 'undefined' ? window : this,
function () {
var cnt = 0, // the timer counter, for timer key
indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),
// build-in locales: en & zh_CN
locales = {
'en': function(number, index) {
if (index === 0) return ['just now', 'right now'];
var unit = indexMapEn[parseInt(index / 2)];
if (number > 1) unit += 's';
return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];
},
},
// second, minute, hour, day, week, month, year(365 days)
SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
SEC_ARRAY_LEN = 6,
ATTR_DATETIME = 'datetime';
// format Date / string / timestamp to Date instance.
function toDate(input) {
if (input instanceof Date) return input;
if (!isNaN(input)) return new Date(toInt(input));
if (/^\d+$/.test(input)) return new Date(toInt(input, 10));
input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
.replace(/-/, '/').replace(/-/, '/')
.replace(/T/, ' ').replace(/Z/, ' UTC')
.replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
return new Date(input);
}
// change f into int, remove Decimal. just for code compression
function toInt(f) {
return parseInt(f);
}
// format the diff second to *** time ago, with setting locale
function formatDiff(diff, locale, defaultLocale) {
// if locale is not exist, use defaultLocale.
// if defaultLocale is not exist, use build-in `en`.
// be sure of no error when locale is not exist.
locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');
// if (! locales[locale]) locale = defaultLocale;
var i = 0;
agoin = diff < 0 ? 1 : 0; // timein or timeago
diff = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
}
diff = toInt(diff);
i *= 2;
if (diff > (i === 0 ? 9 : 1)) i += 1;
return locales[locale](diff, i)[agoin].replace('%s', diff);
}
// calculate the diff second between date to be formated an now date.
function diffSec(date, nowDate) {
nowDate = nowDate ? toDate(nowDate) : new Date();
return (nowDate - toDate(date)) / 1000;
}
/**
* nextInterval: calculate the next interval time.
* - diff: the diff sec between now and date to be formated.
*
* What's the meaning?
* diff = 61 then return 59
* diff = 3601 (an hour + 1 second), then return 3599
* make the interval with high performace.
**/
function nextInterval(diff) {
var rst = 1, i = 0, d = Math.abs(diff);
for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
diff /= SEC_ARRAY[i];
rst *= SEC_ARRAY[i];
}
// return leftSec(d, rst);
d = d % rst;
d = d ? rst - d : rst;
return Math.ceil(d);
}
// get the datetime attribute, jQuery and DOM
function getDateAttr(node) {
if (node.getAttribute) return node.getAttribute(ATTR_DATETIME);
if(node.attr) return node.attr(ATTR_DATETIME);
}
/**
* timeago: the function to get `timeago` instance.
* - nowDate: the relative date, default is new Date().
* - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
*
* How to use it?
* var timeagoLib = require('timeago.js');
* var timeago = timeagoLib(); // all use default.
* var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
* var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
* var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
**/
function Timeago(nowDate, defaultLocale) {
var timers = {}; // real-time render timers
// if do not set the defaultLocale, set it with `en`
if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale
// what the timer will do
function doRender(node, date, locale, cnt) {
var diff = diffSec(date, nowDate);
node.innerHTML = formatDiff(diff, locale, defaultLocale);
// waiting %s seconds, do the next render
timers['k' + cnt] = setTimeout(function() {
doRender(node, date, locale, cnt);
}, nextInterval(diff) * 1000);
}
/**
* nextInterval: calculate the next interval time.
* - diff: the diff sec between now and date to be formated.
*
* What's the meaning?
* diff = 61 then return 59
* diff = 3601 (an hour + 1 second), then return 3599
* make the interval with high performace.
**/
// this.nextInterval = function(diff) { // for dev test
// var rst = 1, i = 0, d = Math.abs(diff);
// for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
// diff /= SEC_ARRAY[i];
// rst *= SEC_ARRAY[i];
// }
// // return leftSec(d, rst);
// d = d % rst;
// d = d ? rst - d : rst;
// return Math.ceil(d);
// }; // for dev test
/**
* format: format the date to *** time ago, with setting or default locale
* - date: the date / string / timestamp to be formated
* - locale: the formated string's locale name, e.g. en / zh_CN
*
* How to use it?
* var timeago = require('timeago.js')();
* timeago.format(new Date(), 'pl'); // Date instance
* timeago.format('2016-09-10', 'fr'); // formated date string
* timeago.format(1473473400269); // timestamp with ms
**/
this.format = function(date, locale) {
return formatDiff(diffSec(date, nowDate), locale, defaultLocale);
};
/**
* render: render the DOM real-time.
* - nodes: which nodes will be rendered.
* - locale: the locale name used to format date.
*
* How to use it?
* var timeago = new require('timeago.js')();
* // 1. javascript selector
* timeago.render(document.querySelectorAll('.need_to_be_rendered'));
* // 2. use jQuery selector
* timeago.render($('.need_to_be_rendered'), 'pl');
*
* Notice: please be sure the dom has attribute `datetime`.
**/
this.render = function(nodes, locale) {
if (nodes.length === undefined) nodes = [nodes];
for (var i = 0; i < nodes.length; i++) {
doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item
}
};
/**
* cancel: cancel all the timers which are doing real-time render.
*
* How to use it?
* var timeago = new require('timeago.js')();
* timeago.render(document.querySelectorAll('.need_to_be_rendered'));
* timeago.cancel(); // will stop all the timer, stop render in real time.
**/
this.cancel = function() {
for (var key in timers) {
clearTimeout(timers[key]);
}
timers = {};
};
/**
* setLocale: set the default locale name.
*
* How to use it?
* var timeago = require('timeago.js');
* timeago = new timeago();
* timeago.setLocale('fr');
**/
this.setLocale = function(locale) {
defaultLocale = locale;
};
return this;
}
/**
* timeago: the function to get `timeago` instance.
* - nowDate: the relative date, default is new Date().
* - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
*
* How to use it?
* var timeagoLib = require('timeago.js');
* var timeago = timeagoLib(); // all use default.
* var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
* var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
* var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
**/
function timeagoFactory(nowDate, defaultLocale) {
return new Timeago(nowDate, defaultLocale);
}
/**
* register: register a new language locale
* - locale: locale name, e.g. en / zh_CN, notice the standard.
* - localeFunc: the locale process function
*
* How to use it?
* var timeagoLib = require('timeago.js');
*
* timeagoLib.register('the locale name', the_locale_func);
* // or
* timeagoLib.register('pl', require('timeago.js/locales/pl'));
**/
timeagoFactory.register = function(locale, localeFunc) {
locales[locale] = localeFunc;
};
return timeagoFactory;
});
\ No newline at end of file
......@@ -218,7 +218,7 @@
}
if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.';
environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.';
} else {
$('.js-environment-timeago', $template).remove();
environment.name += '.';
......
......@@ -162,7 +162,7 @@
if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace;
data.milestone.path = _this.currentProject.path;
data.milestone.remaining = $.timefor(data.milestone.due_date);
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
......
......@@ -9,6 +9,8 @@
(function() {
$(function() {
if (!$(".network-graph").length) return;
var network_graph;
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
......
......@@ -4,7 +4,7 @@
margin-right: $margin-right;
}
.avatar-container {
.avatar-circle {
float: left;
margin-right: 15px;
border-radius: $avatar_radius;
......@@ -27,7 +27,7 @@
}
.avatar {
@extend .avatar-container;
@extend .avatar-circle;
width: 40px;
height: 40px;
padding: 0;
......@@ -64,8 +64,8 @@
&.s160 { font-size: 96px; line-height: 158px; }
}
.image-container {
@extend .avatar-container;
.avatar-container {
@extend .avatar-circle;
overflow: hidden;
display: flex;
......@@ -76,4 +76,4 @@
margin: 0;
align-self: center;
}
}
\ No newline at end of file
}
......@@ -6,7 +6,6 @@
&:focus,
&:active {
outline: none;
background-color: $btn-active-gray;
box-shadow: $gl-btn-active-background;
}
......@@ -267,10 +266,6 @@
outline: none;
}
&:focus {
outline: none;
}
&:active {
outline: none;
}
......
......@@ -38,7 +38,6 @@
text-align: left;
border: 1px solid $border-color;
border-radius: $border-radius-base;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
......@@ -55,6 +54,10 @@
}
}
&.no-outline {
outline: 0;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
......
......@@ -100,10 +100,6 @@ header {
&:hover {
background-color: $btn-gray-hover;
}
&:focus {
outline: none;
}
}
}
......
......@@ -58,7 +58,6 @@
&:active,
&:focus {
text-decoration: none;
outline: none;
}
}
......
......@@ -63,7 +63,7 @@
}
.select2-highlighted {
background: #3084bb !important;
background: $gl-link-color !important;
}
.select2-results li.select2-result-with-children > .select2-result-label {
......
......@@ -83,7 +83,6 @@
display: block;
text-decoration: none;
font-weight: normal;
outline: none;
&:hover,
&:active,
......
......@@ -103,7 +103,7 @@ $gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3084bb;
$gl-link-color: #3777b0;
$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
$gl-icon-color: $gl-placeholder-color;
......@@ -197,7 +197,7 @@ $line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
$match-line: $gray-light;
$table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-target-blue: #f6faff;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
......
......@@ -14,18 +14,10 @@
}
}
.autoscroll-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 100;
}
.scroll-controls {
&.affix-top {
position: absolute;
top: 10px;
right: 25px;
.scroll-step {
width: 31px;
margin: 0 0 0 auto;
}
&.affix-bottom {
......@@ -34,13 +26,13 @@
}
&.affix {
right: 30px;
right: 25px;
bottom: 15px;
z-index: 1;
}
@media (min-width: $screen-md-min) {
right: 26%;
}
&.sidebar-expanded {
right: #{$gutter_width + ($gl-padding * 2)};
}
a {
......
......@@ -36,9 +36,42 @@
padding: 10px 0;
margin-bottom: 0;
.commit-options-dropdown-caret {
@media (max-width: $screen-sm) {
margin-left: 0;
@media (min-width: $screen-sm-min) {
display: flex;
align-items: center;
.commit-meta {
flex: 1;
}
}
.commit-hash-full {
@media (max-width: $screen-sm-max) {
width: 80px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: bottom;
}
}
.commit-action-buttons {
i {
color: $gl-icon-color;
font-size: 13px;
margin-right: 3px;
}
@media (max-width: $screen-xs-max) {
.dropdown {
width: 100%;
margin-top: 10px;
}
.dropdown-toggle {
width: 100%;
}
}
}
}
......@@ -188,17 +221,6 @@
}
}
.commit-action-buttons {
position: relative;
top: -1px;
i {
color: $gl-icon-color;
font-size: 13px;
margin-right: 3px;
}
}
/*
* Commit message textarea for web editor and
* custom merge request message
......
......@@ -92,20 +92,6 @@
&.noteable_line {
position: relative;
&.old {
&::before {
content: '-';
position: absolute;
}
}
&.new {
&::before {
content: '+';
position: absolute;
}
}
}
span {
......@@ -151,8 +137,9 @@
.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
padding: 0 1.5em;
border: none;
position: relative;
&.parallel {
display: table-cell;
......@@ -161,6 +148,22 @@
word-break: break-all;
}
}
&.old {
&::before {
content: '-';
position: absolute;
left: 0.5em;
}
}
&.new {
&::before {
content: '+';
position: absolute;
left: 0.5em;
}
}
}
.text-file.diff-wrap-lines table .line_holder td span {
......
......@@ -228,7 +228,6 @@ $colors: (
position: absolute;
right: 10px;
padding: 0;
outline: none;
color: #fff;
width: 75px; // static width to make 2 buttons have same width
height: 19px;
......
......@@ -23,6 +23,10 @@
color: $md-link-color;
}
.private-tokens-reset div.reset-action:not(:first-child) {
padding-top: 15px;
}
.oauth-buttons {
.btn-group {
margin-right: 10px;
......
......@@ -31,7 +31,6 @@
padding-right: 20px;
border: none;
font-size: 14px;
outline: none;
padding: 0;
margin-left: 5px;
line-height: 25px;
......@@ -229,6 +228,5 @@
&:hover,
&:focus {
color: $gl-link-color;
outline: none;
}
}
......@@ -117,6 +117,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:enabled_git_access_protocol,
:housekeeping_enabled,
:housekeeping_bitmaps_enabled,
:housekeeping_incremental_repack_period,
:housekeeping_full_repack_period,
:housekeeping_gc_period,
repository_storages: [],
restricted_visibility_levels: [],
import_sources: [],
......
......@@ -192,9 +192,10 @@ class ApplicationController < ActionController::Base
end
# JSON for infinite scroll via Pager object
def pager_json(partial, count)
def pager_json(partial, count, locals = {})
html = render_to_string(
partial,
locals: locals,
layout: false,
formats: [:html]
)
......
......@@ -12,7 +12,7 @@ class JwtController < ApplicationController
return head :not_found unless service
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
execute(authentication_abilities: @authentication_result.authentication_abilities || [])
execute(authentication_abilities: @authentication_result.authentication_abilities)
render json: result, status: result[:http_status]
end
......@@ -20,7 +20,7 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
@authentication_result = Gitlab::Auth::Result.new
@authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities)
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
......
......@@ -26,7 +26,15 @@ class ProfilesController < Profiles::ApplicationController
def reset_private_token
if current_user.reset_authentication_token!
flash[:notice] = "Token was successfully updated"
flash[:notice] = "Private token was successfully reset"
end
redirect_to profile_account_path
end
def reset_incoming_email_token
if current_user.reset_incoming_email_token!
flash[:notice] = "Incoming email token was successfully reset"
end
redirect_to profile_account_path
......
......@@ -26,8 +26,15 @@ class Projects::CommitsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json { pager_json("projects/commits/_commits", @commits.size) }
format.atom { render layout: false }
format.json do
pager_json(
'projects/commits/_commits',
@commits.size,
project: @project,
ref: @ref)
end
end
end
end
......@@ -21,10 +21,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
if project && project.public? && download_request?
return # Allow access
end
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
......@@ -41,6 +37,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_final_spnego_response
return # Allow access
end
elsif project && download_request? && Guest.can?(:download_code, project)
@authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
return # Allow access
end
send_challenges
......
......@@ -78,11 +78,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
if user
access_check.allowed?
else
ci? || project.public?
end
access_check.allowed? || ci?
end
def access
......
......@@ -352,13 +352,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def branch_from
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
@commit = @repository.commit(params[:ref]) if params[:ref].present?
if params[:ref].present?
@ref = params[:ref]
@commit = @repository.commit(@ref)
end
render layout: false
end
def branch_to
@target_project = selected_target_project
@commit = @target_project.commit(params[:ref]) if params[:ref].present?
if params[:ref].present?
@ref = params[:ref]
@commit = @target_project.commit(@ref)
end
render layout: false
end
......@@ -589,12 +599,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_request_params
params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :task_num, :force_remove_source_branch,
:lock_version, label_ids: []
)
params.require(:merge_request)
.permit(merge_request_params_ce)
end
def merge_request_params_ce
[
:assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
:milestone_id,
:source_branch,
:source_project_id,
:state_event,
:target_branch,
:target_project_id,
:task_num,
:title,
label_ids: []
]
end
def merge_params
......
......@@ -5,17 +5,29 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
before_action :assign_commit
def show
@url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))
@commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")
respond_to do |format|
format.html
format.html do
if @options[:extended_sha1] && !@commit
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
end
end
format.json do
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
end
end
end
def assign_commit
return if params[:extended_sha1].blank?
@options[:extended_sha1] = params[:extended_sha1]
@commit = @repo.commit(@options[:extended_sha1])
end
end
......@@ -2,9 +2,9 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity, :refs]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create]
before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
......@@ -160,6 +160,13 @@ class ProjectsController < Projects::ApplicationController
end
end
def new_issue_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
current_user.reset_incoming_email_token!
render json: { new_issue_address: @project.new_issue_address(current_user) }
end
def archive
return access_denied! unless can?(current_user, :archive_project, @project)
......@@ -318,25 +325,44 @@ class ProjectsController < Projects::ApplicationController
end
def project_params
project_feature_attributes =
{
project_feature_attributes:
[
:issues_access_level, :builds_access_level,
:wiki_access_level, :merge_requests_access_level,
:snippets_access_level, :repository_access_level
]
}
params.require(:project)
.permit(project_params_ce)
end
params.require(:project).permit(
:name, :path, :description, :issues_tracker, :tag_list, :runners_token,
def project_params_ce
[
:avatar,
:build_allow_git_fetch,
:build_coverage_regex,
:build_timeout_in_minutes,
:container_registry_enabled,
:issues_tracker_id, :default_branch,
:visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled, project_feature_attributes
)
:default_branch,
:description,
:import_url,
:issues_tracker,
:issues_tracker_id,
:last_activity_at,
:lfs_enabled,
:name,
:namespace_id,
:only_allow_merge_if_all_discussions_are_resolved,
:only_allow_merge_if_build_succeeds,
:path,
:public_builds,
:request_access_enabled,
:runners_token,
:tag_list,
:visibility_level,
project_feature_attributes: %i[
builds_access_level
issues_access_level
merge_requests_access_level
repository_access_level
snippets_access_level
wiki_access_level
]
]
end
def repo_exists?
......
......@@ -16,7 +16,7 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
return if params[:search].nil? || params[:search].blank?
return if params[:search].blank?
@search_term = params[:search]
......
......@@ -104,8 +104,7 @@ class UsersController < ApplicationController
end
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.
new(contributed_projects, user)
@contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
end
def load_events
......
......@@ -61,31 +61,26 @@ class IssuableFinder
def project
return @project if defined?(@project)
if project?
@project = Project.find(params[:project_id])
project = Project.find(params[:project_id])
project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
else
@project = nil
end
@project
@project = project
end
def projects
return @projects if defined?(@projects)
return @projects = project if project?
if project?
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
@projects = current_user.authorized_projects.reorder(nil)
elsif group
@projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
else
@projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
GroupProjectsFinder.new(group).execute(current_user)
else
ProjectsFinder.new.execute(current_user)
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end
def search
......
module AccountsHelper
def incoming_email_token_enabled?
current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation?
end
end
......@@ -151,7 +151,6 @@ module ApplicationHelper
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
# skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
......@@ -163,22 +162,19 @@ module ApplicationHelper
# `html_class` argument is provided.
#
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false)
def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
css_classes << " #{html_class}" unless html_class.blank?
css_classes << ' js-timeago-pending' unless skip_js
element = content_tag :time, time.to_s,
class: css_classes,
datetime: time.to_time.getutc.iso8601,
title: time.to_time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js
element << javascript_tag(
"$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
)
end
datetime: time.to_time.getutc.iso8601,
data: {
toggle: 'tooltip',
placement: placement,
container: 'body'
}
element
end
......
......@@ -179,33 +179,6 @@ module BlobHelper
}
end
def selected_template(issuable)
templates = issuable_templates(issuable)
params[:issuable_template] if templates.include?(params[:issuable_template])
end
def can_add_template?(issuable)
names = issuable_templates(issuable)
names.empty? && can?(current_user, :push_code, @project) && !@project.private?
end
def merge_request_template_names
@merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
end
def issue_template_names
@issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
end
def issuable_templates(issuable)
@issuable_templates ||=
if issuable.is_a?(Issue)
issue_template_names
elsif issuable.is_a?(MergeRequest)
merge_request_template_names
end
end
def ref_project
@ref_project ||= @target_project || @project
end
......
......@@ -5,4 +5,14 @@ module BuildsHelper
build_class += ' retried' if build.retried?
build_class
end
def javascript_build_options
{
page_url: namespace_project_build_url(@project.namespace, @project, @build),
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
state1: @build.trace_with_state[:state]
}
end
end
......@@ -56,10 +56,18 @@ module CiStatusHelper
custom_icon(icon_name)
end
def render_commit_status(commit, tooltip_placement: 'auto left')
def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
project = commit.project
path = pipelines_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
path = pipelines_namespace_project_commit_path(
project.namespace,
project,
commit)
render_status_with_link(
'commit',
commit.status(ref),
path,
tooltip_placement: tooltip_placement)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
......
......@@ -25,9 +25,11 @@ module CommitsHelper
end
end
def commit_to_html(commit, project, inline = true)
template = inline ? "inline_commit" : "commit"
render "projects/commits/#{template}", commit: commit, project: project unless commit.nil?
def commit_to_html(commit, ref, project)
render 'projects/commits/commit',
commit: commit,
ref: ref,
project: project
end
# Breadcrumb links for a Project and, if applicable, a tree path
......
module ComponentsHelper
def gitlab_workhorse_version
if request.headers['Gitlab-Workhorse'].present?
request.headers['Gitlab-Workhorse'].split('-').first
else
Gitlab::Workhorse.version
end
end
end
......@@ -51,12 +51,11 @@ module DiffHelper
html.html_safe
end
def diff_line_content(line, line_type = nil)
def diff_line_content(line)
if line.blank?
" &nbsp;".html_safe
"&nbsp;".html_safe
else
line[0] = ' ' if %w[new old].include?(line_type)
line
line.sub(/^[\-+ ]/, '').html_safe
end
end
......
......@@ -30,6 +30,33 @@ module IssuablesHelper
end
end
def can_add_template?(issuable)
names = issuable_templates(issuable)
names.empty? && can?(current_user, :push_code, @project) && !@project.private?
end
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
toggle_class: 'js-issuable-selector',
title: title,
filter: true,
placeholder: 'Filter',
footer_content: true,
data: {
data: issuable_templates(issuable),
field_name: 'issuable_template',
selected: selected_template(issuable),
project_path: ref_project.path,
namespace_path: ref_project.namespace.path
}
}
dropdown_tag(title, options: options) do
capture(&block)
end
end
def user_dropdown_label(user_id, default_label)
return default_label if user_id.nil?
return "Unassigned" if user_id == "0"
......@@ -153,4 +180,28 @@ module IssuablesHelper
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
when Issue
issue_template_names
when MergeRequest
merge_request_template_names
else
raise 'Unknown issuable type!'
end
end
def merge_request_template_names
@merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
end
def issue_template_names
@issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template])
end
end
module LfsHelper
include Gitlab::Routing.url_helpers
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
......@@ -27,7 +27,7 @@ module LfsHelper
def lfs_download_access?
return false unless project.lfs_enabled?
project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
end
def user_can_download_code?
......
......@@ -74,4 +74,13 @@ module NotificationsHelper
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
def notification_event_name(event)
case event
when :success_pipeline
'Successful pipeline'
else
event.to_s.humanize
end
end
end
......@@ -61,6 +61,10 @@ module TodosHelper
}
end
def todos_filter_empty?
todos_filter_params.values.none?
end
def todos_filter_path(options = {})
without = options.delete(:without)
......
class BaseMailer < ActionMailer::Base
add_template_helper ApplicationHelper
add_template_helper GitlabMarkdownHelper
helper ApplicationHelper
helper GitlabMarkdownHelper
attr_accessor :current_user
helper_method :current_user, :can?
......
module Emails
module Pipelines
def pipeline_success_email(pipeline, to)
pipeline_mail(pipeline, to, 'succeeded')
def pipeline_success_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'succeeded')
end
def pipeline_failed_email(pipeline, to)
pipeline_mail(pipeline, to, 'failed')
def pipeline_failed_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'failed')
end
private
def pipeline_mail(pipeline, to, status)
def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project
@pipeline = pipeline
@merge_request = pipeline.merge_requests.first
add_headers
mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
# We use bcc here because we don't want to generate this emails for a
# thousand times. This could be potentially expensive in a loop, and
# recipients would contain all project watchers so it could be a lot.
mail(bcc: recipients,
subject: pipeline_subject(status),
skip_premailer: true) do |format|
format.html { render layout: false }
format.text
end
......
......@@ -10,12 +10,12 @@ class Notify < BaseMailer
include Emails::Pipelines
include Emails::Members
add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper
add_template_helper MembersHelper
add_template_helper GitlabRoutingHelper
helper MergeRequestsHelper
helper DiffHelper
helper BlobHelper
helper EmailsHelper
helper MembersHelper
helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
......
......@@ -85,6 +85,18 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :housekeeping_full_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period }
validates :housekeeping_gc_period,
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
......@@ -168,6 +180,11 @@ class ApplicationSetting < ActiveRecord::Base
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200,
)
end
......@@ -202,11 +219,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def repository_storages
value = read_attribute(:repository_storages)
value = [value] if value.is_a?(String)
value = [] if value.nil?
value
Array(read_attribute(:repository_storages))
end
# repository_storage is still required in the API. Remove in 9.0
......
......@@ -81,6 +81,12 @@ module Ci
PipelineHooksWorker.perform_async(id)
end
end
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
end
end
end
# ref can't be HEAD or SHA, can only be branch/tag name
......@@ -109,6 +115,11 @@ module Ci
project.id
end
# For now the only user who participates is the user who triggered
def participants(_current_user = nil)
Array(user)
end
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
......
......@@ -226,12 +226,19 @@ class Commit
end
def pipelines
@pipeline ||= project.pipelines.where(sha: sha)
project.pipelines.where(sha: sha)
end
def status
return @status if defined?(@status)
@status ||= pipelines.status
def status(ref = nil)
@statuses ||= {}
if @statuses.key?(ref)
@statuses[ref]
elsif ref
@statuses[ref] = pipelines.where(ref: ref).status
else
@statuses[ref] = pipelines.status
end
end
def revert_branch_name
......
......@@ -183,6 +183,10 @@ module Issuable
grouping_columns
end
def to_ability_name
model_name.singular
end
end
def today?
......@@ -244,7 +248,7 @@ module Issuable
# issuable.class # => MergeRequest
# issuable.to_ability_name # => "merge_request"
def to_ability_name
self.class.to_s.underscore
self.class.to_ability_name
end
# Returns a Hash of attributes to be used for Twitter card metadata
......@@ -286,6 +290,11 @@ module Issuable
false
end
def assignee_or_author?(user)
# We're comparing IDs here so we don't need to load any associations.
author_id == user.id || assignee_id == user.id
end
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
......
......@@ -4,17 +4,21 @@ module TokenAuthenticatable
private
def write_new_token(token_field)
new_token = generate_token(token_field)
new_token = generate_available_token(token_field)
write_attribute(token_field, new_token)
end
def generate_token(token_field)
def generate_available_token(token_field)
loop do
token = Devise.friendly_token
token = generate_token(token_field)
break token unless self.class.unscoped.find_by(token_field => token)
end
end
def generate_token(token_field)
Devise.friendly_token
end
class_methods do
def authentication_token_fields
@token_fields || []
......
......@@ -49,6 +49,7 @@ class Event < ActiveRecord::Base
update_all(updated_at: Time.now)
end
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type in (?) AND action in (?))",
Event::PUSHED, ["MergeRequest", "Issue"],
......@@ -62,7 +63,7 @@ class Event < ActiveRecord::Base
def visible_to_user?(user = nil)
if push?
true
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
elsif created_project?
......
......@@ -29,6 +29,15 @@ class ExternalIssue
@project
end
def project_id
@project.id
end
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil)
id
end
......
class Guest
class << self
def can?(action, subject)
Ability.allowed?(nil, action, subject)
end
end
end
......@@ -250,29 +250,9 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
user ? readable_by?(user) : publicly_visible?
end
# Returns `true` if the given User can read the current Issue.
def readable_by?(user)
if user.admin?
true
elsif project.owner == user
true
elsif confidential?
author == user ||
assignee == user ||
project.team.member?(user, Gitlab::Access::REPORTER)
else
project.public? ||
project.internal? && !user.external? ||
project.team.member?(user)
end
end
return false unless project.feature_available?(:issues, user)
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential?
user ? readable_by?(user) : publicly_visible?
end
def overdue?
......@@ -297,4 +277,32 @@ class Issue < ActiveRecord::Base
end
end
end
private
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
# for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
# Make sure to sync this method with issue_policy.rb
def readable_by?(user)
if user.admin?
true
elsif project.owner == user
true
elsif confidential?
author == user ||
assignee == user ||
project.team.member?(user, Gitlab::Access::REPORTER)
else
project.public? ||
project.internal? && !user.external? ||
project.team.member?(user)
end
end
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential?
end
end
# IssueCollection can be used to reduce a list of issues down to a subset.
#
# IssueCollection is not meant to be some sort of Enumerable, instead it's meant
# to take a list of issues and return a new list of issues based on some
# criteria. For example, given a list of issues you may want to return a list of
# issues that can be read or updated by a given user.
class IssueCollection
attr_reader :collection
def initialize(collection)
@collection = collection
end
# Returns all the issues that can be updated by the user.
def updatable_by_user(user)
return collection if user.admin?
# Given all the issue projects we get a list of projects that the current
# user has at least reporter access to.
projects_with_reporter_access = user.
projects_with_reporter_access_limited_to(project_ids).
pluck(:id)
collection.select do |issue|
if projects_with_reporter_access.include?(issue.project_id)
true
elsif issue.is_a?(Issue)
issue.assignee_or_author?(user)
else
false
end
end
end
alias_method :visible_to, :updatable_by_user
private
def project_ids
@project_ids ||= collection.map(&:project_id).uniq
end
end
......@@ -425,6 +425,7 @@ class MergeRequest < ActiveRecord::Base
return false if work_in_progress?
return false if broken?
return false unless skip_ci_check || mergeable_ci_state?
return false unless mergeable_discussions_state?
true
end
......@@ -493,6 +494,12 @@ class MergeRequest < ActiveRecord::Base
discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
end
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
discussions_resolved?
end
def hook_attrs
attrs = {
source: source_project.try(:hook_attrs),
......
......@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
:merge_merge_request
:merge_merge_request,
:failed_pipeline,
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
......
......@@ -207,8 +207,38 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(feature)
with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
}
# Picks a feature where the level is exactly that given.
scope :with_feature_access_level, ->(feature, level) {
access_level_attribute = ProjectFeature.access_level_attribute(feature)
with_project_feature.where(project_features: { access_level_attribute => level })
}
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
# project features may be "disabled", "internal" or "enabled". If "internal",
# they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user.
def self.with_feature_available_for_user(feature, user)
return with_feature_enabled(feature) if user.try(:admin?)
unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED])
return unconditional if user.nil?
conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE)
authorized = user.authorized_projects.merge(conditional.reorder(nil))
union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)])
where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql)))
end
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
......@@ -624,13 +654,12 @@ class Project < ActiveRecord::Base
end
def new_issue_address(author)
# This feature is disabled for the time being.
return nil
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
Gitlab::IncomingEmail.reply_address(
"#{path_with_namespace}+#{author.authentication_token}")
end
author.ensure_incoming_email_token!
Gitlab::IncomingEmail.reply_address(
"#{path_with_namespace}+#{author.incoming_email_token}")
end
def build_commit_note(commit)
......@@ -1067,10 +1096,6 @@ class Project < ActiveRecord::Base
forks.count
end
def find_label(name)
labels.find_by(name: name)
end
def origin_merge_requests
merge_requests.where(source_project_id: self.id)
end
......
......@@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds repository)
class << self
def access_level_attribute(feature)
feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
"#{feature}_access_level".to_sym
end
end
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
......@@ -35,9 +44,8 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
get_permission(user, public_send("#{feature}_access_level"))
access_level = public_send(ProjectFeature.access_level_attribute(feature))
get_permission(user, access_level)
end
def builds_enabled?
......
......@@ -163,6 +163,21 @@ class JiraService < IssueTrackerService
add_comment(data, issue_key)
end
# reason why service cannot be tested
def disabled_title
"Please fill in Password and Username."
end
def can_test?
username.present? && password.present?
end
# JIRA does not need test data.
# We are requesting the project that belongs to the project key.
def test_data(user = nil, project = nil)
nil
end
def test_settings
return unless url.present?
# Test settings by getting the project
......
class PipelinesEmailService < Service
prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_pipelines
validates :recipients,
presence: true,
if: ->(s) { s.activated? && !s.add_pusher? }
validates :recipients, presence: true, if: :activated?
def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true }
......@@ -34,8 +31,8 @@ class PipelinesEmailService < Service
return unless all_recipients.any?
pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
pipeline_id = data[:object_attributes][:id]
PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
end
def can_test?
......@@ -57,9 +54,6 @@ class PipelinesEmailService < Service
{ type: 'textarea',
name: 'recipients',
placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
name: 'add_pusher',
label: 'Add pusher to recipients list' },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' },
]
......@@ -85,12 +79,6 @@ class PipelinesEmailService < Service
end
def retrieve_recipients(data)
all_recipients = recipients.to_s.split(',').reject(&:blank?)
if add_pusher? && data[:user].try(:[], :email)
all_recipients << data[:user][:email]
end
all_recipients
recipients.to_s.split(',').reject(&:blank?)
end
end
......@@ -1064,6 +1064,10 @@ class Repository
end
def search_files(query, ref)
unless exists? && has_visible_content? && query.present?
return []
end
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
......
......@@ -13,6 +13,7 @@ class User < ActiveRecord::Base
DEFAULT_NOTIFICATION_LEVEL = :participating
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
default_value_for :admin, false
default_value_for(:external) { current_application_settings.user_default_external }
......@@ -119,7 +120,7 @@ class User < ActiveRecord::Base
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
......@@ -444,6 +445,16 @@ class User < ActiveRecord::Base
Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
end
# Returns the projects this user has reporter (or greater) access to, limited
# to at most the given projects.
#
# This method is useful when you have a list of projects and want to
# efficiently check to which of these projects the user has at least reporter
# access.
def projects_with_reporter_access_limited_to(projects)
authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
end
def viewable_starred_projects
starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
[Project::PUBLIC, Project::INTERNAL])
......@@ -946,4 +957,13 @@ class User < ActiveRecord::Base
signup_domain =~ regexp
end
end
def generate_token(token_field)
if token_field == :incoming_email_token
# Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
SecureRandom.hex.to_i(16).to_s(36)
else
super
end
end
end
......@@ -5,7 +5,7 @@ module Ci
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
%w(read create update admin).each do |rule|
%w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
end
......
module Ci
class PipelinePolicy < BuildPolicy
end
end
......@@ -4,7 +4,7 @@ class IssuablePolicy < BasePolicy
end
def rules
if @user && (@subject.author == @user || @subject.assignee == @user)
if @user && @subject.assignee_or_author?(@user)
can! :"read_#{action_name}"
can! :"update_#{action_name}"
end
......
class IssuePolicy < IssuablePolicy
# This class duplicates the same check of Issue#readable_by? for performance reasons
# Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
def issue
@subject
end
......@@ -8,9 +12,8 @@ class IssuePolicy < IssuablePolicy
if @subject.confidential? && !can_read_confidential?
cannot! :read_issue
cannot! :admin_issue
cannot! :update_issue
cannot! :read_issue
cannot! :admin_issue
end
end
......@@ -18,11 +21,7 @@ class IssuePolicy < IssuablePolicy
def can_read_confidential?
return false unless @user
return true if @user.admin?
return true if @subject.author == @user
return true if @subject.assignee == @user
return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER)
false
IssueCollection.new([@subject]).visible_to(@user).any?
end
end
class BaseSerializer
def initialize(parameters = {})
@request = EntityRequest.new(parameters)
end
def represent(resource, opts = {})
self.class.entity_class
.represent(resource, opts.merge(request: @request))
end
def self.entity(entity_class)
@entity_class ||= entity_class
end
def self.entity_class
@entity_class
end
end
class BuildEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :name
expose :build_url do |build|
url_to(:namespace_project_build, build)
end
expose :retry_url do |build|
url_to(:retry_namespace_project_build, build)
end
expose :play_url, if: ->(build, _) { build.manual? } do |build|
url_to(:play_namespace_project_build, build)
end
private
def url_to(route, build)
send("#{route}_url", build.project.namespace, build.project, build)
end
end
class CommitEntity < API::Entities::RepoCommit
include RequestAwareEntity
expose :author, using: UserEntity
expose :commit_url do |commit|
namespace_project_tree_url(
request.project.namespace,
request.project,
id: commit.id)
end
end
class DeploymentEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :iid
expose :sha
expose :ref do
expose :name do |deployment|
deployment.ref
end
expose :ref_url do |deployment|
namespace_project_tree_url(
deployment.project.namespace,
deployment.project,
id: deployment.ref)
end
end
expose :tag
expose :last?
expose :user, using: UserEntity
expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity
expose :manual_actions, using: BuildEntity
end
class EntityRequest
# We use EntityRequest object to collect parameters and variables
# from the controller. Because options that are being passed to the entity
# do appear in each entity object in the chain, we need a way to pass data
# that is present in the controller (see #20045).
#
def initialize(parameters)
parameters.each do |key, value|
define_singleton_method(key) { value }
end
end
end
class EnvironmentEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :name
expose :state
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
expose :stoppable?
expose :environment_url do |environment|
namespace_project_environment_url(
environment.project.namespace,
environment.project,
environment)
end
expose :created_at, :updated_at
end
class EnvironmentSerializer < BaseSerializer
entity EnvironmentEntity
end
module RequestAwareEntity
extend ActiveSupport::Concern
included do
include Gitlab::Routing.url_helpers
end
def request
@options.fetch(:request)
end
end
class UserEntity < API::Entities::UserBasic
end
......@@ -9,8 +9,8 @@ module Auth
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project
return error('DENIED', status: 403, message: 'access forbidden') unless scope
unless scope || current_user || project
return error('DENIED', status: 403, message: 'access forbidden')
end
{ token: authorized_token(scope).encoded }
......@@ -76,7 +76,7 @@ module Auth
case requested_action
when 'pull'
requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push'
build_can_push?(requested_project) || user_can_push?(requested_project)
else
......@@ -92,23 +92,23 @@ module Auth
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
@authentication_abilities.include?(:build_read_container_image) &&
has_authentication_ability?(:build_read_container_image) &&
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end
def user_can_pull?(requested_project)
@authentication_abilities.include?(:read_container_image) &&
has_authentication_ability?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
end
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
@authentication_abilities.include?(:build_create_container_image) &&
has_authentication_ability?(:build_create_container_image) &&
requested_project == project
end
def user_can_push?(requested_project)
@authentication_abilities.include?(:create_container_image) &&
has_authentication_ability?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
......@@ -118,5 +118,9 @@ module Auth
http_status: status
}
end
def has_authentication_ability?(capability)
(@authentication_abilities || []).include?(capability)
end
end
end
module Ci
class SendPipelineNotificationService
attr_reader :pipeline
def initialize(new_pipeline)
@pipeline = new_pipeline
end
def execute(recipients)
email_template = "pipeline_#{pipeline.status}_email"
return unless Notify.respond_to?(email_template)
recipients.each do |to|
Notify.public_send(email_template, pipeline, to).deliver_later
end
end
end
end
......@@ -105,35 +105,11 @@ class GitPushService < BaseService
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
def process_commit_messages
is_default_branch = is_default_branch?
authors = Hash.new do |hash, commit|
email = commit.author_email
next hash[email] if hash.has_key?(email)
hash[email] = commit_user(commit)
end
default = is_default_branch?
@push_commits.each do |commit|
# Keep track of the issues that will be actually closed because they are on a default branch.
# Hence, when creating cross-reference notes, the not-closed issues (on non-default branches)
# will also have cross-reference.
closed_issues = []
if is_default_branch
# Close issues if these commits were pushed to the project's default branch and the commit message matches the
# closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
# a different branch.
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
end
end
end
commit.create_cross_references!(authors[commit], closed_issues)
update_issue_metrics(commit, authors)
ProcessCommitWorker.
perform_async(project.id, current_user.id, commit.id, default)
end
end
......@@ -176,11 +152,4 @@ class GitPushService < BaseService
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
def update_issue_metrics(commit, authors)
mentioned_issues = commit.all_references(authors[commit]).issues
Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
update_all(first_mentioned_in_commit_at: commit.committed_date)
end
end
module Issues
class CloseService < Issues::BaseService
# Closes the supplied issue if the current user is able to do so.
def execute(issue, commit: nil, notifications: true, system_note: true)
return issue unless can?(current_user, :update_issue, issue)
close_issue(issue,
commit: commit,
notifications: notifications,
system_note: system_note)
end
# Closes the supplied issue without checking if the user is authorized to
# do so.
#
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
def close_issue(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
......
......@@ -312,6 +312,22 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
def pipeline_finished(pipeline, recipients = nil)
email_template = "pipeline_#{pipeline.status}_email"
return unless mailer.respond_to?(email_template)
recipients ||= build_recipients(
pipeline,
pipeline.project,
nil, # The acting user, who won't be added to recipients
action: pipeline.status).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
end
end
protected
# Get project/group users with CUSTOM notification level
......@@ -475,9 +491,14 @@ class NotificationService
end
def reject_users_without_access(recipients, target)
return recipients unless target.is_a?(Issuable)
ability = case target
when Issuable
:"read_#{target.to_ability_name}"
when Ci::Pipeline
:read_build # We have build trace in pipeline emails
end
ability = :"read_#{target.to_ability_name}"
return recipients unless ability
recipients.select do |user|
user.can?(ability, target)
......@@ -624,6 +645,6 @@ class NotificationService
# Build event key to search on custom notification level
# Check NotificationSetting::EMAIL_EVENTS
def build_custom_key(action, object)
"#{action}_#{object.class.name.underscore}".to_sym
"#{action}_#{object.class.model_name.name.underscore}".to_sym
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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