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.
## 8.15.2 (2016-12-27)
- No changes.
- Fix finding the latest pipeline. !8301
- Fix mr list timestamp alignment. !8271
- Fix discussion overlap text in regular screens. !8273
- Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari. !8282
......
......@@ -343,7 +343,7 @@ gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
......@@ -358,5 +358,5 @@ gem 'paranoia', '~> 2.2'
gem 'health_check', '~> 2.2.0'
# System information
gem 'vmstat', '~> 2.2'
gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
......@@ -180,7 +180,7 @@ GEM
elasticsearch-transport (1.0.15)
faraday
multi_json
email_reply_parser (0.5.8)
email_reply_trimmer (0.1.6)
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
......@@ -800,7 +800,7 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
vmstat (2.2.0)
vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
......@@ -868,7 +868,7 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1)
elasticsearch-model
elasticsearch-rails
email_reply_parser (~> 0.5.8)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0)
......@@ -1016,7 +1016,7 @@ DEPENDENCIES
validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.2)
vmstat (~> 2.3.0)
web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
......
......@@ -69,7 +69,8 @@ to add details to the issue.
- ~UX needs help from a UX designer
- ~Frontend needs help from a Front-end engineer. Please follow the
["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
in support or comment for further detail. Do not use `feature request`.
- ~bug is an issue reporting undesirable or incorrect behavior.
......
......@@ -91,6 +91,14 @@
// Set the default path for all cookies to GitLab's root directory
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
$('.btn').click(function(e) {
if ($(this).hasClass('disabled')) {
......
......@@ -71,3 +71,5 @@ class ListIssue {
return Vue.http.patch(url, data);
}
}
window.ListIssue = ListIssue;
......@@ -10,3 +10,5 @@ class ListLabel {
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
}
window.ListLabel = ListLabel;
......@@ -148,3 +148,5 @@ class List {
});
}
}
window.List = List;
......@@ -6,3 +6,5 @@ class ListMilestone {
this.title = obj.title;
}
}
window.ListMilestone = ListMilestone;
......@@ -8,3 +8,5 @@ class ListUser {
this.avatar = user.avatar_url;
}
}
window.ListUser = ListUser;
......@@ -78,4 +78,6 @@ class BoardService {
issue
});
}
};
}
window.BoardService = BoardService;
......@@ -59,9 +59,11 @@
},
methods: {
updateTooltip: function () {
$(this.$refs.button)
.tooltip('hide')
.tooltip('fixTitle');
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
.tooltip('fixTitle');
});
},
resolve: function () {
if (!this.canResolve) return;
......@@ -90,7 +92,7 @@
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 {
return false;
}
}
window.DiscussionModel = DiscussionModel;
......@@ -9,3 +9,5 @@ class NoteModel {
this.resolved_by = resolved_by;
}
}
window.NoteModel = NoteModel;
......@@ -20,3 +20,5 @@ class EnvironmentsService {
return this.environments.get();
}
}
window.EnvironmentsService = EnvironmentsService;
......@@ -77,7 +77,7 @@
var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
atSymbolsWithBar = 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, "\\$&");
_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
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
// and showing a warning when more than `x` users are referenced.
//
(function() {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
(function () {
var lastTextareaPreviewed;
var markdownPreview;
var previewButtonSelector;
var writeButtonSelector;
window.MarkdownPreview = (function() {
window.MarkdownPreview = (function () {
function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
......@@ -16,73 +19,71 @@
MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function(form) {
var mdText, preview;
preview = form.find('.js-md-preview');
mdText = form.find('textarea.markdown-area').val();
MarkdownPreview.prototype.showPreview = function ($form) {
var mdText;
var preview = $form.find('.js-md-preview');
if (preview.hasClass('md-preview-loading')) {
return;
}
mdText = $form.find('textarea.markdown-area').val();
if (mdText.trim().length === 0) {
preview.text('Nothing to preview.');
return this.hideReferencedUsers(form);
this.hideReferencedUsers($form);
} else {
preview.text('Loading...');
return this.renderMarkdown(mdText, (function(_this) {
return function(response) {
preview.html(response.body);
preview.renderGFM();
return _this.renderReferencedUsers(response.references.users, form);
};
})(this));
preview.addClass('md-preview-loading').text('Loading...');
this.fetchMarkdownPreview(mdText, (function (response) {
preview.removeClass('md-preview-loading').html(response.body);
preview.renderGFM();
this.renderReferencedUsers(response.references.users, $form);
}).bind(this));
}
};
MarkdownPreview.prototype.renderMarkdown = function(text, success) {
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) {
if (!window.preview_markdown_path) {
return;
}
if (text === this.ajaxCache.text) {
return success(this.ajaxCache.response);
success(this.ajaxCache.response);
return;
}
return $.ajax({
$.ajax({
type: 'POST',
url: window.preview_markdown_path,
data: {
text: text
},
dataType: 'json',
success: (function(_this) {
return function(response) {
_this.ajaxCache = {
text: text,
response: response
};
return success(response);
success: (function (response) {
this.ajaxCache = {
text: text,
response: response
};
})(this)
success(response);
}).bind(this)
});
};
MarkdownPreview.prototype.hideReferencedUsers = function(form) {
var referencedUsers;
referencedUsers = form.find('.referenced-users');
return referencedUsers.hide();
MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
$form.find('.referenced-users').hide();
};
MarkdownPreview.prototype.renderReferencedUsers = function(users, form) {
MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
var referencedUsers;
referencedUsers = form.find('.referenced-users');
referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) {
if (users.length >= this.referenceThreshold) {
referencedUsers.show();
return referencedUsers.find('.js-referenced-users-count').text(users.length);
referencedUsers.find('.js-referenced-users-count').text(users.length);
} else {
return referencedUsers.hide();
referencedUsers.hide();
}
}
};
return MarkdownPreview;
})();
}());
markdownPreview = new window.MarkdownPreview();
......@@ -92,19 +93,14 @@
lastTextareaPreviewed = null;
$.fn.setupMarkdownPreview = function() {
var $form, form_textarea;
$form = $(this);
form_textarea = $form.find('textarea.markdown-area');
form_textarea.on('input', function() {
return markdownPreview.hideReferencedUsers($form);
});
return form_textarea.on('blur', function() {
return markdownPreview.showPreview($form);
$.fn.setupMarkdownPreview = function () {
var $form = $(this);
$form.find('textarea.markdown-area').on('input', function () {
markdownPreview.hideReferencedUsers($form);
});
};
$(document).on('markdown-preview:show', function(e, $form) {
$(document).on('markdown-preview:show', function (e, $form) {
if (!$form) {
return;
}
......@@ -115,10 +111,10 @@
// toggle content
$form.find('.md-write-holder').hide();
$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) {
return;
}
......@@ -129,34 +125,33 @@
// toggle content
$form.find('.md-write-holder').show();
$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;
$target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) {
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
return keyboardEvent.preventDefault();
keyboardEvent.preventDefault();
} else if (lastTextareaPreviewed) {
$target = lastTextareaPreviewed;
$(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;
e.preventDefault();
$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;
e.preventDefault();
$form = $(this).closest('form');
return $(document).triggerHandler('markdown-preview:hide', [$form]);
$(document).triggerHandler('markdown-preview:hide', [$form]);
});
}).call(this);
}());
......@@ -15,6 +15,7 @@
},
data: function(term, callback) {
var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) {
return callback(projects);
};
......@@ -34,7 +35,7 @@
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
return Api.projects(term, this.orderBy, projectsCallback);
return Api.projects(term, orderBy, projectsCallback);
}
},
url: function(project) {
......
......@@ -51,7 +51,7 @@
});
// Protected branch dropdown
new gl.ProtectedBranchDropdown({
new window.ProtectedBranchDropdown({
$dropdown: this.$wrap.find('.js-protected-branch-select'),
onSelect: this.onSelectCallback
});
......
/* eslint-disable */
(global => {
global.gl = global.gl || {};
/* eslint-disable comma-dangle, no-unused-vars */
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
this.buildDropdown();
this.bindEvents();
// Hide footer
this.$dropdownFooter.addClass('hidden');
}
// Hide footer
this.$dropdownFooter.addClass('hidden');
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedBranches.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title']
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
},
fieldName: 'protected_branch[name]',
text(protectedBranch) {
return _.escape(protectedBranch.title);
},
id(protectedBranch) {
return _.escape(protectedBranch.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (item, $el, e) => {
e.preventDefault();
this.onSelect();
}
});
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedBranches.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title']
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
},
fieldName: 'protected_branch[name]',
text(protectedBranch) {
return _.escape(protectedBranch.title);
},
id(protectedBranch) {
return _.escape(protectedBranch.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (item, $el, e) => {
e.preventDefault();
this.onSelect();
}
});
}
onClickCreateWildcard() {
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(0);
}
onClickCreateWildcard() {
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(0);
}
bindEvents() {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
bindEvents() {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
getProtectedBranches(term, callback) {
if (this.selectedBranch) {
callback(gon.open_branches.concat(this.selectedBranch));
} else {
callback(gon.open_branches);
}
getProtectedBranches(term, callback) {
if (this.selectedBranch) {
callback(gon.open_branches.concat(this.selectedBranch));
} else {
callback(gon.open_branches);
}
}
toggleCreateNewButton(branchName) {
this.selectedBranch = {
title: branchName,
id: branchName,
text: branchName
};
if (branchName) {
this.$dropdownContainer
.find('.create-new-protected-branch code')
.text(branchName);
}
toggleCreateNewButton(branchName) {
this.selectedBranch = {
title: branchName,
id: branchName,
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);
window.ProtectedBranchDropdown = ProtectedBranchDropdown;
......@@ -57,16 +57,33 @@ pre {
border-radius: 0;
color: $well-pre-color;
}
&.wrap {
word-break: break-word;
white-space: pre-wrap;
}
}
hr {
margin: $gl-padding 0;
border-top: 1px solid darken($gray-normal, 8%);
}
.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; }
/** FLASH message **/
......
......@@ -87,25 +87,25 @@
}
}
$theme-charcoal: #3d454d;
$theme-charcoal-light: #485157;
$theme-charcoal-dark: #383f45;
$theme-charcoal-text: #b9bbbe;
$theme-charcoal-light: #b9bbbe;
$theme-charcoal: #485157;
$theme-charcoal-dark: #3d454d;
$theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9;
$theme-blue: #2980b9;
$theme-blue-dark: #1970a9;
$theme-blue-darker: #096099;
$theme-graphite-lighter: #ccc;
$theme-graphite-light: #777;
$theme-graphite: #666;
$theme-graphite-dark: #555;
$theme-graphite-light: #ccc;
$theme-graphite: #777;
$theme-graphite-dark: #666;
$theme-graphite-darker: #555;
$theme-gray-light: #979797;
$theme-gray: #373737;
$theme-gray-dark: #272727;
$theme-gray-darker: #222;
$theme-black-light: #979797;
$theme-black: #373737;
$theme-black-dark: #272727;
$theme-black-darker: #222;
$theme-green-light: #adc;
$theme-green: #019875;
......@@ -123,15 +123,15 @@ body {
}
&.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 {
@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 {
@include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker);
&.ui_black {
@include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
}
&.ui_green {
......
......@@ -205,6 +205,7 @@ ul.content-list {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
align-items: center;
white-space: nowrap;
}
......@@ -214,6 +215,11 @@ ul.content-list {
padding-right: 8px;
}
.row-fixed-content {
flex: 0 0 auto;
margin-left: auto;
}
.row-title {
font-weight: 600;
}
......
......@@ -116,8 +116,8 @@
padding-top: 16px;
padding-bottom: 11px;
display: inline-block;
width: 50%;
line-height: 28px;
white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) {
......@@ -158,30 +158,24 @@
}
.nav-controls {
width: 50%;
display: inline-block;
float: right;
text-align: right;
padding: 11px 0;
margin-bottom: 0;
> .dropdown {
margin-right: $gl-padding-top;
display: inline-block;
vertical-align: top;
&:last-child {
margin-right: 0;
}
}
> .btn {
> .btn,
> .btn-container,
> .dropdown,
> input,
> form {
margin-right: $gl-padding-top;
display: inline-block;
vertical-align: top;
&:last-child {
margin-right: 0;
float: right;
}
}
......@@ -189,19 +183,21 @@
float: none;
}
> form {
display: inline-block;
}
.icon-label {
display: none;
}
input {
.btn,
.dropdown,
.dropdown-toggle,
input,
form {
height: 35px;
}
input {
display: inline-block;
position: relative;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; }
......@@ -225,6 +221,7 @@
.btn,
form,
.dropdown,
.dropdown-toggle,
.dropdown-menu-toggle,
.form-control {
margin: 0 0 10px;
......@@ -263,6 +260,10 @@
.nav-text,
.nav-controls {
width: auto;
@media (max-width: $screen-xs-max) {
width: 100%;
}
}
}
......@@ -275,6 +276,10 @@
padding: 17px 0;
}
}
pre {
width: 100%;
}
}
.layout-nav {
......@@ -427,4 +432,41 @@
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 @@
// Labels
.label {
padding: 4px 5px;
font-size: 13px;
font-size: 12px;
font-style: normal;
font-weight: normal;
display: inline-block;
......
......@@ -496,9 +496,9 @@ $project-network-controls-color: #888;
*/
$runner-state-shared-bg: #32b186;
$runner-state-specific-bg: #3498db;
$runner-status-online-color: green;
$runner-status-offline-color: gray;
$runner-status-paused-color: red;
$runner-status-online-color: $green-normal;
$runner-status-offline-color: $gray-darkest;
$runner-status-paused-color: $red-normal;
/*
Stat Graph
......
......@@ -108,6 +108,12 @@
&.has-border {
border-top: 3px solid;
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
padding-top: 1px;
padding-right: 1px;
padding-left: 1px;
.board-title {
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 {
section {
.issuable-discussion {
......
......@@ -98,7 +98,7 @@
}
.label {
padding: 8px 9px 9px $gl-padding;
padding: 8px 9px 9px;
font-size: 14px;
}
}
......
......@@ -105,19 +105,19 @@
li {
flex: 1;
text-align: center;
border-left: 1px solid $border-color;
&:first-of-type {
border-left: none;
border-top-left-radius: $border-radius-default;
}
&:last-of-type {
border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
}
&:not(.active) {
background-color: $gray-light;
border-left: 1px solid $border-color;
}
a {
......
......@@ -424,6 +424,10 @@
.merge-request-tabs-holder {
background-color: $white-light;
.container-limited {
max-width: $limited-layout-width;
}
&.affix {
top: 100px;
left: 0;
......
......@@ -557,18 +557,14 @@ ul.notes {
&.is-active {
color: $gl-text-green;
svg path {
svg {
fill: $gl-text-green;
}
}
svg {
position: relative;
color: $gray-darkest;
path {
fill: $gray-darkest;
}
fill: $gray-darkest;
}
}
......
......@@ -22,8 +22,8 @@
background: $theme-graphite;
}
&.ui_gray {
background: $theme-gray;
&.ui_black {
background: $theme-black;
}
&.ui_green {
......
......@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
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
redirect_to admin_deploy_keys_path
......@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def deploy_key_params
params.require(:deploy_key).permit(:key, :title)
params.require(:deploy_key).permit(:key, :title, :can_push)
end
end
......@@ -9,7 +9,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
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])
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.with_statistics.page(params[:projects_page])
......
......@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index
@sort = params[:sort]
@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
def destroy
......
......@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
@key = DeployKey.new(deploy_key_params)
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
set_index_vars
if @key.valid? && @project.deploy_keys << @key
......@@ -59,7 +59,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def deploy_key_params
params.require(:deploy_key).permit(:key, :title)
params.require(:deploy_key).permit(:key, :title, :can_push)
end
def log_audit_event(key_title, options = {})
......
......@@ -25,6 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController
def index
@issues = issues_collection
@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?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
......
......@@ -40,6 +40,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index
@merge_requests = merge_requests_collection
@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?
labels_params = { project_id: @project.id, title: params[:label_name] }
......
......@@ -26,6 +26,9 @@ class Projects::SnippetsController < Projects::ApplicationController
scope: params[:scope]
)
@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
def new
......
......@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
params[:sort] = params[:sort].presence || 'name'
params[:sort] = params[:sort].presence || sort_value_recently_updated
@sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute
......
......@@ -61,7 +61,7 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
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")
end
end
......@@ -90,10 +90,12 @@ module ProjectsHelper
end
def project_for_deploy_key(deploy_key)
if deploy_key.projects.include?(@project)
if deploy_key.has_access_to?(@project)
@project
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
......
......@@ -20,4 +20,18 @@ class DeployKey < Key
def destroyed_when_orphaned?
self.private?
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
......@@ -178,15 +178,17 @@ class Group < Namespace
end
def has_owner?(user)
owners.include?(user)
members_with_parents.owners.where(user_id: user).any?
end
def has_master?(user)
members.masters.where(user_id: user).any?
members_with_parents.masters.where(user_id: user).any?
end
# Check if user is a last owner of the group.
# Parent owners are ignored for nested groups.
def last_owner?(user)
has_owner?(user) && owners.size == 1
owners.include?(user) && owners.size == 1
end
def avatar_type
......@@ -239,6 +241,14 @@ class Group < Namespace
end
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
......@@ -59,10 +59,6 @@ class Key < ActiveRecord::Base
)
end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook
SystemHooksService.new.execute_hooks_for(self, :create)
end
......@@ -88,4 +84,8 @@ class Key < ActiveRecord::Base
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
end
end
......@@ -599,11 +599,7 @@ class MergeRequest < ActiveRecord::Base
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description)
issues = ext.issues
closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description)
issues - closing_issues
ext.issues - closes_issues
end
def target_project_path
......
......@@ -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"
# 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
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
......@@ -1063,7 +1063,7 @@ class Project < ActiveRecord::Base
# 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('repository cannot be renamed')
raise StandardError.new('repository cannot be renamed')
end
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
......
......@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy
return unless @user
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)
master = owner || @subject.has_master?(@user)
......
......@@ -261,7 +261,7 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user)
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)
)
end
......
......@@ -74,7 +74,7 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
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.remove_project_authorizations(remove) unless remove.empty?
......
- page_title "Deploy Keys"
.panel.panel-default.prepend-top-default
.panel-heading
Public deploy keys (#{@deploy_keys.count})
.controls
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
- if @deploy_keys.any?
.table-holder
%table.table
%thead.panel-heading
%h3.page-title.deploy-keys-title
Public deploy keys (#{@deploy_keys.count})
.pull-right
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
- if @deploy_keys.any?
.table-holder.deploy-keys-list
%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
%th Title
%th Fingerprint
%th Added at
%th
%tbody
- @deploy_keys.each do |deploy_key|
%tr
%td
%strong= deploy_key.title
%td
%code.key-fingerprint= deploy_key.fingerprint
%td
%span.cgray
added #{time_ago_with_tooltip(deploy_key.created_at)}
%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"
%td
%strong= deploy_key.title
%td
%code.key-fingerprint= deploy_key.fingerprint
%td
- if deploy_key.can_push?
Yes
- else
No
%td
%span.cgray
added #{time_ago_with_tooltip(deploy_key.created_at)}
%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 @@
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README")
= 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
= f.submit 'Create', class: "btn-create btn"
......
......@@ -3,7 +3,7 @@
= render "projects/commits/head"
%div{ class: container_class }
.top-area
.top-area.adjust
.nav-text
Protected branches can be managed in project settings
......@@ -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 }
.dropdown.inline
%button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
= icon('chevron-down')
......
......@@ -6,6 +6,9 @@
= deploy_key.title
.description
= 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.projects.each do |project|
- if can?(current_user, :read_project, project)
......
......@@ -10,4 +10,13 @@
%p.light.append-bottom-0
Paste a machine public key here. Read more about how to generate it
= 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"
......@@ -10,7 +10,7 @@
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%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
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
......
......@@ -50,7 +50,8 @@
- if mr_issues_mentioned_but_not_closing.present?
#{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
!= 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?
.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
= icon("exclamation-triangle")
This merge request contains merge conflicts
%p
Please
- if @merge_request.conflicts_can_be_resolved_by?(current_user)
- if @merge_request.conflicts_can_be_resolved_in_ui?
= 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
To merge this request, resolve these conflicts
- if can_resolve && !can_resolve_in_ui
locally
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)
#{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
- else
ask someone with write access to this repository to merge this request manually.
- if (can_resolve && can_resolve_in_ui) || can_merge
.btn-group
- if can_resolve && can_resolve_in_ui
= 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)
- release = @releases.find { |release| release.tag == tag.name }
%li
%div
%li.flex-row
.row-main-content.str-truncated
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%span.item-title
= icon('tag')
......@@ -10,24 +10,25 @@
&nbsp;
= strip_gpg_signature(tag.message)
.controls
= render 'projects/buttons/download', project: @project, ref: tag.name
- if commit
.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)
= 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("pencil")
.row-fixed-content.controls
= render 'projects/buttons/download', project: @project, ref: tag.name
- if can?(current_user, :admin_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
= icon("trash-o")
- 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", data: { container: "body" } do
= icon("pencil")
- if commit
= 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, :admin_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
= icon("trash-o")
......@@ -2,16 +2,16 @@
- page_title "Tags"
= render "projects/commits/head"
%div{ class: container_class }
.top-area
.nav-text
%div.flex-list{ class: container_class }
.top-area.flex-row
.nav-text.row-main-content
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
= 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'} }
%span.light
= projects_sort_options_hash[@sort]
......@@ -32,7 +32,7 @@
.tags
- if @tags.any?
%ul.content-list
%ul.flex-list.content-list
= render partial: 'tag', collection: @tags
= paginate @tags, theme: 'gitlab'
......
......@@ -12,21 +12,23 @@
- else
Cant find HEAD commit for this tag
.nav-controls
.nav-controls.controls-flex
- 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")
= 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')
= 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')
= 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)
.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
%i.fa.fa-trash-o
- if @tag.message.present?
%pre.body
%pre.wrap
= strip_gpg_signature(@tag.message)
.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:
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:
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
scope(path: 'groups/*id',
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
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -601,6 +601,7 @@ ActiveRecord::Schema.define(version: 20161221140236) do
t.string "type"
t.string "fingerprint"
t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
......@@ -1509,4 +1510,4 @@ ActiveRecord::Schema.define(version: 20161221140236) do
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
end
\ No newline at end of file
......@@ -20,12 +20,14 @@ Example response:
"id": 1,
"title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T10:12:29Z"
},
{
"id": 3,
"title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": true,
"created_at": "2013-10-02T11:12:29Z"
}
]
......@@ -55,12 +57,14 @@ Example response:
"id": 1,
"title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T10:12:29Z"
},
{
"id": 3,
"title": "Another Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T11:12:29Z"
}
]
......@@ -92,6 +96,7 @@ Example response:
"id": 1,
"title": "Public key",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"can_push": false,
"created_at": "2013-10-02T10:12:29Z"
}
```
......@@ -107,14 +112,15 @@ project only if original one was is accessible by the same user.
POST /projects/:id/deploy_keys
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project |
| `title` | string | yes | New deploy key's title |
| `key` | string | yes | New deploy key |
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project |
| `title` | string | yes | New deploy key's title |
| `key` | string | yes | New deploy key |
| `can_push` | boolean | no | Can deploy key push to the project's repository |
```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:
......@@ -124,6 +130,7 @@ Example response:
"key" : "ssh-rsa AAAA...",
"id" : 12,
"title" : "My deploy key",
"can_push": true,
"created_at" : "2015-08-29T12:44:31.550Z"
}
```
......
......@@ -3,12 +3,15 @@
## Log arguments to Sidekiq jobs
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
jobs (such as sending a password reset email) take secret arguments (for
example the password reset token).
Please note: It is not recommend to enable this setting in production because some
Sidekiq jobs (such as sending a password reset email) take secret arguments (for
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
![Quick update animation](img/animation-quickupdate.gif)
> TODO: Add guidance for other kinds of animation
\ No newline at end of file
### Moving transitions
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.
## Color
| | |
| :------: | :------- |
| ![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.|
| ![Green](img/color-green.png) | Green is for actions that create new objects. |
| ![Orange](img/color-orange.png) | Orange is used for warnings |
| ![Red](img/color-red.png) | Red is reserved for 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. |
| | State | Action |
| :------: | :------- | :------- |
| ![Blue](img/color-blue.png) | Primary and active (such as the current tab) | Organizational, managing, and retry commands|
| ![Green](img/color-green.png) | Opened | Create new objects |
| ![Orange](img/color-orange.png) | Warning | Non destructive action |
| ![Red](img/color-red.png) | Closed | Delete and other destructive commands |
| ![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.
......
@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
end
class SSHKey < Grape::Entity
expose :id, :title, :key, :created_at
expose :id, :title, :key, :created_at, :can_push
end
class SSHKeyWithUser < SSHKey
......
......@@ -96,7 +96,7 @@ module API
end
def authenticate_non_get!
authenticate! unless %w[GET HEAD].include?(route.route_method)
authenticate! unless %w[GET HEAD].include?(route.request_method)
end
def authenticate_by_gitlab_shell_token!
......
......@@ -69,8 +69,6 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
required_attributes! [:body]
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
......
......@@ -87,6 +87,7 @@ module API
given sentry_enabled: ->(val) { val } do
requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
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 :koding_enabled, type: Boolean, desc: 'Enable Koding'
given koding_enabled: ->(val) { val } do
......
......@@ -9,8 +9,7 @@ module Gitlab
def lfs_deploy_token?(for_project)
type == :lfs_deploy_token &&
actor &&
actor.projects.include?(for_project)
actor.try(:has_access_to?, for_project)
end
def success?
......
......@@ -2,14 +2,17 @@ module Gitlab
module Checks
class ChangeAccess
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)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
@env = env
@skip_authorization = skip_authorization
end
def exec
......@@ -25,6 +28,7 @@ module Gitlab
protected
def protected_branch_checks
return if skip_authorization
return unless @branch_name
return unless project.protected_branch?(@branch_name)
......@@ -50,6 +54,8 @@ module Gitlab
end
def tag_checks
return if skip_authorization
tag_ref = Gitlab::Git.tag_name(@ref)
if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
......@@ -58,6 +64,8 @@ module Gitlab
end
def push_checks
return if skip_authorization
if user_access.cannot_do_action?(:push_code)
"You are not allowed to push code to this project."
end
......
......@@ -13,9 +13,17 @@ module Gitlab
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")
end
......@@ -57,30 +65,6 @@ module Gitlab
rescue
nil
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
......@@ -8,7 +8,8 @@ module Gitlab
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for 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.'
}
......@@ -33,14 +34,15 @@ module Gitlab
check_active_user!
check_project_accessibility!
check_command_existence!(cmd)
check_repository_existence!
check_geo_license!
case cmd
when *DOWNLOAD_COMMANDS
download_access_check
check_download_access!
when *PUSH_COMMANDS
push_access_check(changes)
check_push_access!(changes)
when *GIT_ANNEX_COMMANDS
git_annex_access_check(project, changes)
end
......@@ -50,63 +52,96 @@ module Gitlab
build_status_object(false, ex.message)
end
def download_access_check
if user
user_download_access_check
elsif deploy_key.nil? && geo_node_key.nil? && !guest_can_downlod_code?
raise UnauthorizedError, ERROR_MESSAGES[:download]
end
def guest_can_download_code?
Guest.can?(:download_code, project)
end
def push_access_check(changes)
if project.repository_read_only?
raise UnauthorizedError, 'The repository is temporarily read-only. Please try again later.'
end
def user_can_download_code?
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
end
if Gitlab::Geo.secondary?
raise UnauthorizedError, "You can't push code on a secondary GitLab Geo node."
def build_can_download_code?
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
return if git_annex_branch_sync?(changes)
def check_active_user!
return if deploy_key? || geo_node_key?
if user
user_push_access_check(changes)
else
raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked."
end
end
def guest_can_downlod_code?
Guest.can?(:download_code, project)
def check_project_accessibility!
if project.blank? || !can_read_project?
raise UnauthorizedError, 'The project you were looking for could not be found.'
end
end
def user_download_access_check
unless user_can_download_code? || build_can_download_code?
raise UnauthorizedError, ERROR_MESSAGES[:download]
def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd)
raise UnauthorizedError, "The command you're trying to execute is not allowed."
end
end
def user_can_download_code?
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
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 build_can_download_code?
authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
def check_repository_existence!
unless project.repository.exists?
raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
end
end
def user_push_access_check(changes)
unless authentication_abilities.include?(:push_code)
raise UnauthorizedError, ERROR_MESSAGES[:upload]
def check_download_access!
return if deploy_key? || geo_node_key?
passed = user_can_download_code? ||
build_can_download_code? ||
guest_can_download_code?
unless passed
raise UnauthorizedError, ERROR_MESSAGES[:download]
end
end
if changes.blank?
return # Allow access.
# TODO: please clean this up
def check_push_access!(changes)
if project.repository_read_only?
raise UnauthorizedError, 'The repository is temporarily read-only. Please try again later.'
end
unless project.repository.exists?
raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
if Gitlab::Geo.secondary?
raise UnauthorizedError, "You can't push code on a secondary GitLab Geo node."
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?
raise UnauthorizedError, Gitlab::RepositorySizeError.new(project).push_error
end
......@@ -116,13 +151,30 @@ module Gitlab
raise UnauthorizedError, message
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)
push_size_in_bytes = 0
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
status = change_access_check(change)
status = check_single_change_access(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
raise UnauthorizedError, status.message
......@@ -138,97 +190,39 @@ module Gitlab
end
end
def change_access_check(change)
Checks::ChangeAccess.new(change, user_access: user_access, project: project, env: @env).exec
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
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
def check_single_change_access(change)
Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
env: @env,
skip_authorization: deploy_key?).exec
end
def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd)
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)
def deploy_key
actor if deploy_key?
end
def deploy_key
actor if actor.is_a?(DeployKey)
def deploy_key?
actor.is_a?(DeployKey)
end
def geo_node_key
actor if actor.is_a?(GeoNodeKey)
actor if geo_node_key?
end
def deploy_key_can_read_project?
if deploy_key
return true if project.public?
deploy_key.projects.include?(project)
else
false
end
def geo_node_key?
actor.is_a?(GeoNodeKey)
end
def can_read_project?
if user
user_access.can_read_project?
elsif deploy_key
deploy_key_can_read_project?
elsif geo_node_key
if deploy_key?
deploy_key.has_access_to?(project)
elsif geo_node_key?
true
else
Guest.can?(:read_project, project)
end
elsif user
user.can?(:read_project, project)
end || Guest.can?(:read_project, project)
end
protected
......@@ -260,10 +254,6 @@ module Gitlab
raise UnauthorizedError, "You don't have access"
end
unless project.repository.exists?
raise UnauthorizedError, "Repository does not exist"
end
if Gitlab::Geo.enabled? && Gitlab::Geo.secondary?
raise UnauthorizedError, "You can't use git-annex with a secondary GitLab Geo node."
end
......@@ -290,16 +280,5 @@ module Gitlab
true
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
module Gitlab
class GitAccessWiki < GitAccess
def guest_can_downlod_code?
def guest_can_download_code?
Guest.can?(:download_wiki_code, project)
end
......@@ -8,7 +8,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
def change_access_check(change)
def check_single_change_access(change)
if Gitlab::Geo.enabled? && Gitlab::Geo.secondary?
build_status_object(false, "You can't push code to a secondary GitLab Geo node.")
elsif user_access.can_do_action?(:create_wiki)
......
......@@ -15,7 +15,7 @@ module Gitlab
Theme.new(1, 'Graphite', 'ui_graphite'),
Theme.new(2, 'Charcoal', 'ui_charcoal'),
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(6, 'Blue', 'ui_blue')
].freeze
......
......@@ -8,6 +8,8 @@ module Gitlab
end
def can_do_action?(action)
return false if no_user_or_blocked?
@permission_cache ||= {}
@permission_cache[action] ||= user.can?(action, project)
end
......@@ -17,7 +19,7 @@ module Gitlab
end
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
return false unless Gitlab::LDAP::Access.allowed?(user)
......@@ -27,7 +29,7 @@ module Gitlab
end
def can_push_to_branch?(ref)
return false unless user
return false if no_user_or_blocked?
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
......@@ -40,7 +42,7 @@ module Gitlab
end
def can_merge_to_branch?(ref)
return false unless user
return false if no_user_or_blocked?
if project.protected_branch?(ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
......@@ -51,9 +53,15 @@ module Gitlab
end
def can_read_project?
return false unless user
return false if no_user_or_blocked?
user.can?(:read_project, project)
end
private
def no_user_or_blocked?
user.nil? || user.blocked?
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
expect(response).to have_http_status(404)
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
describe 'GET #new' do
......
......@@ -213,11 +213,29 @@ describe Projects::MergeRequestsController do
end
describe 'GET index' do
def get_merge_requests
def get_merge_requests(page = nil)
get :index,
namespace_id: project.namespace.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
context 'when filtering by opened state' do
......
......@@ -11,6 +11,28 @@ describe Projects::SnippetsController do
end
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
let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) }
......
......@@ -17,6 +17,16 @@ feature 'Admin Groups', feature: true do
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
scenario 'shows the visibility level radio populated with the group visibility_level value' do
group = create(:group, :private)
......
......@@ -47,6 +47,18 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(true, label_item, note, label.title)
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
note = find('#note_note')
page.within '.timeline-content-form' 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}" }
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
......@@ -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}" }
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
require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do
include WaitForAjax
let(:milestone) { create(:milestone, project: project, title: 8.14) }
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
......@@ -74,6 +76,8 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
issue.drag_to(issue_target)
wait_for_ajax
end
def create_and_drag_merge_request(params = {})
......@@ -82,5 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
merge_request.drag_to(merge_request_target)
wait_for_ajax
end
end
......@@ -34,7 +34,7 @@ feature 'Master creates tag', feature: true do
expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, '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"
end
end
......
......@@ -4,7 +4,7 @@
GitLab Org
%a.project-item-select-holder{href: "/gitlab-org/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
.dropdown-menu.dropdown-select.dropdown-menu-projects
.dropdown-title
......
......@@ -25,6 +25,7 @@
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
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.resolve(this.projects_data);
return d.promise();
......
......@@ -88,8 +88,6 @@ describe Gitlab::Email::ReplyParser, lib: true do
expect(test_parse_body(fixture_file("emails/inline_reply.eml"))).
to eq(
<<-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>
> November 28
>
......
......@@ -55,7 +55,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
describe 'download_access_check' do
describe '#check_download_access!' do
subject { access.check('git-upload-pack', '_any') }
describe 'master permissions' do
......@@ -87,7 +87,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
describe 'without acccess to project' do
describe 'without access to project' do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
end
......@@ -117,7 +117,7 @@ describe Gitlab::GitAccess, lib: true do
end
describe 'deploy key permissions' do
let(:key) { create(:deploy_key) }
let(:key) { create(:deploy_key, user: user) }
let(:actor) { key }
context 'pull code' do
......@@ -141,7 +141,7 @@ describe Gitlab::GitAccess, lib: true do
end
context 'from private project' do
let(:project) { create(:project, :internal) }
let(:project) { create(:project, :private) }
it { expect(subject).not_to be_allowed }
end
......@@ -154,7 +154,7 @@ describe Gitlab::GitAccess, lib: true do
let(:actor) { key }
context 'pull code' do
subject { access.download_access_check }
subject { access.send(:check_download_access!) }
it { expect { subject }.not_to raise_error }
end
......@@ -199,7 +199,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
describe 'push_access_check' do
describe '#check_push_access!' do
before { merge_into_protected_branch }
let(:unprotected_branch) { FFaker::Internet.user_name }
......@@ -247,7 +247,7 @@ describe Gitlab::GitAccess, lib: true do
permissions_matrix[role].each do |action, allowed|
context action do
subject { access.push_access_check(changes[action]) }
subject { access.send(:check_push_access!, changes[action]) }
it do
if allowed
......@@ -275,7 +275,7 @@ describe Gitlab::GitAccess, lib: true do
permissions_matrix[role].each do |action, allowed|
context action do
subject { access.push_access_check(changes[action]) }
subject { access.send(:check_push_access!, changes[action]) }
it do
if allowed
......@@ -538,20 +538,20 @@ describe Gitlab::GitAccess, lib: true do
allow(Gitlab::Geo).to receive(:secondary?) { true }
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
describe 'and git hooks unset' do
describe 'git annex enabled' do
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
describe 'git annex disabled' do
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
......@@ -566,7 +566,7 @@ describe Gitlab::GitAccess, lib: true do
describe 'git annex enabled' do
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
describe 'git annex enabled, push to master branch' 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' }
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
describe 'git annex disabled' do
......@@ -583,7 +583,7 @@ describe Gitlab::GitAccess, lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:git_annex_enabled).and_return(false)
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
......@@ -597,7 +597,7 @@ describe Gitlab::GitAccess, lib: true do
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.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
describe 'git annex disabled' do
......@@ -606,7 +606,7 @@ describe Gitlab::GitAccess, lib: true do
end
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
......@@ -623,21 +623,21 @@ describe Gitlab::GitAccess, lib: true do
describe "author email check" 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
it 'returns false' do
project.create_push_rule
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
it 'returns true for tags' do
project.create_push_rule
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
it 'allows githook for new branch with an old bad commit' do
......@@ -650,7 +650,7 @@ describe Gitlab::GitAccess, lib: true do
project.push_rule.update(commit_message_regex: "Change some files")
# 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
it 'allows githook for any change with an old bad commit' do
......@@ -663,7 +663,7 @@ describe Gitlab::GitAccess, lib: true do
project.push_rule.update(commit_message_regex: "Change some files")
# 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
it 'does not allow any change from Web UI with bad commit' do
......@@ -678,7 +678,7 @@ describe Gitlab::GitAccess, lib: true do
project.push_rule.update(commit_message_regex: "Change some files")
# 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
......@@ -689,13 +689,13 @@ describe Gitlab::GitAccess, lib: true do
end
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
it 'returns true if committer is a gitlab member' do
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
......@@ -710,14 +710,14 @@ describe Gitlab::GitAccess, lib: true do
project.create_push_rule
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
it 'returns true if file name is allowed' do
project.create_push_rule
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
......@@ -730,14 +730,14 @@ describe Gitlab::GitAccess, lib: true do
project.create_push_rule
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
it "returns true when size is allowed" do
project.create_push_rule
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
it "returns true when size is nil" do
......@@ -745,7 +745,7 @@ describe Gitlab::GitAccess, lib: true do
project.create_push_rule
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
......@@ -757,25 +757,25 @@ describe Gitlab::GitAccess, lib: true 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)
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
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)
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
shared_examples 'can not push code' do
shared_examples 'pushing code' do |can|
subject { access.check('git-receive-pack', '_any') }
context 'when project is authorized' do
before { authorize }
it { expect(subject).not_to be_allowed }
it { expect(subject).public_send(can, be_allowed) }
end
context 'when unauthorized' do
......@@ -802,7 +802,7 @@ describe Gitlab::GitAccess, lib: true do
describe 'build authentication abilities' do
let(:authentication_abilities) { build_authentication_abilities }
it_behaves_like 'can not push code' do
it_behaves_like 'pushing code', :not_to do
def authorize
project.team << [user, :reporter]
end
......@@ -821,12 +821,26 @@ describe Gitlab::GitAccess, lib: true do
end
describe 'deploy key permissions' do
let(:key) { create(:deploy_key) }
let(:key) { create(:deploy_key, user: user, can_push: can_push) }
let(:actor) { key }
it_behaves_like 'can not push code' do
def authorize
key.projects << project
context 'when deploy_key can push' do
let(:can_push) { true }
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
......
......@@ -35,7 +35,7 @@ describe Gitlab::GitAccessWiki, lib: true do
end
end
describe '#download_access_check' do
describe '#access_check_download!' do
subject { access.check('git-upload-pack', '_any') }
before do
......
......@@ -251,6 +251,7 @@ DeployKey:
- type
- fingerprint
- public
- can_push
Service:
- id
- 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'
describe DeployKey, models: true do
include EmailHelpers
describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:projects) }
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
......@@ -307,4 +307,15 @@ describe Group, models: true do
it { is_expected.to be_valid }
it { expect(subject.parent).to be_kind_of(Group) }
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
require 'spec_helper'
describe Key, models: true do
include EmailHelpers
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
......@@ -96,4 +98,16 @@ describe Key, models: true do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
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
......@@ -284,12 +284,16 @@ describe MergeRequest, models: true do
end
describe '#issues_mentioned_but_not_closing' do
it 'detects issues mentioned in description but not closed' do
mentioned_issue = create(:issue, project: subject.project)
let(:closing_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.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).
and_return(subject.target_branch)
......
......@@ -105,4 +105,70 @@ describe GroupPolicy, models: true do
is_expected.to include(*owner_permissions)
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
......@@ -396,7 +396,7 @@ describe API::Helpers, api: true do
%w[HEAD GET].each do |method_name|
context "method is #{method_name}" 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
it 'does not raise an error' do
......@@ -410,7 +410,7 @@ describe API::Helpers, api: true do
%w[POST PUT PATCH DELETE].each do |method_name|
context "method is #{method_name}" 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
it 'calls authenticate!' do
......
......@@ -483,12 +483,26 @@ describe 'Git HTTP requests', lib: true do
shared_examples 'can download code only' 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.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
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
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
......
......@@ -132,12 +132,18 @@ describe Admin::HealthCheckController, "routing" do
end
describe Admin::GroupsController, "routing" do
let(:name) { 'complex.group-namegit' }
it "to #index" do
expect(get("/admin/groups")).to route_to('admin/groups#index')
end
it "to #show" do
expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab')
expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup')
expect(get("/admin/groups/#{name}")).to route_to('admin/groups#show', id: name)
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
......@@ -54,12 +54,37 @@ describe Users::RefreshAuthorizedProjectsService do
end
describe '#update_authorizations' do
it 'does nothing when there are no rows to add and remove' do
expect(user).not_to receive(:remove_project_authorizations)
expect(ProjectAuthorization).not_to receive(:insert_authorizations)
expect(user).not_to receive(:set_authorized_projects_column)
context 'when there are no rows to add and remove' do
it 'does not change authorizations' do
expect(user).not_to receive(:remove_project_authorizations)
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
it 'removes authorizations that should be removed' do
......@@ -84,7 +109,7 @@ describe Users::RefreshAuthorizedProjectsService do
it 'populates the authorized projects column' do
# make sure we start with a nil value no matter what the default in the
# 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]])
......
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