summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--PROCESS.md3
-rw-r--r--app/assets/javascripts/application.js8
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es62
-rw-r--r--app/assets/javascripts/boards/models/label.js.es62
-rw-r--r--app/assets/javascripts/boards/models/list.js.es62
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es62
-rw-r--r--app/assets/javascripts/boards/models/user.js.es62
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es64
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es610
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es62
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es62
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es62
-rw-r--r--app/assets/javascripts/preview_markdown.js115
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es62
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss3
-rw-r--r--app/assets/stylesheets/framework/nav.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss11
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss49
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/login.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb3
-rw-r--r--app/controllers/projects/issues_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/controllers/projects/snippets_controller.rb3
-rw-r--r--app/models/concerns/issuable.rb5
-rw-r--r--app/models/group.rb18
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/issuable_base_service.rb41
-rw-r--r--app/services/issues/base_service.rb4
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/projects/branches/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml30
-rw-r--r--changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml4
-rw-r--r--changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml4
-rw-r--r--changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml4
-rw-r--r--changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml4
-rw-r--r--changelogs/unreleased/24941-login-tabs-border.yml4
-rw-r--r--changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml4
-rw-r--r--changelogs/unreleased/26126-cache-even-when-no-projects.yml4
-rw-r--r--changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml4
-rw-r--r--changelogs/unreleased/dz-improve-admin-group-routing.yml4
-rw-r--r--changelogs/unreleased/fix-api-deprecation.yml4
-rw-r--r--changelogs/unreleased/fix-light-hr-in-descriptions.yml4
-rw-r--r--changelogs/unreleased/fix-mentioned-issue-text-grammar.yml4
-rw-r--r--changelogs/unreleased/gfm-new-line-fix.yml4
-rw-r--r--changelogs/unreleased/issue_22664.yml4
-rw-r--r--changelogs/unreleased/mentioned-but-not-closed-issues-messages.yml4
-rw-r--r--changelogs/unreleased/view-ce-vs-ee.yml4
-rw-r--r--config/routes/admin.rb2
-rw-r--r--db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb25
-rw-r--r--db/schema.rb2
-rw-r--r--doc/development/sidekiq_debugging.md13
-rw-r--r--doc/development/ux_guide/animation.md17
-rw-r--r--doc/development/ux_guide/basics.md14
-rw-r--r--doc/development/ux_guide/img/animation-autoscroll.gifbin0 -> 302217 bytes
-rw-r--r--doc/development/ux_guide/img/animation-reorder.gifbin0 -> 70515 bytes
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/notes.rb2
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb37
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb30
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb22
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb22
-rw-r--r--spec/features/admin/admin_groups_spec.rb10
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb12
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb4
-rw-r--r--spec/features/milestones/milestones_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb39
-rw-r--r--spec/models/group_spec.rb11
-rw-r--r--spec/models/merge_request_spec.rb10
-rw-r--r--spec/policies/group_policy_spec.rb66
-rw-r--r--spec/requests/api/helpers_spec.rb4
-rw-r--r--spec/routing/admin_routing_spec.rb10
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb12
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb11
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb2
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb37
-rw-r--r--spec/support/services/issuable_create_service_shared_examples.rb52
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb52
95 files changed, 771 insertions, 218 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4b2f041ab5..1baf5b3257c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ entry.
## 8.15.2 (2016-12-27)
+- 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
diff --git a/Gemfile b/Gemfile
index fff808e9805..eadb67405ee 100644
--- a/Gemfile
+++ b/Gemfile
@@ -347,5 +347,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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 765d57c6238..1c192d2fc6e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -773,7 +773,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)
@@ -982,7 +982,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.4)
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)
diff --git a/PROCESS.md b/PROCESS.md
index 8af660fbdd1..cbeb781cd3c 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -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.
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 043c6a11c4f..e43afbb4cc9 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -89,6 +89,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')) {
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 1199e022ff1..cd942c8332b 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -71,3 +71,5 @@ class ListIssue {
return Vue.http.patch(url, data);
}
}
+
+window.ListIssue = ListIssue;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 8f20a1bbec7..9af88d167d6 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -10,3 +10,5 @@ class ListLabel {
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
}
+
+window.ListLabel = ListLabel;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index a8d38c16485..fb13f529b11 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -148,3 +148,5 @@ class List {
});
}
}
+
+window.List = List;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
index 9c173c1b70b..c867b06d320 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -6,3 +6,5 @@ class ListMilestone {
this.title = obj.title;
}
}
+
+window.ListMilestone = ListMilestone;
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index a8a3892e2ad..8e9de4d4cbb 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -8,3 +8,5 @@ class ListUser {
this.avatar = user.avatar_url;
}
}
+
+window.ListUser = ListUser;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index 189a8703198..f18633f0913 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -65,4 +65,6 @@ class BoardService {
issue
});
}
-};
+}
+
+window.BoardService = BoardService;
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index 88a19fc6e1d..5852b8bbdb7 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -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();
});
}
},
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index efcd46680a7..fa518ba4d33 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -92,3 +92,5 @@ class DiscussionModel {
return false;
}
}
+
+window.DiscussionModel = DiscussionModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index e3bce1d2038..f3a7cba5ef6 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -9,3 +9,5 @@ class NoteModel {
this.resolved_by = resolved_by;
}
}
+
+window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
index 15ec7b76c3d..575a45d9802 100644
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -20,3 +20,5 @@ class EnvironmentsService {
return this.environments.get();
}
}
+
+window.EnvironmentsService = EnvironmentsService;
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 3857bbb743b..87c579ac757 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -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");
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 1e261cd49c2..89f7e976934 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,14 +1,17 @@
-/* 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);
+}());
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index 08264ad3d2f..03f4531abf5 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -76,3 +76,5 @@ class ProtectedBranchDropdown {
this.$dropdownFooter.toggleClass('hidden', !branchName);
}
}
+
+window.ProtectedBranchDropdown = ProtectedBranchDropdown;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5e3a91af86e..34757c57acf 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -66,6 +66,7 @@ pre {
hr {
margin: $gl-padding 0;
+ border-top: 1px solid darken($gray-normal, 8%);
}
.str-truncated {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 1b52a6a8e71..d6566dc4ec9 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -80,6 +80,9 @@
}
}
+ .about-gitlab {
+ color: $color-light;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index a87e4f2f299..5abda13a963 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -260,6 +260,10 @@
.nav-text,
.nav-controls {
width: auto;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 46a06cd7eab..a8641e83154 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -101,6 +101,17 @@
padding: 0 8px;
border-radius: 6px;
}
+
+ .about-gitlab {
+ padding: 7px $gl-sidebar-padding;
+ font-size: $gl-font-size;
+ line-height: 24px;
+ display: block;
+ text-decoration: none;
+ font-weight: normal;
+ position: absolute;
+ bottom: 10px;
+ }
}
.sidebar-action-buttons {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 3e1fded6b6b..b0c4a6edf57 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -489,9 +489,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
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9478b66f536..6b4d1f85564 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,52 +1,3 @@
-// Limit MR description for side-by-side diff view
-.container-limited {
- &.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 {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 237869aa544..d129eb12a45 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -98,7 +98,7 @@
}
.label {
- padding: 8px 9px 9px $gl-padding;
+ padding: 8px 9px 9px;
font-size: 14px;
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index dd27a06fcd2..712bd3da22f 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -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 {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 41b1b47713d..4cb53ab6fce 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -424,6 +424,10 @@
.merge-request-tabs-holder {
background-color: $white-light;
+ .container-limited {
+ max-width: $limited-layout-width;
+ }
+
&.affix {
top: 100px;
left: 0;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 61a3a03182a..add1c819adf 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -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])
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index d425d0f9014..e3933e3d7b1 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -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
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4f66e01e0f7..2beb0df8a07 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -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
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 3abebdfd032..fc8a289d49d 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,6 +38,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] }
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 0720be2e55d..02a97c1c574 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 0ea7b1b1098..5e63825bf99 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -92,8 +92,9 @@ module Issuable
after_save :record_metrics
def update_assignee_cache_counts
- # make sure we flush the cache for both the old *and* new assignee
- User.find(assignee_id_was).update_cache_counts if assignee_id_was
+ # make sure we flush the cache for both the old *and* new assignees(if they exist)
+ previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
+ previous_assignee.update_cache_counts if previous_assignee
assignee.update_cache_counts if assignee
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 85696ad9747..9888b242e98 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -161,15 +161,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
@@ -195,6 +197,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
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 22490d121c7..61845bf4036 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -576,11 +576,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
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 6f943feb2a7..0be6e113655 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -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)
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index b5db9c12622..eaf3035dfe1 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -245,7 +245,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
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index ab3d2a9a0cd..4ce5fd993d9 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -36,14 +36,10 @@ class IssuableBaseService < BaseService
end
end
- def filter_params(issuable_ability_name = :issue)
- filter_assignee
- filter_milestone
- filter_labels
+ def filter_params(issuable)
+ ability_name = :"admin_#{issuable.to_ability_name}"
- ability = :"admin_#{issuable_ability_name}"
-
- unless can?(current_user, ability, project)
+ unless can?(current_user, ability_name, project)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -52,14 +48,35 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id)
params.delete(:due_date)
end
+
+ filter_assignee(issuable)
+ filter_milestone
+ filter_labels
end
- def filter_assignee
- if params[:assignee_id] == IssuableFinder::NONE
- params[:assignee_id] = ''
+ def filter_assignee(issuable)
+ return unless params[:assignee_id].present?
+
+ assignee_id = params[:assignee_id]
+
+ if assignee_id.to_s == IssuableFinder::NONE
+ params[:assignee_id] = ""
+ else
+ params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
end
end
+ def assignee_can_read?(issuable, assignee_id)
+ new_assignee = User.find_by_id(assignee_id)
+
+ return false unless new_assignee.present?
+
+ ability_name = :"read_#{issuable.to_ability_name}"
+ resource = issuable.persisted? ? issuable : project
+
+ can?(new_assignee, ability_name, resource)
+ end
+
def filter_milestone
milestone_id = params[:milestone_id]
return unless milestone_id
@@ -138,7 +155,7 @@ class IssuableBaseService < BaseService
def create(issuable)
merge_slash_commands_into_params!(issuable)
- filter_params
+ filter_params(issuable)
params.delete(:state_event)
params[:author] ||= current_user
@@ -180,7 +197,7 @@ class IssuableBaseService < BaseService
change_state(issuable)
change_subscription(issuable)
change_todo(issuable)
- filter_params
+ filter_params(issuable)
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 742e834df97..35af867a098 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -17,10 +17,6 @@ module Issues
private
- def filter_params
- super(:issue)
- end
-
def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 800fd39c424..70e25956dc7 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,10 +38,6 @@ module MergeRequests
private
- def filter_params
- super(:merge_request)
- end
-
def merge_requests_for(branch)
origin_merge_requests = @project.origin_merge_requests
.opened.where(source_branch: branch).to_a
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 7d38ac3a374..8559908e0c3 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -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?
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 817e4bebb05..205d23178d2 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -35,3 +35,7 @@
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
Snippets
+
+ = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do
+ %span
+ About GitLab CE
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 5fd664c7a93..c2a5e725efc 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -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')
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 349181be784..34ead6427e0 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -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
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index f4aa1609a1b..40fbac7025a 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -42,6 +42,6 @@
- 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.
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index af3096f04d9..c98b2c42597 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -1,21 +1,23 @@
+- 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"
diff --git a/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml b/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml
new file mode 100644
index 00000000000..5570ede4a9a
--- /dev/null
+++ b/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent empty pagination when list is not empty
+merge_request: 8172
+author:
diff --git a/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml b/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml
new file mode 100644
index 00000000000..574c322803c
--- /dev/null
+++ b/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml
@@ -0,0 +1,4 @@
+---
+title: Improve visibility of "Resolve conflicts" and "Merge locally" actions
+merge_request: 8229
+author:
diff --git a/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml b/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml
new file mode 100644
index 00000000000..fac7697ede1
--- /dev/null
+++ b/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml
@@ -0,0 +1,4 @@
+---
+title: fix button layout issue on branches page
+merge_request: 8074
+author:
diff --git a/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml b/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml
new file mode 100644
index 00000000000..c31c89dc4bc
--- /dev/null
+++ b/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml
@@ -0,0 +1,4 @@
+---
+title: ensure permalinks scroll to correct position on multiple clicks
+merge_request: 8046
+author:
diff --git a/changelogs/unreleased/24941-login-tabs-border.yml b/changelogs/unreleased/24941-login-tabs-border.yml
new file mode 100644
index 00000000000..b06c21ad71a
--- /dev/null
+++ b/changelogs/unreleased/24941-login-tabs-border.yml
@@ -0,0 +1,4 @@
+---
+title: fix border in login session tabs
+merge_request: 8346
+author:
diff --git a/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml b/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml
new file mode 100644
index 00000000000..e60c42cdfba
--- /dev/null
+++ b/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml
@@ -0,0 +1,4 @@
+---
+title: Change status colors of runners to better defaults
+merge_request:
+author:
diff --git a/changelogs/unreleased/26126-cache-even-when-no-projects.yml b/changelogs/unreleased/26126-cache-even-when-no-projects.yml
new file mode 100644
index 00000000000..53e14ac9edf
--- /dev/null
+++ b/changelogs/unreleased/26126-cache-even-when-no-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Cache project authorizations even when user has access to zero projects
+merge_request: 8327
+author:
diff --git a/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml b/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml
new file mode 100644
index 00000000000..40183f8d3fa
--- /dev/null
+++ b/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml
@@ -0,0 +1,4 @@
+---
+title: Make CTRL+Enter submits a new merge request
+merge_request: 8360
+author: Saad Shahd
diff --git a/changelogs/unreleased/dz-improve-admin-group-routing.yml b/changelogs/unreleased/dz-improve-admin-group-routing.yml
new file mode 100644
index 00000000000..2360b965c90
--- /dev/null
+++ b/changelogs/unreleased/dz-improve-admin-group-routing.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 500 error when visit group from admin area if group name contains dot
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-api-deprecation.yml b/changelogs/unreleased/fix-api-deprecation.yml
new file mode 100644
index 00000000000..90285ddf058
--- /dev/null
+++ b/changelogs/unreleased/fix-api-deprecation.yml
@@ -0,0 +1,4 @@
+---
+title: Fix a Grape deprecation, use `#request_method` instead of `#route_method`
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-light-hr-in-descriptions.yml b/changelogs/unreleased/fix-light-hr-in-descriptions.yml
new file mode 100644
index 00000000000..8efd471e416
--- /dev/null
+++ b/changelogs/unreleased/fix-light-hr-in-descriptions.yml
@@ -0,0 +1,4 @@
+---
+title: Darkened hr border color in descriptions because of update of bootstrap
+merge_request: 8333
+author:
diff --git a/changelogs/unreleased/fix-mentioned-issue-text-grammar.yml b/changelogs/unreleased/fix-mentioned-issue-text-grammar.yml
new file mode 100644
index 00000000000..1d001e6b568
--- /dev/null
+++ b/changelogs/unreleased/fix-mentioned-issue-text-grammar.yml
@@ -0,0 +1,4 @@
+---
+title: Fix a minor grammar error in merge request widget
+merge_request: 8337
+author:
diff --git a/changelogs/unreleased/gfm-new-line-fix.yml b/changelogs/unreleased/gfm-new-line-fix.yml
new file mode 100644
index 00000000000..751bb356b5f
--- /dev/null
+++ b/changelogs/unreleased/gfm-new-line-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed GFM dropdown not showing on new lines
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_22664.yml b/changelogs/unreleased/issue_22664.yml
new file mode 100644
index 00000000000..18a8d9ec6be
--- /dev/null
+++ b/changelogs/unreleased/issue_22664.yml
@@ -0,0 +1,4 @@
+---
+title: Check if user can read project before being assigned to issue
+merge_request:
+author:
diff --git a/changelogs/unreleased/mentioned-but-not-closed-issues-messages.yml b/changelogs/unreleased/mentioned-but-not-closed-issues-messages.yml
new file mode 100644
index 00000000000..ba3b13bcdb7
--- /dev/null
+++ b/changelogs/unreleased/mentioned-but-not-closed-issues-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Fix unclear closing issue behaviour on Merge Request show page
+merge_request: 8345
+author: Gabriel Gizotti
diff --git a/changelogs/unreleased/view-ce-vs-ee.yml b/changelogs/unreleased/view-ce-vs-ee.yml
new file mode 100644
index 00000000000..38bce4ac7c3
--- /dev/null
+++ b/changelogs/unreleased/view-ce-vs-ee.yml
@@ -0,0 +1,4 @@
+---
+title: About GitLab link in sidebar that links to help page
+merge_request: 8316
+author:
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 0dd2c8f7aef..23295b43c92 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -32,7 +32,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
diff --git a/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
new file mode 100644
index 00000000000..50ad7437227
--- /dev/null
+++ b/db/migrate/20161227192806_rename_slack_and_mattermost_notification_services.rb
@@ -0,0 +1,25 @@
+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
diff --git a/db/schema.rb b/db/schema.rb
index e1e72bdae8f..923ece86edb 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161226122833) do
+ActiveRecord::Schema.define(version: 20161227192806) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/development/sidekiq_debugging.md b/doc/development/sidekiq_debugging.md
index cea11e5f126..d6d770e27c1 100644
--- a/doc/development/sidekiq_debugging.md
+++ b/doc/development/sidekiq_debugging.md
@@ -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
diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
index daeb15460c2..903e54bf9dc 100644
--- a/doc/development/ux_guide/animation.md
+++ b/doc/development/ux_guide/animation.md
@@ -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
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index e81729556d8..76ec7fd466b 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -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.
diff --git a/doc/development/ux_guide/img/animation-autoscroll.gif b/doc/development/ux_guide/img/animation-autoscroll.gif
new file mode 100644
index 00000000000..155b0234c64
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-autoscroll.gif
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-reorder.gif b/doc/development/ux_guide/img/animation-reorder.gif
new file mode 100644
index 00000000000..ccdeb3d396f
--- /dev/null
+++ b/doc/development/ux_guide/img/animation-reorder.gif
Binary files differ
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index fe00c83bff3..ee9247ee240 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -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!
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d0faf17714b..284e4cf549a 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -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,
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
new file mode 100644
index 00000000000..288984cfba9
--- /dev/null
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -0,0 +1,37 @@
+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
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index dbe5ddccbcf..e2321f2034b 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -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
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 440b897ddc6..2a411d78395 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -127,11 +127,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
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 72a3ebf2ebd..32b0e42c3cd 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -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) }
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 0aa01fc499a..9c19db6b420 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -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)
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 3489331a1b6..82c9bd0e6e6 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -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
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index dc32c8f7373..c73065cdce1 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -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
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index 8b603f51545..aadd72a9f8e 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -1,6 +1,8 @@
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
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4fa06a8c60a..1078c959419 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -44,6 +44,45 @@ describe Issue, "Issuable" do
it { expect(described_class).to respond_to(:assigned) }
end
+ describe "before_save" do
+ describe "#update_cache_counts" do
+ context "when previous assignee exists" do
+ before do
+ assignee = create(:user)
+ issue.project.team << [assignee, :developer]
+ issue.update(assignee: assignee)
+ end
+
+ it "updates cache counts for new assignee" do
+ user = create(:user)
+
+ expect(user).to receive(:update_cache_counts)
+
+ issue.update(assignee: user)
+ end
+
+ it "updates cache counts for previous assignee" do
+ old_assignee = issue.assignee
+ allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
+
+ expect(old_assignee).to receive(:update_cache_counts)
+
+ issue.update(assignee: nil)
+ end
+ end
+
+ context "when previous assignee does not exist" do
+ before{ issue.update(assignee: nil) }
+
+ it "updates cache count for the new assignee" do
+ expect_any_instance_of(User).to receive(:update_cache_counts)
+
+ issue.update(assignee: user)
+ end
+ end
+ end
+ end
+
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 7d5ecfbaa64..45fe927202b 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -277,4 +277,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
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 5da00a8636a..646e6c6dbb3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -283,12 +283,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)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index a20ac303a53..5c34ff04152 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -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
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index c3d7ac3eef8..b8ee2293a33 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -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
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 661b671301e..99c44bde151 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -122,12 +122,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
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 5f3020b6525..0475f38fe5e 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -52,7 +52,10 @@ describe Issuable::BulkUpdateService, services: true do
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
- result = bulk_update(issue, assignee_id: create(:user).id)
+ new_assignee = create(:user)
+ project.team << [new_assignee, :developer]
+
+ result = bulk_update(issue, assignee_id: new_assignee.id)
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
@@ -60,15 +63,16 @@ describe Issuable::BulkUpdateService, services: true do
it 'updates the assignee to the use ID passed' do
assignee = create(:user)
+ project.team << [assignee, :developer]
expect { bulk_update(issue, assignee_id: assignee.id) }
.to change { issue.reload.assignee }.from(user).to(assignee)
end
end
- context 'when the new assignee ID is -1' do
- it 'unassigns the issues' do
- expect { bulk_update(issue, assignee_id: -1) }
+ context "when the new assignee ID is #{IssuableFinder::NONE}" do
+ it "unassigns the issues" do
+ expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) }
.to change { issue.reload.assignee }.to(nil)
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 8bde61ee336..ac3834c32ff 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -135,6 +135,8 @@ describe Issues::CreateService, services: true do
end
end
+ it_behaves_like 'issuable create service'
+
it_behaves_like 'new issuable record that supports slash commands'
context 'for a merge request' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index eafbea46905..d83b09fd32c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -142,6 +142,17 @@ describe Issues::UpdateService, services: true do
update_issue(confidential: true)
end
+
+ it 'does not update assignee_id with unauthorized users' do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ update_issue(confidential: true)
+ non_member = create(:user)
+ original_assignee = issue.assignee
+
+ update_issue(assignee_id: non_member.id)
+
+ expect(issue.reload.assignee_id).to eq(original_assignee.id)
+ end
end
context 'todos' do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index b8142889075..673c0bd6c9c 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -84,6 +84,8 @@ describe MergeRequests::CreateService, services: true do
end
end
+ it_behaves_like 'issuable create service'
+
context 'while saving references to issues that the created merge request closes' do
let(:first_issue) { create(:issue, project: project) }
let(:second_issue) { create(:issue, project: project) }
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index d1099884a02..960b5cd5e6f 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -5,6 +5,8 @@ describe Notes::SlashCommandsService, services: true do
let(:project) { create(:empty_project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:assignee) { create(:user) }
+
+ before { project.team << [assignee, :master] }
end
shared_examples 'note on noteable that does not support slash commands' do
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 72c8f7cd8ec..1f6919151de 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -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]])
diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
new file mode 100644
index 00000000000..93c0267d2db
--- /dev/null
+++ b/spec/support/services/issuable_create_service_shared_examples.rb
@@ -0,0 +1,52 @@
+shared_examples 'issuable create service' do
+ context 'asssignee_id' do
+ let(:assignee) { create(:user) }
+
+ before { project.team << [user, :master] }
+
+ it 'removes assignee_id when user id is invalid' do
+ opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+
+ it 'removes assignee_id when user id is 0' do
+ opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ project.team << [assignee, :master]
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to eq(assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ project.update(visibility_level: level)
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 5f9645ed44f..dd54b0addda 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -11,6 +11,8 @@ shared_examples 'new issuable record that supports slash commands' do
let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
let(:issuable) { described_class.new(project, user, params).execute }
+ before { project.team << [assignee, :master ] }
+
context 'with labels in command only' do
let(:example_params) do
{
@@ -55,7 +57,7 @@ shared_examples 'new issuable record that supports slash commands' do
context 'with assignee and milestone in params and command' do
let(:example_params) do
{
- assignee: build_stubbed(:user),
+ assignee: create(:user),
milestone_id: double(:milestone),
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
}
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
index a3336755773..49cea1e608c 100644
--- a/spec/support/services/issuable_update_service_shared_examples.rb
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -1,4 +1,8 @@
shared_examples 'issuable update service' do
+ def update_issuable(opts)
+ described_class.new(project, user, opts).execute(open_issuable)
+ end
+
context 'changing state' do
before { expect(project).to receive(:execute_hooks).once }
@@ -14,4 +18,52 @@ shared_examples 'issuable update service' do
end
end
end
+
+ context 'asssignee_id' do
+ it 'does not update assignee when assignee_id is invalid' do
+ open_issuable.update(assignee_id: user.id)
+
+ update_issuable(assignee_id: -1)
+
+ expect(open_issuable.reload.assignee).to eq(user)
+ end
+
+ it 'unassigns assignee when user id is 0' do
+ open_issuable.update(assignee_id: user.id)
+
+ update_issuable(assignee_id: 0)
+
+ expect(open_issuable.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ update_issuable(assignee_id: user.id)
+
+ expect(open_issuable.assignee_id).to eq(user.id)
+ end
+
+ it 'does not update assignee_id when user cannot read issue' do
+ non_member = create(:user)
+ original_assignee = open_issuable.assignee
+
+ update_issuable(assignee_id: non_member.id)
+
+ expect(open_issuable.assignee_id).to eq(original_assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ assignee = create(:user)
+ project.update(visibility_level: level)
+ feature_visibility_attr = :"#{open_issuable.model_name.plural}_access_level"
+ project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+ expect{ update_issuable(assignee_id: assignee) }.not_to change{ open_issuable.assignee }
+ end
+ end
+ end
+ end
end