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