summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc3
-rw-r--r--app/assets/javascripts/dispatcher.js.es63
-rw-r--r--app/assets/javascripts/droplab/droplab.js67
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js14
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js3
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js18
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es61
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es619
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es62
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es614
-rw-r--r--app/assets/javascripts/notes.js21
-rw-r--r--app/assets/javascripts/version_check_image.js.es610
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es647
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es67
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es618
-rw-r--r--app/assets/stylesheets/framework/blocks.scss12
-rw-r--r--app/assets/stylesheets/framework/filters.scss24
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss26
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/version_check_helper.rb3
-rw-r--r--app/mailers/emails/notes.rb8
-rw-r--r--app/models/ability.rb11
-rw-r--r--app/models/application_setting.rb84
-rw-r--r--app/models/concerns/cache_markdown_field.rb7
-rw-r--r--app/models/concerns/mentionable.rb10
-rw-r--r--app/models/concerns/participable.rb7
-rw-r--r--app/models/merge_request.rb8
-rw-r--r--app/models/note.rb19
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/notes/create_service.rb7
-rw-r--r--app/services/notes/post_process_service.rb3
-rw-r--r--app/services/notes/slash_commands_service.rb2
-rw-r--r--app/services/notification_service.rb22
-rw-r--r--app/views/discussions/_discussion.html.haml3
-rw-r--r--app/views/notify/note_personal_snippet_email.html.haml1
-rw-r--r--app/views/notify/note_personal_snippet_email.text.erb8
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml8
-rw-r--r--app/views/projects/commits/show.html.haml7
-rw-r--r--changelogs/unreleased/26445-accessible-piplelines-buttons.yml4
-rw-r--r--changelogs/unreleased/26447-fix-tab-list-order.yml4
-rw-r--r--changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml4
-rw-r--r--changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml4
-rw-r--r--changelogs/unreleased/fix_broken_diff_discussions.yml4
-rw-r--r--changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml4
-rw-r--r--changelogs/unreleased/newline-eslint-rule.yml4
-rw-r--r--changelogs/unreleased/no_project_notes.yml4
-rw-r--r--changelogs/unreleased/tc-only-mr-button-if-allowed.yml4
-rw-r--r--config/initializers/metrics.rb219
-rw-r--r--db/migrate/20161223034433_add_estimate_to_issuables_ce.rb12
-rw-r--r--db/migrate/20161223034646_create_timelogs_ce.rb6
-rw-r--r--doc/administration/raketasks/maintenance.md1
-rw-r--r--lib/api/branches.rb7
-rw-r--r--lib/banzai/filter/reference_filter.rb4
-rw-r--r--lib/banzai/filter/user_reference_filter.rb4
-rw-r--r--lib/banzai/renderer.rb4
-rw-r--r--lib/gitlab/current_settings.rb44
-rw-r--r--lib/gitlab/github_import/project_creator.rb4
-rw-r--r--lib/gitlab/import_sources.rb2
-rw-r--r--lib/tasks/gitlab/info.rake5
-rw-r--r--spec/controllers/health_check_controller_spec.rb6
-rw-r--r--spec/factories/notes.rb6
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb3
-rw-r--r--spec/features/admin/admin_health_check_spec.rb9
-rw-r--r--spec/features/admin/admin_runners_spec.rb3
-rw-r--r--spec/features/admin/admin_settings_spec.rb5
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb9
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb10
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb16
-rw-r--r--spec/features/projects/merge_request_button_spec.rb108
-rw-r--r--spec/initializers/metrics_spec.rb16
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es61
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es61
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es617
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb24
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb68
-rw-r--r--spec/models/ability_spec.rb27
-rw-r--r--spec/models/concerns/mentionable_spec.rb24
-rw-r--r--spec/models/note_spec.rb81
-rw-r--r--spec/models/user_spec.rb47
-rw-r--r--spec/requests/api/internal_spec.rb9
-rw-r--r--spec/services/notes/create_service_spec.rb51
-rw-r--r--spec/services/notification_service_spec.rb49
-rw-r--r--spec/spec_helper.rb1
85 files changed, 1095 insertions, 383 deletions
diff --git a/.eslintrc b/.eslintrc
index e13f76b213c..9ab0145820d 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,6 +15,7 @@
"filenames"
],
"rules": {
- "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
+ "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
+ "no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index dcf67a8fd68..529d476ca4e 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -261,6 +261,9 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
+ case 'help:index':
+ gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
+ break;
case 'search:show':
new Search();
break;
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
index c79f0230951..8b14191395b 100644
--- a/app/assets/javascripts/droplab/droplab.js
+++ b/app/assets/javascripts/droplab/droplab.js
@@ -58,6 +58,7 @@ var CustomEvent = require('./custom_event_polyfill');
var utils = require('./utils');
var DropDown = function(list) {
+ this.currentIndex = 0;
this.hidden = true;
this.list = list;
this.items = [];
@@ -164,15 +165,21 @@ Object.assign(DropDown.prototype, {
},
show: function() {
- // debugger
- this.list.style.display = 'block';
- this.hidden = false;
+ if (this.hidden) {
+ // debugger
+ this.list.style.display = 'block';
+ this.currentIndex = 0;
+ this.hidden = false;
+ }
},
hide: function() {
- // debugger
- this.list.style.display = 'none';
- this.hidden = true;
+ if (!this.hidden) {
+ // debugger
+ this.list.style.display = 'none';
+ this.currentIndex = 0;
+ this.hidden = true;
+ }
},
destroy: function() {
@@ -478,6 +485,8 @@ Object.assign(HookInput.prototype, {
this.input = function input(e) {
if(self.hasRemovedEvents) return;
+ self.list.show();
+
var inputEvent = new CustomEvent('input.dl', {
detail: {
hook: self,
@@ -487,7 +496,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(inputEvent);
- self.list.show();
}
this.keyup = function keyup(e) {
@@ -503,6 +511,8 @@ Object.assign(HookInput.prototype, {
}
function keyEvent(e, keyEventName){
+ self.list.show();
+
var keyEvent = new CustomEvent(keyEventName, {
detail: {
hook: self,
@@ -514,7 +524,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(keyEvent);
- self.list.show();
}
this.events = this.events || {};
@@ -572,24 +581,43 @@ require('./window')(function(w){
module.exports = function(){
var currentKey;
var currentFocus;
- var currentIndex = 0;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
- var listItems = list.list.querySelectorAll('li');
+ var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var listItemsTmp = [];
for(var i = 0; i < listItems.length; i++) {
- listItems[i].classList.remove('dropdown-active');
+ var listItem = listItems[i];
+ listItem.classList.remove('dropdown-active');
+
+ if (listItem.style.display !== 'none') {
+ listItemsTmp.push(listItem);
+ }
}
- return listItems;
+ return listItemsTmp;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
- if(currentIndex>0){
- if(!listItems[currentIndex-1]){
- currentIndex = currentIndex-1;
+ if(list.currentIndex>0){
+ if(!listItems[list.currentIndex-1]){
+ list.currentIndex = list.currentIndex-1;
+ }
+
+ if (listItems[list.currentIndex-1]) {
+ var el = listItems[list.currentIndex-1];
+ var filterDropdownEl = el.closest('.filter-dropdown');
+ el.classList.add('dropdown-active');
+
+ if (filterDropdownEl) {
+ var filterDropdownBottom = filterDropdownEl.offsetHeight;
+ var elOffsetTop = el.offsetTop - 30;
+
+ if (elOffsetTop > filterDropdownBottom) {
+ filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
+ }
+ }
}
- listItems[currentIndex-1].classList.add('dropdown-active');
}
};
@@ -597,13 +625,13 @@ require('./window')(function(w){
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
- currentIndex = 0;
+ list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
- var currentItem = listItems[currentIndex-1];
+ var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
@@ -617,6 +645,8 @@ require('./window')(function(w){
var keydown = function keydown(e){
var typedOn = e.target;
+ var list = e.detail.hook.list;
+ var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
@@ -648,6 +678,7 @@ require('./window')(function(w){
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
+ list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index f20610b3811..f7fed0987a2 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -29,6 +29,7 @@ require('../window')(function(w){
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
+ this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
@@ -52,19 +53,26 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
- var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
+ var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
- hook.list[config.method].call(hook.list, d);
+
+ if (!self.hook.list.hidden) {
+ self.hook.list[config.method].call(self.hook.list, d);
+ }
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
+ if (this.listTemplate) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ dynamicList.outerHTML = this.listTemplate;
+ }
}
};
});
@@ -76,4 +84,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
index af163f76851..86a08d0d01d 100644
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -93,6 +93,7 @@ require('../window')(function(w){
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
+ self.hook.list.currentIndex = 0;
});
},
@@ -142,4 +143,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
index 41a220831f9..9b40a3f20a4 100644
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -6,6 +6,8 @@ require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
+ var hiddenCount = 0;
+ var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
@@ -27,10 +29,22 @@ require('../window')(function(w){
};
}
+ dataHiddenCount = data.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
matches = data.map(function(o) {
return filterFunction(o, value);
});
- list.render(matches);
+
+ hiddenCount = matches.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ if (dataHiddenCount !== hiddenCount) {
+ list.render(matches);
+ list.currentIndex = 0;
+ }
},
init: function init(hookInput) {
@@ -57,4 +71,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 9f24a6a4f88..3b003f6f661 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -3,7 +3,6 @@
//= require ./components/environment
//= require ./vue_resource_interceptor
-
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
index 443ac222f70..eeab10fba17 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -84,7 +84,7 @@
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
- inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
+ inputValue = inputValue.replace(/("(.*?)"|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index ae19bb68360..8d62324b79f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -64,13 +64,26 @@
}
checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
+
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
+
if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+
e.preventDefault();
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
+ if (!activeElements.length) {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
- this.search();
+ this.search();
+ }
}
}
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index c260ad03d47..e0ebd36a65c 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -61,7 +61,6 @@
return labels;
}
-
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
@@ -80,7 +79,6 @@
return result;
}
-
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 6d57d31f380..7452879d9a3 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -159,5 +159,19 @@
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
+
+ /**
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeHeaders = (headers) => {
+ const upperCaseHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ upperCaseHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ return upperCaseHeaders;
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 06a72efa21d..9db830a7ada 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -220,7 +220,6 @@
})(this));
};
-
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
@@ -244,7 +243,6 @@
return this.initRefresh();
};
-
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
@@ -294,7 +292,6 @@
}
};
-
/*
Check if note does not exists on page
*/
@@ -307,7 +304,6 @@
return this.view === 'parallel';
};
-
/*
Render note in discussion area.
@@ -358,7 +354,6 @@
return this.updateNotesCount(1);
};
-
/*
Called in response the main target form has been successfully submitted.
@@ -390,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
-
/*
Shows the main form and does some setup on it.
@@ -415,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
-
/*
General note form setup.
@@ -432,7 +425,6 @@
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
-
/*
Called in response to the new note form being submitted
@@ -448,7 +440,6 @@
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
};
-
/*
Called in response to the new note form being submitted
@@ -473,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
-
/*
Called in response to the edit note form being submitted
@@ -498,7 +488,6 @@
}
};
-
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
@@ -522,7 +511,6 @@
return isAllowed;
};
-
/*
Called in response to clicking the edit note link
@@ -551,7 +539,6 @@
this.putEditFormInPlace($target);
};
-
/*
Called in response to clicking the edit note link
@@ -596,7 +583,6 @@
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
-
/*
Called in response to deleting a note of any kind.
@@ -636,7 +622,6 @@
return this.updateNotesCount(-1);
};
-
/*
Called in response to clicking the delete attachment link
@@ -653,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
-
/*
Called when clicking on the "reply" button for a diff line.
@@ -673,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
-
/*
Shows the diff or discussion form and does some setup on it.
@@ -715,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
-
/*
Called when clicking on the "add a comment" button on the side of a diff line.
@@ -772,7 +754,6 @@
}
};
-
/*
Called in response to "cancel" on a diff note form.
@@ -806,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
-
/*
Called after an attachment file has been selected.
@@ -821,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
-
/*
Called when the tab visibility changes
*/
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
new file mode 100644
index 00000000000..1fa2b5ac399
--- /dev/null
+++ b/app/assets/javascripts/version_check_image.js.es6
@@ -0,0 +1,10 @@
+(() => {
+ class VersionCheckImage {
+ static bindErrorEvent(imageElement) {
+ imageElement.off('error').on('error', () => imageElement.hide());
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.VersionCheckImage = VersionCheckImage;
+})();
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index ad5cb30cc42..b195b0ef3ba 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -22,47 +22,51 @@
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
- <a
+ <button
v-if='actions'
- class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
- alt="Manual Build"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Manual build"
>
- <span v-html='svgs.iconPlay'></span>
- <i class="fa fa-caret-down"></i>
- </a>
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
- title="Manual build"
>
- <span v-html='svgs.iconPlay'></span>
- <span title="Manual build">{{action.name}}</span>
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
- <a
+ <button
v-if='artifacts'
- class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ data-toggle="dropdown"
+ title="Artifacts"
+ data-placement="top"
data-toggle="dropdown"
- type="button"
+ aria-label="Artifacts"
>
- <i class="fa fa-download"></i>
- <i class="fa fa-caret-down"></i>
- </a>
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
- <i class="fa fa-download"></i>
+ <i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
@@ -76,9 +80,12 @@
title="Retry"
rel="nofollow"
data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
:href='pipeline.retry_path'
+ aria-label="Retry"
>
- <i class="fa fa-repeat"></i>
+ <i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
@@ -86,10 +93,12 @@
title="Cancel"
rel="nofollow"
data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
:href='pipeline.cancel_path'
- data-original-title="Cancel"
+ aria-label="Cancel"
>
- <i class="fa fa-remove"></i>
+ <i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index 4e85f16ebc5..496df9aaced 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -82,12 +82,13 @@
data-placement="top"
data-toggle="dropdown"
type="button"
+ :aria-label='stage.title'
>
- <span v-html="svg"></span>
- <i class="fa fa-caret-down "></i>
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up"></div>
+ <div class="arrow-up" aria-hidden="true"></div>
<div
@click='keepGraph($event)'
:class="dropdownClass"
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 1982142853a..9e19b1564dc 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -4,19 +4,15 @@
((gl) => {
const pageValues = (headers) => {
- const normalizedHeaders = {};
-
- Object.keys(headers).forEach((e) => {
- normalizedHeaders[e.toUpperCase()] = headers[e];
- });
+ const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
- perPage: +normalizedHeaders['X-PER-PAGE'],
- page: +normalizedHeaders['X-PAGE'],
- total: +normalizedHeaders['X-TOTAL'],
- totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
- nextPage: +normalizedHeaders['X-NEXT-PAGE'],
- previousPage: +normalizedHeaders['X-PREV-PAGE'],
+ perPage: +normalized['X-PER-PAGE'],
+ page: +normalized['X-PAGE'],
+ total: +normalized['X-TOTAL'],
+ totalPages: +normalized['X-TOTAL-PAGES'],
+ nextPage: +normalized['X-NEXT-PAGE'],
+ previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 407c800feb7..592ef0d647f 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -82,7 +82,12 @@
}
.block-controls {
- float: right;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ -webkit-flex: 1;
+ flex: 1;
.control {
float: left;
@@ -282,3 +287,8 @@
}
}
}
+
+.flex-container-block {
+ display: -webkit-flex;
+ display: flex;
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index d957ec64654..4b05ec691a8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -79,6 +79,16 @@
overflow: auto;
}
+%filter-dropdown-item-btn-hover {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ text-decoration: none;
+
+ .avatar {
+ border-color: $white-light;
+ }
+}
+
.filter-dropdown-item {
.btn {
border: none;
@@ -103,13 +113,7 @@
&:hover,
&:focus {
- background-color: $dropdown-hover-color;
- color: $white-light;
- text-decoration: none;
-
- .avatar {
- border-color: $white-light;
- }
+ @extend %filter-dropdown-item-btn-hover;
}
}
@@ -131,6 +135,12 @@
}
}
+.filter-dropdown-item.dropdown-active {
+ .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
+}
+
.hint-dropdown {
width: 250px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8dff22e32bd..5d4bd091a6b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -288,6 +288,10 @@
}
}
}
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
.build-link {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cd0839e58ea..1b0bf4554e6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -929,8 +929,32 @@ pre.light-well {
.variables-table {
table-layout: fixed;
+ &.table-responsive {
+ border: none;
+ }
+
.variable-key {
- width: 30%;
+ width: 300px;
+ max-width: 300px;
+ overflow: hidden;
+ word-wrap: break-word;
+
+ // override bootstrap
+ white-space: normal!important;
+
+ @media (max-width: $screen-sm-max) {
+ width: 150px;
+ max-width: 150px;
+ }
+ }
+
+ .variable-value {
+ @media(max-width: $screen-xs-max) {
+ width: 150px;
+ max-width: 150px;
+ overflow: hidden;
+ word-wrap: break-word;
+ }
}
}
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index aa54ee07bdc..2aa0449c46e 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
- project.feature_available?(:merge_requests, current_user) &&
+ can?(current_user, :create_merge_request, project) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index a674564c4ec..456598b4c28 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,7 +1,8 @@
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
- image_tag VersionCheck.new.url
+ image_url = VersionCheck.new.url
+ image_tag image_url, class: 'js-version-status-badge'
end
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 0d20c9092c4..46fa6fd9f6d 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -38,6 +38,14 @@ module Emails
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
+ def note_personal_snippet_email(recipient_id, note_id)
+ setup_note_mail(note_id, recipient_id)
+
+ @snippet = @note.noteable
+ @target_url = snippet_url(@note.noteable)
+ mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ end
+
private
def note_target_url_options
diff --git a/app/models/ability.rb b/app/models/ability.rb
index fa8f8bc3a5f..ad6c588202e 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -22,6 +22,17 @@ class Ability
end
end
+ # Given a list of users and a snippet this method returns the users that can
+ # read the given snippet.
+ def users_that_can_read_personal_snippet(users, snippet)
+ case snippet.visibility_level
+ when Snippet::INTERNAL, Snippet::PUBLIC
+ users
+ when Snippet::PRIVATE
+ users.include?(snippet.author) ? [snippet.author] : []
+ end
+ end
+
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8fab77cda0a..e33a58d3771 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,49 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}x
+ DEFAULTS_CE = {
+ after_sign_up_text: nil,
+ akismet_enabled: false,
+ container_registry_token_expire_delay: 5,
+ default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_projects_limit: Settings.gitlab['default_projects_limit'],
+ default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ disabled_oauth_sign_in_sources: [],
+ domain_whitelist: Settings.gitlab['domain_whitelist'],
+ gravatar_enabled: Settings.gravatar['enabled'],
+ help_page_text: nil,
+ housekeeping_bitmaps_enabled: true,
+ housekeeping_enabled: true,
+ housekeeping_full_repack_period: 50,
+ housekeeping_gc_period: 200,
+ housekeeping_incremental_repack_period: 10,
+ import_sources: Gitlab::ImportSources.values,
+ koding_enabled: false,
+ koding_url: nil,
+ max_artifacts_size: Settings.artifacts['max_size'],
+ max_attachment_size: Settings.gitlab['max_attachment_size'],
+ plantuml_enabled: false,
+ plantuml_url: nil,
+ recaptcha_enabled: false,
+ repository_checks_enabled: true,
+ repository_storages: ['default'],
+ require_two_factor_authentication: false,
+ restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
+ session_expire_delay: Settings.gitlab['session_expire_delay'],
+ send_user_confirmation_email: false,
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ shared_runners_text: nil,
+ sidekiq_throttling_enabled: false,
+ sign_in_text: nil,
+ signin_enabled: Settings.gitlab['signin_enabled'],
+ signup_enabled: Settings.gitlab['signup_enabled'],
+ two_factor_grace_period: 48,
+ user_default_external: false
+ }
+
+ DEFAULTS = DEFAULTS_CE
+
serialize :restricted_visibility_levels
serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array
@@ -163,46 +206,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
- create(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- signup_enabled: Settings.gitlab['signup_enabled'],
- signin_enabled: Settings.gitlab['signin_enabled'],
- gravatar_enabled: Settings.gravatar['enabled'],
- sign_in_text: nil,
- after_sign_up_text: nil,
- help_page_text: nil,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
- default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: Gitlab::ImportSources.values,
- shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
- max_artifacts_size: Settings.artifacts['max_size'],
- require_two_factor_authentication: false,
- two_factor_grace_period: 48,
- recaptcha_enabled: false,
- akismet_enabled: false,
- koding_enabled: false,
- koding_url: nil,
- plantuml_enabled: false,
- plantuml_url: nil,
- repository_checks_enabled: true,
- disabled_oauth_sign_in_sources: [],
- send_user_confirmation_email: false,
- container_registry_token_expire_delay: 5,
- repository_storages: ['default'],
- user_default_external: false,
- sidekiq_throttling_enabled: false,
- housekeeping_enabled: true,
- housekeeping_bitmaps_enabled: true,
- housekeeping_incremental_repack_period: 10,
- housekeeping_full_repack_period: 50,
- housekeeping_gc_period: 200,
- )
+ create(DEFAULTS)
end
def home_page_url_column_exist
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 90bd6490a02..a600f9c14c5 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -51,6 +51,10 @@ module CacheMarkdownField
CACHING_CLASSES.map(&:constantize)
end
+ def skip_project_check?
+ false
+ end
+
extend ActiveSupport::Concern
included do
@@ -112,7 +116,8 @@ module CacheMarkdownField
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
- html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
+ options = { skip_project_check: skip_project_check? }
+ html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 8ab0401d288..ef2c1e5d414 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -49,7 +49,11 @@ module Mentionable
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
- options = options.merge(cache_key: [self, attr], author: author)
+ options = options.merge(
+ cache_key: [self, attr],
+ author: author,
+ skip_project_check: skip_project_check?
+ )
extractor.analyze(text, options)
end
@@ -121,4 +125,8 @@ module Mentionable
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
+
+ def skip_project_check?
+ false
+ end
end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 70740c76e43..4865c0a14b1 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -96,6 +96,11 @@ module Participable
participants.merge(ext.users)
- Ability.users_that_can_read_project(participants.to_a, project)
+ case self
+ when PersonalSnippet
+ Ability.users_that_can_read_personal_snippet(participants.to_a, self)
+ else
+ Ability.users_that_can_read_project(participants.to_a, project)
+ end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index cd5b345bae5..6753504acff 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -865,9 +865,11 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
- active_diff_notes.each do |note|
- service.execute(note)
- Gitlab::Timeless.timeless(note, &:save)
+ transaction do
+ active_diff_notes.each do |note|
+ service.execute(note)
+ Gitlab::Timeless.timeless(note, &:save)
+ end
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 0c1b05dabf2..bf090a0438c 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -43,7 +43,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
- validates :note, :project, presence: true
+ validates :note, presence: true
+ validates :project, presence: true, unless: :for_personal_snippet?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -53,7 +54,7 @@ class Note < ActiveRecord::Base
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
- validate unless: [:for_commit?, :importing?] do |note|
+ validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
@@ -83,7 +84,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
- after_save :keep_around_commit
+ after_save :keep_around_commit, unless: :for_personal_snippet?
class << self
def model_name
@@ -165,6 +166,14 @@ class Note < ActiveRecord::Base
noteable_type == "Snippet"
end
+ def for_personal_snippet?
+ noteable.is_a?(PersonalSnippet)
+ end
+
+ def skip_project_check?
+ for_personal_snippet?
+ end
+
# override to return commits, which are not active record
def noteable
if for_commit?
@@ -220,6 +229,10 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
+ def to_ability_name
+ for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
+ end
+
private
def keep_around_commit
diff --git a/app/models/user.rb b/app/models/user.rb
index 06dd98a3188..2caa66dd9f7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -179,8 +179,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
- scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
- scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
+ scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index cdd765c85eb..b4f8b33d564 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -3,9 +3,10 @@ module Notes
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
- note = project.notes.new(params)
- note.author = current_user
- note.system = false
+ note = Note.new(params)
+ note.project = project
+ note.author = current_user
+ note.system = false
if note.award_emoji?
noteable = note.noteable
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index e4cd3fc7833..6a10e172483 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -10,6 +10,9 @@ module Notes
# Skip system notes, like status changes and cross-references and awards
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
+
+ return if @note.for_personal_snippet?
+
@note.create_cross_references!
execute_note_hooks
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index aaea9717fc4..56913568cae 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -12,7 +12,7 @@ module Notes
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
- current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
+ current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
end
def supported?(note)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index c3b61e68eab..f74e6cac174 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -178,8 +178,15 @@ class NotificationService
recipients = []
mentioned_users = note.mentioned_users
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
mentioned_users.select! do |user|
- user.can?(:read_project, note.project)
+ user.can?(ability, subject)
end
# Add all users participating in the thread (author, assignee, comment authors)
@@ -192,11 +199,13 @@ class NotificationService
recipients = recipients.concat(participants)
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients, note.project)
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, note.project, :new_note)
+ end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
@@ -211,8 +220,7 @@ class NotificationService
recipients.delete(note.author)
recipients = recipients.uniq
- # build notify method like 'note_commit_email'
- notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
+ notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 2bce2780484..6f5d4bf2a2f 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,6 +1,9 @@
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
+ .timeline-icon
+ = link_to user_path(discussion.author) do
+ = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
diff --git a/app/views/notify/note_personal_snippet_email.html.haml b/app/views/notify/note_personal_snippet_email.html.haml
new file mode 100644
index 00000000000..2fa2f784661
--- /dev/null
+++ b/app/views/notify/note_personal_snippet_email.html.haml
@@ -0,0 +1 @@
+= render 'note_message'
diff --git a/app/views/notify/note_personal_snippet_email.text.erb b/app/views/notify/note_personal_snippet_email.text.erb
new file mode 100644
index 00000000000..b2a8809a23b
--- /dev/null
+++ b/app/views/notify/note_personal_snippet_email.text.erb
@@ -0,0 +1,8 @@
+New comment for Snippet <%= @snippet.id %>
+
+<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %>
+
+
+Author: <%= @note.author_name %>
+
+<%= @note.note %>
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 990bfbcf951..dfdaeb04869 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -78,7 +78,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Manual build' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -89,7 +89,7 @@
%span= build.name
- if artifacts.present?
.btn-group
- %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -102,8 +102,8 @@
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
- = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+ = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
- = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+ = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Cancel' , method: :post do
= icon("remove")
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index e77f23c7fd8..d94f23f5a38 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -9,10 +9,13 @@
= render "head"
%div{ class: container_class }
- .row-content-block.second-block.content-component-block
+ .row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
@@ -30,8 +33,6 @@
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
new file mode 100644
index 00000000000..fb5274e5253
--- /dev/null
+++ b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Improve button accessibility on pipelines page
+merge_request: 8561
+author:
diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml
new file mode 100644
index 00000000000..351c53bd076
--- /dev/null
+++ b/changelogs/unreleased/26447-fix-tab-list-order.yml
@@ -0,0 +1,4 @@
+---
+title: Fix tab index order on branch commits list page
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
new file mode 100644
index 00000000000..87ae8233c4a
--- /dev/null
+++ b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort by Recent Sign-in in Admin Area
+merge_request: 8637
+author: Poornima M
diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
new file mode 100644
index 00000000000..77750b55e7e
--- /dev/null
+++ b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
@@ -0,0 +1,4 @@
+---
+title: Hide version check image if there is no internet connection
+merge_request: 8355
+author: Ken Ding
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
new file mode 100644
index 00000000000..4551212759f
--- /dev/null
+++ b/changelogs/unreleased/fix_broken_diff_discussions.yml
@@ -0,0 +1,4 @@
+---
+title: Make MR-review-discussions more reliable
+merge_request:
+author:
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
new file mode 100644
index 00000000000..f32b3aea3c8
--- /dev/null
+++ b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
@@ -0,0 +1,4 @@
+---
+title: adds avatar for discussion note
+merge_request: 8734
+author:
diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml
new file mode 100644
index 00000000000..5ce080b6912
--- /dev/null
+++ b/changelogs/unreleased/newline-eslint-rule.yml
@@ -0,0 +1,4 @@
+---
+title: Flag multiple empty lines in eslint, fix offenses.
+merge_request: 8137
+author:
diff --git a/changelogs/unreleased/no_project_notes.yml b/changelogs/unreleased/no_project_notes.yml
new file mode 100644
index 00000000000..6106c027360
--- /dev/null
+++ b/changelogs/unreleased/no_project_notes.yml
@@ -0,0 +1,4 @@
+---
+title: Support notes when a project is not specified (personal snippet notes)
+merge_request: 8468
+author:
diff --git a/changelogs/unreleased/tc-only-mr-button-if-allowed.yml b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
new file mode 100644
index 00000000000..a7f5dcb560c
--- /dev/null
+++ b/changelogs/unreleased/tc-only-mr-button-if-allowed.yml
@@ -0,0 +1,4 @@
+---
+title: Only show Merge Request button when user can create a MR
+merge_request: 8639
+author:
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 3b8771543e4..e0702e06cc9 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -1,3 +1,117 @@
+# Autoload all classes that we want to instrument, and instrument the methods we
+# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
+# that we can stub it for testing, as it is only called when metrics are
+# enabled.
+#
+# rubocop:disable Metrics/AbcSize
+def instrument_classes(instrumentation)
+ instrumentation.instrument_instance_methods(Gitlab::Shell)
+
+ instrumentation.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ next unless const.is_a?(Module)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Path to search => prefix to strip from constant
+ paths_to_instrument = {
+ ['app', 'finders'] => ['app', 'finders'],
+ ['app', 'mailers', 'emails'] => ['app', 'mailers'],
+ ['app', 'services', '**'] => ['app', 'services'],
+ ['lib', 'gitlab', 'conflicts'] => ['lib'],
+ ['lib', 'gitlab', 'diff'] => ['lib'],
+ ['lib', 'gitlab', 'email', 'message'] => ['lib'],
+ ['lib', 'gitlab', 'checks'] => ['lib']
+ }
+
+ paths_to_instrument.each do |(path, prefix)|
+ prefix = Rails.root.join(*prefix)
+
+ Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
+ path = Pathname.new(file_path).relative_path_from(prefix)
+ const = path.to_s.sub('.rb', '').camelize.constantize
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
+ instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
+
+ [
+ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
+ :Tag, :TagCollection, :Tree
+ ].each do |name|
+ const = Rugged.const_get(name)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Banzai::Renderer)
+ instrumentation.instrument_methods(Banzai::Querying)
+
+ instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
+ instrumentation.instrument_instance_methods(Banzai::Redactor)
+ instrumentation.instrument_methods(Banzai::NoteRenderer)
+
+ [Issuable, Mentionable, Participable].each do |klass|
+ instrumentation.instrument_instance_methods(klass)
+ instrumentation.instrument_instance_methods(klass::ClassMethods)
+ end
+
+ instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
+ instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
+
+ # Instrument the classes used for checking if somebody has push access.
+ instrumentation.instrument_instance_methods(Gitlab::GitAccess)
+ instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
+
+ instrumentation.instrument_instance_methods(API::Helpers)
+
+ instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
+
+ instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
+ instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+
+ [:XML, :HTML].each do |namespace|
+ namespace_mod = Nokogiri.const_get(namespace)
+
+ instrumentation.instrument_methods(namespace_mod)
+ instrumentation.instrument_methods(namespace_mod::Document)
+ end
+
+ instrumentation.instrument_methods(Rinku)
+ instrumentation.instrument_instance_methods(Repository)
+
+ instrumentation.instrument_methods(Gitlab::Highlight)
+ instrumentation.instrument_instance_methods(Gitlab::Highlight)
+
+ # This is a Rails scope so we have to instrument it manually.
+ instrumentation.instrument_method(Project, :visible_to_user)
+end
+# rubocop:enable Metrics/AbcSize
+
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
@@ -49,110 +163,7 @@ if Gitlab::Metrics.enabled?
end
Gitlab::Metrics::Instrumentation.configure do |config|
- config.instrument_instance_methods(Gitlab::Shell)
-
- config.instrument_methods(Gitlab::Git)
-
- Gitlab::Git.constants.each do |name|
- const = Gitlab::Git.const_get(name)
-
- next unless const.is_a?(Module)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Path to search => prefix to strip from constant
- paths_to_instrument = {
- ['app', 'finders'] => ['app', 'finders'],
- ['app', 'mailers', 'emails'] => ['app', 'mailers'],
- ['app', 'services', '**'] => ['app', 'services'],
- ['lib', 'gitlab', 'conflicts'] => ['lib'],
- ['lib', 'gitlab', 'diff'] => ['lib'],
- ['lib', 'gitlab', 'email', 'message'] => ['lib'],
- ['lib', 'gitlab', 'checks'] => ['lib']
- }
-
- paths_to_instrument.each do |(path, prefix)|
- prefix = Rails.root.join(*prefix)
-
- Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
- path = Pathname.new(file_path).relative_path_from(prefix)
- const = path.to_s.sub('.rb', '').camelize.constantize
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Premailer::Adapter::Nokogiri)
- config.instrument_instance_methods(Premailer::Adapter::Nokogiri)
-
- [
- :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
- :Tag, :TagCollection, :Tree
- ].each do |name|
- const = Rugged.const_get(name)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Instruments all Banzai filters and reference parsers
- {
- Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
- ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
- }.each do |const_name, path|
- Dir[path].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai.const_get(const_name).const_get(klass)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Banzai::Renderer)
- config.instrument_methods(Banzai::Querying)
-
- config.instrument_instance_methods(Banzai::ObjectRenderer)
- config.instrument_instance_methods(Banzai::Redactor)
- config.instrument_methods(Banzai::NoteRenderer)
-
- [Issuable, Mentionable, Participable].each do |klass|
- config.instrument_instance_methods(klass)
- config.instrument_instance_methods(klass::ClassMethods)
- end
-
- config.instrument_methods(Gitlab::ReferenceExtractor)
- config.instrument_instance_methods(Gitlab::ReferenceExtractor)
-
- # Instrument the classes used for checking if somebody has push access.
- config.instrument_instance_methods(Gitlab::GitAccess)
- config.instrument_instance_methods(Gitlab::GitAccessWiki)
-
- config.instrument_instance_methods(API::Helpers)
-
- config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
-
- config.instrument_instance_methods(Rouge::Plugins::Redcarpet)
- config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
-
- [:XML, :HTML].each do |namespace|
- namespace_mod = Nokogiri.const_get(namespace)
-
- config.instrument_methods(namespace_mod)
- config.instrument_methods(namespace_mod::Document)
- end
-
- config.instrument_methods(Rinku)
- config.instrument_instance_methods(Repository)
-
- config.instrument_methods(Gitlab::Highlight)
- config.instrument_instance_methods(Gitlab::Highlight)
-
- # This is a Rails scope so we have to instrument it manually.
- config.instrument_method(Project, :visible_to_user)
+ instrument_classes(config)
end
GC::Profiler.enable
diff --git a/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
index 2cbe626d752..d5116dfab49 100644
--- a/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
+++ b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
@@ -3,7 +3,7 @@ class AddEstimateToIssuablesCe < ActiveRecord::Migration
DOWNTIME = false
- def change
+ def up
unless column_exists?(:issues, :time_estimate)
add_column :issues, :time_estimate, :integer
end
@@ -12,4 +12,14 @@ class AddEstimateToIssuablesCe < ActiveRecord::Migration
add_column :merge_requests, :time_estimate, :integer
end
end
+
+ def down
+ if column_exists?(:issues, :time_estimate)
+ remove_column :issues, :time_estimate
+ end
+
+ if column_exists?(:merge_requests, :time_estimate)
+ remove_column :merge_requests, :time_estimate
+ end
+ end
end
diff --git a/db/migrate/20161223034646_create_timelogs_ce.rb b/db/migrate/20161223034646_create_timelogs_ce.rb
index e8a4b406012..66d9cd823fb 100644
--- a/db/migrate/20161223034646_create_timelogs_ce.rb
+++ b/db/migrate/20161223034646_create_timelogs_ce.rb
@@ -3,7 +3,7 @@ class CreateTimelogsCe < ActiveRecord::Migration
DOWNTIME = false
- def change
+ def up
unless table_exists?(:timelogs)
create_table :timelogs do |t|
t.integer :time_spent, null: false
@@ -17,4 +17,8 @@ class CreateTimelogsCe < ActiveRecord::Migration
add_index :timelogs, :user_id
end
end
+
+ def down
+ drop_table :timelogs if table_exists?(:timelogs)
+ end
end
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index f3c2e72341f..33b9b28433a 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -27,6 +27,7 @@ Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
+Redis Version: 3.2.5
Sidekiq Version: 2.17.8
GitLab information
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 0950c3d2e88..be659fa4a6a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -129,12 +129,7 @@ module API
end
end
- # Delete all merged branches
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id/repository/branches/delete_merged
+ desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index ab7af1cad21..6640168bfa2 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -53,6 +53,10 @@ module Banzai
context[:project]
end
+ def skip_project_check?
+ context[:skip_project_check]
+ end
+
def reference_class(type)
"gfm gfm-#{type} has-tooltip"
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f842b1fb779..1aa9355b256 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc if project.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -58,7 +58,7 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
- if username == 'all'
+ if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
link_to_namespace(namespace, link_content: link_content) || match
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index f31fb6c3f71..74663556cbb 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -52,9 +52,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
- def cacheless_render_field(object, field)
+ def cacheless_render_field(object, field, options = {})
text = object.__send__(field)
- context = object.banzai_render_context(field)
+ context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 2ff27e46d64..4ebd48a3fc7 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -9,7 +9,9 @@ module Gitlab
end
def ensure_application_settings!
- if connect_to_db?
+ return fake_application_settings unless connect_to_db?
+
+ unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
begin
settings = ::ApplicationSetting.current
# In case Redis isn't running or the Redis UNIX socket file is not available
@@ -20,43 +22,23 @@ module Gitlab
settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
- settings || fake_application_settings
+ settings || in_memory_application_settings
end
def sidekiq_throttling_enabled?
current_application_settings.sidekiq_throttling_enabled?
end
+ def in_memory_application_settings
+ @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting::DEFAULTS)
+ # In case migrations the application_settings table is not created yet,
+ # we fallback to a simple OpenStruct
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
+ fake_application_settings
+ end
+
def fake_application_settings
- OpenStruct.new(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- signup_enabled: Settings.gitlab['signup_enabled'],
- signin_enabled: Settings.gitlab['signin_enabled'],
- gravatar_enabled: Settings.gravatar['enabled'],
- koding_enabled: false,
- plantuml_enabled: false,
- sign_in_text: nil,
- after_sign_up_text: nil,
- help_page_text: nil,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
- default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
- shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
- max_artifacts_size: Settings.artifacts['max_size'],
- require_two_factor_authentication: false,
- two_factor_grace_period: 48,
- akismet_enabled: false,
- repository_checks_enabled: true,
- container_registry_token_expire_delay: 5,
- user_default_external: false,
- sidekiq_throttling_enabled: false,
- )
+ OpenStruct.new(::ApplicationSetting::DEFAULTS)
end
private
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 3f635be22ba..a55adc9b1c8 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class ProjectCreator
+ include Gitlab::CurrentSettings
+
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@@ -34,7 +36,7 @@ module Gitlab
end
def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
end
#
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 45958710c13..52276cbcd9a 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -5,8 +5,6 @@
#
module Gitlab
module ImportSources
- extend CurrentSettings
-
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index dffea8ed155..f7c831892ee 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -11,8 +11,10 @@ namespace :gitlab do
gem_version = run_command(%W(gem --version))
# check Bundler version
bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
- # check Bundler version
+ # check Rake version
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ # check redis version
+ redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
@@ -24,6 +26,7 @@ namespace :gitlab do
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
+ puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 56ecf2bb644..cfe18dd4b6c 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
describe HealthCheckController do
+ include StubENV
+
let(:token) { current_application_settings.health_check_access_token }
let(:json_response) { JSON.parse(response.body) }
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
describe 'GET #index' do
context 'when services are up but NO access token' do
it 'returns a not found page' do
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index a10ba629760..476f2a37793 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -13,6 +13,7 @@ FactoryGirl.define do
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
+ factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :system_note, traits: [:system]
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
@@ -70,6 +71,11 @@ FactoryGirl.define do
noteable { create(:project_snippet, project: project) }
end
+ trait :on_personal_snippet do
+ noteable { create(:personal_snippet) }
+ project nil
+ end
+
trait :system do
system true
end
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 66044b44495..e8e080ce3e2 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -1,10 +1,13 @@
require 'rails_helper'
feature 'Admin disables Git access protocol', feature: true do
+ include StubENV
+
let(:project) { create(:empty_project, :empty_repo) }
let(:admin) { create(:admin) }
background do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as(admin)
end
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index dec2dedf2b5..f7e49a56deb 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
feature "Admin Health Check", feature: true do
+ include StubENV
include WaitForAjax
before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
@@ -12,11 +14,12 @@ feature "Admin Health Check", feature: true do
visit admin_health_check_path
end
- it { page.has_text? 'Health Check' }
- it { page.has_text? 'Health information can be retrieved' }
-
it 'has a health check access token' do
+ page.has_text? 'Health Check'
+ page.has_text? 'Health information can be retrieved'
+
token = current_application_settings.health_check_access_token
+
expect(page).to have_content("Access token is #{token}")
expect(page).to have_selector('#health-check-token', text: token)
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index d92c66b689d..f05fbe3d062 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -1,7 +1,10 @@
require 'spec_helper'
describe "Admin Runners" do
+ include StubENV
+
before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 47fa2f14307..de42ab81fac 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -1,7 +1,10 @@
require 'spec_helper'
feature 'Admin updates settings', feature: true do
- before(:each) do
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
visit admin_application_settings_path
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 661fb761809..855247de2ea 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -1,7 +1,12 @@
require 'rails_helper'
feature 'Admin uses repository checks', feature: true do
- before { login_as :admin }
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ login_as :admin
+ end
scenario 'to trigger a single check' do
project = create(:empty_project)
@@ -29,7 +34,7 @@ feature 'Admin uses repository checks', feature: true do
scenario 'to clear all repository checks', js: true do
visit admin_application_settings_path
-
+
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
click_link 'Clear all repository checks'
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index bea00160f96..f09ad2dd86b 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -40,6 +40,16 @@ describe 'Dropdown label', js: true, feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
+ describe 'keyboard navigation' do
+ it 'selects label' do
+ send_keys_to_filtered_search('label:')
+
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq("label:~#{special_label.name} ")
+ end
+ end
+
describe 'behavior' do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 56b1d354eb0..90eb60eb337 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -20,6 +20,22 @@ describe 'Search bar', js: true, feature: true do
left_style.to_s.gsub('left: ', '').to_f
end
+ describe 'keyboard navigation' do
+ it 'makes item active' do
+ filtered_search.native.send_keys(:down)
+
+ page.within '#js-dropdown-hint' do
+ expect(page).to have_selector('.dropdown-active')
+ end
+ end
+
+ it 'selects item' do
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq('author:')
+ end
+ end
+
describe 'clear search button' do
it 'clears text' do
search_text = 'search_text'
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
new file mode 100644
index 00000000000..b6728960fb8
--- /dev/null
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+feature 'Merge Request button', feature: true do
+ shared_examples 'Merge Request button only shown when allowed' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { create(:project, :public, forked_from_project: project) }
+
+ context 'not logged in' do
+ it 'does not show Create Merge Request button' do
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+ end
+
+ context 'logged in as developer' do
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ it 'shows Create Merge Request button' do
+ href = new_namespace_project_merge_request_path(project.namespace,
+ project,
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
+
+ visit url
+
+ within("#content-body") do
+ expect(page).to have_link(label, href: href)
+ end
+ end
+
+ context 'merge requests are disabled' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
+ end
+
+ it 'does not show Create Merge Request button' do
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+ end
+ end
+
+ context 'logged in as non-member' do
+ before do
+ login_as(user)
+ end
+
+ it 'does not show Create Merge Request button' do
+ visit url
+
+ within("#content-body") do
+ expect(page).not_to have_link(label)
+ end
+ end
+
+ context 'on own fork of project' do
+ let(:user) { forked_project.owner }
+
+ it 'shows Create Merge Request button' do
+ href = new_namespace_project_merge_request_path(forked_project.namespace,
+ forked_project,
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
+
+ visit fork_url
+
+ within("#content-body") do
+ expect(page).to have_link(label, href: href)
+ end
+ end
+ end
+ end
+ end
+
+ context 'on branches page' do
+ it_behaves_like 'Merge Request button only shown when allowed' do
+ let(:label) { 'Merge Request' }
+ let(:url) { namespace_project_branches_path(project.namespace, project) }
+ let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
+ end
+ end
+
+ context 'on compare page' do
+ it_behaves_like 'Merge Request button only shown when allowed' do
+ let(:label) { 'Create Merge Request' }
+ let(:url) { namespace_project_compare_path(project.namespace, project, from: 'master', to: 'feature') }
+ let(:fork_url) { namespace_project_compare_path(forked_project.namespace, forked_project, from: 'master', to: 'feature') }
+ end
+ end
+
+ context 'on commits page' do
+ it_behaves_like 'Merge Request button only shown when allowed' do
+ let(:label) { 'Create Merge Request' }
+ let(:url) { namespace_project_commits_path(project.namespace, project, 'feature') }
+ let(:fork_url) { namespace_project_commits_path(forked_project.namespace, forked_project, 'feature') }
+ end
+ end
+end
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/metrics_spec.rb
new file mode 100644
index 00000000000..bb595162370
--- /dev/null
+++ b/spec/initializers/metrics_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require_relative '../../config/initializers/metrics'
+
+describe 'instrument_classes', lib: true do
+ let(:config) { double(:config) }
+
+ before do
+ allow(config).to receive(:instrument_method)
+ allow(config).to receive(:instrument_methods)
+ allow(config).to receive(:instrument_instance_methods)
+ end
+
+ it 'can autoload and instrument all files' do
+ expect { instrument_classes(config) }.not_to raise_error
+ end
+end
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index cf19aa05031..a2d57824585 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -21,7 +21,6 @@
messages = $('.abuse-reports .message');
});
-
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 21241116e29..95796f23894 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -33,7 +33,6 @@ describe('Rollback Component', () => {
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
-
it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index 031f9ca03c9..1ce8f28e568 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -52,5 +52,22 @@
expect(value).toBe(null);
});
});
+
+ describe('gl.utils.normalizedHeaders', () => {
+ it('should upperCase all the header keys to keep them consistent', () => {
+ const apiHeaders = {
+ 'X-Something-Workhorse': { workhorse: 'ok' },
+ 'x-something-nginx': { nginx: 'ok' },
+ };
+
+ const normalized = gl.utils.normalizeHeaders(apiHeaders);
+
+ const WORKHORSE = 'X-SOMETHING-WORKHORSE';
+ const NGINX = 'X-SOMETHING-NGINX';
+
+ expect(normalized[WORKHORSE].workhorse).toBe('ok');
+ expect(normalized[NGINX].nginx).toBe('ok');
+ });
+ });
});
})();
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 5bfeb82e738..3e1ac9fb2b2 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -152,6 +152,30 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
end
+ context 'when a project is not specified' do
+ let(:project) { nil }
+
+ it 'does not link a User' do
+ doc = reference_filter("Hey #{reference}")
+
+ expect(doc).not_to include('a')
+ end
+
+ context 'when skip_project_check set to true' do
+ it 'links to a User' do
+ doc = reference_filter("Hey #{reference}", skip_project_check: true)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ it 'does not link users using @all reference' do
+ doc = reference_filter("Hey #{User.reference_prefix}all", skip_project_check: true)
+
+ expect(doc).not_to include('a')
+ end
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 004341ffd02..b01c4805a34 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -1,36 +1,64 @@
require 'spec_helper'
describe Gitlab::CurrentSettings do
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
describe '#current_application_settings' do
- it 'attempts to use cached values first' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'with DB available' do
+ before do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ end
- expect(current_application_settings).to be_a(ApplicationSetting)
- end
+ it 'attempts to use cached values first' do
+ expect(ApplicationSetting).to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
- it 'does not attempt to connect to DB or Redis' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ it 'falls back to DB if Redis returns an empty value' do
+ expect(ApplicationSetting).to receive(:last).and_call_original
- expect(current_application_settings).to eq fake_application_settings
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
+
+ it 'falls back to DB if Redis fails' do
+ expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
+ expect(ApplicationSetting).to receive(:last).and_call_original
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
end
- it 'falls back to DB if Redis returns an empty value' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ context 'with DB unavailable' do
+ before do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
+ end
- expect(current_application_settings).to be_a(ApplicationSetting)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(OpenStruct)
+ end
end
- it 'falls back to DB if Redis fails' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ end
+
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(current_application_settings).not_to be_persisted
+ end
end
end
end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 1bdf005c823..4d57efd3c53 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -171,6 +171,33 @@ describe Ability, lib: true do
end
end
+ describe '.users_that_can_read_personal_snippet' do
+ def users_for_snippet(snippet)
+ described_class.users_that_can_read_personal_snippet(users, snippet)
+ end
+
+ let(:users) { create_list(:user, 3) }
+ let(:author) { users[0] }
+
+ it 'private snippet is readable only by its author' do
+ snippet = create(:personal_snippet, :private, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array([author])
+ end
+
+ it 'internal snippet is readable by all registered users' do
+ snippet = create(:personal_snippet, :public, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array(users)
+ end
+
+ it 'public snippet is readable by all users' do
+ snippet = create(:personal_snippet, :public, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array(users)
+ end
+ end
+
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 132858950d5..b73028f0bc0 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -30,12 +30,20 @@ describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
let!(:user2) { create(:user, username: 'john') }
- let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
+ let!(:user3) { create(:user, username: 'jim') }
+ let(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
subject { issue.mentioned_users }
- it { is_expected.to include(user) }
- it { is_expected.not_to include(user2) }
+ it { expect(subject).to contain_exactly(user) }
+
+ context 'when a note on personal snippet' do
+ let!(:note) { create(:note_on_personal_snippet, note: "#{user.to_reference} mentioned #{user3.to_reference}") }
+
+ subject { note.mentioned_users }
+
+ it { expect(subject).to contain_exactly(user, user3) }
+ end
end
describe '#referenced_mentionables' do
@@ -138,6 +146,16 @@ describe Issue, "Mentionable" do
issue.update_attributes(description: issues[1].to_reference)
issue.create_new_cross_references!
end
+
+ it 'notifies new references from project snippet note' do
+ snippet = create(:snippet, project: project)
+ note = create(:note, note: issues[0].to_reference, noteable: snippet, project: project, author: author)
+
+ expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
+
+ note.update_attributes(note: issues[1].to_reference)
+ note.create_new_cross_references!
+ end
end
def create_issue(description:)
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 310fecd8a5c..1b8ae356f1f 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -52,6 +52,19 @@ describe Note, models: true do
subject { create(:note) }
it { is_expected.to be_valid }
end
+
+ context 'when project is missing for a project related note' do
+ subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when noteable is a personal snippet' do
+ subject { build(:note_on_personal_snippet) }
+
+ it 'is valid without project' do
+ is_expected.to be_valid
+ end
+ end
end
describe "Commit notes" do
@@ -139,6 +152,7 @@ describe Note, models: true do
with([{
text: note1.note,
context: {
+ skip_project_check: false,
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
@@ -150,6 +164,7 @@ describe Note, models: true do
with([{
text: note2.note,
context: {
+ skip_project_check: false,
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
@@ -306,4 +321,70 @@ describe Note, models: true do
end
end
end
+
+ describe '#for_personal_snippet?' do
+ it 'returns false for a project snippet note' do
+ expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
+ end
+
+ it 'returns true for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy
+ end
+ end
+
+ describe '#to_ability_name' do
+ it 'returns snippet for a project snippet note' do
+ expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
+ end
+
+ it 'returns personal_snippet for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet')
+ end
+
+ it 'returns merge_request for an MR note' do
+ expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request')
+ end
+
+ it 'returns issue for an issue note' do
+ expect(build(:note_on_issue).to_ability_name).to eq('issue')
+ end
+
+ it 'returns issue for a commit note' do
+ expect(build(:note_on_commit).to_ability_name).to eq('commit')
+ end
+ end
+
+ describe '#cache_markdown_field' do
+ let(:html) { '<p>some html</p>'}
+
+ context 'note for a project snippet' do
+ let(:note) { build(:note_on_project_snippet) }
+
+ before do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).
+ with(note, :note, { skip_project_check: false }).and_return(html)
+
+ note.save
+ end
+
+ it 'creates a note' do
+ expect(note.note_html).to eq(html)
+ end
+ end
+
+ context 'note for a personal snippet' do
+ let(:note) { build(:note_on_personal_snippet) }
+
+ before do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).
+ with(note, :note, { skip_project_check: true }).and_return(html)
+
+ note.save
+ end
+
+ it 'creates a note' do
+ expect(note.note_html).to eq(html)
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ca3d4ff0aa9..c45d0b9317b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -797,14 +797,14 @@ describe User, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
- it "is true if avatar is image" do
+ it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
- it "is false if avatar is html page" do
+ it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(["only images allowed"])
+ expect(user.avatar_type).to eq(['only images allowed'])
end
end
@@ -926,8 +926,8 @@ describe User, models: true do
end
end
- describe "#starred?" do
- it "determines if user starred a project" do
+ describe '#starred?' do
+ it 'determines if user starred a project' do
user = create :user
project1 = create(:empty_project, :public)
project2 = create(:empty_project, :public)
@@ -953,8 +953,8 @@ describe User, models: true do
end
end
- describe "#toggle_star" do
- it "toggles stars" do
+ describe '#toggle_star' do
+ it 'toggles stars' do
user = create :user
project = create(:empty_project, :public)
@@ -966,31 +966,44 @@ describe User, models: true do
end
end
- describe "#sort" do
+ describe '#sort' do
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
+ @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
end
- it "sorts users by the recent sign-in time" do
- expect(User.sort('recent_sign_in').first).to eq(@user)
+ context 'when sort by recent_sign_in' do
+ it 'sorts users by the recent sign-in time' do
+ expect(User.sort('recent_sign_in').first).to eq(@user)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('recent_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users by the oldest sign-in time" do
- expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ context 'when sort by oldest_sign_in' do
+ it 'sorts users by the oldest sign-in time' do
+ expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('oldest_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users in descending order by their creation time" do
+ it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
end
- it "sorts users in ascending order by their creation time" do
- expect(User.sort('created_asc').first).to eq(@user1)
+ it 'sorts users in ascending order by their creation time' do
+ expect(User.sort('created_asc').first).to eq(@user2)
end
- it "sorts users by id in descending order when nil is passed" do
- expect(User.sort(nil).first).to eq(@user1)
+ it 'sorts users by id in descending order when nil is passed' do
+ expect(User.sort(nil).first).to eq(@user2)
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index a3798c8cd6c..91202244227 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -337,8 +337,7 @@ describe API::Internal, api: true do
context 'ssh access has been disabled' do
before do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'http')
+ stub_application_setting(enabled_git_access_protocol: 'http')
end
it 'rejects the SSH push' do
@@ -360,8 +359,7 @@ describe API::Internal, api: true do
context 'http access has been disabled' do
before do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
end
it 'rejects the HTTP push' do
@@ -383,8 +381,7 @@ describe API::Internal, api: true do
context 'web actions are always allowed' do
it 'allows WEB push' do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
project.team << [user, :developer]
push(key, project, 'web')
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index b0cc3ce5f5a..9c92a5080c6 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -15,39 +15,45 @@ describe Notes::CreateService, services: true do
context "valid params" do
it 'returns a valid note' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
end
it 'returns a persisted note' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_persisted
end
it 'note has valid content' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note.note).to eq(opts[:note])
end
+ it 'note belongs to the correct project' do
+ note = described_class.new(project, user, opts).execute
+
+ expect(note.project).to eq(project)
+ end
+
it 'TodoService#new_note is called' do
- note = build(:note)
- allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+ note = build(:note, project: project)
+ allow(Note).to receive(:new).with(opts) { note }
expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
it 'enqueues NewNoteWorker' do
- note = build(:note, id: 999)
- allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+ note = build(:note, id: 999, project: project)
+ allow(Note).to receive(:new).with(opts) { note }
expect(NewNoteWorker).to receive(:perform_async).with(note.id)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
@@ -75,6 +81,27 @@ describe Notes::CreateService, services: true do
end
end
end
+
+ describe 'personal snippet note' do
+ subject { described_class.new(nil, user, params).execute }
+
+ let(:snippet) { create(:personal_snippet) }
+ let(:params) do
+ { note: 'comment', noteable_type: 'Snippet', noteable_id: snippet.id }
+ end
+
+ it 'returns a valid note' do
+ expect(subject).to be_valid
+ end
+
+ it 'returns a persisted note' do
+ expect(subject).to be_persisted
+ end
+
+ it 'note has valid content' do
+ expect(subject.note).to eq(params[:note])
+ end
+ end
end
describe "award emoji" do
@@ -88,7 +115,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.name).to eq('smile')
@@ -100,7 +127,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.note).to eq(opts[:note])
@@ -115,7 +142,7 @@ describe Notes::CreateService, services: true do
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f3e80ac22a0..bfbee7ca35f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -269,6 +269,55 @@ describe NotificationService, services: true do
end
end
+ context 'personal snippet note' do
+ let(:snippet) { create(:personal_snippet, :public, author: @u_snippet_author) }
+ let(:note) { create(:note_on_personal_snippet, noteable: snippet, note: '@mentioned note', author: @u_note_author) }
+
+ before do
+ @u_watcher = create_global_setting_for(create(:user), :watch)
+ @u_participant = create_global_setting_for(create(:user), :participating)
+ @u_disabled = create_global_setting_for(create(:user), :disabled)
+ @u_mentioned = create_global_setting_for(create(:user, username: 'mentioned'), :mention)
+ @u_mentioned_level = create_global_setting_for(create(:user, username: 'participator'), :mention)
+ @u_note_author = create(:user, username: 'note_author')
+ @u_snippet_author = create(:user, username: 'snippet_author')
+ @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
+
+ reset_delivered_emails!
+ end
+
+ let!(:notes) do
+ [
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_watcher),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
+ ]
+ end
+
+ describe '#new_note' do
+ it 'notifies the participants' do
+ notification.new_note(note)
+
+ # it emails participants
+ should_email(@u_watcher)
+ should_email(@u_participant)
+ should_email(@u_watcher)
+ should_email(@u_snippet_author)
+
+ # it emails mentioned users
+ should_email(@u_mentioned)
+
+ # it does not email participants with mention notification level
+ should_not_email(@u_mentioned_level)
+
+ # it does not email note author
+ should_not_email(@u_note_author)
+ end
+ end
+ end
+
context 'commit note' do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_commit, project: project) }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6ee3307512d..f78899134d5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,6 +2,7 @@ require './spec/simplecov_env'
SimpleCovEnv.start!
ENV["RAILS_ENV"] ||= 'test'
+ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'