summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/activities.js4
-rw-r--r--app/assets/javascripts/application.js7
-rw-r--r--app/assets/javascripts/build.js99
-rw-r--r--app/assets/javascripts/compare.js3
-rw-r--r--app/assets/javascripts/diff.js4
-rw-r--r--app/assets/javascripts/dispatcher.js.es63
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es64
-rw-r--r--app/assets/javascripts/issuable.js.es622
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js19
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js95
-rw-r--r--app/assets/javascripts/lib/utils/timeago.js237
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es62
-rw-r--r--app/assets/javascripts/milestone_select.js2
-rw-r--r--app/assets/javascripts/network/network_bundle.js2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss5
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/header.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/pages/builds.scss22
-rw-r--r--app/assets/stylesheets/pages/diff.scss33
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss1
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb5
-rw-r--r--app/controllers/jwt_controller.rb4
-rw-r--r--app/controllers/profiles_controller.rb10
-rw-r--r--app/controllers/projects/git_http_client_controller.rb8
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/projects/network_controller.rb14
-rw-r--r--app/controllers/projects_controller.rb65
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/users_controller.rb3
-rw-r--r--app/finders/issuable_finder.rb33
-rw-r--r--app/helpers/accounts_helper.rb5
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/blob_helper.rb27
-rw-r--r--app/helpers/builds_helper.rb10
-rw-r--r--app/helpers/components_helper.rb9
-rw-r--r--app/helpers/diff_helper.rb7
-rw-r--r--app/helpers/issuables_helper.rb51
-rw-r--r--app/helpers/lfs_helper.rb4
-rw-r--r--app/helpers/notifications_helper.rb9
-rw-r--r--app/helpers/todos_helper.rb4
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/mailers/emails/pipelines.rb17
-rw-r--r--app/mailers/notify.rb12
-rw-r--r--app/models/application_setting.rb23
-rw-r--r--app/models/ci/pipeline.rb11
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/token_authenticatable.rb10
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/external_issue.rb9
-rw-r--r--app/models/guest.rb7
-rw-r--r--app/models/issue.rb52
-rw-r--r--app/models/issue_collection.rb42
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/project.rb45
-rw-r--r--app/models/project_feature.rb14
-rw-r--r--app/models/project_services/pipelines_email_service.rb20
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/user.rb22
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/ci/pipeline_policy.rb4
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/policies/issue_policy.rb13
-rw-r--r--app/services/auth/container_registry_authentication_service.rb18
-rw-r--r--app/services/ci/send_pipeline_notification_service.rb19
-rw-r--r--app/services/git_push_service.rb37
-rw-r--r--app/services/issues/close_service.rb13
-rw-r--r--app/services/notification_service.rb27
-rw-r--r--app/services/projects/housekeeping_service.rb59
-rw-r--r--app/views/admin/application_settings/_form.html.haml39
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml22
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml14
-rw-r--r--app/views/notify/pipeline_success_email.html.haml12
-rw-r--r--app/views/profiles/accounts/show.html.haml32
-rw-r--r--app/views/projects/_last_commit.html.haml2
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml28
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_line.html.haml4
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_issue_by_email.html.haml21
-rw-r--r--app/views/projects/network/show.html.haml5
-rw-r--r--app/views/projects/refs/logs_tree.js.haml3
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml23
-rw-r--r--app/views/shared/issuable/form/_template_selector.html.haml13
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/workers/git_garbage_collect_worker.rb47
-rw-r--r--app/workers/pipeline_notification_worker.rb12
-rw-r--r--app/workers/process_commit_worker.rb67
99 files changed, 1230 insertions, 517 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 59ac9b9cef5..919107b8cb9 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -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) {
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 7d942de0184..33c1708e1a9 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -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);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 12e653f4122..5133e361001 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -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')), ' '));
}
};
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index b3f769d4129..61cc91c524b 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -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));
}
});
};
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 4ddafff428f..82bfdcea0ca 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -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) {
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 8e4fd1f19ba..756a24cc0fc 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -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();
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 824413bf20f..e72e2194be8 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -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) {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 8fc498be27d..46503c290ae 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -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');
+ }
+ });
+ });
}
};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8447421195d..6cb3d95f984 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -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);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 59e526ed623..3965109dd65 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -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);
diff --git a/app/assets/javascripts/lib/utils/timeago.js b/app/assets/javascripts/lib/utils/timeago.js
new file mode 100644
index 00000000000..42606dd2d46
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/timeago.js
@@ -0,0 +1,237 @@
+/**
+ * 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
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 3a2fe454b68..56c87af3226 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -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 += '.';
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index c909b53dc21..d1cd38ad110 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -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 {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 42d6799c82f..a192273a180 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -9,6 +9,8 @@
(function() {
$(function() {
+ if (!$(".network-graph").length) return;
+
var network_graph;
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ed21ad83a1c..e7aff2d0cec 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 3e34ec98427..583c17e4a83 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -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;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4993ca7572a..5a34132112a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -100,10 +100,6 @@ header {
&:hover {
background-color: $btn-gray-hover;
}
-
- &:focus {
- outline: none;
- }
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index fcaf5e18633..ce864c2de5e 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -58,7 +58,6 @@
&:active,
&:focus {
text-decoration: none;
- outline: none;
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 13749f1b7bd..920ce249b9a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -63,7 +63,7 @@
}
.select2-highlighted {
- background: #3084bb !important;
+ background: $gl-link-color !important;
}
.select2-results li.select2-result-with-children > .select2-result-label {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d74c14ee2a4..44c445c0543 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -83,7 +83,6 @@
display: block;
text-decoration: none;
font-weight: normal;
- outline: none;
&:hover,
&:active,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index be2a7ceefff..e0d00759c9c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6300ac9662f..f1d311cabbe 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -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 {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fde138c874d..99fdea15218 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -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 {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 032feae8854..19ab198c2e7 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index ede29db1979..6fab97a71aa 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index bf688af50e2..b4761df3f23 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -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;
}
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 86e808314f4..52e0256943a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -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: [],
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7e4da73bc11..c736200a104 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -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)
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index f71e0a1302b..f0c71725ea8 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -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
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 383e184d796..3f41916e6d3 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -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
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 662d38b10a5..13caeb42d40 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -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
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 34318391dd9..33a152ad34f 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -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
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6988527a3be..a8a18b4fa16 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -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,26 +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,
+ :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,
- :lfs_enabled, project_feature_attributes
- )
+ :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?
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index d01e0dedf52..b666aa01d6b 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -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]
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6a881b271d7..c4508ccc3b9 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -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
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index cc2073081b5..6297b2db369 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -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
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
new file mode 100644
index 00000000000..5d27d30eaa3
--- /dev/null
+++ b/app/helpers/accounts_helper.rb
@@ -0,0 +1,5 @@
+module AccountsHelper
+ def incoming_email_token_enabled?
+ current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation?
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ebd78bf9888..c816b616631 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index e13b7cdd707..07ff6fb9488 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -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
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index f3aaff9140d..fde297c588e 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -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
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
new file mode 100644
index 00000000000..8893209b314
--- /dev/null
+++ b/app/helpers/components_helper.rb
@@ -0,0 +1,9 @@
+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
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 0725c3f4c56..f489f9aa0d6 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -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
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index ef6cfb235a9..8127c3f3ee3 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -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
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index 95b60aeab5f..d3966ba1f10 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,6 +1,6 @@
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?
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 7e8369d0a05..03cc8f2b6bd 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -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
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index a9db8bb2b82..09c69786791 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -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)
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 61a574d3dc0..79c3c2e62c5 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,6 +1,6 @@
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?
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 601c8b5cd62..9460a6cd2be 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -1,22 +1,27 @@
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
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index eca6ec29767..0bc1c19e9cd 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -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,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6e7a90e7d9c..bb60cc8736c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -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
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d3432632899..3fee6c18770 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -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)")
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 613444e0d70..664bb594aa9 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -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!
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 24c7b26d223..04d30f46210 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -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 || []
diff --git a/app/models/event.rb b/app/models/event.rb
index 43e67069b70..c76d88b1c7b 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -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?
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index fd9a8c1b8b7..91b508eb325 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -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
diff --git a/app/models/guest.rb b/app/models/guest.rb
new file mode 100644
index 00000000000..01285ca1264
--- /dev/null
+++ b/app/models/guest.rb
@@ -0,0 +1,7 @@
+class Guest
+ class << self
+ def can?(action, subject)
+ Ability.allowed?(nil, action, subject)
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4f02b02c488..adbca510ef7 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -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
diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb
new file mode 100644
index 00000000000..f0b7d9914c8
--- /dev/null
+++ b/app/models/issue_collection.rb
@@ -0,0 +1,42 @@
+# 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
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 121b598b8f3..43fc218de2b 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index 686d285410b..bbe590b5a8a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index b37ce1d3cf6..34fd5a57b5e 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -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?
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index ec3c1bc85ee..745f9bd1b43 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -1,10 +1,7 @@
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?
@@ -58,9 +55,6 @@ class PipelinesEmailService < Service
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' },
]
end
@@ -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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f11e0b34a9d..fe991904601 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -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(/^--$/)
diff --git a/app/models/user.rb b/app/models/user.rb
index 65e96ee6b2e..3813df6684e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 2232e231cf8..8b25332b73c 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -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
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
new file mode 100644
index 00000000000..3d2eef1c50c
--- /dev/null
+++ b/app/policies/ci/pipeline_policy.rb
@@ -0,0 +1,4 @@
+module Ci
+ class PipelinePolicy < BuildPolicy
+ end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index c253f9a9399..9501e499507 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -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
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index bd1811a3c54..88f3179c6ff 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -1,4 +1,8 @@
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
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 8ea88da8a53..c00c5aebf57 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -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
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
deleted file mode 100644
index ceb182801f7..00000000000
--- a/app/services/ci/send_pipeline_notification_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-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
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index e8415862de5..de313095bed 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -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
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 45cca216ccc..ab4c51386a4 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,8 +1,21 @@
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)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 72712afc07e..6697840cc26 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -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
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index c3dfc8cfbe8..4b8946f8ee2 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -7,6 +7,8 @@
#
module Projects
class HousekeepingService < BaseService
+ include Gitlab::CurrentSettings
+
LEASE_TIMEOUT = 3600
class LeaseTaken < StandardError
@@ -20,13 +22,14 @@ module Projects
end
def execute
- raise LeaseTaken unless try_obtain_lease
+ lease_uuid = try_obtain_lease
+ raise LeaseTaken unless lease_uuid.present?
- execute_gitlab_shell_gc
+ execute_gitlab_shell_gc(lease_uuid)
end
def needed?
- @project.pushes_since_gc >= 10
+ pushes_since_gc > 0 && period_match? && housekeeping_enabled?
end
def increment!
@@ -37,19 +40,59 @@ module Projects
private
- def execute_gitlab_shell_gc
- GitGarbageCollectWorker.perform_async(@project.id)
+ def execute_gitlab_shell_gc(lease_uuid)
+ GitGarbageCollectWorker.perform_async(@project.id, task, lease_key, lease_uuid)
ensure
- Gitlab::Metrics.measure(:reset_pushes_since_gc) do
- @project.reset_pushes_since_gc
+ if pushes_since_gc >= gc_period
+ Gitlab::Metrics.measure(:reset_pushes_since_gc) do
+ @project.reset_pushes_since_gc
+ end
end
end
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
- lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
+ lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end
+
+ def lease_key
+ "project_housekeeping:#{@project.id}"
+ end
+
+ def pushes_since_gc
+ @project.pushes_since_gc
+ end
+
+ def task
+ if pushes_since_gc % gc_period == 0
+ :gc
+ elsif pushes_since_gc % full_repack_period == 0
+ :full_repack
+ else
+ :incremental_repack
+ end
+ end
+
+ def period_match?
+ [gc_period, full_repack_period, repack_period].any? { |period| pushes_since_gc % period == 0 }
+ end
+
+ def housekeeping_enabled?
+ current_application_settings.housekeeping_enabled
+ end
+
+ def gc_period
+ current_application_settings.housekeeping_gc_period
+ end
+
+ def full_repack_period
+ current_application_settings.housekeeping_full_repack_period
+ end
+
+ def repack_period
+ current_application_settings.housekeeping_incremental_repack_period
+ end
end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 28003e5f509..450ec322f2c 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -422,5 +422,44 @@
Enable this option to include the name of the author of the issue,
merge request or comment in the email body instead.
+ %fieldset
+ %legend Automatic Git repository housekeeping
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :housekeeping_enabled do
+ = f.check_box :housekeeping_enabled
+ Enable automatic repository housekeeping (git repack, git gc)
+ .help-block
+ If you keep automatic housekeeping disabled for a long time Git
+ repository access on your GitLab server will become slower and your
+ repositories will use more disk space. We recommend to always leave
+ this enabled.
+ .checkbox
+ = f.label :housekeeping_bitmaps_enabled do
+ = f.check_box :housekeeping_bitmaps_enabled
+ Enable Git pack file bitmap creation
+ .help-block
+ Creating pack file bitmaps makes housekeeping take a little longer but
+ bitmaps should accelerate 'git clone' performance.
+ .form-group
+ = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+ .help-block
+ Number of Git pushes after which an incremental 'git repack' is run.
+ .form-group
+ = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+ .help-block
+ Number of Git pushes after which a full 'git repack' is run.
+ .form-group
+ = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :housekeeping_gc_period, class: 'form-control'
+ .help-block
+ Number of Git pushes after which 'git gc' is run.
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 90798c47d97..1db2150f336 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -87,7 +87,7 @@
%p
GitLab Workhorse
%span.pull-right
- = Gitlab::Workhorse.version
+ = gitlab_workhorse_version
%p
GitLab API
%span.pull-right
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index e247eebc3fc..5b2465e25ee 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -82,15 +82,19 @@
- elsif current_user.todos.any?
.todos-all-done
= render "shared/empty_states/todos_all_done.svg"
- %h4.text-center
- Good job! Looks like you don't have any todos left.
- %p.text-center
- Are you looking for things to do? Take a look at
- = succeed "," do
- = link_to "the opened issues", issues_dashboard_path
- contribute to
- = link_to "merge requests", merge_requests_dashboard_path
- or mention someone in a comment to assign a new todo automatically.
+ - if todos_filter_empty?
+ %h4.text-center
+ Good job! Looks like you don't have any todos left.
+ %p.text-center
+ Are you looking for things to do? Take a look at
+ = succeed "," do
+ = link_to "the opened issues", issues_dashboard_path
+ contribute to
+ = link_to "merge requests", merge_requests_dashboard_path
+ or mention someone in a comment to assign a new todo automatically.
+ - else
+ %h4.text-center
+ There are no todos to show.
- else
.todos-empty
.todos-empty-hero
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 31fdcc5e21b..5c318cd3b8b 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,7 +1,7 @@
- if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) }
.event-item-timestamp
- #{time_ago_with_tooltip(event.created_at, skip_js: true)}
+ #{time_ago_with_tooltip(event.created_at)}
= cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40)
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index d7386105b7d..8e65bd12c56 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -13,7 +13,7 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
- = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
+ = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 0995826775a..38c852f0a3a 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -103,11 +103,11 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
%img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
= @merge_request.to_reference
.commit{style: "color:#5c5c5c;font-weight:300;"}
= @pipeline.git_commit_message.truncate(50)
@@ -134,7 +134,7 @@
%tr.pre-section
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= "\##{@pipeline.id}"
had
= failed.size
@@ -158,7 +158,7 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
= build.stage
%td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
- %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"}
= build.name
%tr.build-log
%td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
@@ -168,10 +168,10 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
%img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
%div
- %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index cf9c1d4d72c..697c8d19257 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -103,11 +103,11 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
%img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
= @merge_request.to_reference
.commit{style: "color:#5c5c5c;font-weight:300;"}
= @pipeline.git_commit_message.truncate(50)
@@ -135,7 +135,7 @@
- build_count = @pipeline.statuses.latest.size
- stage_count = @pipeline.stages.size
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= "\##{@pipeline.id}"
successfully completed
= "#{build_count} #{'build'.pluralize(build_count)}"
@@ -145,10 +145,10 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
%img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
%div
- %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index e2e974ba072..72f658d1b68 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -8,24 +8,36 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- Private Token
+ = incoming_email_token_enabled? ? "Private Tokens" : "Private Token"
%p
- Your private token is used to access application resources without authentication.
- .col-lg-9
- = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f|
+ Keep
+ = incoming_email_token_enabled? ? "these tokens" : "this token"
+ secret, anyone with access to them can interact with GitLab as if they were you.
+ .col-lg-9.private-tokens-reset
+ .reset-action
%p.cgray
- if current_user.private_token
- = label_tag "token", "Private token", class: "label-light"
- = text_field_tag "token", current_user.private_token, class: "form-control"
+ = label_tag "private-token", "Private token", class: "label-light"
+ = text_field_tag "private-token", current_user.private_token, class: "form-control", readonly: true, onclick: "this.select()"
- else
- %span You don`t have one yet. Click generate to fix it.
- %p.help-block
- It can be used for atom feeds or the API. Keep it secret!
+ %span You don't have one yet. Click generate to fix it.
+ %p.help-block
+ Your private token is used to access the API and Atom feeds without username/password authentication.
.prepend-top-default
- if current_user.private_token
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+ = link_to 'Reset private token', reset_private_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default private-token"
- else
= f.submit 'Generate', class: "btn btn-default"
+ - if incoming_email_token_enabled?
+ .reset-action
+ %p.cgray
+ = label_tag "incoming-email-token", "Incoming Email Token", class: 'label-light'
+ = text_field_tag "incoming-email-token", current_user.incoming_email_token, class: "form-control", readonly: true, onclick: "this.select()"
+ %p.help-block
+ Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.
+ .prepend-top-default
+ = link_to 'Reset incoming email token', reset_incoming_email_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default incoming-email-token"
+
%hr
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 8e23d51b224..7f530708947 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -8,5 +8,5 @@
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot;
-#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
+#{time_ago_with_tooltip(commit.committed_date)} by
= commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index dfb96305f48..cadfe5a3e30 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -32,7 +32,7 @@
.light
= commit_author_link(commit, avatar: false)
authored
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
+ #{time_ago_with_tooltip(commit.committed_date)}
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b5e8b0bf6eb..ae7a7ecb392 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,5 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
-- trace_with_state = @build.trace_with_state
- header_title project_title(@project, "Builds", project_builds_path(@project))
= render "projects/pipelines/head", build_subnav: true
@@ -28,32 +27,27 @@
Runners page
.prepend-top-default
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ .scroll-step
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
+ Enable autoscroll
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
- #down-build-trace
+ #down-build-trace
= render "sidebar"
- :javascript
- new Build({
- page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
- build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
- build_status: "#{@build.status}",
- build_stage: "#{@build.stage}",
- state1: "#{trace_with_state[:state]}"
- })
+.js-build-options{ data: javascript_build_options }
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 1f748d73d06..2a2d24be736 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -59,7 +59,7 @@
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
- #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
+ #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 9f80a974d64..34855c54176 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,7 +6,7 @@
- note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
-- cache_key.push(commit.status) if commit.status
+- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 7042e9f1fc9..a3e4b5b777e 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -25,9 +25,9 @@
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
- %pre= diff_line_content(line.text, type)
+ %pre= diff_line_content(line.text)
- else
- = diff_line_content(line.text, type)
+ = diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta?
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 4d8ee562e6a..c52b3860636 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "Import in progress"
+- page_title @project.forked? ? "Forking in progress" : "Import in progress"
.save-project-loader
.center
%h2
diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml
index 72669372497..d2038a2be68 100644
--- a/app/views/projects/issues/_issue_by_email.html.haml
+++ b/app/views/projects/issues/_issue_by_email.html.haml
@@ -12,16 +12,23 @@
Create new issue by email
.modal-body
%p
- Write an email to the below email address. (This is a private email address, so keep it secret.)
+ You can create a new issue inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
= clipboard_button(clipboard_target: '#issue_email')
%p
- Send an email to this address to create an issue.
- %p
- Use the subject line as the title of your issue.
+ The subject will be used as the title of the new issue, and the message will be the description.
+
+ = link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1
+ and styling with
+ = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+ are supported.
+
%p
- Use the message as the body of your issue (feel free to include some nice
- = succeed ")." do
- = link_to "Markdown", help_page_path('markdown', 'markdown')
+ This is a private email address, generated just for you.
+
+ Anyone who gets ahold of it can create issues as if they were you.
+ You should
+ = link_to 'reset it', new_issue_address_namespace_project_path(@project.namespace, @project), class: 'incoming-email-token-reset'
+ if that ever happens.
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 29df1bab04e..d8951e69242 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -17,5 +17,6 @@
= check_box_tag :filter_ref, 1, @options[:filter_ref]
%span Begin with the selected commit
- .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
- = spinner nil, true
+ - if @commit
+ .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
+ = spinner nil, true
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 1141168f037..44fa4b60343 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -16,3 +16,6 @@
var url = "#{escape_javascript(@more_log_url)}";
ajaxGet(url);
}
+
+:plain
+ gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody')); \ No newline at end of file
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index 5b2d83d6b92..f34eaf89027 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1 +1 @@
-= render 'projects/commits/commit', project: @project, commit: commit
+= render 'projects/commits/commit', project: @project, commit: commit, ref: nil
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 0ace6be8f4e..3176af9c19b 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,4 +1,5 @@
- project = @target_project || @project
+
= form_errors(issuable)
- if @conflict
@@ -11,23 +12,9 @@
.form-group
= f.label :title, class: 'control-label'
- - issuable_template_names = issuable_templates(issuable)
-
- - if issuable_template_names.any?
- .col-sm-3.col-lg-2
- .js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } }
- - title = selected_template(issuable) || "Choose a template"
-
- = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
- title: title, filter: true, placeholder: 'Filter', footer_content: true,
- data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do
- %ul.dropdown-footer-list
- %li
- %a.no-template
- No template
- %a.reset-template
- Reset template
- %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
+ = render 'shared/issuable/form/template_selector', issuable: issuable
+
+ %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad', required: true
@@ -142,7 +129,7 @@
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[force_remove_source_branch]' do
- = hidden_field_tag 'merge_request[force_remove_source_branch]', '0'
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_template_selector.html.haml b/app/views/shared/issuable/form/_template_selector.html.haml
new file mode 100644
index 00000000000..d613bd31d81
--- /dev/null
+++ b/app/views/shared/issuable/form/_template_selector.html.haml
@@ -0,0 +1,13 @@
+- issuable = local_assigns.fetch(:issuable, nil)
+
+- return unless issuable && issuable_templates(issuable).any?
+
+.col-sm-3.col-lg-2
+ .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } }
+ = template_dropdown_tag(issuable) do
+ %ul.dropdown-footer-list
+ %li
+ %a.no-template
+ No template
+ %a.reset-template
+ Reset template
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b704981e3db..a82fc95df84 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -27,5 +27,5 @@
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
%strong
- = event.to_s.humanize
+ = notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 65f8093b5b0..d369b639ae9 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,17 +1,58 @@
class GitGarbageCollectWorker
include Sidekiq::Worker
- include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
+ include Gitlab::CurrentSettings
sidekiq_options retry: false
- def perform(project_id)
+ def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id)
+ task = task.to_sym
+
+ cmd = command(task)
+ repo_path = project.repository.path_to_repo
+ description = "'#{cmd.join(' ')}' in #{repo_path}"
+
+ Gitlab::GitLogger.info(description)
+
+ output, status = Gitlab::Popen.popen(cmd, repo_path)
+ Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
- gitlab_shell.gc(project.repository_storage_path, project.path_with_namespace)
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
+ flush_ref_caches(project) if task == :gc
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
+ end
+
+ private
+
+ def command(task)
+ case task
+ when :gc
+ git(write_bitmaps: bitmaps_enabled?) + %w[gc]
+ when :full_repack
+ git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects]
+ when :incremental_repack
+ # Normal git repack fails when bitmaps are enabled. It is impossible to
+ # create a bitmap here anyway.
+ git(write_bitmaps: false) + %w[repack -d]
+ else
+ raise "Invalid gc task: #{task.inspect}"
+ end
+ end
+
+ def flush_ref_caches(project)
project.repository.after_create_branch
project.repository.branch_names
project.repository.has_visible_content?
end
+
+ def bitmaps_enabled?
+ current_application_settings.housekeeping_bitmaps_enabled
+ end
+
+ def git(write_bitmaps:)
+ config_value = write_bitmaps ? 'true' : 'false'
+ %W[git -c repack.writeBitmaps=#{config_value}]
+ end
end
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
new file mode 100644
index 00000000000..cdb860b6675
--- /dev/null
+++ b/app/workers/pipeline_notification_worker.rb
@@ -0,0 +1,12 @@
+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
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
new file mode 100644
index 00000000000..071741fbacd
--- /dev/null
+++ b/app/workers/process_commit_worker.rb
@@ -0,0 +1,67 @@
+# Worker for processing individiual commit messages pushed to a repository.
+#
+# Jobs for this worker are scheduled for every commit that is being pushed. As a
+# result of this the workload of this worker should be kept to a bare minimum.
+# Consider using an extra worker if you need to add any extra (and potentially
+# slow) processing of commits.
+class ProcessCommitWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ # project_id - The ID of the project this commit belongs to.
+ # user_id - The ID of the user that pushed the commit.
+ # commit_sha - The SHA1 of the commit to process.
+ # default - The data was pushed to the default branch.
+ def perform(project_id, user_id, commit_sha, default = false)
+ project = Project.find_by(id: project_id)
+
+ return unless project
+
+ user = User.find_by(id: user_id)
+
+ return unless user
+
+ commit = find_commit(project, commit_sha)
+
+ return unless commit
+
+ author = commit.author || user
+
+ process_commit_message(project, commit, user, author, default)
+
+ update_issue_metrics(commit, author)
+ end
+
+ def process_commit_message(project, commit, user, author, default = false)
+ closed_issues = default ? commit.closes_issues(user) : []
+
+ unless closed_issues.empty?
+ close_issues(project, user, author, commit, closed_issues)
+ end
+
+ commit.create_cross_references!(author, closed_issues)
+ end
+
+ def close_issues(project, user, author, commit, issues)
+ # We don't want to run permission related queries for every single issue,
+ # therefor we use IssueCollection here and skip the authorization check in
+ # Issues::CloseService#execute.
+ IssueCollection.new(issues).updatable_by_user(user).each do |issue|
+ Issues::CloseService.new(project, author).
+ close_issue(issue, commit: commit)
+ end
+ end
+
+ def update_issue_metrics(commit, author)
+ mentioned_issues = commit.all_references(author).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
+
+ private
+
+ def find_commit(project, sha)
+ project.commit(sha)
+ end
+end