Commit 363059e6 authored by Regis's avatar Regis

Merge branch 'master' into auto-pipelines-vue

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