Commit aafb0171 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into all-skipped-equals-success

* upstream/master: (55 commits)
  Restrict failed login attempts for users with 2FA
  Update RuboCop to 0.43.0 and update configuration
  Use SELECT 1, instead SELECT COUNT(*) to ask for notes existency
  Simplify Mentionable concern instance methods
  Fix issues importing services via Import/Export
  deployment refs in own folder, new method for creating refs
  Update method name
  Save a fetchable ref per deployement
  GrapeDSL for Namespace endpoint
  Remove SCSS rules for short hex chars.
  Fix bug when trying to cache closed issues from external issue trackers
  Upgrade acts-as-taggable-on from 3.5.0 to 4.0.0.
  Remove useless code now that Member#add_user handles it
  Combine requestFileSuccess arguments into `opts`
  Append issue template to existing description
  Invert method's naming
  Fix a few things after the initial improvment to Members::DestroyService
  Improve Members::DestroyService
  Add Container Registry on/off status to admin area
  Enable Lint/StringConversionInInterpolation cop and autocorrect offenses
  ...
parents 218331e6 c38b85f3
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"always-semicolon": true, "always-semicolon": true,
"color-case": "lower", "color-case": "lower",
"block-indent": " ", "block-indent": " ",
"color-shorthand": true, "color-shorthand": false,
"element-case": "lower", "element-case": "lower",
"space-before-colon": "", "space-before-colon": "",
"space-after-colon": " ", "space-after-colon": " ",
......
...@@ -453,6 +453,10 @@ Style/VariableName: ...@@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case EnforcedStyle: snake_case
Enabled: true Enabled: true
# Use the configured style when numbering variables.
Style/VariableNumber:
Enabled: false
# Use when x then ... for one-line cases. # Use when x then ... for one-line cases.
Style/WhenThen: Style/WhenThen:
Enabled: true Enabled: true
...@@ -639,6 +643,10 @@ Lint/RescueException: ...@@ -639,6 +643,10 @@ Lint/RescueException:
Lint/ShadowedException: Lint/ShadowedException:
Enabled: false Enabled: false
# Checks for Object#to_s usage in string interpolation.
Lint/StringConversionInInterpolation:
Enabled: true
# Do not use prefix `_` for a variable that is used. # Do not use prefix `_` for a variable that is used.
Lint/UnderscorePrefixedVariableName: Lint/UnderscorePrefixedVariableName:
Enabled: true Enabled: true
......
This diff is collapsed.
...@@ -79,7 +79,7 @@ linters: ...@@ -79,7 +79,7 @@ linters:
# HEX colors should use three-character values where possible. # HEX colors should use three-character values where possible.
HexLength: HexLength:
enabled: true enabled: false
# HEX color values should use lower-case colors to differentiate between # HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
......
...@@ -8,9 +8,11 @@ v 8.13.0 (unreleased) ...@@ -8,9 +8,11 @@ v 8.13.0 (unreleased)
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell) - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
...@@ -20,6 +22,7 @@ v 8.13.0 (unreleased) ...@@ -20,6 +22,7 @@ v 8.13.0 (unreleased)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604 - Fix that manual jobs would no longer block jobs in the next stage. !6604
- Add configurable email subject suffix (Fu Xu)
- Use a ConnectionPool for Rails.cache on Sidekiq servers - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend` - Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users. - Enable GitLab Import/Export for non-admin users.
...@@ -27,6 +30,7 @@ v 8.13.0 (unreleased) ...@@ -27,6 +30,7 @@ v 8.13.0 (unreleased)
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496 - Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Append issue template to existing description !6149 (Joseph Frazier)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
...@@ -42,8 +46,14 @@ v 8.13.0 (unreleased) ...@@ -42,8 +46,14 @@ v 8.13.0 (unreleased)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
v 8.12.4 (unreleased) v 8.12.4 (unreleased)
- Fix type mismatch bug when closing Jira issue
- Fix issues importing services via Import/Export
- Restrict failed login attempts for users with 2FA enabled
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
v 8.12.3 v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves - Update Gitlab Shell to support low IO priority for storage moves
...@@ -102,6 +112,7 @@ v 8.12.0 ...@@ -102,6 +112,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width - Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page - Fix pagination on user snippets page
- Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735 - Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Fix download artifacts button links !6407 - Fix download artifacts button links !6407
...@@ -118,6 +129,7 @@ v 8.12.0 ...@@ -118,6 +129,7 @@ v 8.12.0
- Reduce contributions calendar data payload (ClemMakesApps) - Reduce contributions calendar data payload (ClemMakesApps)
- Show all pipelines for merge requests even from discarded commits !6414 - Show all pipelines for merge requests even from discarded commits !6414
- Replace contributions calendar timezone payload with dates (ClemMakesApps) - Replace contributions calendar timezone payload with dates (ClemMakesApps)
- Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278 - Enable pipeline events by default !6278
- Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
...@@ -141,6 +153,7 @@ v 8.12.0 ...@@ -141,6 +153,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files - Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps) - Add textarea autoresize after comment (ClemMakesApps)
- Do not write SSH public key 'comments' to authorized_keys !6381 - Do not write SSH public key 'comments' to authorized_keys !6381
- Add due date to issue todos
- Refresh todos count cache when an Issue/MR is deleted - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions - Hides merge request button on branches page is user doesn't have permissions
......
...@@ -99,17 +99,17 @@ gem 'unf', '~> 0.1.4' ...@@ -99,17 +99,17 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5' gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
gem 'github-markup', '~> 1.4' gem 'github-markup', '~> 1.4'
gem 'redcarpet', '~> 3.3.3' gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6' gem 'rdoc', '~>3.6'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0' ...@@ -130,7 +130,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0' gem 'after_commit_queue', '~> 1.3.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 3.4' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2'
...@@ -295,7 +295,7 @@ group :development, :test do ...@@ -295,7 +295,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.42.0', require: false gem 'rubocop', '~> 0.43.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false gem 'haml_lint', '~> 0.18.2', require: false
......
...@@ -44,8 +44,8 @@ GEM ...@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0) acts-as-taggable-on (4.0.0)
activerecord (>= 3.2, < 5) activerecord (>= 4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -157,6 +157,10 @@ GEM ...@@ -157,6 +157,10 @@ GEM
database_cleaner (1.5.3) database_cleaner (1.5.3)
debug_inspector (0.0.2) debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8) debugger-ruby_core_source (1.3.8)
deckar01-task_list (1.0.5)
activesupport (~> 4.0)
html-pipeline
rack (~> 1.0)
default_value_for (3.0.2) default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1) activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
...@@ -483,7 +487,7 @@ GEM ...@@ -483,7 +487,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
paranoia (2.1.4) paranoia (2.1.4)
activerecord (~> 4.0) activerecord (~> 4.0)
parser (2.3.1.2) parser (2.3.1.4)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
pkg-config (1.1.7) pkg-config (1.1.7)
...@@ -616,7 +620,7 @@ GEM ...@@ -616,7 +620,7 @@ GEM
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.42.0) rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
...@@ -725,8 +729,6 @@ GEM ...@@ -725,8 +729,6 @@ GEM
ffi ffi
sysexits (1.2.0) sysexits (1.2.0)
systemu (2.6.5) systemu (2.6.5)
task_list (1.0.2)
html-pipeline
teaspoon (1.1.5) teaspoon (1.1.5)
railties (>= 3.2.5, < 6) railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0) teaspoon-jasmine (2.2.0)
...@@ -800,7 +802,7 @@ DEPENDENCIES ...@@ -800,7 +802,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0) after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
...@@ -831,6 +833,7 @@ DEPENDENCIES ...@@ -831,6 +833,7 @@ DEPENDENCIES
creole (~> 0.5.0) creole (~> 0.5.0)
d3_rails (~> 3.5.0) d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 1.0.5)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
...@@ -935,7 +938,7 @@ DEPENDENCIES ...@@ -935,7 +938,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0) rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0) rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
...@@ -963,7 +966,6 @@ DEPENDENCIES ...@@ -963,7 +966,6 @@ DEPENDENCIES
sprockets-es6 (~> 0.9.2) sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0) state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6) sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
teaspoon (~> 1.1.0) teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0) teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2) test_after_commit (~> 0.4.2)
...@@ -984,4 +986,4 @@ DEPENDENCIES ...@@ -984,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.13.1 1.13.2
...@@ -72,9 +72,17 @@ ...@@ -72,9 +72,17 @@
// To be implemented on the extending class // To be implemented on the extending class
// e.g. // e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@) // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { TemplateSelector.prototype.requestFileSuccess = function(file, opts) {
this.editor.setValue(file.content, 1); var oldValue = this.editor.getValue();
if (!skipFocus) this.editor.focus(); var newValue = file.content;
if (opts == null) {
opts = {};
}
if (opts.append && oldValue.length && oldValue !== newValue) {
newValue = oldValue + '\n\n' + newValue;
}
this.editor.setValue(newValue, 1);
if (!opts.skipFocus) this.editor.focus();
if (this.editor instanceof jQuery) { if (this.editor instanceof jQuery) {
this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
......
...@@ -26,15 +26,15 @@ ...@@ -26,15 +26,15 @@
}; };
showTooltip = function(target, title) { showTooltip = function(target, title) {
return $(target).tooltip({ var $target = $(target);
container: 'body', var originalTitle = $target.data('original-title');
html: 'true',
placement: 'auto bottom', $target
title: title, .attr('title', 'Copied!')
trigger: 'manual' .tooltip('fixTitle')
}).tooltip('show').one('mouseleave', function() { .tooltip('show')
return $(this).tooltip('hide'); .attr('title', originalTitle)
}); .tooltip('fixTitle');
}; };
$(function() { $(function() {
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
function Diff() { function Diff() {
$('.files .diff-file').singleFileDiff(); $('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton(); this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold'); $(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) { $(document).on('click', '.js-unfold', (function(_this) {
return function(event) { return function(event) {
...@@ -52,6 +55,10 @@ ...@@ -52,6 +55,10 @@
})(this)); })(this));
} }
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) { Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) { if (!line.children().length) {
return [0, 0]; return [0, 0];
......
...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs'; ...@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider { class MergeConflictDataProvider {
getInitialData() { getInitialData() {
// TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view'); const diffViewType = $.cookie('diff_view');
const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return { return {
isLoading : true, isLoading : true,
hasError : false, hasError : false,
isParallel : diffViewType === 'parallel', isParallel : diffViewType === 'parallel',
diffViewType : diffViewType, diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false, isSubmitting : false,
conflictsData : {}, conflictsData : {},
resolutionData : {} resolutionData : {}
...@@ -192,14 +195,17 @@ class MergeConflictDataProvider { ...@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) { updateViewType(newType) {
const vi = this.vueInstance; const vi = this.vueInstance;
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) { if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return; return;
} }
vi.diffView = newType; vi.diffViewType = newType;
vi.isParallel = newType === 'parallel'; vi.isParallel = newType === 'parallel';
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added. $.cookie('diff_view', newType, {
$('.content-wrapper .container-fluid').toggleClass('container-limited'); path: (gon && gon.relative_url_root) || '/'
});
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
} }
......
...@@ -60,9 +60,8 @@ class MergeConflictResolver { ...@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight(); $('#conflicts .js-syntax-highlight').syntaxHighlight();
}); });
if (this.vue.diffViewType === 'parallel') { $('.content-wrapper .container-fluid')
$('.content-wrapper .container-fluid').removeClass('container-limited'); .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
}
}) })
} }
......
...@@ -36,13 +36,10 @@ ...@@ -36,13 +36,10 @@
}; };
MergeRequest.prototype.initTabs = function() { MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') { if (window.mrTabs) {
// `MergeRequests#new` has no tab-persisting or lazy-loading behavior window.mrTabs.unbindEvents();
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
// Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
} }
window.mrTabs = new MergeRequestTabs(this.opts);
}; };
MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.showAllCommits = function() {
......
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
MergeRequestTabs.prototype.commitsLoaded = false; MergeRequestTabs.prototype.commitsLoaded = false;
MergeRequestTabs.prototype.fixedLayoutPref = null;
function MergeRequestTabs(opts) { function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true; this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
...@@ -70,7 +72,12 @@ ...@@ -70,7 +72,12 @@
MergeRequestTabs.prototype.bindEvents = function() { MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown); $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
return $(document).on('click', '.js-show-tab', this.showTab); $(document).on('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.unbindEvents = function() {
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).off('click', '.js-show-tab', this.showTab);
}; };
MergeRequestTabs.prototype.showTab = function(event) { MergeRequestTabs.prototype.showTab = function(event) {
...@@ -85,11 +92,15 @@ ...@@ -85,11 +92,15 @@
if (action === 'commits') { if (action === 'commits') {
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'diffs') { } else if (action === 'diffs') {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') { if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight(); navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", { $.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight offset: -navBarHeight
...@@ -97,11 +108,14 @@ ...@@ -97,11 +108,14 @@
} else if (action === 'builds') { } else if (action === 'builds') {
this.loadBuilds($target.attr('href')); this.loadBuilds($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.loadPipelines($target.attr('href')); this.loadPipelines($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer();
} }
if (this.opts.setUrl) { if (this.opts.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -126,7 +140,7 @@ ...@@ -126,7 +140,7 @@
if (action === 'show') { if (action === 'show') {
action = 'notes'; action = 'notes';
} }
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show'); $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
}; };
// Replaces the current Merge Request-specific action in the URL with a new one // Replaces the current Merge Request-specific action in the URL with a new one
...@@ -209,7 +223,7 @@ ...@@ -209,7 +223,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); $('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff(); $('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel') { if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
...@@ -308,11 +322,21 @@ ...@@ -308,11 +322,21 @@
MergeRequestTabs.prototype.diffViewType = function() { MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
// Returns diff view type
}; };
MergeRequestTabs.prototype.expandViewContainer = function() { MergeRequestTabs.prototype.expandViewContainer = function() {
return $('.container-fluid').removeClass('container-limited'); var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
};
MergeRequestTabs.prototype.resetViewContainer = function() {
if (this.fixedLayoutPref !== null) {
$('.content-wrapper .container-fluid')
.toggleClass('container-limited', this.fixedLayoutPref);
}
}; };
MergeRequestTabs.prototype.shrinkView = function() { MergeRequestTabs.prototype.shrinkView = function() {
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery); if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => { $('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent(); if (this.currentTemplate) this.setInputValueToTemplateContent(false);
}); });
} }
...@@ -26,22 +26,24 @@ ...@@ -26,22 +26,24 @@
this.currentTemplate = currentTemplate; this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner(); this.stopLoadingSpinner();
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent(true);
}); });
return; return;
} }
setInputValueToTemplateContent() { setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field // `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected. // to the content of the template selected. If `append` is true, the
// template content will be appended to the previous value of the field,
// separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') { if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and // If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd // skip focusing the description input by setting `true` as the
// argument to `requestFileSuccess`. // `skipFocus` option to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true); this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus(); this.titleInput.focus();
} else { } else {
this.requestFileSuccess(this.currentTemplate); this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
} }
return; return;
} }
......
...@@ -70,7 +70,8 @@ ...@@ -70,7 +70,8 @@
&.ci-success { &.ci-success {
color: $gl-success; color: $gl-success;
a.environment { a.environment,
a.pipeline {
color: inherit; color: inherit;
} }
} }
......
...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor ...@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user) setup_u2f_authentication(user)
render 'devise/sessions/two_factor' render 'devise/sessions/two_factor'
end end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id] if user.access_locked?
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id] elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user) authenticate_with_two_factor_via_u2f(user)
...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor ...@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.' flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor prompt_for_two_factor(user)
end end
end end
...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor ...@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.' flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
......
...@@ -15,18 +15,17 @@ module MembershipActions ...@@ -15,18 +15,17 @@ module MembershipActions
end end
def leave def leave
@member = membershipable.members.find_by(user_id: current_user) || member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
membershipable.requesters.find_by(user_id: current_user) execute(:all)
Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false) source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if @member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
else else
"You left the \"#{@member.source.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
end end
......
...@@ -7,7 +7,7 @@ module SpammableActions ...@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam def mark_as_spam
if SpamService.new(spammable).mark_as_spam! if SpamService.new(spammable).mark_as_spam!
redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully." redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
else else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end end
......
...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
def destroy def destroy
@group_member = @group.members.find_by(id: params[:id]) || Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
@group.requesters.find_by(id: params[:id])
Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
......
...@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
def destroy def destroy
@project_member = @project.members.find_by(id: params[:id]) || Members::DestroyService.new(@project, current_user, params).
@project.requesters.find_by(id: params[:id]) execute(:all)
Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -92,12 +92,8 @@ module PageLayoutHelper ...@@ -92,12 +92,8 @@ module PageLayoutHelper
end end
end end
def fluid_layout(enabled = false) def fluid_layout
if @fluid_layout.nil? current_user && current_user.layout == "fluid"
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
end end
def blank_container(enabled = false) def blank_container(enabled = false)
......
...@@ -139,7 +139,7 @@ module ProjectsHelper ...@@ -139,7 +139,7 @@ module ProjectsHelper
end end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe
end end
private private
......
...@@ -114,6 +114,26 @@ module TodosHelper ...@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type selected_type ? selected_type[:text] : default_type
end end
def todo_due_date(todo)
return unless todo.target.try(:due_date)
is_due_today = todo.target.due_date.today?
is_overdue = todo.target.overdue?
css_class =
if is_due_today
'text-warning'
elsif is_overdue
'text-danger'
else
''
end
html = "&middot; ".html_safe
html << content_tag(:span, class: css_class) do
"Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
end
end
private private
def show_todo_state?(todo) def show_todo_state?(todo)
......
...@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer ...@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer
default reply_to: Gitlab.config.gitlab.email_reply_to default reply_to: Gitlab.config.gitlab.email_reply_to
layout 'devise_mailer' layout 'devise_mailer'
protected
def subject_for(key)
subject = super
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
end end
...@@ -45,7 +45,7 @@ module Emails ...@@ -45,7 +45,7 @@ module Emails
@token = token @token = token
mail(to: member.invite_email, mail(to: member.invite_email,
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}") subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end end
def member_invite_accepted_email(member_source_type, member_id) def member_invite_accepted_email(member_source_type, member_id)
......
...@@ -92,6 +92,7 @@ class Notify < BaseMailer ...@@ -92,6 +92,7 @@ class Notify < BaseMailer
subject = "" subject = ""
subject << "#{@project.name} | " if @project subject << "#{@project.name} | " if @project
subject << extra.join(' | ') if extra.present? subject << extra.join(' | ') if extra.present?
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject subject
end end
......
...@@ -80,7 +80,7 @@ class CommitRange ...@@ -80,7 +80,7 @@ class CommitRange
end end
def inspect def inspect
%(#<#{self.class}:#{object_id} #{to_s}>) %(#<#{self.class}:#{object_id} #{self}>)
end end
def to_s def to_s
......
...@@ -43,19 +43,15 @@ module Mentionable ...@@ -43,19 +43,15 @@ module Mentionable
self self
end end
def all_references(current_user = nil, text = nil, extractor: nil) def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor. extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user) new(project, current_user)
if text self.class.mentionable_attrs.each do |attr, options|
extractor.analyze(text, author: author) text = __send__(attr)
else options = options.merge(cache_key: [self, attr], author: author)
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
extractor.analyze(text, options) extractor.analyze(text, options)
end
end end
extractor extractor
...@@ -66,8 +62,8 @@ module Mentionable ...@@ -66,8 +62,8 @@ module Mentionable
end end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = nil) def referenced_mentionables(current_user = self.author)
refs = all_references(current_user, text) refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits) refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
...@@ -77,8 +73,8 @@ module Mentionable ...@@ -77,8 +73,8 @@ module Mentionable
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
def create_cross_references!(author = self.author, without = [], text = nil) def create_cross_references!(author = self.author, without = [])
refs = referenced_mentionables(author, text) refs = referenced_mentionables(author)
# We're using this method instead of Array diffing because that requires # We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the # both of the object's `hash` values to be the same, which may not be the
...@@ -97,10 +93,7 @@ module Mentionable ...@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty? return if changes.empty?
original_text = changes.collect { |_, vals| vals.first }.join(' ') create_cross_references!(author)
preexisting = referenced_mentionables(author, original_text)
create_cross_references!(author, preexisting)
end end
private private
......
...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base ...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true delegate :name, to: :environment, prefix: true
after_save :keep_around_commit after_save :create_ref
def commit def commit
project.commit(sha) project.commit(sha)
...@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base ...@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment self == environment.last_deployment
end end
def keep_around_commit def create_ref
project.repository.keep_around(self.sha) project.repository.create_ref(ref, ref_path)
end end
def manual_actions def manual_actions
...@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base ...@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id). where.not(id: self.id).
take take
end end
private
def ref_path
File.join(environment.ref_path, 'deployments', id.to_s)
end
end end
...@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base ...@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics? def update_merge_request_metrics?
self.name == "production" self.name == "production"
end end
def ref_path
"refs/environments/#{Shellwords.shellescape(name)}"
end
end end
...@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -523,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately. # running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author) def cache_merge_request_closes_issues!(current_user = self.author)
return if project.has_external_issue_tracker?
transaction do transaction do
self.merge_requests_closing_issues.delete_all self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue| closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue) self.merge_requests_closing_issues.create!(issue: issue)
end end
......
...@@ -11,7 +11,7 @@ class SlackService ...@@ -11,7 +11,7 @@ class SlackService
attr_reader :description attr_reader :description
def initialize(params) def initialize(params)
@user_name = params[:user][:name] @user_name = params[:user][:username]
@project_name = params[:project_name] @project_name = params[:project_name]
@project_url = params[:project_url] @project_url = params[:project_url]
......
...@@ -10,7 +10,7 @@ class SlackService ...@@ -10,7 +10,7 @@ class SlackService
attr_reader :title attr_reader :title
def initialize(params) def initialize(params)
@user_name = params[:user][:name] @user_name = params[:user][:username]
@project_name = params[:project_name] @project_name = params[:project_name]
@project_url = params[:project_url] @project_url = params[:project_url]
......
...@@ -10,7 +10,7 @@ class SlackService ...@@ -10,7 +10,7 @@ class SlackService
def initialize(params) def initialize(params)
params = HashWithIndifferentAccess.new(params) params = HashWithIndifferentAccess.new(params)
@user_name = params[:user][:name] @user_name = params[:user][:username]
@project_name = params[:project_name] @project_name = params[:project_name]
@project_url = params[:project_url] @project_url = params[:project_url]
......
...@@ -9,7 +9,7 @@ class SlackService ...@@ -9,7 +9,7 @@ class SlackService
attr_reader :description attr_reader :description
def initialize(params) def initialize(params)
@user_name = params[:user][:name] @user_name = params[:user][:username]
@project_name = params[:project_name] @project_name = params[:project_name]
@project_url = params[:project_url] @project_url = params[:project_url]
......
...@@ -997,6 +997,10 @@ class Repository ...@@ -997,6 +997,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
end end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
end
def update_branch_with_hooks(current_user, branch) def update_branch_with_hooks(current_user, branch)
update_autocrlf_option update_autocrlf_option
......
...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base ...@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end end
def #{arg}=(value) def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value self.properties['#{arg}'] = value
end end
......
...@@ -827,6 +827,22 @@ class User < ActiveRecord::Base ...@@ -827,6 +827,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true) todos_pending_count(force: true)
end end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
end
private private
def projects_union(min_access_level = nil) def projects_union(min_access_level = nil)
......
...@@ -14,6 +14,8 @@ module Members ...@@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user if member.request? && member.user != user
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
end end
member
end end
end end
end end
module Members module Members
class DestroyService < BaseService class DestroyService < BaseService
attr_accessor :member, :current_user include MembersHelper
def initialize(member, current_user) attr_accessor :source
@member = member
ALLOWED_SCOPES = %i[members requesters all]
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user @current_user = current_user
@params = params
end end
def execute def execute(scope = :members)
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
raise Gitlab::Access::AccessDeniedError
end member = find_member!(scope)
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
AuthorizedDestroyService.new(member, current_user).execute AuthorizedDestroyService.new(member, current_user).execute
end end
private
def find_member!(scope)
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
case scope
when :all
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition)
end
end
def can_destroy_member?(member)
member && can?(current_user, action_member_permission(:destroy, member), member)
end
end end
end end
...@@ -72,7 +72,7 @@ class SystemHooksService ...@@ -72,7 +72,7 @@ class SystemHooksService
return 'user_add_to_group' if event == :create return 'user_add_to_group' if event == :create
return 'user_remove_from_group' if event == :destroy return 'user_remove_from_group' if event == :destroy
else else
"#{model.class.name.downcase}_#{event.to_s}" "#{model.class.name.downcase}_#{event}"
end end
end end
......
...@@ -246,7 +246,7 @@ module SystemNoteService ...@@ -246,7 +246,7 @@ module SystemNoteService
'deleted' 'deleted'
end end
body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize body = "#{verb} #{branch_type} branch `#{branch}`".capitalize
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
...@@ -347,7 +347,7 @@ module SystemNoteService ...@@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id) notes = notes.where(noteable_id: noteable.id)
end end
notes_for_mentioner(mentioner, noteable, notes).count > 0 notes_for_mentioner(mentioner, noteable, notes).exists?
end end
# Build an Array of lines detailing each commit added in a merge request # Build an Array of lines detailing each commit added in a merge request
......
...@@ -63,6 +63,11 @@ ...@@ -63,6 +63,11 @@
Reply by email Reply by email
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
%p
Container Registry
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
.col-md-4 .col-md-4
%h4 %h4
Components Components
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
(removed) (removed)
&middot; #{time_ago_with_tooltip(todo.created_at)} &middot; #{time_ago_with_tooltip(todo.created_at)}
= todo_due_date(todo)
.todo-body .todo-body
.todo-note .todo-note
......
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" } %div{ class: "container-fluid" }
.header-content .header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
......
...@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>') ...@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class // Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') { if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper').find('.container-fluid').removeClass('container-limited') $('.content-wrapper .container-fluid').removeClass('container-limited')
} else { } else {
$('.content-wrapper').find('.container-fluid').addClass('container-limited') $('.content-wrapper .container-fluid').addClass('container-limited')
} }
// Re-enable the "Save" button // Re-enable the "Save" button
......
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- diff_files = diffs.diff_files - diff_files = diffs.diff_files
- if diff_view == :parallel
- fluid_layout true
.content-block.oneline-block.files-changed .content-block.oneline-block.files-changed
.inline-parallel-buttons .inline-parallel-buttons
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
- if diff_view == :parallel
- fluid_layout true
.merge-request{'data-url' => merge_request_path(@merge_request)} .merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
......
...@@ -4,14 +4,15 @@ ...@@ -4,14 +4,15 @@
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
= ci_icon_for_status(status) = ci_icon_for_status(status)
%span %span
CI build Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status) = ci_label_for_status(status)
for for
- commit = @merge_request.diff_head_commit - commit = @merge_request.diff_head_commit
= succeed "." do = succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage %span.ci-coverage
= link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'}
- elsif @merge_request.has_ci? - elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
......
...@@ -70,6 +70,7 @@ production: &base ...@@ -70,6 +70,7 @@ production: &base
email_from: example@example.com email_from: example@example.com
email_display_name: GitLab email_display_name: GitLab
email_reply_to: noreply@example.com email_reply_to: noreply@example.com
email_subject_suffix: ''
# Email server smtp settings are in config/initializers/smtp_settings.rb.sample # Email server smtp settings are in config/initializers/smtp_settings.rb.sample
......
...@@ -186,6 +186,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni ...@@ -186,6 +186,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni
Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}" Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || ""
Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url) Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url)
Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git' Settings.gitlab['user'] ||= 'git'
......
...@@ -13,15 +13,17 @@ override certain values. ...@@ -13,15 +13,17 @@ override certain values.
Variable | Type | Description Variable | Type | Description
-------- | ---- | ----------- -------- | ---- | -----------
`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development` `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab `GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab `GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab `GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer `GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer `GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
## Complete database variables ## Complete database variables
......
...@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track ...@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track
Deployments are created when [jobs] deploy versions of code to [environments]. Deployments are created when [jobs] deploy versions of code to [environments].
### Checkout deployments locally
Since 8.13, a reference in the git repository is saved for each deployment. So
knowing what the state is of your current environments is only a `git fetch`
away.
In your git config, append the `[remote "<your-remote>"]` block with an extra
fetch line:
```
fetch = +refs/environments/*:refs/remotes/origin/environments/*
```
## Defining environments ## Defining environments
You can create and delete environments manually in the web interface, but we You can create and delete environments manually in the web interface, but we
......
...@@ -75,9 +75,8 @@ module API ...@@ -75,9 +75,8 @@ module API
required_attributes! [:user_id] required_attributes! [:user_id]
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
access_requester = source.requesters.find_by!(user_id: params[:user_id]) ::Members::DestroyService.new(source, current_user, params).
execute(:requesters)
::Members::DestroyService.new(access_requester, current_user).execute
end end
end end
end end
......
...@@ -8,16 +8,19 @@ module API ...@@ -8,16 +8,19 @@ module API
awardable_string = awardable_type.pluralize awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id" awardable_id_string = "#{awardable_type}_id"
params do
requires :id, type: String, desc: 'The ID of a project'
requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
end
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint| ].each do |endpoint|
# Get a list of project +awardable+ award emoji
# desc 'Get a list of project +awardable+ award emoji' do
# Parameters: detail 'This feature was introduced in 8.9'
# id (required) - The ID of a project success Entities::AwardEmoji
# awardable_id (required) - The ID of an issue or MR end
# Example Request:
# GET /projects/:id/issues/:awardable_id/award_emoji
get endpoint do get endpoint do
if can_read_awardable? if can_read_awardable?
awards = paginate(awardable.award_emoji) awards = paginate(awardable.award_emoji)
...@@ -27,14 +30,13 @@ module API ...@@ -27,14 +30,13 @@ module API
end end
end end
# Get a specific award emoji desc 'Get a specific award emoji' do
# detail 'This feature was introduced in 8.9'
# Parameters: success Entities::AwardEmoji
# id (required) - The ID of a project end
# awardable_id (required) - The ID of an issue or MR params do
# award_id (required) - The ID of the award requires :award_id, type: Integer, desc: 'The ID of the award'
# Example Request: end
# GET /projects/:id/issues/:awardable_id/award_emoji/:award_id
get "#{endpoint}/:award_id" do get "#{endpoint}/:award_id" do
if can_read_awardable? if can_read_awardable?
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
...@@ -43,17 +45,14 @@ module API ...@@ -43,17 +45,14 @@ module API
end end
end end
# Award a new Emoji desc 'Award a new Emoji' do
# detail 'This feature was introduced in 8.9'
# Parameters: success Entities::AwardEmoji
# id (required) - The ID of a project end
# awardable_id (required) - The ID of an issue or mr params do
# name (required) - The name of a award_emoji (without colons) requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
# Example Request: end
# POST /projects/:id/issues/:awardable_id/award_emoji
post endpoint do post endpoint do
required_attributes! [:name]
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user) award = awardable.create_award_emoji(params[:name], current_user)
...@@ -65,14 +64,13 @@ module API ...@@ -65,14 +64,13 @@ module API
end end
end end
# Delete a +awardables+ award emoji desc 'Delete a +awardables+ award emoji' do
# detail 'This feature was introduced in 8.9'
# Parameters: success Entities::AwardEmoji
# id (required) - The ID of a project end
# awardable_id (required) - The ID of an issue or MR params do
# award_emoji_id (required) - The ID of an award emoji requires :award_id, type: Integer, desc: 'The ID of an award emoji'
# Example Request: end
# DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
delete "#{endpoint}/:award_id" do delete "#{endpoint}/:award_id" do
award = awardable.award_emoji.find(params[:award_id]) award = awardable.award_emoji.find(params[:award_id])
......
...@@ -134,7 +134,7 @@ module API ...@@ -134,7 +134,7 @@ module API
if member.nil? if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i } { message: "Access revoked", id: params[:user_id].to_i }
else else
::Members::DestroyService.new(member, current_user).execute ::Members::DestroyService.new(source, current_user, params).execute
present member.user, with: Entities::Member, member: member present member.user, with: Entities::Member, member: member
end end
......
...@@ -4,20 +4,18 @@ module API ...@@ -4,20 +4,18 @@ module API
before { authenticate! } before { authenticate! }
resource :namespaces do resource :namespaces do
# Get a namespaces list desc 'Get a namespaces list' do
# success Entities::Namespace
# Example Request: end
# GET /namespaces params do
optional :search, type: String, desc: "Search query for namespaces"
end
get do get do
@namespaces = if current_user.admin namespaces = current_user.admin ? Namespace.all : current_user.namespaces
Namespace.all
else namespaces = namespaces.search(params[:search]) if params[:search].present?
current_user.namespaces
end
@namespaces = @namespaces.search(params[:search]) if params[:search].present?
@namespaces = paginate @namespaces
present @namespaces, with: Entities::Namespace present paginate(namespaces), with: Entities::Namespace
end end
end end
end end
......
...@@ -2,29 +2,7 @@ require 'task_list/filter' ...@@ -2,29 +2,7 @@ require 'task_list/filter'
module Banzai module Banzai
module Filter module Filter
# Work around a bug in the default TaskList::Filter that adds a `task-list`
# class to every list element, regardless of whether or not it contains a
# task list.
#
# This is a (hopefully) temporary fix, pending a new release of the
# task_list gem.
#
# See https://github.com/github/task_list/pull/60
module ClassNamesFilter
def add_css_class(node, *new_class_names)
if new_class_names.include?('task-list')
# Don't add class to all lists
return
elsif new_class_names.include?('task-list-item')
super(node.parent, 'task-list')
end
super(node, *new_class_names)
end
end
class TaskListFilter < TaskList::Filter class TaskListFilter < TaskList::Filter
prepend ClassNamesFilter
end end
end end
end end
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module SidekiqMiddleware module SidekiqMiddleware
class ArgumentsLogger class ArgumentsLogger
def call(worker, job, queue) def call(worker, job, queue)
Sidekiq.logger.info "arguments: #{job['args']}" Sidekiq.logger.info "arguments: #{JSON.dump(job['args'])}"
yield yield
end end
end end
......
...@@ -87,10 +87,10 @@ describe Groups::GroupMembersController do ...@@ -87,10 +87,10 @@ describe Groups::GroupMembersController do
context 'when member is not found' do context 'when member is not found' do
before { sign_in(user) } before { sign_in(user) }
it 'returns 403' do it 'returns 404' do
delete :leave, group_id: group delete :leave, group_id: group
expect(response).to have_http_status(403) expect(response).to have_http_status(404)
end end
end end
......
...@@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do ...@@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do
context 'when member is not found' do context 'when member is not found' do
before { sign_in(user) } before { sign_in(user) }
it 'returns 403' do it 'returns 404' do
delete :leave, namespace_id: project.namespace, delete :leave, namespace_id: project.namespace,
project_id: project project_id: project
expect(response).to have_http_status(403) expect(response).to have_http_status(404)
end end
end end
......
...@@ -109,6 +109,44 @@ describe SessionsController do ...@@ -109,6 +109,44 @@ describe SessionsController do
end end
end end
context 'when the user is on their last attempt' do
before do
user.update(failed_attempts: User.maximum_attempts.pred)
end
context 'when OTP is valid' do
it 'authenticates correctly' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(subject.current_user).to eq user
end
end
context 'when OTP is invalid' do
before { authenticate_2fa(otp_attempt: 'invalid') }
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
end
it 'warns about invalid login' do
expect(response).to set_flash.now[:alert]
.to /Invalid Login or password/
end
it 'locks the user' do
expect(user.reload).to be_access_locked
end
it 'keeps the user locked on future login attempts' do
post(:create, user: { login: user.username, password: user.password })
expect(response)
.to set_flash.now[:alert].to /Invalid Login or password/
end
end
end
context 'when another user does not have 2FA enabled' do context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) } let(:another_user) { create(:user) }
......
...@@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do scenario 'user selects "bug" template' do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
preview_template preview_template(template_content)
save_changes save_changes
end end
...@@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do ...@@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do
end end
end end
context 'user creates an issue using templates, with a prior description' do
let(:prior_description) { 'test issue description' }
let(:template_content) { 'this is a test "bug" template' }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
fill_in :'issue[description]', with: prior_description
end
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
preview_template("#{prior_description}\n\n#{template_content}")
save_changes
end
end
context 'user creates a merge request using templates' do context 'user creates a merge request using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' } let(:template_content) { 'this is a test "feature-proposal" template' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
...@@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal' select_template 'feature-proposal'
wait_for_ajax wait_for_ajax
preview_template preview_template(template_content)
save_changes save_changes
end end
end end
...@@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do ...@@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do scenario 'user selects template' do
select_template 'feature-proposal' select_template 'feature-proposal'
wait_for_ajax wait_for_ajax
preview_template preview_template(template_content)
save_changes save_changes
end end
end end
end end
end end
def preview_template def preview_template(expected_content)
click_link 'Preview' click_link 'Preview'
expect(page).to have_content template_content expect(page).to have_content expected_content
end end
def save_changes def save_changes
......
...@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do ...@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:issue) { create(:issue) } let(:issue) { create(:issue, due_date: Date.today) }
describe 'GET /dashboard/todos' do describe 'GET /dashboard/todos' do
context 'User does not have todos' do context 'User does not have todos' do
...@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do ...@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.todos-list .todo', count: 1) expect(page).to have_selector('.todos-list .todo', count: 1)
end end
it 'shows due date as today' do
page.within first('.todo') do
expect(page).to have_content 'Due today'
end
end
describe 'deleting the todo' do describe 'deleting the todo' do
before do before do
first('.done-todo').click first('.done-todo').click
......
...@@ -16,7 +16,7 @@ describe AccessRequestsFinder, services: true do ...@@ -16,7 +16,7 @@ describe AccessRequestsFinder, services: true do
access_requesters = described_class.new(source).public_send(method_name, user) access_requesters = described_class.new(source).public_send(method_name, user)
expect(access_requesters.size).to eq(1) expect(access_requesters.size).to eq(1)
expect(access_requesters.first).to be_a "#{source.class.to_s}Member".constantize expect(access_requesters.first).to be_a "#{source.class}Member".constantize
expect(access_requesters.first.user).to eq(access_requester) expect(access_requesters.first.user).to eq(access_requester)
end end
end end
......
require 'spec_helper'
describe Banzai::Filter::TaskListFilter, lib: true do
include FilterSpecHelper
it 'does not apply `task-list` class to non-task lists' do
exp = act = %(<ul><li>Item</li></ul>)
expect(filter(act).to_html).to eq exp
end
it 'applies `task-list` to single-item task lists' do
act = filter('<ul><li>[ ] Task 1</li></ul>')
expect(act.to_html).to start_with '<ul class="task-list">'
end
end
...@@ -27,7 +27,7 @@ describe 'Import/Export attribute configuration', lib: true do ...@@ -27,7 +27,7 @@ describe 'Import/Export attribute configuration', lib: true do
relation_names.each do |relation_name| relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name) relation_class = relation_class_for_name(relation_name)
expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes" expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
current_attributes = parsed_attributes(relation_name, relation_class.attribute_names) current_attributes = parsed_attributes(relation_name, relation_class.attribute_names)
safe_attributes = safe_model_attributes[relation_class.to_s] safe_attributes = safe_model_attributes[relation_class.to_s]
......
...@@ -831,6 +831,7 @@ describe Notify do ...@@ -831,6 +831,7 @@ describe Notify do
let(:user) { create(:user, email: 'old-email@mail.com') } let(:user) { create(:user, email: 'old-email@mail.com') }
before do before do
stub_config_setting(email_subject_suffix: 'A Nice Suffix')
perform_enqueued_jobs do perform_enqueued_jobs do
user.email = "new-email@mail.com" user.email = "new-email@mail.com"
user.save user.save
...@@ -847,7 +848,7 @@ describe Notify do ...@@ -847,7 +848,7 @@ describe Notify do
end end
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject "Confirmation instructions" is_expected.to have_subject /^Confirmation instructions/
end end
it 'includes a link to the site' do it 'includes a link to the site' do
......
...@@ -37,6 +37,16 @@ shared_examples 'an email sent from GitLab' do ...@@ -37,6 +37,16 @@ shared_examples 'an email sent from GitLab' do
reply_to = subject.header[:reply_to].addresses reply_to = subject.header[:reply_to].addresses
expect(reply_to).to eq([gitlab_sender_reply_to]) expect(reply_to).to eq([gitlab_sender_reply_to])
end end
context 'when custom suffix for email subject is set' do
before do
stub_config_setting(email_subject_suffix: 'A Nice Suffix')
end
it 'ends the subject with the suffix' do
is_expected.to have_subject /\ \| A Nice Suffix$/
end
end
end end
shared_examples 'an email that contains a header with author username' do shared_examples 'an email that contains a header with author username' do
......
require 'spec_helper' require 'spec_helper'
describe Mentionable do describe Mentionable do
include Mentionable class Example
include Mentionable
def author attr_accessor :project, :message
nil attr_mentionable :message
def author
nil
end
end end
describe 'references' do describe 'references' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:mentionable) { Example.new }
it 'excludes JIRA references' do it 'excludes JIRA references' do
allow(project).to receive_messages(jira_tracker?: true) allow(project).to receive_messages(jira_tracker?: true)
expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
mentionable.project = project
mentionable.message = 'JIRA-123'
expect(mentionable.referenced_mentionables).to be_empty
end end
end end
end end
...@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do ...@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do
let(:user) { create(:user) } let(:user) { create(:user) }
def referenced_issues(current_user) def referenced_issues(current_user)
text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}" issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
issue.referenced_mentionables(current_user)
issue.referenced_mentionables(current_user, text)
end end
context 'when the current user can see the issue' do context 'when the current user can see the issue' do
......
...@@ -86,6 +86,30 @@ describe MergeRequest, models: true do ...@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
end end
end end
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.team << [subject.author, :developer]
subject.target_branch = subject.project.default_branch
end
it 'caches closed issues' do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
subject.project.update_attribute(:has_external_issue_tracker, true)
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
describe '#source_branch_sha' do describe '#source_branch_sha' do
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
...@@ -522,7 +546,7 @@ describe MergeRequest, models: true do ...@@ -522,7 +546,7 @@ describe MergeRequest, models: true do
end end
it_behaves_like 'an editable mentionable' do it_behaves_like 'an editable mentionable' do
subject { create(:merge_request) } subject { create(:merge_request, :simple) }
let(:backref_text) { "merge request #{subject.to_reference}" } let(:backref_text) { "merge request #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } } let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
......
...@@ -7,7 +7,7 @@ describe SlackService::IssueMessage, models: true do ...@@ -7,7 +7,7 @@ describe SlackService::IssueMessage, models: true do
{ {
user: { user: {
name: 'Test User', name: 'Test User',
username: 'Test User' username: 'test.user'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'somewhere.com',
...@@ -40,7 +40,7 @@ describe SlackService::IssueMessage, models: true do ...@@ -40,7 +40,7 @@ describe SlackService::IssueMessage, models: true do
context 'open' do context 'open' do
it 'returns a message regarding opening of issues' do it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'<somewhere.com|[project_name>] Issue opened by Test User') '<somewhere.com|[project_name>] Issue opened by test.user')
expect(subject.attachments).to eq([ expect(subject.attachments).to eq([
{ {
title: "#100 Issue title", title: "#100 Issue title",
...@@ -60,7 +60,7 @@ describe SlackService::IssueMessage, models: true do ...@@ -60,7 +60,7 @@ describe SlackService::IssueMessage, models: true do
it 'returns a message regarding closing of issues' do it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq( expect(subject.pretext). to eq(
'<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User') '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
......
...@@ -7,7 +7,7 @@ describe SlackService::MergeMessage, models: true do ...@@ -7,7 +7,7 @@ describe SlackService::MergeMessage, models: true do
{ {
user: { user: {
name: 'Test User', name: 'Test User',
username: 'Test User' username: 'test.user'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'somewhere.com',
...@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do ...@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do
context 'open' do context 'open' do
it 'returns a message regarding opening of merge requests' do it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\ 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*') 'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
...@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do ...@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do
end end
it 'returns a message regarding closing of merge requests' do it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\ 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*') 'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
......
...@@ -7,7 +7,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -7,7 +7,7 @@ describe SlackService::NoteMessage, models: true do
@args = { @args = {
user: { user: {
name: 'Test User', name: 'Test User',
username: 'username', username: 'test.user',
avatar_url: 'http://fakeavatar' avatar_url: 'http://fakeavatar'
}, },
project_name: 'project_name', project_name: 'project_name',
...@@ -37,7 +37,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -37,7 +37,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on commits' do it 'returns a message regarding notes on commits' do
message = SlackService::NoteMessage.new(@args) message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq("Test User commented on " \ expect(message.pretext).to eq("test.user commented on " \
"<url|commit 5f163b2b> in <somewhere.com|project_name>: " \ "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \
"*Added a commit message*") "*Added a commit message*")
expected_attachments = [ expected_attachments = [
...@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args) message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq("Test User commented on " \ expect(message.pretext).to eq("test.user commented on " \
"<url|merge request !30> in <somewhere.com|project_name>: " \ "<url|merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*") "*merge request title*")
expected_attachments = [ expected_attachments = [
...@@ -90,7 +90,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -90,7 +90,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on an issue' do it 'returns a message regarding notes on an issue' do
message = SlackService::NoteMessage.new(@args) message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq( expect(message.pretext).to eq(
"Test User commented on " \ "test.user commented on " \
"<url|issue #20> in <somewhere.com|project_name>: " \ "<url|issue #20> in <somewhere.com|project_name>: " \
"*issue title*") "*issue title*")
expected_attachments = [ expected_attachments = [
...@@ -115,7 +115,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -115,7 +115,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a project snippet' do it 'returns a message regarding notes on a project snippet' do
message = SlackService::NoteMessage.new(@args) message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq("Test User commented on " \ expect(message.pretext).to eq("test.user commented on " \
"<url|snippet #5> in <somewhere.com|project_name>: " \ "<url|snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*") "*snippet title*")
expected_attachments = [ expected_attachments = [
......
...@@ -9,7 +9,7 @@ describe SlackService::PushMessage, models: true do ...@@ -9,7 +9,7 @@ describe SlackService::PushMessage, models: true do
before: 'before', before: 'before',
project_name: 'project_name', project_name: 'project_name',
ref: 'refs/heads/master', ref: 'refs/heads/master',
user_name: 'user_name', user_name: 'test.user',
project_url: 'url' project_url: 'url'
} }
end end
...@@ -26,7 +26,7 @@ describe SlackService::PushMessage, models: true do ...@@ -26,7 +26,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'user_name pushed to branch <url/commits/master|master> of '\ 'test.user pushed to branch <url/commits/master|master> of '\
'<url|project_name> (<url/compare/before...after|Compare changes>)' '<url|project_name> (<url/compare/before...after|Compare changes>)'
) )
expect(subject.attachments).to eq([ expect(subject.attachments).to eq([
...@@ -46,13 +46,13 @@ describe SlackService::PushMessage, models: true do ...@@ -46,13 +46,13 @@ describe SlackService::PushMessage, models: true do
before: Gitlab::Git::BLANK_SHA, before: Gitlab::Git::BLANK_SHA,
project_name: 'project_name', project_name: 'project_name',
ref: 'refs/tags/new_tag', ref: 'refs/tags/new_tag',
user_name: 'user_name', user_name: 'test.user',
project_url: 'url' project_url: 'url'
} }
end end
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('user_name pushed new tag ' \ expect(subject.pretext).to eq('test.user pushed new tag ' \
'<url/commits/new_tag|new_tag> to ' \ '<url/commits/new_tag|new_tag> to ' \
'<url|project_name>') '<url|project_name>')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
...@@ -66,7 +66,7 @@ describe SlackService::PushMessage, models: true do ...@@ -66,7 +66,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding a new branch' do it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'user_name pushed new branch <url/commits/master|master> to '\ 'test.user pushed new branch <url/commits/master|master> to '\
'<url|project_name>' '<url|project_name>'
) )
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
...@@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do ...@@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding a removed branch' do it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'user_name removed branch master from <url|project_name>' 'test.user removed branch master from <url|project_name>'
) )
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
......
...@@ -7,7 +7,7 @@ describe SlackService::WikiPageMessage, models: true do ...@@ -7,7 +7,7 @@ describe SlackService::WikiPageMessage, models: true do
{ {
user: { user: {
name: 'Test User', name: 'Test User',
username: 'Test User' username: 'test.user'
}, },
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'somewhere.com',
...@@ -25,7 +25,7 @@ describe SlackService::WikiPageMessage, models: true do ...@@ -25,7 +25,7 @@ describe SlackService::WikiPageMessage, models: true do
it 'returns a message that a new wiki page was created' do it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'Test User created <url|wiki page> in <somewhere.com|project_name>: '\ 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
'*Wiki page title*') '*Wiki page title*')
end end
end end
...@@ -35,7 +35,7 @@ describe SlackService::WikiPageMessage, models: true do ...@@ -35,7 +35,7 @@ describe SlackService::WikiPageMessage, models: true do
it 'returns a message that a wiki page was updated' do it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\ 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
'*Wiki page title*') '*Wiki page title*')
end end
end end
......
...@@ -320,6 +320,16 @@ describe Repository, models: true do ...@@ -320,6 +320,16 @@ describe Repository, models: true do
end end
end end
describe '#create_ref' do
it 'redirects the call to fetch_ref' do
ref, ref_path = '1', '2'
expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
repository.create_ref(ref, ref_path)
end
end
describe "#changelog" do describe "#changelog" do
before do before do
repository.send(:cache).expire(:changelog) repository.send(:cache).expire(:changelog)
......
...@@ -203,6 +203,23 @@ describe Service, models: true do ...@@ -203,6 +203,23 @@ describe Service, models: true do
end end
end end
describe 'initialize service with no properties' do
let(:service) do
GitlabIssueTrackerService.create(
project: create(:project),
title: 'random title'
)
end
it 'does not raise error' do
expect { service }.not_to raise_error
end
it 'creates the properties' do
expect(service.properties).to eq({ "title" => "random title" })
end
end
describe "callbacks" do describe "callbacks" do
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:service) do let!(:service) do
......
...@@ -195,7 +195,7 @@ describe API::AccessRequests, api: true do ...@@ -195,7 +195,7 @@ describe API::AccessRequests, api: true do
end end
context 'when authenticated as the access requester' do context 'when authenticated as the access requester' do
it 'returns 200' do it 'deletes the access requester' do
expect do expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
...@@ -205,7 +205,7 @@ describe API::AccessRequests, api: true do ...@@ -205,7 +205,7 @@ describe API::AccessRequests, api: true do
end end
context 'when authenticated as a master/owner' do context 'when authenticated as a master/owner' do
it 'returns 200' do it 'deletes the access requester' do
expect do expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
...@@ -213,6 +213,16 @@ describe API::AccessRequests, api: true do ...@@ -213,6 +213,16 @@ describe API::AccessRequests, api: true do
end.to change { source.requesters.count }.by(-1) end.to change { source.requesters.count }.by(-1)
end end
context 'user_id matches a member, not an access requester' do
it 'returns 404' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
expect(response).to have_http_status(404)
end.not_to change { source.requesters.count }
end
end
context 'user_id does not match an existing access requester' do context 'user_id does not match an existing access requester' do
it 'returns 404' do it 'returns 404' do
expect do expect do
......
...@@ -26,7 +26,7 @@ describe Members::ApproveAccessRequestService, services: true do ...@@ -26,7 +26,7 @@ describe Members::ApproveAccessRequestService, services: true do
it 'returns a <Source>Member' do it 'returns a <Source>Member' do
member = described_class.new(source, user, params).execute member = described_class.new(source, user, params).execute
expect(member).to be_a "#{source.class.to_s}Member".constantize expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_nil expect(member.requested_at).to be_nil
end end
......
...@@ -2,70 +2,111 @@ require 'spec_helper' ...@@ -2,70 +2,111 @@ require 'spec_helper'
describe Members::DestroyService, services: true do describe Members::DestroyService, services: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:member_user) { create(:user) }
let!(:member) { create(:project_member, source: project) } let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
context 'when member is nil' do shared_examples 'a service raising ActiveRecord::RecordNotFound' do
before do it 'raises ActiveRecord::RecordNotFound' do
project.team << [user, :developer] expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
end end
end
it 'does not destroy the member' do shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError) it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end end
end end
context 'when current user cannot destroy the given member' do shared_examples 'a service destroying a member' do
before do it 'destroys the member' do
project.team << [user, :developer] expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1)
end
context 'when the given member is an access requester' do
before do
source.members.find_by(user_id: member_user).destroy
source.request_access(member_user)
end
let(:access_requester) { source.requesters.find_by(user_id: member_user) }
it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
%i[requesters all].each do |scope|
context "and #{scope} scope is passed" do
it 'destroys the access requester' do
expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1)
end
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
described_class.new(source, user, params).execute(scope)
end
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester)
described_class.new(source, member_user, params).execute(scope)
end
end
end
end
end end
end
context 'when no member are found' do
let(:params) { { user_id: 42 } }
it 'does not destroy the member' do it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError) let(:source) { project }
end
it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
let(:source) { group }
end end
end end
context 'when current user can destroy the given member' do context 'when a member is found' do
before do before do
project.team << [user, :master] project.team << [member_user, :developer]
group.add_developer(member_user)
end end
let(:params) { { user_id: member_user.id } }
it 'destroys the member' do context 'when current user cannot destroy the given member' do
destroy_member(member, user) it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
expect(member).to be_destroyed it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end end
context 'when the given member is a requester' do context 'when current user can destroy the given member' do
before do before do
member.update_column(:requested_at, Time.now) project.team << [user, :master]
group.add_owner(user)
end end
it 'calls Member#after_decline_request' do it_behaves_like 'a service destroying a member' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) let(:source) { project }
destroy_member(member, user)
end end
context 'when current user is the member' do it_behaves_like 'a service destroying a member' do
it 'does not call Member#after_decline_request' do let(:source) { group }
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
destroy_member(member, member.user)
end
end end
context 'when current user is the member and ' do context 'when given a :id' do
it 'does not call Member#after_decline_request' do let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
destroy_member(member, member.user) it 'destroys the member' do
expect { described_class.new(project, user, params).execute }.
to change { project.members.count }.by(-1)
end end
end end
end end
end end
def destroy_member(member, user)
Members::DestroyService.new(member, user).execute
end
end end
...@@ -19,7 +19,7 @@ describe Members::RequestAccessService, services: true do ...@@ -19,7 +19,7 @@ describe Members::RequestAccessService, services: true do
it 'returns a <Source>Member' do it 'returns a <Source>Member' do
member = described_class.new(source, user).execute member = described_class.new(source, user).execute
expect(member).to be_a "#{source.class.to_s}Member".constantize expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_present expect(member.requested_at).to be_present
end end
end end
......
...@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do ...@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do
end end
end end
context 'closes related issues' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
end
it 'closes GitLab issue tracker issues' do
issue = create :issue, project: project
commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
service.execute(merge_request)
expect(issue.reload.closed?).to be_truthy
end
context 'with JIRA integration' do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service }
before { jira_service_settings }
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
service.execute(merge_request)
end
end
end
context 'closes related todos' do context 'closes related todos' do
let(:merge_request) { create(:merge_request, assignee: user, author: user) } let(:merge_request) { create(:merge_request, assignee: user, author: user) }
let(:project) { merge_request.project } let(:project) { merge_request.project }
......
...@@ -9,7 +9,7 @@ shared_context 'mentionable context' do ...@@ -9,7 +9,7 @@ shared_context 'mentionable context' do
let(:author) { subject.author } let(:author) { subject.author }
let(:mentioned_issue) { create(:issue, project: project) } let(:mentioned_issue) { create(:issue, project: project) }
let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } let!(:mentioned_mr) { create(:merge_request, source_project: project) }
let(:mentioned_commit) { project.commit("HEAD~1") } let(:mentioned_commit) { project.commit("HEAD~1") }
let(:ext_proj) { create(:project, :public) } let(:ext_proj) { create(:project, :public) }
...@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do ...@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do
it 'creates new cross-reference notes when the mentionable text is edited' do it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save subject.save
subject.create_cross_references!
new_text = <<-MSG.strip_heredoc new_text = <<-MSG.strip_heredoc
These references already existed: These references already existed:
...@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do ...@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do
end end
# These two issues are new and should receive reference notes # These two issues are new and should receive reference notes
# In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
new_issues.each do |newref| new_issues.each do |newref|
expect(SystemNoteService).to receive(:cross_reference). expect(SystemNoteService).to receive(:cross_reference).
with(newref, subject.local_reference, author) with(newref, subject.local_reference, author)
......
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