Commit ba0b7c55 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

Closes #1376 and gitlab-ce#26114

See merge request !1014
parents 017288f4 38199935
...@@ -24,7 +24,7 @@ entry. ...@@ -24,7 +24,7 @@ entry.
## 8.15.2 (2016-12-27) ## 8.15.2 (2016-12-27)
- No changes. - Fix finding the latest pipeline. !8301
- Fix mr list timestamp alignment. !8271 - Fix mr list timestamp alignment. !8271
- Fix discussion overlap text in regular screens. !8273 - Fix discussion overlap text in regular screens. !8273
- Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari. !8282 - Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari. !8282
......
...@@ -343,7 +343,7 @@ gem 'octokit', '~> 4.3.0' ...@@ -343,7 +343,7 @@ gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.9.0' gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8' gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text' gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.16.2'
...@@ -358,5 +358,5 @@ gem 'paranoia', '~> 2.2' ...@@ -358,5 +358,5 @@ gem 'paranoia', '~> 2.2'
gem 'health_check', '~> 2.2.0' gem 'health_check', '~> 2.2.0'
# System information # System information
gem 'vmstat', '~> 2.2' gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
...@@ -180,7 +180,7 @@ GEM ...@@ -180,7 +180,7 @@ GEM
elasticsearch-transport (1.0.15) elasticsearch-transport (1.0.15)
faraday faraday
multi_json multi_json
email_reply_parser (0.5.8) email_reply_trimmer (0.1.6)
email_spec (1.6.0) email_spec (1.6.0)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.2) mail (~> 2.2)
...@@ -800,7 +800,7 @@ GEM ...@@ -800,7 +800,7 @@ GEM
coercible (~> 1.0) coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
vmstat (2.2.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
web-console (2.3.0) web-console (2.3.0)
...@@ -868,7 +868,7 @@ DEPENDENCIES ...@@ -868,7 +868,7 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
elasticsearch-model elasticsearch-model
elasticsearch-rails elasticsearch-rails
email_reply_parser (~> 0.5.8) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0) ffaker (~> 2.0.0)
...@@ -1016,7 +1016,7 @@ DEPENDENCIES ...@@ -1016,7 +1016,7 @@ DEPENDENCIES
validates_hostname (~> 1.0.6) validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0) version_sorter (~> 2.1.0)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.2) vmstat (~> 2.3.0)
web-console (~> 2.0) web-console (~> 2.0)
webmock (~> 1.21.0) webmock (~> 1.21.0)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
......
...@@ -69,7 +69,8 @@ to add details to the issue. ...@@ -69,7 +69,8 @@ to add details to the issue.
- ~UX needs help from a UX designer - ~UX needs help from a UX designer
- ~Frontend needs help from a Front-end engineer. Please follow the - ~Frontend needs help from a Front-end engineer. Please follow the
["Implement design & UI elements" guidelines]. ["Implement design & UI elements" guidelines].
- ~up-for-grabs is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels. - ~"Accepting Merge Requests" is a low priority, well-defined issue that we
encourage people to contribute to. Not exclusive with other labels.
- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote - ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
in support or comment for further detail. Do not use `feature request`. in support or comment for further detail. Do not use `feature request`.
- ~bug is an issue reporting undesirable or incorrect behavior. - ~bug is an issue reporting undesirable or incorrect behavior.
......
...@@ -91,6 +91,14 @@ ...@@ -91,6 +91,14 @@
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; Cookies.defaults.path = gon.relative_url_root || '/';
// `hashchange` is not triggered when link target is already in window.location
$body.on('click', 'a[href^="#"]', function() {
var href = this.getAttribute('href');
if (href.substr(1) === gl.utils.getLocationHash()) {
setTimeout(gl.utils.handleLocationHash, 1);
}
});
// prevent default action for disabled buttons // prevent default action for disabled buttons
$('.btn').click(function(e) { $('.btn').click(function(e) {
if ($(this).hasClass('disabled')) { if ($(this).hasClass('disabled')) {
......
...@@ -71,3 +71,5 @@ class ListIssue { ...@@ -71,3 +71,5 @@ class ListIssue {
return Vue.http.patch(url, data); return Vue.http.patch(url, data);
} }
} }
window.ListIssue = ListIssue;
...@@ -10,3 +10,5 @@ class ListLabel { ...@@ -10,3 +10,5 @@ class ListLabel {
this.priority = (obj.priority !== null) ? obj.priority : Infinity; this.priority = (obj.priority !== null) ? obj.priority : Infinity;
} }
} }
window.ListLabel = ListLabel;
...@@ -148,3 +148,5 @@ class List { ...@@ -148,3 +148,5 @@ class List {
}); });
} }
} }
window.List = List;
...@@ -6,3 +6,5 @@ class ListMilestone { ...@@ -6,3 +6,5 @@ class ListMilestone {
this.title = obj.title; this.title = obj.title;
} }
} }
window.ListMilestone = ListMilestone;
...@@ -8,3 +8,5 @@ class ListUser { ...@@ -8,3 +8,5 @@ class ListUser {
this.avatar = user.avatar_url; this.avatar = user.avatar_url;
} }
} }
window.ListUser = ListUser;
...@@ -78,4 +78,6 @@ class BoardService { ...@@ -78,4 +78,6 @@ class BoardService {
issue issue
}); });
} }
}; }
window.BoardService = BoardService;
...@@ -59,9 +59,11 @@ ...@@ -59,9 +59,11 @@
}, },
methods: { methods: {
updateTooltip: function () { updateTooltip: function () {
$(this.$refs.button) this.$nextTick(() => {
.tooltip('hide') $(this.$refs.button)
.tooltip('fixTitle'); .tooltip('hide')
.tooltip('fixTitle');
});
}, },
resolve: function () { resolve: function () {
if (!this.canResolve) return; if (!this.canResolve) return;
...@@ -90,7 +92,7 @@ ...@@ -90,7 +92,7 @@
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
} }
this.$nextTick(this.updateTooltip); this.updateTooltip();
}); });
} }
}, },
......
...@@ -92,3 +92,5 @@ class DiscussionModel { ...@@ -92,3 +92,5 @@ class DiscussionModel {
return false; return false;
} }
} }
window.DiscussionModel = DiscussionModel;
...@@ -9,3 +9,5 @@ class NoteModel { ...@@ -9,3 +9,5 @@ class NoteModel {
this.resolved_by = resolved_by; this.resolved_by = resolved_by;
} }
} }
window.NoteModel = NoteModel;
...@@ -20,3 +20,5 @@ class EnvironmentsService { ...@@ -20,3 +20,5 @@ class EnvironmentsService {
return this.environments.get(); return this.environments.get();
} }
} }
window.EnvironmentsService = EnvironmentsService;
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
subtext = subtext.split(' ').pop(); subtext = subtext.split(/\s+/g).pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80"); _a = decodeURI("%C3%80");
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */ /* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */
// MarkdownPreview // MarkdownPreview
// //
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
// and showing a warning when more than `x` users are referenced. // and showing a warning when more than `x` users are referenced.
// //
(function() { (function () {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; var lastTextareaPreviewed;
var markdownPreview;
var previewButtonSelector;
var writeButtonSelector;
window.MarkdownPreview = (function() { window.MarkdownPreview = (function () {
function MarkdownPreview() {} function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning // Minimum number of users referenced before triggering a warning
...@@ -16,73 +19,71 @@ ...@@ -16,73 +19,71 @@
MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function(form) { MarkdownPreview.prototype.showPreview = function ($form) {
var mdText, preview; var mdText;
preview = form.find('.js-md-preview'); var preview = $form.find('.js-md-preview');
mdText = form.find('textarea.markdown-area').val(); if (preview.hasClass('md-preview-loading')) {
return;
}
mdText = $form.find('textarea.markdown-area').val();
if (mdText.trim().length === 0) { if (mdText.trim().length === 0) {
preview.text('Nothing to preview.'); preview.text('Nothing to preview.');
return this.hideReferencedUsers(form); this.hideReferencedUsers($form);
} else { } else {
preview.text('Loading...'); preview.addClass('md-preview-loading').text('Loading...');
return this.renderMarkdown(mdText, (function(_this) { this.fetchMarkdownPreview(mdText, (function (response) {
return function(response) { preview.removeClass('md-preview-loading').html(response.body);
preview.html(response.body); preview.renderGFM();
preview.renderGFM(); this.renderReferencedUsers(response.references.users, $form);
return _this.renderReferencedUsers(response.references.users, form); }).bind(this));
};
})(this));
} }
}; };
MarkdownPreview.prototype.renderMarkdown = function(text, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) {
if (!window.preview_markdown_path) { if (!window.preview_markdown_path) {
return; return;
} }
if (text === this.ajaxCache.text) { if (text === this.ajaxCache.text) {
return success(this.ajaxCache.response); success(this.ajaxCache.response);
return;
} }
return $.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: window.preview_markdown_path, url: window.preview_markdown_path,
data: { data: {
text: text text: text
}, },
dataType: 'json', dataType: 'json',
success: (function(_this) { success: (function (response) {
return function(response) { this.ajaxCache = {
_this.ajaxCache = { text: text,
text: text, response: response
response: response
};
return success(response);
}; };
})(this) success(response);
}).bind(this)
}); });
}; };
MarkdownPreview.prototype.hideReferencedUsers = function(form) { MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
var referencedUsers; $form.find('.referenced-users').hide();
referencedUsers = form.find('.referenced-users');
return referencedUsers.hide();
}; };
MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
var referencedUsers; var referencedUsers;
referencedUsers = form.find('.referenced-users'); referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) { if (referencedUsers.length) {
if (users.length >= this.referenceThreshold) { if (users.length >= this.referenceThreshold) {
referencedUsers.show(); referencedUsers.show();
return referencedUsers.find('.js-referenced-users-count').text(users.length); referencedUsers.find('.js-referenced-users-count').text(users.length);
} else { } else {
return referencedUsers.hide(); referencedUsers.hide();
} }
} }
}; };
return MarkdownPreview; return MarkdownPreview;
}());
})();
markdownPreview = new window.MarkdownPreview(); markdownPreview = new window.MarkdownPreview();
...@@ -92,19 +93,14 @@ ...@@ -92,19 +93,14 @@
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
$.fn.setupMarkdownPreview = function() { $.fn.setupMarkdownPreview = function () {
var $form, form_textarea; var $form = $(this);
$form = $(this); $form.find('textarea.markdown-area').on('input', function () {
form_textarea = $form.find('textarea.markdown-area'); markdownPreview.hideReferencedUsers($form);
form_textarea.on('input', function() {
return markdownPreview.hideReferencedUsers($form);
});
return form_textarea.on('blur', function() {
return markdownPreview.showPreview($form);
}); });
}; };
$(document).on('markdown-preview:show', function(e, $form) { $(document).on('markdown-preview:show', function (e, $form) {
if (!$form) { if (!$form) {
return; return;
} }
...@@ -115,10 +111,10 @@ ...@@ -115,10 +111,10 @@
// toggle content // toggle content
$form.find('.md-write-holder').hide(); $form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show(); $form.find('.md-preview-holder').show();
return markdownPreview.showPreview($form); markdownPreview.showPreview($form);
}); });
$(document).on('markdown-preview:hide', function(e, $form) { $(document).on('markdown-preview:hide', function (e, $form) {
if (!$form) { if (!$form) {
return; return;
} }
...@@ -129,34 +125,33 @@ ...@@ -129,34 +125,33 @@
// toggle content // toggle content
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
return $form.find('.md-preview-holder').hide(); $form.find('.md-preview-holder').hide();
}); });
$(document).on('markdown-preview:toggle', function(e, keyboardEvent) { $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
var $target; var $target;
$target = $(keyboardEvent.target); $target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) { if ($target.is('textarea.markdown-area')) {
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
return keyboardEvent.preventDefault(); keyboardEvent.preventDefault();
} else if (lastTextareaPreviewed) { } else if (lastTextareaPreviewed) {
$target = lastTextareaPreviewed; $target = lastTextareaPreviewed;
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]);
return keyboardEvent.preventDefault(); keyboardEvent.preventDefault();
} }
}); });
$(document).on('click', previewButtonSelector, function(e) { $(document).on('click', previewButtonSelector, function (e) {
var $form; var $form;
e.preventDefault(); e.preventDefault();
$form = $(this).closest('form'); $form = $(this).closest('form');
return $(document).triggerHandler('markdown-preview:show', [$form]); $(document).triggerHandler('markdown-preview:show', [$form]);
}); });
$(document).on('click', writeButtonSelector, function(e) { $(document).on('click', writeButtonSelector, function (e) {
var $form; var $form;
e.preventDefault(); e.preventDefault();
$form = $(this).closest('form'); $form = $(this).closest('form');
return $(document).triggerHandler('markdown-preview:hide', [$form]); $(document).triggerHandler('markdown-preview:hide', [$form]);
}); });
}());
}).call(this);
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
}, },
data: function(term, callback) { data: function(term, callback) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) { finalCallback = function(projects) {
return callback(projects); return callback(projects);
}; };
...@@ -34,7 +35,7 @@ ...@@ -34,7 +35,7 @@
if (this.groupId) { if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback); return Api.groupProjects(this.groupId, term, projectsCallback);
} else { } else {
return Api.projects(term, this.orderBy, projectsCallback); return Api.projects(term, orderBy, projectsCallback);
} }
}, },
url: function(project) { url: function(project) {
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
}); });
// Protected branch dropdown // Protected branch dropdown
new gl.ProtectedBranchDropdown({ new window.ProtectedBranchDropdown({
$dropdown: this.$wrap.find('.js-protected-branch-select'), $dropdown: this.$wrap.find('.js-protected-branch-select'),
onSelect: this.onSelectCallback onSelect: this.onSelectCallback
}); });
......
/* eslint-disable */ /* eslint-disable comma-dangle, no-unused-vars */
(global => {
global.gl = global.gl || {};
class ProtectedBranchDropdown { class ProtectedBranchDropdown {
constructor(options) { constructor(options) {
this.onSelect = options.onSelect; this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
this.buildDropdown(); this.buildDropdown();
this.bindEvents(); this.bindEvents();
// Hide footer // Hide footer
this.$dropdownFooter.addClass('hidden'); this.$dropdownFooter.addClass('hidden');
} }
buildDropdown() { buildDropdown() {
this.$dropdown.glDropdown({ this.$dropdown.glDropdown({
data: this.getProtectedBranches.bind(this), data: this.getProtectedBranches.bind(this),
filterable: true, filterable: true,
remote: false, remote: false,
search: { search: {
fields: ['title'] fields: ['title']
}, },
selectable: true, selectable: true,
toggleLabel(selected) { toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
}, },
fieldName: 'protected_branch[name]', fieldName: 'protected_branch[name]',
text(protectedBranch) { text(protectedBranch) {
return _.escape(protectedBranch.title); return _.escape(protectedBranch.title);
}, },
id(protectedBranch) { id(protectedBranch) {
return _.escape(protectedBranch.id); return _.escape(protectedBranch.id);
}, },
onFilter: this.toggleCreateNewButton.bind(this), onFilter: this.toggleCreateNewButton.bind(this),
clicked: (item, $el, e) => { clicked: (item, $el, e) => {
e.preventDefault(); e.preventDefault();
this.onSelect(); this.onSelect();
} }
}); });
} }
onClickCreateWildcard() { onClickCreateWildcard() {
// Refresh the dropdown's data, which ends up calling `getProtectedBranches` // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(0); this.$dropdown.data('glDropdown').selectRowAtIndex(0);
} }
bindEvents() { bindEvents() {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
} }
getProtectedBranches(term, callback) { getProtectedBranches(term, callback) {
if (this.selectedBranch) { if (this.selectedBranch) {
callback(gon.open_branches.concat(this.selectedBranch)); callback(gon.open_branches.concat(this.selectedBranch));
} else { } else {
callback(gon.open_branches); callback(gon.open_branches);
}
} }
}
toggleCreateNewButton(branchName) { toggleCreateNewButton(branchName) {
this.selectedBranch = { this.selectedBranch = {
title: branchName, title: branchName,
id: branchName, id: branchName,
text: branchName text: branchName
}; };
if (branchName) {
this.$dropdownContainer
.find('.create-new-protected-branch code')
.text(branchName);
}
this.$dropdownFooter.toggleClass('hidden', !branchName); if (branchName) {
this.$dropdownContainer
.find('.create-new-protected-branch code')
.text(branchName);
} }
this.$dropdownFooter.toggleClass('hidden', !branchName);
} }
}
global.gl.ProtectedBranchDropdown = ProtectedBranchDropdown; window.ProtectedBranchDropdown = ProtectedBranchDropdown;
})(window);
...@@ -57,16 +57,33 @@ pre { ...@@ -57,16 +57,33 @@ pre {
border-radius: 0; border-radius: 0;
color: $well-pre-color; color: $well-pre-color;
} }
&.wrap {
word-break: break-word;
white-space: pre-wrap;
}
} }
hr { hr {
margin: $gl-padding 0; margin: $gl-padding 0;
border-top: 1px solid darken($gray-normal, 8%);
} }
.str-truncated { .str-truncated {
@include str-truncated; @include str-truncated;
} }
.block-truncated {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
> div,
.str-truncated {
display: inline;
}
}
.item-title { font-weight: 600; } .item-title { font-weight: 600; }
/** FLASH message **/ /** FLASH message **/
......
...@@ -87,25 +87,25 @@ ...@@ -87,25 +87,25 @@
} }
} }
$theme-charcoal: #3d454d; $theme-charcoal-light: #b9bbbe;
$theme-charcoal-light: #485157; $theme-charcoal: #485157;
$theme-charcoal-dark: #383f45; $theme-charcoal-dark: #3d454d;
$theme-charcoal-text: #b9bbbe; $theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9; $theme-blue-light: #becde9;
$theme-blue: #2980b9; $theme-blue: #2980b9;
$theme-blue-dark: #1970a9; $theme-blue-dark: #1970a9;
$theme-blue-darker: #096099; $theme-blue-darker: #096099;
$theme-graphite-lighter: #ccc; $theme-graphite-light: #ccc;
$theme-graphite-light: #777; $theme-graphite: #777;
$theme-graphite: #666; $theme-graphite-dark: #666;
$theme-graphite-dark: #555; $theme-graphite-darker: #555;
$theme-gray-light: #979797; $theme-black-light: #979797;
$theme-gray: #373737; $theme-black: #373737;
$theme-gray-dark: #272727; $theme-black-dark: #272727;
$theme-gray-darker: #222; $theme-black-darker: #222;
$theme-green-light: #adc; $theme-green-light: #adc;
$theme-green: #019875; $theme-green: #019875;
...@@ -123,15 +123,15 @@ body { ...@@ -123,15 +123,15 @@ body {
} }
&.ui_charcoal { &.ui_charcoal {
@include gitlab-theme($theme-charcoal-text, $theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark); @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
} }
&.ui_graphite { &.ui_graphite {
@include gitlab-theme($theme-graphite-lighter, $theme-graphite-light, $theme-graphite, $theme-graphite-dark); @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
} }
&.ui_gray { &.ui_black {
@include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker); @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
} }
&.ui_green { &.ui_green {
......
...@@ -205,6 +205,7 @@ ul.content-list { ...@@ -205,6 +205,7 @@ ul.content-list {
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
align-items: center;
white-space: nowrap; white-space: nowrap;
} }
...@@ -214,6 +215,11 @@ ul.content-list { ...@@ -214,6 +215,11 @@ ul.content-list {
padding-right: 8px; padding-right: 8px;
} }
.row-fixed-content {
flex: 0 0 auto;
margin-left: auto;
}
.row-title { .row-title {
font-weight: 600; font-weight: 600;
} }
......
...@@ -116,8 +116,8 @@ ...@@ -116,8 +116,8 @@
padding-top: 16px; padding-top: 16px;
padding-bottom: 11px; padding-bottom: 11px;
display: inline-block; display: inline-block;
width: 50%;
line-height: 28px; line-height: 28px;
white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -158,30 +158,24 @@ ...@@ -158,30 +158,24 @@
} }
.nav-controls { .nav-controls {
width: 50%;
display: inline-block; display: inline-block;
float: right; float: right;
text-align: right; text-align: right;
padding: 11px 0; padding: 11px 0;
margin-bottom: 0; margin-bottom: 0;
> .dropdown { > .btn,
margin-right: $gl-padding-top; > .btn-container,
display: inline-block; > .dropdown,
vertical-align: top; > input,
> form {
&:last-child {
margin-right: 0;
}
}
> .btn {
margin-right: $gl-padding-top; margin-right: $gl-padding-top;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
float: right;
} }
} }
...@@ -189,19 +183,21 @@ ...@@ -189,19 +183,21 @@
float: none; float: none;
} }
> form {
display: inline-block;
}
.icon-label { .icon-label {
display: none; display: none;
} }
input { .btn,
.dropdown,
.dropdown-toggle,
input,
form {
height: 35px; height: 35px;
}
input {
display: inline-block; display: inline-block;
position: relative; position: relative;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */ /* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; } @media (min-width: $screen-md-min) { width: 200px; }
...@@ -225,6 +221,7 @@ ...@@ -225,6 +221,7 @@
.btn, .btn,
form, form,
.dropdown, .dropdown,
.dropdown-toggle,
.dropdown-menu-toggle, .dropdown-menu-toggle,
.form-control { .form-control {
margin: 0 0 10px; margin: 0 0 10px;
...@@ -263,6 +260,10 @@ ...@@ -263,6 +260,10 @@
.nav-text, .nav-text,
.nav-controls { .nav-controls {
width: auto; width: auto;
@media (max-width: $screen-xs-max) {
width: 100%;
}
} }
} }
...@@ -275,6 +276,10 @@ ...@@ -275,6 +276,10 @@
padding: 17px 0; padding: 17px 0;
} }
} }
pre {
width: 100%;
}
} }
.layout-nav { .layout-nav {
...@@ -427,4 +432,41 @@ ...@@ -427,4 +432,41 @@
border-bottom: none; border-bottom: none;
} }
} }
} }
\ No newline at end of file
@media (max-width: $screen-xs-max) {
.top-area {
flex-flow: row wrap;
.nav-controls {
$controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
&.controls-flex {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
}
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
.btn,
.dropdown {
margin: 0;
}
}
.controls-item-full {
flex: 1 1 100%;
}
}
}
}
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
// Labels // Labels
.label { .label {
padding: 4px 5px; padding: 4px 5px;
font-size: 13px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
......
...@@ -496,9 +496,9 @@ $project-network-controls-color: #888; ...@@ -496,9 +496,9 @@ $project-network-controls-color: #888;
*/ */
$runner-state-shared-bg: #32b186; $runner-state-shared-bg: #32b186;
$runner-state-specific-bg: #3498db; $runner-state-specific-bg: #3498db;
$runner-status-online-color: green; $runner-status-online-color: $green-normal;
$runner-status-offline-color: gray; $runner-status-offline-color: $gray-darkest;
$runner-status-paused-color: red; $runner-status-paused-color: $red-normal;
/* /*
Stat Graph Stat Graph
......
...@@ -108,6 +108,12 @@ ...@@ -108,6 +108,12 @@
&.has-border { &.has-border {
border-top: 3px solid; border-top: 3px solid;
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
padding-top: 1px;
padding-right: 1px;
padding-left: 1px;
.board-title { .board-title {
padding-top: ($gl-padding - 3px); padding-top: ($gl-padding - 3px);
......
.deploy-keys-list {
width: 100%;
overflow: auto;
table {
border: 1px solid $table-border-color;
}
}
.deploy-keys-title {
padding-bottom: 2px;
line-height: 2;
}
// Limit MR description for side-by-side diff view
.limit-container-width {
.detail-page-header {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.issuable-details {
.detail-page-description,
.mr-source-target,
.mr-state-widget,
.merge-manually {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
.container-fluid {
padding-left: 0;
padding-right: 0;
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.diffs {
.mr-version-controls,
.files-changed {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.issuable-details { .issuable-details {
section { section {
.issuable-discussion { .issuable-discussion {
......
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
} }
.label { .label {
padding: 8px 9px 9px $gl-padding; padding: 8px 9px 9px;
font-size: 14px; font-size: 14px;
} }
} }
......
...@@ -105,19 +105,19 @@ ...@@ -105,19 +105,19 @@
li { li {
flex: 1; flex: 1;
text-align: center; text-align: center;
border-left: 1px solid $border-color;
&:first-of-type { &:first-of-type {
border-left: none;
border-top-left-radius: $border-radius-default; border-top-left-radius: $border-radius-default;
} }
&:last-of-type { &:last-of-type {
border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default; border-top-right-radius: $border-radius-default;
} }
&:not(.active) { &:not(.active) {
background-color: $gray-light; background-color: $gray-light;
border-left: 1px solid $border-color;
} }
a { a {
......
...@@ -424,6 +424,10 @@ ...@@ -424,6 +424,10 @@
.merge-request-tabs-holder { .merge-request-tabs-holder {
background-color: $white-light; background-color: $white-light;
.container-limited {
max-width: $limited-layout-width;
}
&.affix { &.affix {
top: 100px; top: 100px;
left: 0; left: 0;
......
...@@ -557,18 +557,14 @@ ul.notes { ...@@ -557,18 +557,14 @@ ul.notes {
&.is-active { &.is-active {
color: $gl-text-green; color: $gl-text-green;
svg path { svg {
fill: $gl-text-green; fill: $gl-text-green;
} }
} }
svg { svg {
position: relative; position: relative;
color: $gray-darkest; fill: $gray-darkest;
path {
fill: $gray-darkest;
}
} }
} }
......
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
background: $theme-graphite; background: $theme-graphite;
} }
&.ui_gray { &.ui_black {
background: $theme-gray; background: $theme-black;
} }
&.ui_green { &.ui_green {
......
...@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end end
def create def create
@deploy_key = deploy_keys.new(deploy_key_params) @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user))
if @deploy_key.save if @deploy_key.save
redirect_to admin_deploy_keys_path redirect_to admin_deploy_keys_path
...@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController
end end
def deploy_key_params def deploy_key_params
params.require(:deploy_key).permit(:key, :title) params.require(:deploy_key).permit(:key, :title, :can_push)
end end
end end
...@@ -9,7 +9,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -9,7 +9,7 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def show def show
@group = Group.with_statistics.find_by_full_path(params[:id]) @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page]) @members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.with_statistics.page(params[:projects_page]) @projects = @group.projects.with_statistics.page(params[:projects_page])
......
...@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index def index
@sort = params[:sort] @sort = params[:sort]
@todos = @todos.page(params[:page]) @todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0
redirect_to url_for(params.merge(page: @todos.total_pages))
end
end end
def destroy def destroy
......
...@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def create def create
@key = DeployKey.new(deploy_key_params) @key = DeployKey.new(deploy_key_params.merge(user: current_user))
set_index_vars set_index_vars
if @key.valid? && @project.deploy_keys << @key if @key.valid? && @project.deploy_keys << @key
...@@ -59,7 +59,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -59,7 +59,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def deploy_key_params def deploy_key_params
params.require(:deploy_key).permit(:key, :title) params.require(:deploy_key).permit(:key, :title, :can_push)
end end
def log_audit_event(key_title, options = {}) def log_audit_event(key_title, options = {})
......
...@@ -25,6 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -25,6 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController
def index def index
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
end
if params[:label_name].present? if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
......
...@@ -40,6 +40,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -40,6 +40,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index def index
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end
if params[:label_name].present? if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] } labels_params = { project_id: @project.id, title: params[:label_name] }
......
...@@ -26,6 +26,9 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -26,6 +26,9 @@ class Projects::SnippetsController < Projects::ApplicationController
scope: params[:scope] scope: params[:scope]
) )
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
if @snippets.out_of_range? && @snippets.total_pages != 0
redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
end
end end
def new def new
......
...@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy] before_action :authorize_admin_project!, only: [:destroy]
def index def index
params[:sort] = params[:sort].presence || 'name' params[:sort] = params[:sort].presence || sort_value_recently_updated
@sort = params[:sort] @sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute @tags = TagsFinder.new(@repository, params).execute
......
...@@ -61,7 +61,7 @@ module ProjectsHelper ...@@ -61,7 +61,7 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user if current_user
project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
icon("chevron-down") icon("chevron-down")
end end
end end
...@@ -90,10 +90,12 @@ module ProjectsHelper ...@@ -90,10 +90,12 @@ module ProjectsHelper
end end
def project_for_deploy_key(deploy_key) def project_for_deploy_key(deploy_key)
if deploy_key.projects.include?(@project) if deploy_key.has_access_to?(@project)
@project @project
else else
deploy_key.projects.find { |project| can?(current_user, :read_project, project) } deploy_key.projects.find do |project|
can?(current_user, :read_project, project)
end
end end
end end
......
...@@ -20,4 +20,18 @@ class DeployKey < Key ...@@ -20,4 +20,18 @@ class DeployKey < Key
def destroyed_when_orphaned? def destroyed_when_orphaned?
self.private? self.private?
end end
def has_access_to?(project)
projects.include?(project)
end
def can_push_to?(project)
can_push? && has_access_to?(project)
end
private
# we don't want to notify the user for deploy keys
def notify_user
end
end end
...@@ -178,15 +178,17 @@ class Group < Namespace ...@@ -178,15 +178,17 @@ class Group < Namespace
end end
def has_owner?(user) def has_owner?(user)
owners.include?(user) members_with_parents.owners.where(user_id: user).any?
end end
def has_master?(user) def has_master?(user)
members.masters.where(user_id: user).any? members_with_parents.masters.where(user_id: user).any?
end end
# Check if user is a last owner of the group.
# Parent owners are ignored for nested groups.
def last_owner?(user) def last_owner?(user)
has_owner?(user) && owners.size == 1 owners.include?(user) && owners.size == 1
end end
def avatar_type def avatar_type
...@@ -239,6 +241,14 @@ class Group < Namespace ...@@ -239,6 +241,14 @@ class Group < Namespace
end end
def refresh_members_authorized_projects def refresh_members_authorized_projects
UserProjectAccessChangedService.new(users.pluck(:id)).execute UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
end
def members_with_parents
GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id))
end
def users_with_parents
User.where(id: members_with_parents.select(:user_id))
end end
end end
...@@ -59,10 +59,6 @@ class Key < ActiveRecord::Base ...@@ -59,10 +59,6 @@ class Key < ActiveRecord::Base
) )
end end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook def post_create_hook
SystemHooksService.new.execute_hooks_for(self, :create) SystemHooksService.new.execute_hooks_for(self, :create)
end end
...@@ -88,4 +84,8 @@ class Key < ActiveRecord::Base ...@@ -88,4 +84,8 @@ class Key < ActiveRecord::Base
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
end
end end
...@@ -599,11 +599,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -599,11 +599,7 @@ class MergeRequest < ActiveRecord::Base
ext = Gitlab::ReferenceExtractor.new(project, current_user) ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description) ext.analyze(description)
issues = ext.issues ext.issues - closes_issues
closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description)
issues - closing_issues
end end
def target_project_path def target_project_path
......
...@@ -1036,7 +1036,7 @@ class Project < ActiveRecord::Base ...@@ -1036,7 +1036,7 @@ class Project < ActiveRecord::Base
Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present"
# we currently doesn't support renaming repository if it contains tags in container registry # we currently doesn't support renaming repository if it contains tags in container registry
raise Exception.new('Project cannot be renamed, because tags are present in its container registry') raise StandardError.new('Project cannot be renamed, because tags are present in its container registry')
end end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
...@@ -1063,7 +1063,7 @@ class Project < ActiveRecord::Base ...@@ -1063,7 +1063,7 @@ class Project < ActiveRecord::Base
# if we cannot move namespace directory we should rollback # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
raise Exception.new('repository cannot be renamed') raise StandardError.new('repository cannot be renamed')
end end
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
......
...@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy ...@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy
return unless @user return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
member = @subject.users.include?(@user) member = @subject.users_with_parents.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user) owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user) master = owner || @subject.has_master?(@user)
......
...@@ -261,7 +261,7 @@ class ProjectPolicy < BasePolicy ...@@ -261,7 +261,7 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user) def project_group_member?(user)
project.group && project.group &&
( (
project.group.members.exists?(user_id: user.id) || project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id) project.group.requesters.exists?(user_id: user.id)
) )
end end
......
...@@ -74,7 +74,7 @@ module Users ...@@ -74,7 +74,7 @@ module Users
# remove - The IDs of the authorization rows to remove. # remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]` # add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = []) def update_authorizations(remove = [], add = [])
return if remove.empty? && add.empty? return if remove.empty? && add.empty? && user.authorized_projects_populated
User.transaction do User.transaction do
user.remove_project_authorizations(remove) unless remove.empty? user.remove_project_authorizations(remove) unless remove.empty?
......
- page_title "Deploy Keys" - page_title "Deploy Keys"
.panel.panel-default.prepend-top-default
.panel-heading %h3.page-title.deploy-keys-title
Public deploy keys (#{@deploy_keys.count}) Public deploy keys (#{@deploy_keys.count})
.controls .pull-right
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
- if @deploy_keys.any?
.table-holder - if @deploy_keys.any?
%table.table .table-holder.deploy-keys-list
%thead.panel-heading %table.table
%thead
%tr
%th.col-sm-2 Title
%th.col-sm-4 Fingerprint
%th.col-sm-2 Write access allowed
%th.col-sm-2 Added at
%th.col-sm-2
%tbody
- @deploy_keys.each do |deploy_key|
%tr %tr
%th Title %td
%th Fingerprint %strong= deploy_key.title
%th Added at %td
%th %code.key-fingerprint= deploy_key.fingerprint
%tbody %td
- @deploy_keys.each do |deploy_key| - if deploy_key.can_push?
%tr Yes
%td - else
%strong= deploy_key.title No
%td %td
%code.key-fingerprint= deploy_key.fingerprint %span.cgray
%td added #{time_ago_with_tooltip(deploy_key.created_at)}
%span.cgray %td
added #{time_ago_with_tooltip(deploy_key.created_at)} = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right'
%td
= link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
...@@ -16,6 +16,14 @@ ...@@ -16,6 +16,14 @@
Paste a machine public key here. Read more about how to generate it Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README") = link_to "here", help_page_path("ssh/README")
= f.text_area :key, class: "form-control thin_area", rows: 5 = f.text_area :key, class: "form-control thin_area", rows: 5
.form-group
.control-label
.col-sm-10
= f.label :can_push do
= f.check_box :can_push
%strong Write access allowed
%p.light.append-bottom-0
Allow this key to push to repository as well? (Default only allows pull access.)
.form-actions .form-actions
= f.submit 'Create', class: "btn-create btn" = f.submit 'Create', class: "btn-create btn"
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area.adjust
.nav-text .nav-text
Protected branches can be managed in project settings Protected branches can be managed in project settings
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline .dropdown.inline
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light %span.light
= projects_sort_options_hash[@sort] = projects_sort_options_hash[@sort]
= icon('chevron-down') = icon('chevron-down')
......
...@@ -6,6 +6,9 @@ ...@@ -6,6 +6,9 @@
= deploy_key.title = deploy_key.title
.description .description
= deploy_key.fingerprint = deploy_key.fingerprint
- if deploy_key.can_push?
.write-access-allowed
Write access allowed
.deploy-key-content.prepend-left-default.deploy-key-projects .deploy-key-content.prepend-left-default.deploy-key-projects
- deploy_key.projects.each do |project| - deploy_key.projects.each do |project|
- if can?(current_user, :read_project, project) - if can?(current_user, :read_project, project)
......
...@@ -10,4 +10,13 @@ ...@@ -10,4 +10,13 @@
%p.light.append-bottom-0 %p.light.append-bottom-0
Paste a machine public key here. Read more about how to generate it Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README") = link_to "here", help_page_path("ssh/README")
.form-group
.checkbox
= f.label :can_push do
= f.check_box :can_push
%strong Write access allowed
.form-group
%p.light.append-bottom-0
Allow this key to push to repository as well? (Default only allows pull access.)
= f.submit "Add key", class: "btn-create btn" = f.submit "Add key", class: "btn-create btn"
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%span.pull-right %span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request) = link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr %hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request = render 'shared/issuable/form', f: f, issuable: @merge_request
= f.hidden_field :source_project_id = f.hidden_field :source_project_id
= f.hidden_field :source_branch = f.hidden_field :source_branch
......
...@@ -50,7 +50,8 @@ ...@@ -50,7 +50,8 @@
- if mr_issues_mentioned_but_not_closing.present? - if mr_issues_mentioned_but_not_closing.present?
#{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)} #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
!= markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
#{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed. #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed.
- if @merge_request.approvals.any? - if @merge_request.approvals.any?
.mr-widget-footer.approved-by-users .mr-widget-footer.approved-by-users
......
- can_resolve = @merge_request.conflicts_can_be_resolved_by?(current_user)
- can_resolve_in_ui = @merge_request.conflicts_can_be_resolved_in_ui?
- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
%h4.has-conflicts %h4.has-conflicts
= icon("exclamation-triangle") = icon("exclamation-triangle")
This merge request contains merge conflicts This merge request contains merge conflicts
%p %p
Please To merge this request, resolve these conflicts
- if @merge_request.conflicts_can_be_resolved_by?(current_user) - if can_resolve && !can_resolve_in_ui
- if @merge_request.conflicts_can_be_resolved_in_ui? locally
= link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- else
%span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"}
resolve these conflicts locally
- else
resolve these conflicts
or or
- unless can_merge
ask someone with write access to this repository to
merge it locally.
- if @merge_request.can_be_merged_via_command_line_by?(current_user) - if (can_resolve && can_resolve_in_ui) || can_merge
#{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. .btn-group
- else - if can_resolve && can_resolve_in_ui
ask someone with write access to this repository to merge this request manually. = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
- if can_merge
= link_to "Merge locally", "#modal_merge_info", class: "btn how_to_merge_link vlink", "data-toggle" => "modal"
- commit = @repository.commit(tag.dereferenced_target) - commit = @repository.commit(tag.dereferenced_target)
- release = @releases.find { |release| release.tag == tag.name } - release = @releases.find { |release| release.tag == tag.name }
%li %li.flex-row
%div .row-main-content.str-truncated
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%span.item-title %span.item-title
= icon('tag') = icon('tag')
...@@ -10,24 +10,25 @@ ...@@ -10,24 +10,25 @@
&nbsp; &nbsp;
= strip_gpg_signature(tag.message) = strip_gpg_signature(tag.message)
.controls - if commit
= render 'projects/buttons/download', project: @project, ref: tag.name .block-truncated
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
Cant find HEAD commit for this tag
- if release && release.description.present?
.description.prepend-top-default
.wiki
= preserve do
= markdown_field(release, :description)
- if can?(current_user, :push_code, @project) .row-fixed-content.controls
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do = render 'projects/buttons/download', project: @project, ref: tag.name
= icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :push_code, @project)
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
= icon("trash-o") = icon("pencil")
- if commit - if can?(current_user, :admin_project, @project)
= render 'projects/branches/commit', commit: commit, project: @project = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
- else = icon("trash-o")
%p
Cant find HEAD commit for this tag
- if release && release.description.present?
.description.prepend-top-default
.wiki
= preserve do
= markdown_field(release, :description)
...@@ -2,16 +2,16 @@ ...@@ -2,16 +2,16 @@
- page_title "Tags" - page_title "Tags"
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: container_class } %div.flex-list{ class: container_class }
.top-area .top-area.flex-row
.nav-text .nav-text.row-main-content
Tags give the ability to mark specific points in history as being important Tags give the ability to mark specific points in history as being important
.nav-controls .nav-controls.row-fixed-content
= form_tag(filter_tags_path, method: :get) do = form_tag(filter_tags_path, method: :get) do
= search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline .dropdown
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} } %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light %span.light
= projects_sort_options_hash[@sort] = projects_sort_options_hash[@sort]
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
.tags .tags
- if @tags.any? - if @tags.any?
%ul.content-list %ul.flex-list.content-list
= render partial: 'tag', collection: @tags = render partial: 'tag', collection: @tags
= paginate @tags, theme: 'gitlab' = paginate @tags, theme: 'gitlab'
......
...@@ -12,21 +12,23 @@ ...@@ -12,21 +12,23 @@
- else - else
Cant find HEAD commit for this tag Cant find HEAD commit for this tag
.nav-controls .nav-controls.controls-flex
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do
= icon("pencil") = icon("pencil")
= link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse files' do = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do
= icon('files-o') = icon('files-o')
= link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do
= icon('history') = icon('history')
= render 'projects/buttons/download', project: @project, ref: @tag.name .btn-container.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
.pull-right .btn-container.controls-item-full
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o %i.fa.fa-trash-o
- if @tag.message.present? - if @tag.message.present?
%pre.body %pre.wrap
= strip_gpg_signature(@tag.message) = strip_gpg_signature(@tag.message)
.append-bottom-default.prepend-top-default .append-bottom-default.prepend-top-default
......
---
title: Go to a project order
merge_request: 7737
author: Jacopo Beschi @jacopo-beschi
---
title: Prevent empty pagination when list is not empty
merge_request: 8172
author:
---
title: Improve visibility of "Resolve conflicts" and "Merge locally" actions
merge_request: 8229
author:
---
title: ensure permalinks scroll to correct position on multiple clicks
merge_request: 8046
author:
---
title: fix border in login session tabs
merge_request: 8346
author:
--- ---
title: Fixed GFM autocomplete error when no data exists title: Change status colors of runners to better defaults
merge_request: merge_request:
author: author:
---
title: Fix mr list timestamp alignment
merge_request: 8271
author:
---
title: Fix discussion overlap text in regular screens
merge_request: 8273
author:
---
title: Fix timeout when MR contains large files marked as binary by .gitattributes
merge_request:
author:
---
title: Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari
merge_request: 8282
author:
---
title: Fix line breaking in nodes of the pipeline graph in firefox
merge_request: 8292
author:
---
title: Fixes confendential warning text alignment
merge_request: 8293
author:
---
title: Hide Scroll Top button for failed build page
merge_request: 8295
author:
---
title: Cache project authorizations even when user has access to zero projects
merge_request: 8327
author:
---
title: Make CTRL+Enter submits a new merge request
merge_request: 8360
author: Saad Shahd
---
title: Fixes issue boards list colored top border visual glitch
merge_request: 7898
author: Pier Paolo Ramon
---
title: Disable PostgreSQL statement timeouts when removing unneeded services
merge_request: 8322
author:
---
title: Fix 500 error when visit group from admin area if group name contains dot
merge_request:
author:
---
title: Rename users with namespace ending with .git
merge_request: 8309
author:
---
title: Rename projects wth reserved names
merge_request: 8234
author:
---
title: Allow to add deploy keys with write-access
merge_request: 5807
author: Ali Ibrahim
---
title: Fix a Grape deprecation, use `#request_method` instead of `#route_method`
merge_request:
author:
---
title: Fix finding the latest pipeline
merge_request: 8301
author:
---
title: Darkened hr border color in descriptions because of update of bootstrap
merge_request: 8333
author:
---
title: Fix a minor grammar error in merge request widget
merge_request: 8337
author:
--- ---
title: Rename "autodeploy" to "auto deploy" title: Fixed GFM dropdown not showing on new lines
merge_request: merge_request:
author: author:
---
title: change 'gray' color theme name to 'black' to match the actual color
merge_request: 7908
author: BM5k
---
title: Fix unclear closing issue behaviour on Merge Request show page
merge_request: 8345
author: Gabriel Gizotti
---
title: About GitLab link in sidebar that links to help page
merge_request: 8316
author:
...@@ -36,7 +36,7 @@ namespace :admin do ...@@ -36,7 +36,7 @@ namespace :admin do
scope(path: 'groups/*id', scope(path: 'groups/*id',
controller: :groups, controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex }) do constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
scope(as: :group) do scope(as: :group) do
put :members_update put :members_update
......
class AddCanPushToKeys < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_column_with_default(:keys, :can_push, :boolean, default: false, allow_null: false)
end
def down
remove_column(:keys, :can_push)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveDotGitFromUsernames < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
include Gitlab::ShellAdapter
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
invalid_users.each do |user|
id = user['id']
namespace_id = user['namespace_id']
path_was = user['username']
path_was_wildcard = quote_string("#{path_was}/%")
path = quote_string(rename_path(path_was))
move_namespace(namespace_id, path_was, path)
execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}"
execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}"
execute "UPDATE users SET username = '#{path}' WHERE id = #{id}"
select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
new_path = "#{path}/#{route['path'].split('/').last}"
execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
end
end
end
def down
# nothing to do here
end
private
def invalid_users
select_all("SELECT u.id, u.username, n.path AS namespace_path, n.id AS namespace_id FROM users u
INNER JOIN namespaces n ON n.owner_id = u.id
WHERE n.type is NULL AND n.path LIKE '%.git'")
end
def route_exists?(path)
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end
# Accepts invalid path like test.git and returns test_git or
# test_git1 if test_git already taken
def rename_path(path)
# To stay closer with original name and reduce risk of duplicates
# we rename suffix instead of removing it
path = path.sub(/\.git\z/, '_git')
counter = 0
base = path
while route_exists?(path)
counter += 1
path = "#{base}#{counter}"
end
path
end
def move_namespace(namespace_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
Gitlab.config.repositories.storages[row['repository_storage']]
end.compact
# Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('namespace directory cannot be moved')
end
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
end
end
class RenameSlackAndMattermostNotificationServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
update_column_in_batches(:services, :type, 'SlackService') do |table, query|
query.where(table[:type].eq('SlackNotificationService'))
end
update_column_in_batches(:services, :type, 'MattermostService') do |table, query|
query.where(table[:type].eq('MattermostNotificationService'))
end
end
def down
update_column_in_batches(:services, :type, 'SlackNotificationService') do |table, query|
query.where(table[:type].eq('SlackService'))
end
update_column_in_batches(:services, :type, 'MattermostNotificationService') do |table, query|
query.where(table[:type].eq('MattermostService'))
end
end
end
require 'thread'
class RenameReservedProjectNames < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
include Gitlab::ShellAdapter
DOWNTIME = false
THREAD_COUNT = 8
KNOWN_PATHS = %w(.well-known
all
assets
blame
blob
commits
create
create_dir
edit
files
files
find_file
groups
hooks
issues
logs_tree
merge_requests
new
new
preview
profile
projects
public
raw
repository
robots.txt
s
snippets
teams
tree
u
unsubscribes
update
users
wikis)
def up
queues = Array.new(THREAD_COUNT) { Queue.new }
start = false
threads = Array.new(THREAD_COUNT) do |index|
Thread.new do
queue = queues[index]
# Wait until we have input to process.
until start; end
rename_projects(queue.pop) until queue.empty?
end
end
enum = queues.each
reserved_projects.each_slice(100) do |slice|
begin
queue = enum.next
rescue StopIteration
enum.rewind
retry
end
queue << slice
end
start = true
threads.each(&:join)
end
def down
# nothing to do here
end
private
def reserved_projects
Project.unscoped.
includes(:namespace).
where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)').
where('projects.path' => KNOWN_PATHS)
end
def route_exists?(full_path)
quoted_path = ActiveRecord::Base.connection.quote_string(full_path)
ActiveRecord::Base.connection.
select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present?
end
# Adds number to the end of the path that is not taken by other route
def rename_path(namespace_path, path_was)
counter = 0
path = "#{path_was}#{counter}"
while route_exists?("#{namespace_path}/#{path}")
counter += 1
path = "#{path_was}#{counter}"
end
path
end
def rename_projects(projects)
projects.each do |project|
id = project.id
path_was = project.path
namespace_path = project.namespace.path
path = rename_path(namespace_path, path_was)
begin
# Because project path update is quite complex operation we can't safely
# copy-paste all code from GitLab. As exception we use Rails code here
project.rename_repo if rename_project_row(project, path)
rescue Exception => e # rubocop: disable Lint/RescueException
Rails.logger.error "Exception when renaming project #{id}: #{e.message}"
end
end
end
def rename_project_row(project, path)
project.respond_to?(:update_attributes) &&
project.update_attributes(path: path) &&
project.respond_to?(:rename_repo)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161221140236) do ActiveRecord::Schema.define(version: 20161227192806) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -601,6 +601,7 @@ ActiveRecord::Schema.define(version: 20161221140236) do ...@@ -601,6 +601,7 @@ ActiveRecord::Schema.define(version: 20161221140236) do
t.string "type" t.string "type"
t.string "fingerprint" t.string "fingerprint"
t.boolean "public", default: false, null: false t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false
end end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
...@@ -1509,4 +1510,4 @@ ActiveRecord::Schema.define(version: 20161221140236) do ...@@ -1509,4 +1510,4 @@ ActiveRecord::Schema.define(version: 20161221140236) do
add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users" add_foreign_key "u2f_registrations", "users"
end end
\ No newline at end of file
...@@ -20,12 +20,14 @@ Example response: ...@@ -20,12 +20,14 @@ Example response:
"id": 1, "id": 1,
"title": "Public key", "title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T10:12:29Z" "created_at": "2013-10-02T10:12:29Z"
}, },
{ {
"id": 3, "id": 3,
"title": "Another Public key", "title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": true,
"created_at": "2013-10-02T11:12:29Z" "created_at": "2013-10-02T11:12:29Z"
} }
] ]
...@@ -55,12 +57,14 @@ Example response: ...@@ -55,12 +57,14 @@ Example response:
"id": 1, "id": 1,
"title": "Public key", "title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T10:12:29Z" "created_at": "2013-10-02T10:12:29Z"
}, },
{ {
"id": 3, "id": 3,
"title": "Another Public key", "title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T11:12:29Z" "created_at": "2013-10-02T11:12:29Z"
} }
] ]
...@@ -92,6 +96,7 @@ Example response: ...@@ -92,6 +96,7 @@ Example response:
"id": 1, "id": 1,
"title": "Public key", "title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T10:12:29Z" "created_at": "2013-10-02T10:12:29Z"
} }
``` ```
...@@ -107,14 +112,15 @@ project only if original one was is accessible by the same user. ...@@ -107,14 +112,15 @@ project only if original one was is accessible by the same user.
POST /projects/:id/deploy_keys POST /projects/:id/deploy_keys
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project | | `id` | integer | yes | The ID of the project |
| `title` | string | yes | New deploy key's title | | `title` | string | yes | New deploy key's title |
| `key` | string | yes | New deploy key | | `key` | string | yes | New deploy key |
| `can_push` | boolean | no | Can deploy key push to the project's repository |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/" curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA...", "can_push": "true"}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
``` ```
Example response: Example response:
...@@ -124,6 +130,7 @@ Example response: ...@@ -124,6 +130,7 @@ Example response:
"key" : "ssh-rsa AAAA...", "key" : "ssh-rsa AAAA...",
"id" : 12, "id" : 12,
"title" : "My deploy key", "title" : "My deploy key",
"can_push": true,
"created_at" : "2015-08-29T12:44:31.550Z" "created_at" : "2015-08-29T12:44:31.550Z"
} }
``` ```
......
...@@ -3,12 +3,15 @@ ...@@ -3,12 +3,15 @@
## Log arguments to Sidekiq jobs ## Log arguments to Sidekiq jobs
If you want to see what arguments are being passed to Sidekiq jobs you can set If you want to see what arguments are being passed to Sidekiq jobs you can set
the SIDEKIQ_LOG_ARGUMENTS environment variable. the `SIDEKIQ_LOG_ARGUMENTS` [environment variable]
(https://docs.gitlab.com/omnibus/settings/environment-variables.html) to `1` (true).
Example:
``` ```
SIDEKIQ_LOG_ARGUMENTS=1 bundle exec foreman start gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"}
``` ```
It is not recommend to enable this setting in production because some Sidekiq Please note: It is not recommend to enable this setting in production because some
jobs (such as sending a password reset email) take secret arguments (for Sidekiq jobs (such as sending a password reset email) take secret arguments (for
example the password reset token). example the password reset token).
\ No newline at end of file
...@@ -39,4 +39,19 @@ When information is updating in place, a quick, subtle animation is needed. The ...@@ -39,4 +39,19 @@ When information is updating in place, a quick, subtle animation is needed. The
![Quick update animation](img/animation-quickupdate.gif) ![Quick update animation](img/animation-quickupdate.gif)
> TODO: Add guidance for other kinds of animation ### Moving transitions
\ No newline at end of file
When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`.
#### Shifting elements on reorder
An example of a moving transition is when elements have to move out of the way when you drag an element.
View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here.
![Reorder animation](img/animation-reorder.gif)
#### Autoscroll the page
Another example of a moving transition is when you have to autoscroll the page to keep an active element visible.
View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here.
![Autoscroll animation](img/animation-autoscroll.gif)
\ No newline at end of file
...@@ -50,13 +50,13 @@ GitLab uses Font Awesome icons throughout our interface. ...@@ -50,13 +50,13 @@ GitLab uses Font Awesome icons throughout our interface.
## Color ## Color
| | | | | State | Action |
| :------: | :------- | | :------: | :------- | :------- |
| ![Blue](img/color-blue.png) | Blue is used to highlight primary active elements (such as the current tab), as well as other organizational and managing commands.| | ![Blue](img/color-blue.png) | Primary and active (such as the current tab) | Organizational, managing, and retry commands|
| ![Green](img/color-green.png) | Green is for actions that create new objects. | | ![Green](img/color-green.png) | Opened | Create new objects |
| ![Orange](img/color-orange.png) | Orange is used for warnings | | ![Orange](img/color-orange.png) | Warning | Non destructive action |
| ![Red](img/color-red.png) | Red is reserved for delete and other destructive commands | | ![Red](img/color-red.png) | Closed | Delete and other destructive commands |
| ![Grey](img/color-grey.png) | Grey is used for neutral secondary elements. Depending on context, white is sometimes used instead. | | ![Grey](img/color-grey.png) | Neutral | Neutral secondary commands |
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
......
@admin
Feature: Admin Deploy Keys
Background:
Given I sign in as an admin
And there are public deploy keys in system
Scenario: Deploy Keys list
When I visit admin deploy keys page
Then I should see all public deploy keys
Scenario: Deploy Keys new
When I visit admin deploy keys page
And I click 'New Deploy Key'
And I submit new deploy key
Then I should be on admin deploy keys page
And I should see newly created deploy key without write access
Scenario: Deploy Keys new with write access
When I visit admin deploy keys page
And I click 'New Deploy Key'
And I submit new deploy key with write access
Then I should be on admin deploy keys page
And I should see newly created deploy key with write access
class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedAdmin
step 'there are public deploy keys in system' do
create(:deploy_key, public: true)
create(:another_deploy_key, public: true)
end
step 'I should see all public deploy keys' do
DeployKey.are_public.each do |p|
expect(page).to have_content p.title
end
end
step 'I visit admin deploy key page' do
visit admin_deploy_key_path(deploy_key)
end
step 'I visit admin deploy keys page' do
visit admin_deploy_keys_path
end
step 'I click \'New Deploy Key\'' do
click_link 'New Deploy Key'
end
step 'I submit new deploy key' do
fill_in "deploy_key_title", with: "laptop"
fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
click_button "Create"
end
step 'I submit new deploy key with write access' do
fill_in "deploy_key_title", with: "server"
fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
check "deploy_key_can_push"
click_button "Create"
end
step 'I should be on admin deploy keys page' do
expect(current_path).to eq admin_deploy_keys_path
end
step 'I should see newly created deploy key without write access' do
expect(page).to have_content(deploy_key.title)
expect(page).to have_content('No')
end
step 'I should see newly created deploy key with write access' do
expect(page).to have_content(deploy_key.title)
expect(page).to have_content('Yes')
end
def deploy_key
@deploy_key ||= DeployKey.are_public.first
end
end
...@@ -356,7 +356,7 @@ module API ...@@ -356,7 +356,7 @@ module API
end end
class SSHKey < Grape::Entity class SSHKey < Grape::Entity
expose :id, :title, :key, :created_at expose :id, :title, :key, :created_at, :can_push
end end
class SSHKeyWithUser < SSHKey class SSHKeyWithUser < SSHKey
......
...@@ -96,7 +96,7 @@ module API ...@@ -96,7 +96,7 @@ module API
end end
def authenticate_non_get! def authenticate_non_get!
authenticate! unless %w[GET HEAD].include?(route.route_method) authenticate! unless %w[GET HEAD].include?(route.request_method)
end end
def authenticate_by_gitlab_shell_token! def authenticate_by_gitlab_shell_token!
......
...@@ -69,8 +69,6 @@ module API ...@@ -69,8 +69,6 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note' optional :created_at, type: String, desc: 'The creation date of the note'
end end
post ":id/#{noteables_str}/:noteable_id/notes" do post ":id/#{noteables_str}/:noteable_id/notes" do
required_attributes! [:body]
opts = { opts = {
note: params[:body], note: params[:body],
noteable_type: noteables_str.classify, noteable_type: noteables_str.classify,
......
...@@ -87,6 +87,7 @@ module API ...@@ -87,6 +87,7 @@ module API
given sentry_enabled: ->(val) { val } do given sentry_enabled: ->(val) { val } do
requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name' requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
end end
optional :repository_storage, type: String, desc: 'Storage paths for new projects'
optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
optional :koding_enabled, type: Boolean, desc: 'Enable Koding' optional :koding_enabled, type: Boolean, desc: 'Enable Koding'
given koding_enabled: ->(val) { val } do given koding_enabled: ->(val) { val } do
......
...@@ -9,8 +9,7 @@ module Gitlab ...@@ -9,8 +9,7 @@ module Gitlab
def lfs_deploy_token?(for_project) def lfs_deploy_token?(for_project)
type == :lfs_deploy_token && type == :lfs_deploy_token &&
actor && actor.try(:has_access_to?, for_project)
actor.projects.include?(for_project)
end end
def success? def success?
......
...@@ -2,14 +2,17 @@ module Gitlab ...@@ -2,14 +2,17 @@ module Gitlab
module Checks module Checks
class ChangeAccess class ChangeAccess
include PathLocksHelper include PathLocksHelper
attr_reader :user_access, :project
def initialize(change, user_access:, project:, env: {}) attr_reader :user_access, :project, :skip_authorization
def initialize(
change, user_access:, project:, env: {}, skip_authorization: false)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref) @branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access @user_access = user_access
@project = project @project = project
@env = env @env = env
@skip_authorization = skip_authorization
end end
def exec def exec
...@@ -25,6 +28,7 @@ module Gitlab ...@@ -25,6 +28,7 @@ module Gitlab
protected protected
def protected_branch_checks def protected_branch_checks
return if skip_authorization
return unless @branch_name return unless @branch_name
return unless project.protected_branch?(@branch_name) return unless project.protected_branch?(@branch_name)
...@@ -50,6 +54,8 @@ module Gitlab ...@@ -50,6 +54,8 @@ module Gitlab
end end
def tag_checks def tag_checks
return if skip_authorization
tag_ref = Gitlab::Git.tag_name(@ref) tag_ref = Gitlab::Git.tag_name(@ref)
if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project) if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
...@@ -58,6 +64,8 @@ module Gitlab ...@@ -58,6 +64,8 @@ module Gitlab
end end
def push_checks def push_checks
return if skip_authorization
if user_access.cannot_do_action?(:push_code) if user_access.cannot_do_action?(:push_code)
"You are not allowed to push code to this project." "You are not allowed to push code to this project."
end end
......
...@@ -13,9 +13,17 @@ module Gitlab ...@@ -13,9 +13,17 @@ module Gitlab
encoding = body.encoding encoding = body.encoding
body = discourse_email_trimmer(body) body = EmailReplyTrimmer.trim(body)
body = EmailReplyParser.parse_reply(body) return '' unless body
# not using /\s+$/ here because that deletes empty lines
body = body.gsub(/[ \t]$/, '')
# NOTE: We currently don't support empty quotes.
# EmailReplyTrimmer allows this as a special case,
# so we detect it manually here.
return "" if body.lines.all? { |l| l.strip.empty? || l.start_with?('>') }
body.force_encoding(encoding).encode("UTF-8") body.force_encoding(encoding).encode("UTF-8")
end end
...@@ -57,30 +65,6 @@ module Gitlab ...@@ -57,30 +65,6 @@ module Gitlab
rescue rescue
nil nil
end end
REPLYING_HEADER_LABELS = %w(From Sent To Subject Reply To Cc Bcc Date)
REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |label| "#{label}:" })
def discourse_email_trimmer(body)
lines = body.scrub.lines.to_a
range_end = 0
lines.each_with_index do |l, idx|
# This one might be controversial but so many reply lines have years, times and end with a colon.
# Let's try it and see how well it works.
break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
(l =~ /On \w+ \d+,? \d+,?.*wrote:/)
# Headers on subsequent lines
break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
# Headers on the same line
break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3
range_end = idx
end
lines[0..range_end].join.strip
end
end end
end end
end end
...@@ -8,7 +8,8 @@ module Gitlab ...@@ -8,7 +8,8 @@ module Gitlab
ERROR_MESSAGES = { ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.', upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.', download: 'You are not allowed to download code from this project.',
deploy_key: 'Deploy keys are not allowed to push code.', deploy_key_upload:
'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.' no_repo: 'A repository for this project does not exist yet.'
} }
...@@ -33,14 +34,15 @@ module Gitlab ...@@ -33,14 +34,15 @@ module Gitlab
check_active_user! check_active_user!
check_project_accessibility! check_project_accessibility!
check_command_existence!(cmd) check_command_existence!(cmd)
check_repository_existence!
check_geo_license! check_geo_license!
case cmd case cmd
when *DOWNLOAD_COMMANDS when *DOWNLOAD_COMMANDS
download_access_check check_download_access!
when *PUSH_COMMANDS when *PUSH_COMMANDS
push_access_check(changes) check_push_access!(changes)
when *GIT_ANNEX_COMMANDS when *GIT_ANNEX_COMMANDS
git_annex_access_check(project, changes) git_annex_access_check(project, changes)
end end
...@@ -50,63 +52,96 @@ module Gitlab ...@@ -50,63 +52,96 @@ module Gitlab
build_status_object(false, ex.message) build_status_object(false, ex.message)
end end
def download_access_check def guest_can_download_code?
if user Guest.can?(:download_code, project)
user_download_access_check
elsif deploy_key.nil? && geo_node_key.nil? && !guest_can_downlod_code?
raise UnauthorizedError, ERROR_MESSAGES[:download]
end
end end
def push_access_check(changes) def user_can_download_code?
if project.repository_read_only? authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
raise UnauthorizedError, 'The repository is temporarily read-only. Please try again later.' end
end
if Gitlab::Geo.secondary? def build_can_download_code?
raise UnauthorizedError, "You can't push code on a secondary GitLab Geo node." authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
end
def protocol_allowed?
Gitlab::ProtocolAccess.allowed?(protocol)
end
private
def check_protocol!
unless protocol_allowed?
raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
end end
end
return if git_annex_branch_sync?(changes) def check_active_user!
return if deploy_key? || geo_node_key?
if user if user && !user_access.allowed?
user_push_access_check(changes) raise UnauthorizedError, "Your account has been blocked."
else
raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
end end
end end
def guest_can_downlod_code? def check_project_accessibility!
Guest.can?(:download_code, project) if project.blank? || !can_read_project?
raise UnauthorizedError, 'The project you were looking for could not be found.'
end
end end
def user_download_access_check def check_command_existence!(cmd)
unless user_can_download_code? || build_can_download_code? unless ALL_COMMANDS.include?(cmd)
raise UnauthorizedError, ERROR_MESSAGES[:download] raise UnauthorizedError, "The command you're trying to execute is not allowed."
end end
end end
def user_can_download_code? def check_geo_license!
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code) if Gitlab::Geo.secondary? && !Gitlab::Geo.license_allows?
raise UnauthorizedError, 'Your current license does not have GitLab Geo add-on enabled.'
end
end end
def build_can_download_code? def check_repository_existence!
authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code) unless project.repository.exists?
raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
end
end end
def user_push_access_check(changes) def check_download_access!
unless authentication_abilities.include?(:push_code) return if deploy_key? || geo_node_key?
raise UnauthorizedError, ERROR_MESSAGES[:upload]
passed = user_can_download_code? ||
build_can_download_code? ||
guest_can_download_code?
unless passed
raise UnauthorizedError, ERROR_MESSAGES[:download]
end end
end
if changes.blank? # TODO: please clean this up
return # Allow access. def check_push_access!(changes)
if project.repository_read_only?
raise UnauthorizedError, 'The repository is temporarily read-only. Please try again later.'
end end
unless project.repository.exists? if Gitlab::Geo.secondary?
raise UnauthorizedError, ERROR_MESSAGES[:no_repo] raise UnauthorizedError, "You can't push code on a secondary GitLab Geo node."
end end
return if git_annex_branch_sync?(changes)
if deploy_key
check_deploy_key_push_access!
elsif user
check_user_push_access!
else
raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
return if changes.blank? # Allow access.
if project.above_size_limit? if project.above_size_limit?
raise UnauthorizedError, Gitlab::RepositorySizeError.new(project).push_error raise UnauthorizedError, Gitlab::RepositorySizeError.new(project).push_error
end end
...@@ -116,13 +151,30 @@ module Gitlab ...@@ -116,13 +151,30 @@ module Gitlab
raise UnauthorizedError, message raise UnauthorizedError, message
end end
check_change_access!(changes)
end
def check_user_push_access!
unless authentication_abilities.include?(:push_code)
raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
end
def check_deploy_key_push_access!
unless deploy_key.can_push_to?(project)
raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
end
end
def check_change_access!(changes)
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
push_size_in_bytes = 0 push_size_in_bytes = 0
# Iterate over all changes to find if user allowed all of them to be applied # Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change| changes_list.each do |change|
status = change_access_check(change) status = check_single_change_access(change)
unless status.allowed? unless status.allowed?
# If user does not have access to make at least one change - cancel all push # If user does not have access to make at least one change - cancel all push
raise UnauthorizedError, status.message raise UnauthorizedError, status.message
...@@ -138,97 +190,39 @@ module Gitlab ...@@ -138,97 +190,39 @@ module Gitlab
end end
end end
def change_access_check(change) def check_single_change_access(change)
Checks::ChangeAccess.new(change, user_access: user_access, project: project, env: @env).exec Checks::ChangeAccess.new(
end change,
user_access: user_access,
def protocol_allowed? project: project,
Gitlab::ProtocolAccess.allowed?(protocol) env: @env,
end skip_authorization: deploy_key?).exec
private
def check_protocol!
unless protocol_allowed?
raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
end
end
def check_active_user!
if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked."
end
end
def check_project_accessibility!
if project.blank? || !can_read_project?
raise UnauthorizedError, 'The project you were looking for could not be found.'
end
end end
def check_command_existence!(cmd) def deploy_key
unless ALL_COMMANDS.include?(cmd) actor if deploy_key?
raise UnauthorizedError, "The command you're trying to execute is not allowed."
end
end
def check_geo_license!
if Gitlab::Geo.secondary? && !Gitlab::Geo.license_allows?
raise UnauthorizedError, 'Your current license does not have GitLab Geo add-on enabled.'
end
end
def matching_merge_request?(newrev, branch_name)
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
def protected_branch_action(oldrev, newrev, branch_name)
# we dont allow force push to protected branch
if forced_push?(oldrev, newrev)
:force_push_code_to_protected_branches
elsif Gitlab::Git.blank_ref?(newrev)
# and we dont allow remove of protected branch
:remove_protected_branches
elsif matching_merge_request?(newrev, branch_name) && project.developers_can_merge_to_protected_branch?(branch_name)
:push_code
elsif project.developers_can_push_to_protected_branch?(branch_name)
:push_code
else
:push_code_to_protected_branches
end
end
def protected_tag?(tag_name)
project.repository.tag_exists?(tag_name)
end end
def deploy_key def deploy_key?
actor if actor.is_a?(DeployKey) actor.is_a?(DeployKey)
end end
def geo_node_key def geo_node_key
actor if actor.is_a?(GeoNodeKey) actor if geo_node_key?
end end
def deploy_key_can_read_project? def geo_node_key?
if deploy_key actor.is_a?(GeoNodeKey)
return true if project.public?
deploy_key.projects.include?(project)
else
false
end
end end
def can_read_project? def can_read_project?
if user if deploy_key?
user_access.can_read_project? deploy_key.has_access_to?(project)
elsif deploy_key elsif geo_node_key?
deploy_key_can_read_project?
elsif geo_node_key
true true
else elsif user
Guest.can?(:read_project, project) user.can?(:read_project, project)
end end || Guest.can?(:read_project, project)
end end
protected protected
...@@ -260,10 +254,6 @@ module Gitlab ...@@ -260,10 +254,6 @@ module Gitlab
raise UnauthorizedError, "You don't have access" raise UnauthorizedError, "You don't have access"
end end
unless project.repository.exists?
raise UnauthorizedError, "Repository does not exist"
end
if Gitlab::Geo.enabled? && Gitlab::Geo.secondary? if Gitlab::Geo.enabled? && Gitlab::Geo.secondary?
raise UnauthorizedError, "You can't use git-annex with a secondary GitLab Geo node." raise UnauthorizedError, "You can't use git-annex with a secondary GitLab Geo node."
end end
...@@ -290,16 +280,5 @@ module Gitlab ...@@ -290,16 +280,5 @@ module Gitlab
true true
end end
def commit_from_annex_sync?(commit_message)
return false unless Gitlab.config.gitlab_shell.git_annex_enabled
# Commit message starting with <git-annex in > so avoid push rules on this
commit_message.start_with?('git-annex in')
end
def old_commit?(commit)
commit.refs(project.repository).any?
end
end end
end end
module Gitlab module Gitlab
class GitAccessWiki < GitAccess class GitAccessWiki < GitAccess
def guest_can_downlod_code? def guest_can_download_code?
Guest.can?(:download_wiki_code, project) Guest.can?(:download_wiki_code, project)
end end
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code) authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end end
def change_access_check(change) def check_single_change_access(change)
if Gitlab::Geo.enabled? && Gitlab::Geo.secondary? if Gitlab::Geo.enabled? && Gitlab::Geo.secondary?
build_status_object(false, "You can't push code to a secondary GitLab Geo node.") build_status_object(false, "You can't push code to a secondary GitLab Geo node.")
elsif user_access.can_do_action?(:create_wiki) elsif user_access.can_do_action?(:create_wiki)
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
Theme.new(1, 'Graphite', 'ui_graphite'), Theme.new(1, 'Graphite', 'ui_graphite'),
Theme.new(2, 'Charcoal', 'ui_charcoal'), Theme.new(2, 'Charcoal', 'ui_charcoal'),
Theme.new(3, 'Green', 'ui_green'), Theme.new(3, 'Green', 'ui_green'),
Theme.new(4, 'Gray', 'ui_gray'), Theme.new(4, 'Black', 'ui_black'),
Theme.new(5, 'Violet', 'ui_violet'), Theme.new(5, 'Violet', 'ui_violet'),
Theme.new(6, 'Blue', 'ui_blue') Theme.new(6, 'Blue', 'ui_blue')
].freeze ].freeze
......
...@@ -8,6 +8,8 @@ module Gitlab ...@@ -8,6 +8,8 @@ module Gitlab
end end
def can_do_action?(action) def can_do_action?(action)
return false if no_user_or_blocked?
@permission_cache ||= {} @permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project) @permission_cache[action] ||= user.can?(action, project)
end end
...@@ -17,7 +19,7 @@ module Gitlab ...@@ -17,7 +19,7 @@ module Gitlab
end end
def allowed? def allowed?
return false if user.blank? || user.blocked? return false if no_user_or_blocked?
if user.requires_ldap_check? && user.try_obtain_ldap_lease if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user) return false unless Gitlab::LDAP::Access.allowed?(user)
...@@ -27,7 +29,7 @@ module Gitlab ...@@ -27,7 +29,7 @@ module Gitlab
end end
def can_push_to_branch?(ref) def can_push_to_branch?(ref)
return false unless user return false if no_user_or_blocked?
if project.protected_branch?(ref) if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
...@@ -40,7 +42,7 @@ module Gitlab ...@@ -40,7 +42,7 @@ module Gitlab
end end
def can_merge_to_branch?(ref) def can_merge_to_branch?(ref)
return false unless user return false if no_user_or_blocked?
if project.protected_branch?(ref) if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
...@@ -51,9 +53,15 @@ module Gitlab ...@@ -51,9 +53,15 @@ module Gitlab
end end
def can_read_project? def can_read_project?
return false unless user return false if no_user_or_blocked?
user.can?(:read_project, project) user.can?(:read_project, project)
end end
private
def no_user_or_blocked?
user.nil? || user.blocked?
end
end end
end end
require 'spec_helper'
describe Dashboard::TodosController do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:todo_service) { TodoService.new }
describe 'GET #index' do
before do
sign_in(user)
project.team << [user, :developer]
end
context 'when using pagination' do
let(:last_page) { user.todos.page().total_pages }
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
it 'redirects to last_page if page number is larger than number of pages' do
get :index, page: (last_page + 1).to_param
expect(response).to redirect_to(dashboard_todos_path(page: last_page))
end
it 'redirects to correspondent page' do
get :index, page: last_page
expect(assigns(:todos).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
end
end
end
...@@ -52,6 +52,36 @@ describe Projects::IssuesController do ...@@ -52,6 +52,36 @@ describe Projects::IssuesController do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
context 'with page param' do
let(:last_page) { project.issues.page().total_pages }
let!(:issue_list) { create_list(:issue, 2, project: project) }
before do
sign_in(user)
project.team << [user, :developer]
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
it 'redirects to last_page if page number is larger than number of pages' do
get :index,
namespace_id: project.namespace.path.to_param,
project_id: project.path.to_param,
page: (last_page + 1).to_param
expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
it 'redirects to specified page' do
get :index,
namespace_id: project.namespace.path.to_param,
project_id: project.path.to_param,
page: last_page.to_param
expect(assigns(:issues).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
end
end end
describe 'GET #new' do describe 'GET #new' do
......
...@@ -213,11 +213,29 @@ describe Projects::MergeRequestsController do ...@@ -213,11 +213,29 @@ describe Projects::MergeRequestsController do
end end
describe 'GET index' do describe 'GET index' do
def get_merge_requests def get_merge_requests(page = nil)
get :index, get :index,
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project.to_param, project_id: project.to_param,
state: 'opened' state: 'opened', page: page.to_param
end
context 'when page param' do
let(:last_page) { project.merge_requests.page().total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
it 'redirects to last_page if page number is larger than number of pages' do
get_merge_requests(last_page + 1)
expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
it 'redirects to specified page' do
get_merge_requests(last_page)
expect(assigns(:merge_requests).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
end end
context 'when filtering by opened state' do context 'when filtering by opened state' do
......
...@@ -11,6 +11,28 @@ describe Projects::SnippetsController do ...@@ -11,6 +11,28 @@ describe Projects::SnippetsController do
end end
describe 'GET #index' do describe 'GET #index' do
context 'when page param' do
let(:last_page) { project.snippets.page().total_pages }
let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) }
it 'redirects to last_page if page number is larger than number of pages' do
get :index,
namespace_id: project.namespace.path,
project_id: project.path, page: (last_page + 1).to_param
expect(response).to redirect_to(namespace_project_snippets_path(page: last_page))
end
it 'redirects to specified page' do
get :index,
namespace_id: project.namespace.path,
project_id: project.path, page: last_page.to_param
expect(assigns(:snippets).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
end
context 'when the project snippet is private' do context 'when the project snippet is private' do
let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
......
...@@ -17,6 +17,16 @@ feature 'Admin Groups', feature: true do ...@@ -17,6 +17,16 @@ feature 'Admin Groups', feature: true do
end end
end end
describe 'show a group' do
scenario 'shows the group' do
group = create(:group, :private)
visit admin_group_path(group)
expect(page).to have_content("Group: #{group.name}")
end
end
describe 'group edit' do describe 'group edit' do
scenario 'shows the visibility level radio populated with the group visibility_level value' do scenario 'shows the visibility level radio populated with the group visibility_level value' do
group = create(:group, :private) group = create(:group, :private)
......
...@@ -47,6 +47,18 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -47,6 +47,18 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(true, label_item, note, label.title) expect_to_wrap(true, label_item, note, label.title)
end end
it "shows dropdown after a new line" do
note = find('#note_note')
page.within '.timeline-content-form' do
note.native.send_keys('test')
note.native.send_keys(:enter)
note.native.send_keys(:enter)
note.native.send_keys('@')
end
expect(page).to have_selector('.atwho-container')
end
it "does not show dropdown when preceded with a special character" do it "does not show dropdown when preceded with a special character" do
note = find('#note_note') note = find('#note_note')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
......
...@@ -41,7 +41,7 @@ feature 'Merge Request closing issues message', feature: true do ...@@ -41,7 +41,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.") expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
end end
end end
...@@ -49,7 +49,7 @@ feature 'Merge Request closing issues message', feature: true do ...@@ -49,7 +49,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.") expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
end end
end end
end end
require 'rails_helper' require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do describe 'Milestone draggable', feature: true, js: true do
include WaitForAjax
let(:milestone) { create(:milestone, project: project, title: 8.14) } let(:milestone) { create(:milestone, project: project, title: 8.14) }
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -74,6 +76,8 @@ describe 'Milestone draggable', feature: true, js: true do ...@@ -74,6 +76,8 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone) visit namespace_project_milestone_path(project.namespace, project, milestone)
issue.drag_to(issue_target) issue.drag_to(issue_target)
wait_for_ajax
end end
def create_and_drag_merge_request(params = {}) def create_and_drag_merge_request(params = {})
...@@ -82,5 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do ...@@ -82,5 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone) visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click page.find("a[href='#tab-merge-requests']").click
merge_request.drag_to(merge_request_target) merge_request.drag_to(merge_request_target)
wait_for_ajax
end end
end end
...@@ -34,7 +34,7 @@ feature 'Master creates tag', feature: true do ...@@ -34,7 +34,7 @@ feature 'Master creates tag', feature: true do
expect(current_path).to eq( expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v3.0')) namespace_project_tag_path(project.namespace, project, 'v3.0'))
expect(page).to have_content 'v3.0' expect(page).to have_content 'v3.0'
page.within 'pre.body' do page.within 'pre.wrap' do
expect(page).to have_content "Awesome tag message\n\n- hello\n- world" expect(page).to have_content "Awesome tag message\n\n- hello\n- world"
end end
end end
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
GitLab Org GitLab Org
%a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"}
GitLab Test GitLab Test
%i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content" } %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" }
.js-dropdown-menu-projects .js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects .dropdown-menu.dropdown-select.dropdown-menu-projects
.dropdown-title .dropdown-title
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
var fakeAjaxResponse = function fakeAjaxResponse(req) { var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d; var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true'); expect(req.url).toBe('/api/v3/projects.json?simple=true');
expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 });
d = $.Deferred(); d = $.Deferred();
d.resolve(this.projects_data); d.resolve(this.projects_data);
return d.promise(); return d.promise();
......
...@@ -88,8 +88,6 @@ describe Gitlab::Email::ReplyParser, lib: true do ...@@ -88,8 +88,6 @@ describe Gitlab::Email::ReplyParser, lib: true do
expect(test_parse_body(fixture_file("emails/inline_reply.eml"))). expect(test_parse_body(fixture_file("emails/inline_reply.eml"))).
to eq( to eq(
<<-BODY.strip_heredoc.chomp <<-BODY.strip_heredoc.chomp
On Wed, Oct 8, 2014 at 11:12 AM, techAPJ <info@unconfigured.discourse.org> wrote:
> techAPJ <https://meta.discourse.org/users/techapj> > techAPJ <https://meta.discourse.org/users/techapj>
> November 28 > November 28
> >
......
...@@ -55,7 +55,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -55,7 +55,7 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
describe 'download_access_check' do describe '#check_download_access!' do
subject { access.check('git-upload-pack', '_any') } subject { access.check('git-upload-pack', '_any') }
describe 'master permissions' do describe 'master permissions' do
...@@ -87,7 +87,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -87,7 +87,7 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
describe 'without acccess to project' do describe 'without access to project' do
context 'pull code' do context 'pull code' do
it { expect(subject.allowed?).to be_falsey } it { expect(subject.allowed?).to be_falsey }
end end
...@@ -117,7 +117,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -117,7 +117,7 @@ describe Gitlab::GitAccess, lib: true do
end end
describe 'deploy key permissions' do describe 'deploy key permissions' do
let(:key) { create(:deploy_key) } let(:key) { create(:deploy_key, user: user) }
let(:actor) { key } let(:actor) { key }
context 'pull code' do context 'pull code' do
...@@ -141,7 +141,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -141,7 +141,7 @@ describe Gitlab::GitAccess, lib: true do
end end
context 'from private project' do context 'from private project' do
let(:project) { create(:project, :internal) } let(:project) { create(:project, :private) }
it { expect(subject).not_to be_allowed } it { expect(subject).not_to be_allowed }
end end
...@@ -154,7 +154,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -154,7 +154,7 @@ describe Gitlab::GitAccess, lib: true do
let(:actor) { key } let(:actor) { key }
context 'pull code' do context 'pull code' do
subject { access.download_access_check } subject { access.send(:check_download_access!) }
it { expect { subject }.not_to raise_error } it { expect { subject }.not_to raise_error }
end end
...@@ -199,7 +199,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -199,7 +199,7 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
describe 'push_access_check' do describe '#check_push_access!' do
before { merge_into_protected_branch } before { merge_into_protected_branch }
let(:unprotected_branch) { FFaker::Internet.user_name } let(:unprotected_branch) { FFaker::Internet.user_name }
...@@ -247,7 +247,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -247,7 +247,7 @@ describe Gitlab::GitAccess, lib: true do
permissions_matrix[role].each do |action, allowed| permissions_matrix[role].each do |action, allowed|
context action do context action do
subject { access.push_access_check(changes[action]) } subject { access.send(:check_push_access!, changes[action]) }
it do it do
if allowed if allowed
...@@ -275,7 +275,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -275,7 +275,7 @@ describe Gitlab::GitAccess, lib: true do
permissions_matrix[role].each do |action, allowed| permissions_matrix[role].each do |action, allowed|
context action do context action do
subject { access.push_access_check(changes[action]) } subject { access.send(:check_push_access!, changes[action]) }
it do it do
if allowed if allowed
...@@ -538,20 +538,20 @@ describe Gitlab::GitAccess, lib: true do ...@@ -538,20 +538,20 @@ describe Gitlab::GitAccess, lib: true do
allow(Gitlab::Geo).to receive(:secondary?) { true } allow(Gitlab::Geo).to receive(:secondary?) { true }
end end
it { expect { access.push_access_check(git_annex_changes) }.to raise_error(described_class::UnauthorizedError) } it { expect { access.send(:check_push_access!, git_annex_changes) }.to raise_error(described_class::UnauthorizedError) }
end end
describe 'and git hooks unset' do describe 'and git hooks unset' do
describe 'git annex enabled' do describe 'git annex enabled' do
before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(true) } before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(true) }
it { expect { access.push_access_check(git_annex_changes) }.not_to raise_error } it { expect { access.send(:check_push_access!, git_annex_changes) }.not_to raise_error }
end end
describe 'git annex disabled' do describe 'git annex disabled' do
before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(false) } before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(false) }
it { expect { access.push_access_check(git_annex_changes) }.not_to raise_error } it { expect { access.send(:check_push_access!, git_annex_changes) }.not_to raise_error }
end end
end end
...@@ -566,7 +566,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -566,7 +566,7 @@ describe Gitlab::GitAccess, lib: true do
describe 'git annex enabled' do describe 'git annex enabled' do
before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(true) } before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(true) }
it { expect { access.push_access_check(git_annex_changes) }.not_to raise_error } it { expect { access.send(:check_push_access!, git_annex_changes) }.not_to raise_error }
end end
describe 'git annex enabled, push to master branch' do describe 'git annex enabled, push to master branch' do
...@@ -575,7 +575,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -575,7 +575,7 @@ describe Gitlab::GitAccess, lib: true do
allow_any_instance_of(Commit).to receive(:safe_message) { 'git-annex in me@host:~/repo' } allow_any_instance_of(Commit).to receive(:safe_message) { 'git-annex in me@host:~/repo' }
end end
it { expect { access.push_access_check(git_annex_master_changes) }.not_to raise_error } it { expect { access.send(:check_push_access!, git_annex_master_changes) }.not_to raise_error }
end end
describe 'git annex disabled' do describe 'git annex disabled' do
...@@ -583,7 +583,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -583,7 +583,7 @@ describe Gitlab::GitAccess, lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(false) allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(false)
end end
it { expect { access.push_access_check(git_annex_changes) }.to raise_error(described_class::UnauthorizedError) } it { expect { access.send(:check_push_access!, git_annex_changes) }.to raise_error(described_class::UnauthorizedError) }
end end
end end
...@@ -597,7 +597,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -597,7 +597,7 @@ describe Gitlab::GitAccess, lib: true do
before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(true) } before { allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(true) }
it { expect(access.check('git-annex-shell', git_annex_changes).allowed?).to be_truthy } it { expect(access.check('git-annex-shell', git_annex_changes).allowed?).to be_truthy }
it { expect { access.push_access_check(git_annex_changes) }.not_to raise_error } it { expect { access.send(:check_push_access!, git_annex_changes) }.not_to raise_error }
end end
describe 'git annex disabled' do describe 'git annex disabled' do
...@@ -606,7 +606,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -606,7 +606,7 @@ describe Gitlab::GitAccess, lib: true do
end end
it { expect(access.check('git-annex-shell', git_annex_changes).allowed?).to be_falsey } it { expect(access.check('git-annex-shell', git_annex_changes).allowed?).to be_falsey }
it { expect { access.push_access_check(git_annex_changes) }.to raise_error(described_class::UnauthorizedError) } it { expect { access.send(:check_push_access!, git_annex_changes) }.to raise_error(described_class::UnauthorizedError) }
end end
end end
end end
...@@ -623,21 +623,21 @@ describe Gitlab::GitAccess, lib: true do ...@@ -623,21 +623,21 @@ describe Gitlab::GitAccess, lib: true do
describe "author email check" do describe "author email check" do
it 'returns true' do it 'returns true' do
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.not_to raise_error
end end
it 'returns false' do it 'returns false' do
project.create_push_rule project.create_push_rule
project.push_rule.update(commit_message_regex: "@only.com") project.push_rule.update(commit_message_regex: "@only.com")
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.to raise_error(described_class::UnauthorizedError) expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.to raise_error(described_class::UnauthorizedError)
end end
it 'returns true for tags' do it 'returns true for tags' do
project.create_push_rule project.create_push_rule
project.push_rule.update(commit_message_regex: "@only.com") project.push_rule.update(commit_message_regex: "@only.com")
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/tags/v1') }.not_to raise_error expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/tags/v1') }.not_to raise_error
end end
it 'allows githook for new branch with an old bad commit' do it 'allows githook for new branch with an old bad commit' do
...@@ -650,7 +650,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -650,7 +650,7 @@ describe Gitlab::GitAccess, lib: true do
project.push_rule.update(commit_message_regex: "Change some files") project.push_rule.update(commit_message_regex: "Change some files")
# push to new branch, so use a blank old rev and new ref # push to new branch, so use a blank old rev and new ref
expect { access.push_access_check("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new-branch") }.not_to raise_error expect { access.send(:check_push_access!, "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new-branch") }.not_to raise_error
end end
it 'allows githook for any change with an old bad commit' do it 'allows githook for any change with an old bad commit' do
...@@ -663,7 +663,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -663,7 +663,7 @@ describe Gitlab::GitAccess, lib: true do
project.push_rule.update(commit_message_regex: "Change some files") project.push_rule.update(commit_message_regex: "Change some files")
# push to new branch, so use a blank old rev and new ref # push to new branch, so use a blank old rev and new ref
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.not_to raise_error
end end
it 'does not allow any change from Web UI with bad commit' do it 'does not allow any change from Web UI with bad commit' do
...@@ -678,7 +678,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -678,7 +678,7 @@ describe Gitlab::GitAccess, lib: true do
project.push_rule.update(commit_message_regex: "Change some files") project.push_rule.update(commit_message_regex: "Change some files")
# push to new branch, so use a blank old rev and new ref # push to new branch, so use a blank old rev and new ref
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.to raise_error(described_class::UnauthorizedError) expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.to raise_error(described_class::UnauthorizedError)
end end
end end
...@@ -689,13 +689,13 @@ describe Gitlab::GitAccess, lib: true do ...@@ -689,13 +689,13 @@ describe Gitlab::GitAccess, lib: true do
end end
it 'returns false for non-member user' do it 'returns false for non-member user' do
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.to raise_error(described_class::UnauthorizedError) expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.to raise_error(described_class::UnauthorizedError)
end end
it 'returns true if committer is a gitlab member' do it 'returns true if committer is a gitlab member' do
create(:user, email: 'dmitriy.zaporozhets@gmail.com') create(:user, email: 'dmitriy.zaporozhets@gmail.com')
expect { access.push_access_check('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master') }.not_to raise_error
end end
end end
...@@ -710,14 +710,14 @@ describe Gitlab::GitAccess, lib: true do ...@@ -710,14 +710,14 @@ describe Gitlab::GitAccess, lib: true do
project.create_push_rule project.create_push_rule
project.push_rule.update(file_name_regex: "jpg$") project.push_rule.update(file_name_regex: "jpg$")
expect { access.push_access_check('913c66a37b4a45b9769037c55c2d238bd0942d2e 33f3729a45c02fc67d00adb1b8bca394b0e761d9 refs/heads/master') }.to raise_error(described_class::UnauthorizedError) expect { access.send(:check_push_access!, '913c66a37b4a45b9769037c55c2d238bd0942d2e 33f3729a45c02fc67d00adb1b8bca394b0e761d9 refs/heads/master') }.to raise_error(described_class::UnauthorizedError)
end end
it 'returns true if file name is allowed' do it 'returns true if file name is allowed' do
project.create_push_rule project.create_push_rule
project.push_rule.update(file_name_regex: "exe$") project.push_rule.update(file_name_regex: "exe$")
expect { access.push_access_check('913c66a37b4a45b9769037c55c2d238bd0942d2e 33f3729a45c02fc67d00adb1b8bca394b0e761d9 refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, '913c66a37b4a45b9769037c55c2d238bd0942d2e 33f3729a45c02fc67d00adb1b8bca394b0e761d9 refs/heads/master') }.not_to raise_error
end end
end end
...@@ -730,14 +730,14 @@ describe Gitlab::GitAccess, lib: true do ...@@ -730,14 +730,14 @@ describe Gitlab::GitAccess, lib: true do
project.create_push_rule project.create_push_rule
project.push_rule.update(max_file_size: 1) project.push_rule.update(max_file_size: 1)
expect { access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.to raise_error(described_class::UnauthorizedError) expect { access.send(:check_push_access!, 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.to raise_error(described_class::UnauthorizedError)
end end
it "returns true when size is allowed" do it "returns true when size is allowed" do
project.create_push_rule project.create_push_rule
project.push_rule.update(max_file_size: 2) project.push_rule.update(max_file_size: 2)
expect { access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.not_to raise_error
end end
it "returns true when size is nil" do it "returns true when size is nil" do
...@@ -745,7 +745,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -745,7 +745,7 @@ describe Gitlab::GitAccess, lib: true do
project.create_push_rule project.create_push_rule
project.push_rule.update(max_file_size: 2) project.push_rule.update(max_file_size: 2)
expect { access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.not_to raise_error
end end
end end
...@@ -757,25 +757,25 @@ describe Gitlab::GitAccess, lib: true do ...@@ -757,25 +757,25 @@ describe Gitlab::GitAccess, lib: true do
it 'returns false when blob is too big' do it 'returns false when blob is too big' do
allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(100.megabytes.to_i) allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(100.megabytes.to_i)
expect { access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.to raise_error(described_class::UnauthorizedError) expect { access.send(:check_push_access!, 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.to raise_error(described_class::UnauthorizedError)
end end
it 'returns true when blob is just right' do it 'returns true when blob is just right' do
allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(2.megabytes.to_i) allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(2.megabytes.to_i)
expect { access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.not_to raise_error expect { access.send(:check_push_access!, 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master') }.not_to raise_error
end end
end end
end end
end end
shared_examples 'can not push code' do shared_examples 'pushing code' do |can|
subject { access.check('git-receive-pack', '_any') } subject { access.check('git-receive-pack', '_any') }
context 'when project is authorized' do context 'when project is authorized' do
before { authorize } before { authorize }
it { expect(subject).not_to be_allowed } it { expect(subject).public_send(can, be_allowed) }
end end
context 'when unauthorized' do context 'when unauthorized' do
...@@ -802,7 +802,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -802,7 +802,7 @@ describe Gitlab::GitAccess, lib: true do
describe 'build authentication abilities' do describe 'build authentication abilities' do
let(:authentication_abilities) { build_authentication_abilities } let(:authentication_abilities) { build_authentication_abilities }
it_behaves_like 'can not push code' do it_behaves_like 'pushing code', :not_to do
def authorize def authorize
project.team << [user, :reporter] project.team << [user, :reporter]
end end
...@@ -821,12 +821,26 @@ describe Gitlab::GitAccess, lib: true do ...@@ -821,12 +821,26 @@ describe Gitlab::GitAccess, lib: true do
end end
describe 'deploy key permissions' do describe 'deploy key permissions' do
let(:key) { create(:deploy_key) } let(:key) { create(:deploy_key, user: user, can_push: can_push) }
let(:actor) { key } let(:actor) { key }
it_behaves_like 'can not push code' do context 'when deploy_key can push' do
def authorize let(:can_push) { true }
key.projects << project
it_behaves_like 'pushing code', :to do
def authorize
key.projects << project
end
end
end
context 'when deploy_key cannot push' do
let(:can_push) { false }
it_behaves_like 'pushing code', :not_to do
def authorize
key.projects << project
end
end end
end end
end end
......
...@@ -35,7 +35,7 @@ describe Gitlab::GitAccessWiki, lib: true do ...@@ -35,7 +35,7 @@ describe Gitlab::GitAccessWiki, lib: true do
end end
end end
describe '#download_access_check' do describe '#access_check_download!' do
subject { access.check('git-upload-pack', '_any') } subject { access.check('git-upload-pack', '_any') }
before do before do
......
...@@ -251,6 +251,7 @@ DeployKey: ...@@ -251,6 +251,7 @@ DeployKey:
- type - type
- fingerprint - fingerprint
- public - public
- can_push
Service: Service:
- id - id
- type - type
......
# encoding: utf-8
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb')
describe RemoveDotGitFromUsernames do
let(:user) { create(:user) }
describe '#up' do
let(:migration) { described_class.new }
before do
namespace = user.namespace
namespace.path = 'test.git'
namespace.save!(validate: false)
user.username = 'test.git'
user.save!(validate: false)
end
it 'renames user with .git in username' do
migration.up
expect(user.reload.username).to eq('test_git')
expect(user.namespace.reload.path).to eq('test_git')
expect(user.namespace.route.path).to eq('test_git')
end
end
end
# encoding: utf-8
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_project_names.rb')
# This migration uses multiple threads, and thus different transactions. This
# means data created in this spec may not be visible to some threads. To work
# around this we use the TRUNCATE cleaning strategy.
describe RenameReservedProjectNames, truncate: true do
let(:migration) { described_class.new }
let!(:project) { create(:empty_project) }
before do
project.path = 'projects'
project.save!(validate: false)
end
describe '#up' do
context 'when project repository exists' do
before { project.create_repository }
context 'when no exception is raised' do
it 'renames project with reserved names' do
migration.up
expect(project.reload.path).to eq('projects0')
end
end
context 'when exception is raised during rename' do
before do
allow(project).to receive(:rename_repo).and_raise(StandardError)
end
it 'captures exception from project rename' do
expect { migration.up }.not_to raise_error
end
end
end
context 'when project repository does not exist' do
it 'does not raise error' do
expect { migration.up }.not_to raise_error
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe DeployKey, models: true do describe DeployKey, models: true do
include EmailHelpers
describe "Associations" do describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) } it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:projects) } it { is_expected.to have_many(:projects) }
end end
describe 'notification' do
let(:user) { create(:user) }
it 'does not send a notification' do
perform_enqueued_jobs do
create(:deploy_key, user: user)
end
should_not_email(user)
end
end
end end
...@@ -307,4 +307,15 @@ describe Group, models: true do ...@@ -307,4 +307,15 @@ describe Group, models: true do
it { is_expected.to be_valid } it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(Group) } it { expect(subject.parent).to be_kind_of(Group) }
end end
describe '#members_with_parents' do
let!(:group) { create(:group, :nested) }
let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
it 'returns parents members' do
expect(group.members_with_parents).to include(developer)
expect(group.members_with_parents).to include(master)
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Key, models: true do describe Key, models: true do
include EmailHelpers
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
end end
...@@ -96,4 +98,16 @@ describe Key, models: true do ...@@ -96,4 +98,16 @@ describe Key, models: true do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end end
end end
describe 'notification' do
let(:user) { create(:user) }
it 'sends a notification' do
perform_enqueued_jobs do
create(:key, user: user)
end
should_email(user)
end
end
end end
...@@ -284,12 +284,16 @@ describe MergeRequest, models: true do ...@@ -284,12 +284,16 @@ describe MergeRequest, models: true do
end end
describe '#issues_mentioned_but_not_closing' do describe '#issues_mentioned_but_not_closing' do
it 'detects issues mentioned in description but not closed' do let(:closing_issue) { create :issue, project: subject.project }
mentioned_issue = create(:issue, project: subject.project) let(:mentioned_issue) { create :issue, project: subject.project }
let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
it 'detects issues mentioned in description but not closed' do
subject.project.team << [subject.author, :developer] subject.project.team << [subject.author, :developer]
subject.description = "Is related to #{mentioned_issue.to_reference}" subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
allow(subject).to receive(:commits).and_return([commit])
allow(subject.project).to receive(:default_branch). allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch) and_return(subject.target_branch)
......
...@@ -105,4 +105,70 @@ describe GroupPolicy, models: true do ...@@ -105,4 +105,70 @@ describe GroupPolicy, models: true do
is_expected.to include(*owner_permissions) is_expected.to include(*owner_permissions)
end end
end end
describe 'private nested group inherit permissions' do
let(:nested_group) { create(:group, :private, parent: group) }
subject { described_class.abilities(current_user, nested_group).to_set }
context 'with no user' do
let(:current_user) { nil }
it do
is_expected.not_to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'guests' do
let(:current_user) { guest }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'reporter' do
let(:current_user) { reporter }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'developer' do
let(:current_user) { developer }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'master' do
let(:current_user) { master }
it do
is_expected.to include(:read_group)
is_expected.to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'owner' do
let(:current_user) { owner }
it do
is_expected.to include(:read_group)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
end
end
end end
...@@ -396,7 +396,7 @@ describe API::Helpers, api: true do ...@@ -396,7 +396,7 @@ describe API::Helpers, api: true do
%w[HEAD GET].each do |method_name| %w[HEAD GET].each do |method_name|
context "method is #{method_name}" do context "method is #{method_name}" do
before do before do
expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name))
end end
it 'does not raise an error' do it 'does not raise an error' do
...@@ -410,7 +410,7 @@ describe API::Helpers, api: true do ...@@ -410,7 +410,7 @@ describe API::Helpers, api: true do
%w[POST PUT PATCH DELETE].each do |method_name| %w[POST PUT PATCH DELETE].each do |method_name|
context "method is #{method_name}" do context "method is #{method_name}" do
before do before do
expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name)) expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name))
end end
it 'calls authenticate!' do it 'calls authenticate!' do
......
...@@ -483,12 +483,26 @@ describe 'Git HTTP requests', lib: true do ...@@ -483,12 +483,26 @@ describe 'Git HTTP requests', lib: true do
shared_examples 'can download code only' do shared_examples 'can download code only' do
it 'downloads get status 200' do it 'downloads get status 200' do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token allow_any_instance_of(Repository).
to receive(:exists?).and_return(true)
clone_get "#{project.path_with_namespace}.git",
user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end end
it 'downloads from non-existing repository and gets 403' do
allow_any_instance_of(Repository).
to receive(:exists?).and_return(false)
clone_get "#{project.path_with_namespace}.git",
user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(403)
end
it 'uploads get status 403' do it 'uploads get status 403' do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
......
...@@ -132,12 +132,18 @@ describe Admin::HealthCheckController, "routing" do ...@@ -132,12 +132,18 @@ describe Admin::HealthCheckController, "routing" do
end end
describe Admin::GroupsController, "routing" do describe Admin::GroupsController, "routing" do
let(:name) { 'complex.group-namegit' }
it "to #index" do it "to #index" do
expect(get("/admin/groups")).to route_to('admin/groups#index') expect(get("/admin/groups")).to route_to('admin/groups#index')
end end
it "to #show" do it "to #show" do
expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab') expect(get("/admin/groups/#{name}")).to route_to('admin/groups#show', id: name)
expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup') expect(get("/admin/groups/#{name}/subgroup")).to route_to('admin/groups#show', id: "#{name}/subgroup")
end
it "to #edit" do
expect(get("/admin/groups/#{name}/edit")).to route_to('admin/groups#edit', id: name)
end end
end end
...@@ -54,12 +54,37 @@ describe Users::RefreshAuthorizedProjectsService do ...@@ -54,12 +54,37 @@ describe Users::RefreshAuthorizedProjectsService do
end end
describe '#update_authorizations' do describe '#update_authorizations' do
it 'does nothing when there are no rows to add and remove' do context 'when there are no rows to add and remove' do
expect(user).not_to receive(:remove_project_authorizations) it 'does not change authorizations' do
expect(ProjectAuthorization).not_to receive(:insert_authorizations) expect(user).not_to receive(:remove_project_authorizations)
expect(user).not_to receive(:set_authorized_projects_column) expect(ProjectAuthorization).not_to receive(:insert_authorizations)
service.update_authorizations([], []) service.update_authorizations([], [])
end
context 'when the authorized projects column is not set' do
before do
user.update!(authorized_projects_populated: nil)
end
it 'populates the authorized projects column' do
service.update_authorizations([], [])
expect(user.authorized_projects_populated).to eq true
end
end
context 'when the authorized projects column is set' do
before do
user.update!(authorized_projects_populated: true)
end
it 'does nothing' do
expect(user).not_to receive(:set_authorized_projects_column)
service.update_authorizations([], [])
end
end
end end
it 'removes authorizations that should be removed' do it 'removes authorizations that should be removed' do
...@@ -84,7 +109,7 @@ describe Users::RefreshAuthorizedProjectsService do ...@@ -84,7 +109,7 @@ describe Users::RefreshAuthorizedProjectsService do
it 'populates the authorized projects column' do it 'populates the authorized projects column' do
# make sure we start with a nil value no matter what the default in the # make sure we start with a nil value no matter what the default in the
# factory may be. # factory may be.
user.update(authorized_projects_populated: nil) user.update!(authorized_projects_populated: nil)
service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment