diff options
350 files changed, 3646 insertions, 1286 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/.haml-lint.yml b/.haml-lint.yml index 7c8a9c4fd17..528f99d08d2 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -46,7 +46,7 @@ linters: max: 80 MultilinePipe: - enabled: false + enabled: true MultilineScript: enabled: true @@ -77,7 +77,7 @@ linters: - Style/WhileUntilModifier RubyComments: - enabled: false + enabled: true SpaceBeforeScript: enabled: true @@ -97,7 +97,7 @@ linters: enabled: true UnnecessaryInterpolation: - enabled: false + enabled: true UnnecessaryStringOutput: - enabled: false + enabled: true @@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0' # Authentication libraries gem 'devise', '~> 4.2' gem 'doorkeeper', '~> 4.2.0' -gem 'omniauth', '~> 1.3.1' +gem 'omniauth', '~> 1.3.2' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-cas3', '~> 1.1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 104e6444803..c9115982838 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -449,7 +449,7 @@ GEM octokit (4.6.2) sawyer (~> 0.8.0, >= 0.5.3) oj (2.17.4) - omniauth (1.3.1) + omniauth (1.3.2) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) @@ -925,7 +925,7 @@ DEPENDENCIES oauth2 (~> 1.2.0) octokit (~> 4.6.2) oj (~> 2.17.4) - omniauth (~> 1.3.1) + omniauth (~> 1.3.2) omniauth-auth0 (~> 1.4.1) omniauth-authentiq (~> 0.2.0) omniauth-azure-oauth2 (~> 0.0.6) diff --git a/README.md b/README.md index 4e28f3aacfd..4f85fac4a56 100644 --- a/README.md +++ b/README.md @@ -113,4 +113,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ## Is it awesome? Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. -[These people](https://twitter.com/gitlab/favorites) seem to like it. +[These people](https://twitter.com/gitlab/likes) seem to like it. 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_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index f4ec3b206cc..7d297b8eee8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -20,6 +20,9 @@ if (selected.tagName === 'LI') { if (selected.hasAttribute('data-value')) { this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); } else { const token = selected.querySelector('.js-filter-hint').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); 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_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 9128ea907b3..859d6515531 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -39,6 +39,7 @@ } this.dismissDropdown(); + this.dispatchInputEvent(); } } @@ -84,6 +85,12 @@ })); } + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); + } + hideDropdown() { this.getCurrentHook().list.hide(); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 408a0dfd768..00e1c28692f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -61,11 +61,19 @@ const word = `${tokenName}:${tokenValue}`; // Get the string to replace - const selectionStart = input.selectionStart; + let newCaretPosition = input.selectionStart; const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; - gl.FilteredSearchDropdownManager.updateInputCaretPosition(selectionStart, input); + + // If we have added a tokenValue at the end of the input, + // add a space and set selection to the end + if (right >= inputValue.length && tokenValue !== '') { + input.value += ' '; + newCaretPosition = input.value.length; + } + + gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); } static updateInputCaretPosition(selectionStart, input) { 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 c7b72b36561..8d62324b79f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -25,6 +25,7 @@ } bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); @@ -32,6 +33,7 @@ this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.tokenChange = this.tokenChange.bind(this); + this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); @@ -42,6 +44,7 @@ } unbindEvents() { + this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); @@ -61,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(); + } } } @@ -88,6 +104,11 @@ this.dropdownManager.resetDropdowns(); } + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } + loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); const usernameParams = this.getUsernameParams(); 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/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index dccf5177e35..868f28cd356 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -15,6 +15,7 @@ } .ci-status-icon-pending, +.ci-status-icon-failed_with_warnings, .ci-status-icon-success_with_warnings { color: $gl-warning; diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 4decee2c525..5f4211147f3 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -46,10 +46,6 @@ font-weight: bold; } - .fa-clipboard { - color: $dropdown-title-btn-color; - } - .commit-info { &.branches { margin-left: 8px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index cbe38b60d60..da0caa30c26 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -195,10 +195,10 @@ ul.notes { } .note-body { - overflow: auto; + overflow-x: auto; + overflow-y: hidden; .note-text { - overflow: auto; word-wrap: break-word; @include md-typography; // Reset ul style types since we're nested inside a ul already 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/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 12bff32bbf3..88ea92c5afb 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -18,7 +18,6 @@ .file-finder-input:hover, .issuable-search-form:hover, .search-text-input:hover, -textarea:hover, .form-control:hover { border-color: lighten($dropdown-input-focus-border, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index f19275770be..6f31d4ed789 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -19,7 +19,8 @@ overflow: visible; } - &.ci-failed { + &.ci-failed, + &.ci-failed_with_warnings { color: $gl-danger; border-color: $gl-danger; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 1b4987dd738..543d5eac504 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -5,7 +5,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def update - if @application_setting.update_attributes(application_setting_params) + successful = ApplicationSettings::UpdateService + .new(@application_setting, current_user, application_setting_params) + .execute + + if successful redirect_to admin_application_settings_path, notice: 'Application settings saved successfully' else diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 3da44b9b888..306afb65f10 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController if signed_in?(resource_name) after_sign_in_path_for(resource) else - sign_in(resource) - if signed_in?(resource_name) - after_sign_in_path_for(resource) - else - new_session_path(resource_name) - end + flash[:notice] += " Please sign in." + new_session_path(resource_name) end end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b666aa01d6b..6576ebd5235 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -45,6 +45,8 @@ class SearchController < ApplicationController end @search_objects = @search_results.objects(@scope, params[:page]) + + check_single_commit_result end def autocomplete @@ -59,4 +61,16 @@ class SearchController < ApplicationController render json: search_autocomplete_opts(term).to_json end + + private + + def check_single_commit_result + if @search_results.single_commit_result? + only_commit = @search_results.objects('commits').first + query = params[:search].strip.downcase + found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query) + + redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha + end + end end 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/groups_helper.rb b/app/helpers/groups_helper.rb index 77dc9e7d538..926c9703628 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -14,7 +14,7 @@ module GroupsHelper def group_title(group, name = nil, url = nil) full_title = '' - group.parents.each do |parent| + group.ancestors.each do |parent| full_title += link_to(simple_sanitize(parent.name), group_path(parent)) full_title += ' / '.html_safe end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 9bab140e60a..715e5893a2c 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,23 +1,23 @@ module ServicesHelper def service_event_description(event) case event - when "push" + when "push", "push_events" "Event will be triggered by a push to the repository" - when "tag_push" + when "tag_push", "tag_push_events" "Event will be triggered when a new tag is pushed to the repository" - when "note" + when "note", "note_events" "Event will be triggered when someone adds a comment" - when "issue" + when "issue", "issue_events" "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue" + when "confidential_issue", "confidential_issue_events" "Event will be triggered when a confidential issue is created/updated/closed" - when "merge_request" + when "merge_request", "merge_request_events" "Event will be triggered when a merge request is created/updated/merged" - when "build" + when "build", "build_events" "Event will be triggered when a build status changes" - when "wiki_page" + when "wiki_page", "wiki_page_events" "Event will be triggered when a wiki page is created/updated" - when "commit" + when "commit", "commit_events" "Event will be triggered when a commit is created/updated" end end @@ -26,4 +26,6 @@ module ServicesHelper event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" end + + extend self 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/mailers/notify.rb b/app/mailers/notify.rb index 0bc1c19e9cd..0cd3456b4de 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -107,15 +107,11 @@ class Notify < BaseMailer def mail_thread(model, headers = {}) add_project_headers + add_unsubscription_headers_and_links + headers["X-GitLab-#{model.class.name}-ID"] = model.id headers['X-GitLab-Reply-Key'] = reply_key - if !@labels_url && @sent_notification && @sent_notification.unsubscribable? - headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>" - - @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) - end - if Gitlab::IncomingEmail.enabled? address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address.display_name = @project.name_with_namespace @@ -171,4 +167,16 @@ class Notify < BaseMailer headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Path'] = @project.path_with_namespace end + + def add_unsubscription_headers_and_links + return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable? + + list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)] + if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard? + list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}" + end + + headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',') + @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) + end end 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/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2a97e8bae4a..fab8497ec7d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -128,16 +128,21 @@ module Ci end def stages + # TODO, this needs refactoring, see gitlab-ce#26481. + + stages_query = statuses + .group('stage').select(:stage).order('max(stage_idx)') + status_sql = statuses.latest.where('stage=sg.stage').status_sql - stages_query = statuses.group('stage').select(:stage) - .order('max(stage_idx)') + warnings_sql = statuses.latest.select('COUNT(*) > 0') + .where('stage=sg.stage').failed_but_allowed.to_sql - stages_with_statuses = CommitStatus.from(stages_query, :sg). - pluck('sg.stage', status_sql) + stages_with_statuses = CommitStatus.from(stages_query, :sg) + .pluck('sg.stage', status_sql, "(#{warnings_sql})") stages_with_statuses.map do |stage| - Ci::Stage.new(self, name: stage.first, status: stage.last) + Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)]) end end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d035eda6df5..ca74c91b062 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -8,10 +8,11 @@ module Ci delegate :project, to: :pipeline - def initialize(pipeline, name:, status: nil) + def initialize(pipeline, name:, status: nil, warnings: nil) @pipeline = pipeline @name = name @status = status + @warnings = warnings end def to_param @@ -39,5 +40,17 @@ module Ci def builds @builds ||= pipeline.builds.where(stage: name) end + + def success? + status.to_s == 'success' + end + + def has_warnings? + if @warnings.nil? + statuses.latest.failed_but_allowed.any? + else + @warnings + end + end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 5d942cb0422..316bd2e512b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -21,6 +21,9 @@ class Commit DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_LINES = 50000 + # The SHA can be between 7 and 40 hex characters. + COMMIT_SHA_PATTERN = '\h{7,40}' + class << self def decorate(commits, project) commits.map do |commit| @@ -52,6 +55,10 @@ class Commit def from_hash(hash, project) new(Gitlab::Git::Commit.new(hash), project) end + + def valid_hash?(key) + !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key) + end end attr_accessor :raw @@ -77,8 +84,6 @@ class Commit # Pattern used to extract commit references from text # - # The SHA can be between 7 and 40 hex characters. - # # This pattern supports cross-project references. def self.reference_pattern @reference_pattern ||= %r{ @@ -88,7 +93,7 @@ class Commit end def self.link_reference_pattern - @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/) end def to_reference(from_project = nil, full: false) 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/has_status.rb b/app/models/concerns/has_status.rb index 90432fc4050..431c0354969 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -1,6 +1,7 @@ module HasStatus extend ActiveSupport::Concern + DEFAULT_STATUS = 'created' AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] 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/concerns/routable.rb b/app/models/concerns/routable.rb index 1108a64c59e..2b93aa30c0f 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -60,6 +60,21 @@ module Routable joins(:route).where(wheres.join(' OR ')) end end + + # Builds a relation to find multiple objects that are nested under user membership + # + # Usage: + # + # Klass.member_descendants(1) + # + # Returns an ActiveRecord::Relation. + def member_descendants(user_id) + joins(:route). + joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%') + INNER JOIN members ON members.source_id = r2.source_id + AND members.source_type = r2.source_type"). + where('members.user_id = ?', user_id) + end end private diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index ebc75100a54..68385dc47eb 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -11,7 +11,7 @@ module Taskable INCOMPLETE = 'incomplete'.freeze ITEM_PATTERN = / ^ - (?:\s*[-+*]|(?:\d+\.))? # optional list prefix + \s*(?:[-+*]|(?:\d+\.))? # optional list prefix \s* # optional whitespace prefix (\[\s\]|\[[xX]\]) # checkbox (\s.+) # followed by whitespace and some text. diff --git a/app/models/group.rb b/app/models/group.rb index 99675ddb366..4cdfd022094 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -201,7 +201,7 @@ class Group < Namespace end def members_with_parents - GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id)) + GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id)) end def users_with_parents 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/namespace.rb b/app/models/namespace.rb index d41833de66f..05f01445e67 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -130,6 +130,8 @@ class Namespace < ActiveRecord::Base Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + remove_exports! + # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir @@ -183,8 +185,26 @@ class Namespace < ActiveRecord::Base end end - def parents - @parents ||= parent ? parent.parents + [parent] : [] + # Scopes the model on ancestors of the record + def ancestors + if parent_id + path = route.path + paths = [] + + until path.blank? + path = path.rpartition('/').first + paths << path + end + + self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC') + else + self.class.none + end + end + + # Scopes the model on direct and indirect children of the record + def descendants + self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC') end private @@ -214,6 +234,8 @@ class Namespace < ActiveRecord::Base GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) end end + + remove_exports! end def refresh_access_of_projects_invited_groups @@ -226,4 +248,20 @@ class Namespace < ActiveRecord::Base def full_path_changed? path_changed? || parent_id_changed? end + + def remove_exports! + Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) + end + + def export_path + File.join(Gitlab::ImportExport.storage_path, full_path_was) + end + + def full_path_was + if parent + parent.full_path + '/' + path_was + else + path_was + 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/project.rb b/app/models/project.rb index 1630975b0d3..cd35601d76b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -121,8 +121,6 @@ class Project < ActiveRecord::Base # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' - # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 7c23b766763..3728f5642e4 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -25,7 +25,7 @@ You can create a Personal Access Token here: http://app.asana.com/-/account_api' end - def to_param + def self.to_param 'asana' end @@ -44,7 +44,7 @@ http://app.asana.com/-/account_api' ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index d839221d315..aeeff8917bf 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -12,7 +12,7 @@ class AssemblaService < Service 'Project Management Software (Source Commits Endpoint)' end - def to_param + def self.to_param 'assembla' end @@ -23,7 +23,7 @@ class AssemblaService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 4819bdbef8c..400020ee04a 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -40,7 +40,7 @@ class BambooService < CiService 'You must set up automatic revision labeling and a repository trigger in Bamboo.' end - def to_param + def self.to_param 'bamboo' end @@ -56,10 +56,6 @@ class BambooService < CiService ] end - def supported_events - %w(push) - end - def build_page(sha, ref) with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 338e685339a..046e2809f45 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService end end - def to_param + def self.to_param 'bugzilla' end end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index e77942d8f3c..0956c4a4ede 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -24,10 +24,6 @@ class BuildkiteService < CiService hook.save end - def supported_events - %w(push) - end - def execute(data) return unless supported_events.include?(data[:object_kind]) @@ -54,7 +50,7 @@ class BuildkiteService < CiService 'Continuous integration and deployments' end - def to_param + def self.to_param 'buildkite' end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 201b94b065b..ebd21e37189 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -19,11 +19,11 @@ class BuildsEmailService < Service 'Email the builds status to a list of recipients.' end - def to_param + def self.to_param 'builds_email' end - def supported_events + def self.supported_events %w(build) end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 5af93860d09..0de59af5652 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -12,7 +12,7 @@ class CampfireService < Service 'Simple web-based real-time group chat' end - def to_param + def self.to_param 'campfire' end @@ -24,7 +24,7 @@ class CampfireService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index b7ef44c3054..8468934425f 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -25,7 +25,7 @@ class ChatNotificationService < Service valid? end - def supported_events + def self.supported_events %w[push issue confidential_issue merge_request note tag_push build pipeline wiki_page] end @@ -82,19 +82,19 @@ class ChatNotificationService < Service def get_message(object_kind, data) case object_kind when "push", "tag_push" - PushMessage.new(data) + ChatMessage::PushMessage.new(data) when "issue" - IssueMessage.new(data) unless is_update?(data) + ChatMessage::IssueMessage.new(data) unless is_update?(data) when "merge_request" - MergeMessage.new(data) unless is_update?(data) + ChatMessage::MergeMessage.new(data) unless is_update?(data) when "note" - NoteMessage.new(data) + ChatMessage::NoteMessage.new(data) when "build" - BuildMessage.new(data) if should_build_be_notified?(data) + ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data) when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) + ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) when "wiki_page" - WikiPageMessage.new(data) + ChatMessage::WikiPageMessage.new(data) end end diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 0bc160af604..2bcff541cc0 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end - def supported_events - [] + def self.supported_events + %w() end def can_test? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 4de0106707e..82979c8bd34 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -8,7 +8,7 @@ class CiService < Service self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index b2f426dc2ac..dea915a4d05 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService end end - def to_param + def self.to_param 'custom_issue_tracker' end diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index ab353a1abe6..91a55514a9a 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -5,8 +5,8 @@ class DeploymentService < Service default_value_for :category, 'deployment' - def supported_events - [] + def self.supported_events + %w() end def predefined_variables diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 4bbbebf54cb..0a217d8caba 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -32,7 +32,7 @@ class DroneCiService < CiService true end - def supported_events + def self.supported_events %w(push merge_request tag_push) end @@ -87,7 +87,7 @@ class DroneCiService < CiService 'Drone is a Continuous Integration platform built on Docker, written in Go' end - def to_param + def self.to_param 'drone_ci' end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 79285cbd26d..f4f913ee0b6 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -12,11 +12,11 @@ class EmailsOnPushService < Service 'Email the commits and diff of each push to a list of recipients.' end - def to_param + def self.to_param 'emails_on_push' end - def supported_events + def self.supported_events %w(push tag_push) end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index d7b6e505191..bdf6fa6a586 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -13,7 +13,7 @@ class ExternalWikiService < Service 'Replaces the link to the internal wiki with a link to an external wiki.' end - def to_param + def self.to_param 'external_wiki' end @@ -29,4 +29,8 @@ class ExternalWikiService < Service nil end end + + def self.supported_events + %w() + end end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index dd00275187f..10a13c3fbdc 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -12,7 +12,7 @@ class FlowdockService < Service 'Flowdock is a collaboration web app for technical teams.' end - def to_param + def self.to_param 'flowdock' end @@ -22,7 +22,7 @@ class FlowdockService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 598aca5e06d..f271e1f1739 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -12,7 +12,7 @@ class GemnasiumService < Service 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' end - def to_param + def self.to_param 'gemnasium' end @@ -23,7 +23,7 @@ class GemnasiumService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 6bd8d4ec568..ad4eb9536e1 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService default_value_for :default, true - def to_param + def self.to_param 'gitlab' end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 915f6fed74c..72da219df28 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -27,7 +27,7 @@ class HipchatService < Service 'Private group chat and IM' end - def to_param + def self.to_param 'hipchat' end @@ -45,7 +45,7 @@ class HipchatService < Service ] end - def supported_events + def self.supported_events %w(push issue confidential_issue merge_request note tag_push build) end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 7355918feab..5d93064f9b3 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -17,11 +17,11 @@ class IrkerService < Service 'gateway.' end - def to_param + def self.to_param 'irker' end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index bce2cdd5516..9e65fdbf9d6 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -57,7 +57,7 @@ class IssueTrackerService < Service end end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2d969d2fcb6..2ac76e97de0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -12,7 +12,7 @@ class JiraService < IssueTrackerService # This is confusing, but JiraService does not really support these events. # The values here are required to display correct options in the service # configuration screen. - def supported_events + def self.supported_events %w(commit merge_request) end @@ -81,7 +81,7 @@ class JiraService < IssueTrackerService end end - def to_param + def self.to_param 'jira' end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 085125ca9dc..fa3cedc4354 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -52,7 +52,7 @@ class KubernetesService < DeploymentService 'deployments with `app=$CI_ENVIRONMENT_SLUG`' end - def to_param + def self.to_param 'kubernetes' end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index ee8a0b55275..4ebc5318da1 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService 'Receive event notifications in Mattermost' end - def to_param + def self.to_param 'mattermost' end @@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService end def default_channel_placeholder - "#town-square" + "town-square" end end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 2cb481182d7..50a011db74e 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService "Perform common operations on GitLab in Mattermost" end - def to_param + def self.to_param 'mattermost_slash_commands' end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 745f9bd1b43..ac617f409d9 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -15,11 +15,11 @@ class PipelinesEmailService < Service 'Email the pipelines status to a list of recipients.' end - def to_param + def self.to_param 'pipelines_email' end - def supported_events + def self.supported_events %w[pipeline] end diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 5301f9fa0ff..9cc642591f4 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -14,7 +14,7 @@ class PivotaltrackerService < Service 'Project Management Software (Source Commits Endpoint)' end - def to_param + def self.to_param 'pivotaltracker' end @@ -34,7 +34,7 @@ class PivotaltrackerService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3dd878e4c7d..a963d27a376 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -13,7 +13,7 @@ class PushoverService < Service 'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.' end - def to_param + def self.to_param 'pushover' end @@ -61,7 +61,7 @@ class PushoverService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index f9da273cf08..6acf611eba5 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService end end - def to_param + def self.to_param 'redmine' end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 76d233a3cca..f77d2d7c60b 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -7,7 +7,7 @@ class SlackService < ChatNotificationService 'Receive event notifications in Slack' end - def to_param + def self.to_param 'slack' end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 5a7cc0fb329..c34991e4262 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService "Perform common operations on GitLab in Slack" end - def to_param + def self.to_param 'slack_slash_commands' end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 6726082048f..cbaffb8ce48 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -43,14 +43,10 @@ class TeamcityService < CiService 'requests build, that setting is in the vsc root advanced settings.' end - def to_param + def self.to_param 'teamcity' end - def supported_events - %w(push) - end - def fields [ { type: 'text', name: 'teamcity_url', diff --git a/app/models/route.rb b/app/models/route.rb index caf596efa79..dd171fdb069 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -8,15 +8,16 @@ class Route < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - after_update :rename_children, if: :path_changed? + after_update :rename_descendants, if: :path_changed? - def rename_children + def rename_descendants # We update each row separately because MySQL does not have regexp_replace. # rubocop:disable Rails/FindEach Route.where('path LIKE ?', "#{path_was}/%").each do |route| # Note that update column skips validation and callbacks. - # We need this to avoid recursive call of rename_children method + # We need this to avoid recursive call of rename_descendants method route.update_column(:path, route.path.sub(path_was, path)) end + # rubocop:enable Rails/FindEach end end diff --git a/app/models/service.rb b/app/models/service.rb index 19ef3ba9c23..043be222f3a 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -76,6 +76,11 @@ class Service < ActiveRecord::Base def to_param # implement inside child + self.class.to_param + end + + def self.to_param + raise NotImplementedError end def fields @@ -92,7 +97,11 @@ class Service < ActiveRecord::Base end def event_names - supported_events.map { |event| "#{event}_events" } + self.class.event_names + end + + def self.event_names + self.supported_events.map { |event| "#{event}_events" } end def event_field(event) @@ -104,6 +113,10 @@ class Service < ActiveRecord::Base end def supported_events + self.class.supported_events + end + + def self.supported_events %w(push tag_push issue confidential_issue merge_request wiki_page) end diff --git a/app/models/user.rb b/app/models/user.rb index 06dd98a3188..54f5388eb2c 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"). @@ -439,6 +439,15 @@ class User < ActiveRecord::Base Group.where("namespaces.id IN (#{union.to_sql})") end + def nested_groups + Group.member_descendants(id) + end + + def nested_projects + Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). + member_descendants(id) + end + def refresh_authorized_projects Users::RefreshAuthorizedProjectsService.new(self).execute end diff --git a/app/services/application_settings/base_service.rb b/app/services/application_settings/base_service.rb new file mode 100644 index 00000000000..2bcc7d7c08b --- /dev/null +++ b/app/services/application_settings/base_service.rb @@ -0,0 +1,7 @@ +module ApplicationSettings + class BaseService < ::BaseService + def initialize(application_setting, user, params = {}) + @application_setting, @current_user, @params = application_setting, user, params.dup + end + end +end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb new file mode 100644 index 00000000000..61589a07250 --- /dev/null +++ b/app/services/application_settings/update_service.rb @@ -0,0 +1,7 @@ +module ApplicationSettings + class UpdateService < ApplicationSettings::BaseService + def execute + @application_setting.update(@params) + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 70e25956dc7..5a53b973059 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,15 +38,13 @@ module MergeRequests private - def merge_requests_for(branch) - origin_merge_requests = @project.origin_merge_requests - .opened.where(source_branch: branch).to_a - - fork_merge_requests = @project.fork_merge_requests - .opened.where(source_branch: branch).to_a - - (origin_merge_requests + fork_merge_requests) - .uniq.select(&:source_project) + # Returns all origin and fork merge requests from `@project` satisfying passed arguments. + def merge_requests_for(source_branch, mr_states: [:opened]) + MergeRequest + .with_state(mr_states) + .where(source_branch: source_branch, source_project_id: @project.id) + .preload(:source_project) # we don't need a #includes since we're just preloading for the #select + .select(&:source_project) end def pipeline_merge_requests(pipeline) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 51d5d7563fc..b4bfb0e5e8c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -42,7 +42,7 @@ module MergeRequests commit_ids.include?(merge_request.diff_head_sha) end - merge_requests.uniq.select(&:source_project).each do |merge_request| + filter_merge_requests(merge_requests).each do |merge_request| MergeRequests::PostMergeService. new(merge_request.target_project, @current_user). execute(merge_request) @@ -58,10 +58,13 @@ module MergeRequests def reload_merge_requests merge_requests = @project.merge_requests.opened. by_source_or_target_branch(@branch_name).to_a - merge_requests += fork_merge_requests - merge_requests = filter_merge_requests(merge_requests) - merge_requests.each do |merge_request| + # Fork merge requests + merge_requests += MergeRequest.opened + .where(source_branch: @branch_name, source_project: @project) + .where.not(target_project: @project).to_a + + filter_merge_requests(merge_requests).each do |merge_request| if merge_request.source_branch == @branch_name || force_push? merge_request.reload_diff else @@ -175,16 +178,7 @@ module MergeRequests end def merge_requests_for_source_branch - @source_merge_requests ||= begin - merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += fork_merge_requests - filter_merge_requests(merge_requests) - end - end - - def fork_merge_requests - @fork_merge_requests ||= @project.fork_merge_requests.opened. - where(source_branch: @branch_name).to_a + @source_merge_requests ||= merge_requests_for(@branch_name) end def branch_added? 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/services/projects/create_service.rb b/app/services/projects/create_service.rb index 159f46cd465..c7cce0c55b9 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -22,17 +22,7 @@ module Projects return @project end - # Set project name from path - if @project.name.present? && @project.path.present? - # if both name and path set - everything is ok - elsif @project.path.present? - # Set project name from path - @project.name = @project.path.dup - elsif @project.name.present? - # For compatibility - set path from name - # TODO: remove this in 8.0 - @project.path = @project.name.dup.parameterize - end + set_project_name_from_path # get namespace id namespace_id = params[:namespace_id] @@ -144,5 +134,19 @@ module Projects service.save! end end + + def set_project_name_from_path + # Set project name from path + if @project.name.present? && @project.path.present? + # if both name and path set - everything is ok + elsif @project.path.present? + # Set project name from path + @project.name = @project.path.dup + elsif @project.name.present? + # For compatibility - set path from name + # TODO: remove this in 8.0 + @project.path = @project.name.dup.parameterize + end + end end end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 2d211d5ebbe..fad741531ea 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -118,7 +118,8 @@ module Users user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), user.groups_projects.select_for_project_authorization, user.projects.select_for_project_authorization, - user.groups.joins(:shared_projects).select_for_project_authorization + user.groups.joins(:shared_projects).select_for_project_authorization, + user.nested_projects.select_for_project_authorization ] Gitlab::SQL::Union.new(relations) diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 3132d157f29..2269fb1fd8c 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -4,7 +4,7 @@ - if @broadcast_message.message.present? = render_broadcast_message(@broadcast_message) - else - = "Your message here" + Your message here = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_errors(@broadcast_message) diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index 7362d904b94..8c658905bd6 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -1,6 +1,6 @@ %tr %td - = "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})" + #{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider}) %td = identity.extern_uid %td diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 37bb6a3b0e0..124f970524e 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -11,7 +11,7 @@ that for future communication. %br Registration token is - %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} + %code#runners-token= current_application_settings.runners_registration_token .bs-callout.clearfix .pull-left diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index ca503e35623..39e103e3062 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -100,7 +100,7 @@ %td.build-link - if project = link_to ci_status_path(build.pipeline) do - %strong #{build.pipeline.short_sha} + %strong= build.pipeline.short_sha %td.timestamp - if build.finished_at diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index bfc6142067a..2e5f120c4e4 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -10,7 +10,7 @@ %h4 CPU .data - if @cpus - %h1= "#{@cpus.length} cores" + %h1 #{@cpus.length} cores - else = icon('warning', class: 'text-warning') Unable to collect CPU info @@ -19,7 +19,7 @@ %h4 Memory .data - if @memory - %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}" + %h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)} - else = icon('warning', class: 'text-warning') Unable to collect memory info @@ -28,6 +28,6 @@ %h4 Disks .data - @disks.each do |disk| - %h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}" - %p= "#{disk[:disk_name]}" - %p= "#{disk[:mount_path]}" + %h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])} + %p= disk[:disk_name] + %p= disk[:mount_path] diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a71240986c9..76b1291fe10 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -186,6 +186,6 @@ - if @user.solo_owned_groups.present? %p This user is currently an owner in these groups: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete this user. diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 95eb9a57152..b0bee1c6204 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -13,7 +13,7 @@ .file-holder .file-title.clearfix Content of .gitlab-ci.yml - #ci-editor.ci-editor #{@content} + #ci-editor.ci-editor= @content = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) .col-sm-12 .pull-left.prepend-top-10 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/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index ef16b516e2c..3a19e021643 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -6,7 +6,7 @@ .content{ class: ('hide' unless discussion_left.expanded?) } = render "discussions/notes", discussion: discussion_left, line_type: 'old' - else - %td.notes_line.old= "" + %td.notes_line.old= ("") %td.notes_content.parallel.old .content @@ -16,6 +16,6 @@ .content{ class: ('hide' unless discussion_right.expanded?) } = render "discussions/notes", discussion: discussion_right, line_type: 'new' - else - %td.notes_line.new= "" + %td.notes_line.new= ("") %td.notes_content.parallel.new .content diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 2a0e301c8dd..a196561f381 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -10,7 +10,7 @@ %p = icon("exclamation-triangle fw") You are an admin, which means granting access to - %strong #{@pre_auth.client.name} + %strong= @pre_auth.client.name will allow them to interact with GitLab as an admin as well. Proceed with caution. - if @pre_auth.scopes diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index bc5d3c797ac..f4c432a095a 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -25,7 +25,7 @@ .panel.panel-default .panel-heading Users with access to - %strong #{@group.name} + %strong= @group.name %span.badge= @members.total_count %ul.content-list = render partial: 'shared/members/member', collection: @members, as: :member diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index b4aa4f24d9e..6ad03a60b3a 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -18,7 +18,7 @@ .row-content-block.second-block Only issues from the - %strong #{@group.name} + %strong= @group.name group are listed here. - if current_user To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index dbbdb583a24..af73554086b 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -10,7 +10,7 @@ .row-content-block.second-block Only merge requests from - %strong #{@group.name} + %strong= @group.name group are listed here. - if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index a8fdbd8c426..cd5388fe402 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -10,7 +10,7 @@ .row-content-block Only milestones from - %strong #{@group.name} + %strong= @group.name group are listed here. .milestones diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index 864c5c0ff95..0e7f0b5ed4f 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -16,7 +16,7 @@ %colgroup.import-jobs-status-col %thead %tr - %th= "From #{provider_title}" + %th From #{provider_title} %th To GitLab %th Status %tbody diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index 07338736bac..9999a4362c6 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -37,7 +37,7 @@ %tbody - @user_map.each do |id, user| %tr - %td= id + %td= (id) %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control' %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control' %td diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index 97e5e51abe0..5de5da5e6a2 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -50,7 +50,7 @@ %td = repo.name %td.import-target - = "#{current_user.username}/#{repo.name}" + #{current_user.username}/#{repo.name} %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 9f1507cade6..5e01af008be 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -55,7 +55,7 @@ %td = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" %td.import-target - = "#{current_user.username}/#{repo.name}" + #{current_user.username}/#{repo.name} %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml index 56d81b2ed2e..fd35713f79c 100644 --- a/app/views/notify/_reassigned_issuable_email.html.haml +++ b/app/views/notify/_reassigned_issuable_email.html.haml @@ -2,9 +2,9 @@ Assignee changed - if @previous_assignee from - %strong #{@previous_assignee.name} + %strong= @previous_assignee.name to - if issuable.assignee_id - %strong #{issuable.assignee_name} + %strong= issuable.assignee_name - else %strong Unassigned diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml index 56c18cd83cd..b7284dd819b 100644 --- a/app/views/notify/closed_issue_email.html.haml +++ b/app/views/notify/closed_issue_email.html.haml @@ -1,2 +1,2 @@ %p - = "Issue was closed by #{@updated_by.name}" + Issue was closed by #{@updated_by.name} diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index ac703b31edd..bc12e38675f 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ -= "Issue was closed by #{@updated_by.name}" +Issue was closed by #{@updated_by.name} Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)} diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 81c7c88fc96..44e018304e1 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" + Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index b435067d5a6..d0c96b83976 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" +Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml index 482c884a9db..b6051b11cea 100644 --- a/app/views/notify/issue_status_changed_email.html.haml +++ b/app/views/notify/issue_status_changed_email.html.haml @@ -1,2 +1,2 @@ %p - = "Issue was #{@issue_status} by #{@updated_by.name}" + Issue was #{@issue_status} by #{@updated_by.name} diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index 41a320d6bd8..b487e26b122 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" + Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index 7a5074a1dc3..4c9719ba732 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" +Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index fbe506d4f4d..0fe54e73313 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request #{@merge_request.to_reference} was merged" + Merge Request #{@merge_request.to_reference} was merged diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index bfbae01094f..46c1c9dee0b 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was merged" +Merge Request #{@merge_request.to_reference} was merged Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} 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/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 82c7fe229b8..d9ebbaa2704 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -139,7 +139,7 @@ had = failed.size failed - = "#{'build'.pluralize(failed.size)}." + #{'build'.pluralize(failed.size)}. %tr.warning %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } Logs may contain sensitive data. Please consider before forwarding this email. diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 6dddb3b6373..8add2e18206 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -138,9 +138,9 @@ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = "\##{@pipeline.id}" successfully completed - = "#{build_count} #{'build'.pluralize(build_count)}" + #{build_count} #{'build'.pluralize(build_count)} in - = "#{stage_count} #{'stage'.pluralize(stage_count)}." + #{stage_count} #{'stage'.pluralize(stage_count)}. %tr.footer %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml index 27785165c2d..6c6902994a1 100644 --- a/app/views/notify/project_was_not_exported_email.text.haml +++ b/app/views/notify/project_was_not_exported_email.text.haml @@ -1,6 +1,6 @@ -= "Project #{@project.name} couldn't be exported." +Project #{@project.name} couldn't be exported. -= "The errors we encountered were:" +The errors we encountered were: - @errors.each do |error| #{error} diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 36858fa6f34..c6b1db17f91 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -17,7 +17,7 @@ %ul - @message.commits.each do |commit| %li - %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))} + %strong= link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit)) %div %span by #{commit.author_name} %i at #{commit.committed_date.to_s(:iso8601)} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 72f658d1b68..14b330d16ad 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -102,7 +102,7 @@ = f.text_field :username, required: true, class: 'form-control' .help-block Current path: - = "#{root_url}#{current_user.username}" + #{root_url}#{current_user.username} .prepend-top-default = f.button class: "btn btn-warning", type: "submit" do = icon "spinner spin", class: "hidden loading-username" @@ -128,7 +128,7 @@ - if @user.solo_owned_groups.present? %p Your account is currently an owner in these groups: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete your account. .append-bottom-default diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index c0c82cde2f6..d551754a2e5 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -62,7 +62,7 @@ %span.help-block Please click the link in the confirmation email before continuing. It was sent to = succeed "." do - %strong #{@user.unconfirmed_email} + %strong= @user.unconfirmed_email %p = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 1d058daa094..228ac61fc8c 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -34,7 +34,7 @@ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-editor.code - %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} + %pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data] - if local_assigns[:path] .js-edit-mode-pane#preview.hide .center diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index a486b2fe491..f864702d862 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -1,8 +1,8 @@ .file-content.image_file - if blob.svg? - if blob.size_within_svg_limits? - - # We need to scrub SVG but we cannot do so in the RawController: it would - - # be wrong/strange if RawController modified the data. + -# We need to scrub SVG but we cannot do so in the RawController: it would + -# be wrong/strange if RawController modified the data. - blob.load_all_data!(@repository) - blob = sanitize_svg(blob) %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" } diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 61a7ffdd0ab..4924c73cf8e 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -3,7 +3,7 @@ .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title #{title} + %h3.page-title= title .modal-body = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do .dropzone diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 520113639b7..c1e496455d1 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -85,7 +85,7 @@ - if build.finished_at %p.finished-at = icon("calendar") - %span #{time_ago_with_tooltip(build.finished_at)} + %span= time_ago_with_tooltip(build.finished_at) %td.coverage - if coverage && build.try(:coverage) 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/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index fcc367951ad..904cdb5767f 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -2,7 +2,7 @@ - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" + %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')} %li.commits-row %ul.content-list.commit-list.table-list.table-wide = render commits, project: project, ref: ref 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/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 819e9bc15ae..9c8f58d4aea 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -16,9 +16,9 @@ There isn't anything to compare. %p.slead - if params[:to] == params[:from] - %span.label-branch #{params[:from]} + %span.label-branch= params[:from] and - %span.label-branch #{params[:to]} + %span.label-branch= params[:to] are the same. - else You'll need to use different branch names to get a valid comparison. diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index 9238f232c7e..c468202569f 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -1,6 +1,6 @@ %tr.deployment %td - %strong= "##{deployment.iid}" + %strong ##{deployment.iid} %td = render 'projects/deployments/commit', deployment: deployment @@ -8,7 +8,7 @@ %td.build-column - if deployment.deployable = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do - = "#{deployment.deployable.name} (##{deployment.deployable.id})" + #{deployment.deployable.name} (##{deployment.deployable.id}) - if deployment.user by = user_avatar(user: deployment.user, size: 20) diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index 52a1ece7d60..b87b79b170e 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -1,5 +1,5 @@ .diff-content.diff-wrap-lines - - # Skip all non non-supported blobs + -# Skip all non non-supported blobs - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 90c9a0c6c2b..ddec775b789 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -25,4 +25,4 @@ - if diff_file.mode_changed? %small - = "#{diff_file.a_mode} → #{diff_file.b_mode}" + #{diff_file.a_mode} → #{diff_file.b_mode} diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 1bccaaf5273..ca10921c5e2 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -9,7 +9,7 @@ %span.wrap .frame{ class: image_diff_class(diff) } %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path } - %p.image-info= "#{number_to_human_size file.size}" + %p.image-info= number_to_human_size(file.size) - else .image .two-up.view @@ -18,7 +18,7 @@ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) } %img{ src: old_file_raw_path, alt: diff.old_path } %p.image-info.hide - %span.meta-filesize= "#{number_to_human_size old_file.size}" + %span.meta-filesize= number_to_human_size(old_file.size) | %b W: %span.meta-width @@ -30,7 +30,7 @@ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) } %img{ src: file_raw_path, alt: diff.new_path } %p.image-info.hide - %span.meta-filesize= "#{number_to_human_size file.size}" + %span.meta-filesize= number_to_human_size(file.size) | %b W: %span.meta-width diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 290f696d582..8e24e28765f 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -2,7 +2,7 @@ .commit-stat-summary Showing = link_to '#', class: 'js-toggle-button' do - %strong #{pluralize(diff_files.size, "changed file")} + %strong= pluralize(diff_files.size, "changed file") with %strong.cgreen #{diff_files.sum(&:added_lines)} additions and diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 6c8a6f051a9..f4aa523b32d 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,7 +1,7 @@ .top-area .nav-text - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private" - = "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}" + #{pluralize(@total_forks_count, 'fork')}: #{full_count_title} .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 6d7af1685fd..07fb80750d6 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -77,7 +77,7 @@ - if generic_commit_status.finished_at %p.finished-at = icon("calendar") - %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} + %span= time_ago_with_tooltip(generic_commit_status.finished_at) %td.coverage - if coverage && generic_commit_status.try(:coverage) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 7e34a89f9ae..c8a82f7bca3 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -11,7 +11,7 @@ %p.lead Commit statistics for - %strong #{@ref} + %strong= @ref #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} .row @@ -19,19 +19,19 @@ %ul %li %p.lead - %strong #{@commits_graph.commits.size} + %strong= @commits_graph.commits.size commits during - %strong #{@commits_graph.duration} + %strong= @commits_graph.duration days %li %p.lead Average - %strong #{@commits_graph.commit_per_day} + %strong= @commits_graph.commit_per_day commits per day %li %p.lead Contributed by - %strong #{@commits_graph.authors} + %strong= @commits_graph.authors authors .col-md-6 %div diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 36c6e7a8dad..d3c013b3f21 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -3,9 +3,9 @@ %p.slead - source_title, target_title = format_mr_branch_names(@merge_request) From - %strong.label-branch #{source_title} + %strong.label-branch= source_title %span into - %strong.label-branch #{target_title} + %strong.label-branch= target_title %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) @@ -43,7 +43,7 @@ #commits.commits.tab-pane.active = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane = render "projects/merge_requests/show/pipelines" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 2a7cd3a19d0..eade0c2a668 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -92,11 +92,11 @@ = render "projects/merge_requests/discussion" #commits.commits.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index b0f3c86fd21..74a7b1dc498 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -25,7 +25,7 @@ latest version - else version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} + .monospace= short_sha(merge_request_diff.head_commit_sha) %small #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)}, = time_ago_with_tooltip(merge_request_diff.created_at) @@ -55,14 +55,14 @@ latest version - else version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} + .monospace= short_sha(merge_request_diff.head_commit_sha) %small = time_ago_with_tooltip(merge_request_diff.created_at) %li = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do %strong #{@merge_request.target_branch} (base) - .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + .monospace= short_sha(@merge_request_diff.base_commit_sha) - if different_base?(@start_version, @merge_request_diff) .content-block @@ -72,7 +72,7 @@ = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do new commits from - %code #{@merge_request.target_branch} + %code= @merge_request.target_branch - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index c80dc33058d..5faa6c43f9f 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -13,8 +13,8 @@ %span.ci-coverage - elsif @merge_request.has_ci? - - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API + -# Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + -# TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 36c388c3318..09339e520dd 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -69,7 +69,7 @@ - if note_editable .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } #{note.note} - %textarea.hidden.js-task-list-field.original-task-list #{note.note} + %textarea.hidden.js-task-list-field.original-task-list= note.note .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 9738f369a35..c7996077bc7 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -1,7 +1,7 @@ .panel.panel-default .panel-heading Group members with access to - %strong #{@group.name} + %strong= @group.name %span.badge= members.size - if can?(current_user, :admin_group_member, @group) .controls diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index d7f5fa96527..fdeb5f21fbe 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,7 +1,7 @@ .panel.panel-default.project-members-groups .panel-heading Groups with access to - %strong #{@project.name} + %strong= @project.name %span.badge= group_links.size %ul.content-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml index 77370c14def..7902ddb1ae9 100644 --- a/app/views/projects/project_members/_shared_group_members.html.haml +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -5,9 +5,9 @@ .panel.panel-default .panel-heading Shared with - %strong #{shared_group.name} + %strong= shared_group.name group, members with - %strong #{group_links.human_access} + %strong= group_links.human_access role (#{shared_group_users_count}) - if can?(current_user, :admin_group, shared_group) .panel-head-actions diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 5292e73be7a..81d57c77edf 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,7 +1,7 @@ .panel.panel-default .panel-heading Members with access to - %strong #{@project.name} + %strong= @project.name %span.badge= @project_members.total_count = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do .form-group diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 33d5cbff420..79d8d721aa9 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -7,7 +7,7 @@ .oneline .title Release notes for tag - %strong #{@tag.name} + %strong= @tag.name = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 51b0939564e..dcff675eafc 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -9,10 +9,10 @@ (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it). %li Specify the following URL during the Runner setup: - %code #{ci_root_url(only_path: false)} + %code= ci_root_url(only_path: false) %li Use the following registration token during setup: - %code #{@project.runners_token} + %code= @project.runners_token %li Start the Runner! diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index c929eee3bb9..fcc91be11cd 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -4,4 +4,4 @@ .col-sm-9.col-sm-offset-3 = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do = custom_icon('mattermost_logo', size: 15) - = 'Add to Mattermost' + Add to Mattermost diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 05a63016c09..821a39d61f5 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -3,4 +3,4 @@ %h4 = icon('search') We couldn't find any results matching - %code #{@search_term} + %code= @search_term diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 07b17bc69c0..2e6adf3027c 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -2,7 +2,7 @@ %h4 = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - .pull-right #{merge_request.to_reference} + .pull-right= merge_request.to_reference - if merge_request.description.present? .description.term = preserve do diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index c9b7bd154af..23ca6479414 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -9,7 +9,7 @@ = link_to user_snippets_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = snippet.author_name - %span.light #{time_ago_with_tooltip(snippet.created_at)} + %span.light= time_ago_with_tooltip(snippet.created_at) %h4.snippet-title - snippet_path = reliable_snippet_path(snippet) = link_to snippet_path do diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 027d42396b4..704d1d01a81 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -20,4 +20,4 @@ = link_to user_snippets_path(snippet_title.author) do = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name - %span.light #{time_ago_with_tooltip(snippet_title.created_at)} + %span.light= time_ago_with_tooltip(snippet_title.created_at) diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index e7cb93b17a7..f94f8ffc604 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -14,7 +14,7 @@ To prevent accidental actions we ask you to confirm your intention. %br Please type - %code.js-confirm-danger-match #{phrase} + %code.js-confirm-danger-match= phrase to proceed or close this modal to cancel. .form-group diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 39294fe1a09..704893b4d5b 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -6,14 +6,14 @@ = link_to milestones_filter_path(state: 'opened') do Open - if @project - %span.badge #{counts[:opened]} + %span.badge= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed') do Closed - if @project - %span.badge #{counts[:closed]} + %span.badge= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all') do All - if @project - %span.badge #{counts[:all]} + %span.badge= counts[:all] diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index f9a7aa4e29b..dd9e433491b 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -32,7 +32,7 @@ - if group_member as - %span #{group_member.human_access} + %span= group_member.human_access - if group.description.present? .description diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c0e8a498316..0a4de709fcd 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -68,7 +68,7 @@ - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) .inline.prepend-left-10 Please review the - %strong #{link_to 'contribution guidelines', guide_url} + %strong= link_to('contribution guidelines', guide_url) for this project. - if issuable.new_record? diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 44152319736..e9644ca0f12 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -17,7 +17,7 @@ = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value' => '' } + %li.filter-dropdown-item{ 'data-action' => 'submit' } %button.btn.btn-link = icon('search') %span @@ -125,10 +125,6 @@ new MilestoneSelect(); new IssueStatusSelect(); new SubscriptionSelect(); - $('form.filter-form').on('submit', function (event) { - event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); - }); $(document).off('page:restore').on('page:restore', function (event) { if (gl.FilteredSearchManager) { diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index 5074afb63a1..8bbaf431536 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -5,5 +5,5 @@ - scopes.each do |scope| %fieldset = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" - = label_tag "#{prefix}_scopes_#{scope}", scope - %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = label_tag ("#{prefix}_scopes_#{scope}"), scope + %span= t(scope, scope: [:doorkeeper, :scopes]) diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index f51599212db..b09782749f5 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,6 +1,6 @@ %h4.prepend-top-20 Contributions for - %strong #{@calendar_date.to_s(:short)} + %strong= @calendar_date.to_s(:short) - if @events.any? %ul.bordered-list diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index fb25eed4f37..c3d33d49c1e 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -110,16 +110,16 @@ = spinner #groups.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #contributed.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #projects.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #snippets.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX .loading-status = spinner diff --git a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email new file mode 100644 index 00000000000..f4011b756a5 --- /dev/null +++ b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email @@ -0,0 +1,4 @@ +--- +title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address +merge_request: 6597 +author: diff --git a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml new file mode 100644 index 00000000000..2c6883bcf7b --- /dev/null +++ b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml @@ -0,0 +1,4 @@ +--- +title: Allow creating protected branches when user can merge to such branch +merge_request: 8458 +author: diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml new file mode 100644 index 00000000000..57106e8c676 --- /dev/null +++ b/changelogs/unreleased/22974-trigger-service-events-through-api.yml @@ -0,0 +1,4 @@ +--- +title: Adds service trigger events to api +merge_request: 8324 +author: diff --git a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml new file mode 100644 index 00000000000..be66c370f36 --- /dev/null +++ b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml @@ -0,0 +1,4 @@ +--- +title: 'Allows to search within project by commit hash' +merge_request: +author: YarNayar diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml new file mode 100644 index 00000000000..de35cad3dd6 --- /dev/null +++ b/changelogs/unreleased/24923_nested_tasks.yml @@ -0,0 +1,4 @@ +--- +title: Fix nested tasks in ordered list +merge_request: 8626 +author: diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml new file mode 100644 index 00000000000..e67a9c0da15 --- /dev/null +++ b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml @@ -0,0 +1,4 @@ +--- +title: Remove rogue scrollbars for issue comments with inline elements +merge_request: +author: 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/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml new file mode 100644 index 00000000000..31f1812c6f8 --- /dev/null +++ b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml @@ -0,0 +1,4 @@ +--- +title: Add hover style to copy icon on commit page header +merge_request: +author: Ryan Harris diff --git a/changelogs/unreleased/27066-textarea-border.yml b/changelogs/unreleased/27066-textarea-border.yml new file mode 100644 index 00000000000..e45cb3aced5 --- /dev/null +++ b/changelogs/unreleased/27066-textarea-border.yml @@ -0,0 +1,4 @@ +--- +title: Remove blue border from comment box hover +merge_request: +author: diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml new file mode 100644 index 00000000000..75502e139e7 --- /dev/null +++ b/changelogs/unreleased/8-15-stable.yml @@ -0,0 +1,4 @@ +--- +title: Ensure export files are removed after a namespace is deleted +merge_request: +author: 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/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml new file mode 100644 index 00000000000..6dd0d748001 --- /dev/null +++ b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml @@ -0,0 +1,4 @@ +--- +title: Disable automatic login after clicking email confirmation links +merge_request: 7472 +author: diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml new file mode 100644 index 00000000000..5fba0332881 --- /dev/null +++ b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml @@ -0,0 +1,4 @@ +--- +title: Use warning icon in mini-graph if stage passed conditionally +merge_request: 8503 +author: diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml new file mode 100644 index 00000000000..33b677b1f29 --- /dev/null +++ b/changelogs/unreleased/fix-api-mr-permissions.yml @@ -0,0 +1,4 @@ +--- +title: Don't allow project guests to subscribe to merge requests through the API +merge_request: +author: Robert Schilling diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml new file mode 100644 index 00000000000..81377c0c6f0 --- /dev/null +++ b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml @@ -0,0 +1,4 @@ +--- +title: Prevent users from creating notes on resources they can't access +merge_request: +author: diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml new file mode 100644 index 00000000000..c9edd1de86c --- /dev/null +++ b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml @@ -0,0 +1,4 @@ +--- +title: Prevent users from deleting system deploy keys via the project deploy key API +merge_request: +author: 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/issue-filter-click-to-search.yml b/changelogs/unreleased/issue-filter-click-to-search.yml new file mode 100644 index 00000000000..c024ea48dc7 --- /dev/null +++ b/changelogs/unreleased/issue-filter-click-to-search.yml @@ -0,0 +1,4 @@ +--- +title: allow issue filter bar to be operated with mouse only +merge_request: 8681 +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/redirect-to-commit-when-only-commit-found.yml b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml new file mode 100644 index 00000000000..e0f7e11b6d1 --- /dev/null +++ b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml @@ -0,0 +1,5 @@ +--- +title: 'Search feature: redirects to commit page if query is commit sha and only commit + found' +merge_request: 8028 +author: YarNayar 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/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml new file mode 100644 index 00000000000..7e0334566dc --- /dev/null +++ b/changelogs/unreleased/upgrade-omniauth.yml @@ -0,0 +1,4 @@ +--- +title: Upgrade omniauth gem to 1.3.2 +merge_request: +author: diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml new file mode 100644 index 00000000000..464c5948f8c --- /dev/null +++ b/changelogs/unreleased/zj-requeue-pending-delete.yml @@ -0,0 +1,4 @@ +--- +title: Requeue pending deletion projects +merge_request: +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/config/routes/group.rb b/config/routes/group.rb index 776c31c9dac..60a1175fe80 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -19,13 +19,11 @@ end scope(path: 'groups/*id', controller: :groups, - constraints: { id: Gitlab::Regex.namespace_route_regex }) do + constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do get :edit, as: :edit_group get :issues, as: :issues_group get :merge_requests, as: :merge_requests_group get :projects, as: :projects_group get :activity, as: :activity_group + get '/', action: :show, as: :group_canonical end - -# Must be last route in this file -get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex } 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/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb new file mode 100644 index 00000000000..f399950bd5e --- /dev/null +++ b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb @@ -0,0 +1,49 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RequeuePendingDeleteProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + admin = User.find_by(admin: true) + return unless admin + + @offset = 0 + + loop do + ids = pending_delete_batch + + break if ids.rows.count.zero? + + args = ids.map { |id| [id['id'], admin.id, {}] } + + Sidekiq::Client.push_bulk('class' => "ProjectDestroyWorker", 'args' => args) + + @offset += 1 + end + end + + def down + # noop + end + + private + + def pending_delete_batch + connection.exec_query(find_batch) + end + + BATCH_SIZE = 5000 + + def find_batch + projects = Arel::Table.new(:projects) + projects.project(projects[:id]). + where(projects[:pending_delete].eq(true)). + where(projects[:namespace_id].not_eq(nil)). + skip(@offset * BATCH_SIZE). + take(BATCH_SIZE). + to_sql + end +end diff --git a/doc/README.md b/doc/README.md index e329131b8ee..993b30ccdb5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -6,7 +6,7 @@ ## User documentation -- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc. +- [Account Security](user/profile/account/two_factor_authentication.md) Securing your account via two-factor authentication, etc. - [API](api/README.md) Automate GitLab via a simple and powerful API. - [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. 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/doc/api/enviroments.md b/doc/api/enviroments.md index 1299aca8c45..e0ee20d9610 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id | `external_url` | string | no | The new external_url | ```bash -curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1" ``` Example response: @@ -106,7 +106,7 @@ DELETE /projects/:id/environments/:environment_id | `environment_id` | integer | yes | The ID of the environment | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1" +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1" ``` Example response: diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 1ef34c79971..e4a0e0b92bc 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -9,7 +9,7 @@ code is effective, understandable, and maintainable. Any developer can, and is encouraged to, perform code review on merge requests of colleagues and contributors. However, the final decision to accept a merge -request is up to one of our merge request "endbosses", denoted on the +request is up to one the project's maintainers, denoted on the [team page](https://about.gitlab.com/team). ## Everyone @@ -81,15 +81,15 @@ balance in how deep the reviewer can interfere with the code created by a reviewee. - Learning how to find the right balance takes time; that is why we have - minibosses that become merge request endbosses after some time spent on - reviewing merge requests. + reviewers that become maintainers after some time spent on reviewing merge + requests. - Finding bugs and improving code style is important, but thinking about good design is important as well. Building abstractions and good design is what makes it possible to hide complexity and makes future changes easier. - Asking the reviewee to change the design sometimes means the complete rewrite - of the contributed code. It's usually a good idea to ask another merge - request endboss before doing it, but have the courage to do it when you - believe it is important. + of the contributed code. It's usually a good idea to ask another maintainer or + reviewer before doing it, but have the courage to do it when you believe it is + important. - There is a difference in doing things right and doing things right now. Ideally, we should do the former, but in the real world we need the latter as well. A good example is a security fix which should be released as soon as diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md index 0363bf8c1d5..8232a0a113c 100644 --- a/doc/development/merge_request_performance_guidelines.md +++ b/doc/development/merge_request_performance_guidelines.md @@ -3,7 +3,7 @@ To ensure a merge request does not negatively impact performance of GitLab _every_ merge request **must** adhere to the guidelines outlined in this document. There are no exceptions to this rule unless specifically discussed -with and agreed upon by merge request endbosses and performance specialists. +with and agreed upon by backend maintainers and performance specialists. To measure the impact of a merge request you can use [Sherlock](profiling.md#sherlock). It's also highly recommended that you read @@ -40,9 +40,9 @@ section below for more information. about the impact. Sometimes it's hard to assess the impact of a merge request. In this case you -should ask one of the merge request (mini) endbosses to review your changes. You -can find a list of these endbosses at <https://about.gitlab.com/team/>. An -endboss in turn can request a performance specialist to review the changes. +should ask one of the merge request reviewers to review your changes. You can +find a list of these reviewers at <https://about.gitlab.com/team/>. A reviewer +in turn can request a performance specialist to review the changes. ## Query Counts diff --git a/doc/install/installation.md b/doc/install/installation.md index 9cebed34b7e..3e7674e13ab 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -124,7 +124,7 @@ Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz - echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz + echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz cd ruby-2.3.3 ./configure --disable-install-rdoc make diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png Binary files differdeleted file mode 100644 index b224ab14195..00000000000 --- a/doc/profile/2fa_u2f_authenticate.png +++ /dev/null diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md index 3f6dfe03d14..60918a0339c 100644 --- a/doc/profile/two_factor_authentication.md +++ b/doc/profile/two_factor_authentication.md @@ -1,143 +1 @@ -# Two-factor Authentication (2FA) - -Two-factor Authentication (2FA) provides an additional level of security to your -GitLab account. Once enabled, in addition to supplying your username and -password to login, you'll be prompted for a code generated by an application on -your phone. - -By enabling 2FA, the only way someone other than you can log into your account -is to know your username and password *and* have access to your phone. - -> **Note:** -When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you -lose your codes for GitLab.com, we can't disable or recover them. - -In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as -the second factor of authentication. Once enabled, in addition to supplying your username and -password to login, you'll be prompted to activate your U2F device (usually by pressing -a button on it), and it will perform secure authentication on your behalf. - -> **Note:** Support for U2F devices was added in version 8.8 - -The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend -that you set up both methods of two-factor authentication, so you can still access your account -from other browsers. - -> **Note:** GitLab officially only supports [Yubikey] U2F devices. - -## Enabling 2FA - -### Enable 2FA via mobile application - -**In GitLab:** - -1. Log in to your GitLab account. -1. Go to your **Profile Settings**. -1. Go to **Account**. -1. Click **Enable Two-factor Authentication**. - -![Two-factor setup](2fa.png) - -**On your phone:** - -1. Install a compatible application. We recommend [Google Authenticator] -\(proprietary\) or [FreeOTP] \(open source\). -1. In the application, add a new entry in one of two ways: - * Scan the code with your phone's camera to add the entry automatically. - * Enter the details provided to add the entry manually. - -**In GitLab:** - -1. Enter the six-digit pin number from the entry on your phone into the **Pin - code** field. -1. Click **Submit**. - -If the pin you entered was correct, you'll see a message indicating that -Two-Factor Authentication has been enabled, and you'll be presented with a list -of recovery codes. - -### Enable 2FA via U2F device - -**In GitLab:** - -1. Log in to your GitLab account. -1. Go to your **Profile Settings**. -1. Go to **Account**. -1. Click **Enable Two-Factor Authentication**. -1. Plug in your U2F device. -1. Click on **Setup New U2F Device**. -1. A light will start blinking on your device. Activate it by pressing its button. - -You will see a message indicating that your device was successfully set up. -Click on **Register U2F Device** to complete the process. - -![Two-Factor U2F Setup](2fa_u2f_register.png) - -## Recovery Codes - -Should you ever lose access to your phone, you can use one of the ten provided -backup codes to login to your account. We suggest copying or printing them for -storage in a safe place. **Each code can be used only once** to log in to your -account. - -If you lose the recovery codes or just want to generate new ones, you can do so -from the **Profile Settings** > **Account** page where you first enabled 2FA. - -> **Note:** Recovery codes are not generated for U2F devices. - -## Logging in with 2FA Enabled - -Logging in with 2FA enabled is only slightly different than a normal login. -Enter your username and password credentials as you normally would, and you'll -be presented with a second prompt, depending on which type of 2FA you've enabled. - -### Log in via mobile application - -Enter the pin from your phone's application or a recovery code to log in. - -![Two-Factor Authentication on sign in via OTP](2fa_auth.png) - -### Log in via U2F device - -1. Click **Login via U2F Device** -1. A light will start blinking on your device. Activate it by pressing its button. - -You will see a message indicating that your device responded to the authentication request. -Click on **Authenticate via U2F Device** to complete the process. - -![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png) - -## Disabling 2FA - -1. Log in to your GitLab account. -1. Go to your **Profile Settings**. -1. Go to **Account**. -1. Click **Disable**, under **Two-Factor Authentication**. - -This will clear all your two-factor authentication registrations, including mobile -applications and U2F devices. - -## Personal access tokens - -When 2FA is enabled, you can no longer use your normal account password to -authenticate with Git over HTTPS on the command line, you must use a personal -access token instead. - -1. Log in to your GitLab account. -1. Go to your **Profile Settings**. -1. Go to **Access Tokens**. -1. Choose a name and expiry date for the token. -1. Click on **Create Personal Access Token**. -1. Save the personal access token somewhere safe. - -When using git over HTTPS on the command line, enter the personal access token -into the password field. - -## Note to GitLab administrators - -You need to take special care to that 2FA keeps working after -[restoring a GitLab backup](../raketasks/backup_restore.md). - -[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en -[FreeOTP]: https://fedorahosted.org/freeotp/ -[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ +This document was moved to [user/profile/account](../user/profile/account/two_factor_authentication.md). diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index 59d5da702f8..99aa9e44bdb 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -8,7 +8,7 @@ the [configuration](#configuration) section. If you have a single cluster that you want to use for all your projects, you can pre-fill the settings page with a default template. To configure the -template, see the [Services Templates](services-templates.md) document. +template, see the [Services Templates](services_templates.md) document. ## Configuration diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md index 3d68fe201a7..63f3c3fdda9 100644 --- a/doc/update/8.15-to-8.16.md +++ b/doc/update/8.15-to-8.16.md @@ -36,7 +36,7 @@ Download and compile Ruby: ```bash mkdir /tmp/ruby && cd /tmp/ruby curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz -echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz cd ruby-2.3.3 ./configure --disable-install-rdoc make diff --git a/doc/user/account/security.md b/doc/user/account/security.md index 816094bf8d2..9336dee7451 100644 --- a/doc/user/account/security.md +++ b/doc/user/account/security.md @@ -1,3 +1 @@ -# Account Security - -- [Two-Factor Authentication](two_factor_authentication.md) +This document was moved to [profile](../profile/index.md#security). diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md index 881358ed94d..ea2c8307860 100644 --- a/doc/user/account/two_factor_authentication.md +++ b/doc/user/account/two_factor_authentication.md @@ -1,68 +1 @@ -# Two-Factor Authentication - -## Recovery options - -If you lose your code generation device (such as your mobile phone) and you need -to disable two-factor authentication on your account, you have several options. - -### Use a saved recovery code - -When you enabled two-factor authentication for your account, a series of -recovery codes were generated. If you saved those codes somewhere safe, you -may use one to sign in. - -First, enter your username/email and password on the GitLab sign in page. When -prompted for a two-factor code, enter one of the recovery codes you saved -previously. - -> **Note:** Once a particular recovery code has been used, it cannot be used again. - You may still use the other saved recovery codes at a later time. - -### Generate new recovery codes using SSH - -It's not uncommon for users to forget to save the recovery codes when enabling -two-factor authentication. If you have an SSH key added to your GitLab account, -you can generate a new set of recovery codes using SSH. - -Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to -confirm that you wish to generate new codes. If you choose to continue, any -previously saved codes will be invalidated. - -```bash -$ ssh git@gitlab.example.com 2fa_recovery_codes -Are you sure you want to generate new two-factor recovery codes? -Any existing recovery codes you saved will be invalidated. (yes/no) -yes - -Your two-factor authentication recovery codes are: - -119135e5a3ebce8e -11f6v2a498810dcd -3924c7ab2089c902 -e79a3398bfe4f224 -34bd7b74adbc8861 -f061691d5107df1a -169bf32a18e63e7f -b510e7422e81c947 -20dbed24c5e74663 -df9d3b9403b9c9f0 - -During sign in, use one of the codes above when prompted for -your two-factor code. Then, visit your Profile Settings and add -a new device so you do not lose access to your account again. -``` - -Next, go to the GitLab sign in page and enter your username/email and password. -When prompted for a two-factor code, enter one of the recovery codes obtained -from the command line output. - -> **Note:** After signing in, you should immediately visit your **Profile Settings - -> Account** to set up two-factor authentication with a new device. - -### Ask a GitLab administrator to disable two-factor on your account - -If the above two methods are not possible, you may ask a GitLab global -administrator to disable two-factor authentication for your account. Please -be aware that this will temporarily leave your account in a less secure state. -You should sign in and re-enable two-factor authentication as soon as possible -after the administrator disables it. +This document was moved to [profile/account/two_factor_authentication](../profile/account/two_factor_authentication.md). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index f6484688721..008872b59a7 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -300,6 +300,20 @@ You can add task lists to issues, merge requests and comments. To create a task - [x] Sub-task 2 - [ ] Sub-task 3 +Tasks formatted as ordered lists are supported as well: + +```no-highlight +1. [x] Completed task +1. [ ] Incomplete task + 1. [ ] Sub-task 1 + 1. [x] Sub-task 2 +``` + +1. [x] Completed task +1. [ ] Incomplete task + 1. [ ] Sub-task 1 + 1. [x] Sub-task 2 + Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes. ### Videos @@ -650,7 +664,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. ``` @@ -662,7 +676,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also begins a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. @@ -800,4 +814,4 @@ A link starting with a `/` is relative to the wiki root. [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [katex]: https://github.com/Khan/KaTeX "KaTeX website" [katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" -[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
\ No newline at end of file +[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 5ada8748d85..678fc3ffd1f 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -19,10 +19,12 @@ The following table depicts the various user permission levels in a project. | Action | Guest | Reporter | Developer | Master | Owner | |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | +| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ | +| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | -| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | -| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | -| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| See a list of builds | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | +| See a build log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | +| Download and browse build artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | | View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ | | Pull project code | | ✓ | ✓ | ✓ | ✓ | | Download project | | ✓ | ✓ | ✓ | ✓ | @@ -63,11 +65,8 @@ The following table depicts the various user permission levels in a project. | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | -| Force push to protected branches [^2] | | | | | | -| Remove protected branches [^2] | | | | | | - -[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines** -[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner +| Force push to protected branches [^3] | | | | | | +| Remove protected branches [^3] | | | | | | ## Group @@ -156,17 +155,20 @@ users: | Run CI build | | ✓ | ✓ | ✓ | | Clone source and LFS from current project | | ✓ | ✓ | ✓ | | Clone source and LFS from public projects | | ✓ | ✓ | ✓ | -| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ | -| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] | +| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ | +| Clone source and LFS from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] | | Push source and LFS | | | | | | Pull container images from current project | | ✓ | ✓ | ✓ | | Pull container images from public projects | | ✓ | ✓ | ✓ | -| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ | -| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] | +| Pull container images from internal projects| | ✓ [^4] | ✓ [^4] | ✓ | +| Pull container images from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] | | Push container images to current project | | ✓ | ✓ | ✓ | | Push container images to other projects | | | | | -[^3]: Only if user is not external one. -[^4]: Only if user is a member of the project. +[^1]: Guest users can only view the confidential issues they created themselves +[^2]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines** +[^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner +[^4]: Only if user is not external one. +[^5]: Only if user is a member of the project. [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [new-mod]: project/new_ci_build_permissions_model.md diff --git a/doc/profile/2fa.png b/doc/user/profile/account/img/2fa.png Binary files differindex bb464efa685..bb464efa685 100644 --- a/doc/profile/2fa.png +++ b/doc/user/profile/account/img/2fa.png diff --git a/doc/profile/2fa_auth.png b/doc/user/profile/account/img/2fa_auth.png Binary files differindex 0caaed10805..0caaed10805 100644 --- a/doc/profile/2fa_auth.png +++ b/doc/user/profile/account/img/2fa_auth.png diff --git a/doc/user/profile/account/img/2fa_u2f_authenticate.png b/doc/user/profile/account/img/2fa_u2f_authenticate.png Binary files differnew file mode 100644 index 00000000000..ff2e936764d --- /dev/null +++ b/doc/user/profile/account/img/2fa_u2f_authenticate.png diff --git a/doc/profile/2fa_u2f_register.png b/doc/user/profile/account/img/2fa_u2f_register.png Binary files differindex 1cc142aa851..1cc142aa851 100644 --- a/doc/profile/2fa_u2f_register.png +++ b/doc/user/profile/account/img/2fa_u2f_register.png diff --git a/doc/user/profile/account/index.md b/doc/user/profile/account/index.md new file mode 100644 index 00000000000..764354e1e96 --- /dev/null +++ b/doc/user/profile/account/index.md @@ -0,0 +1,5 @@ +# Profile settings + +## Account + +Set up [two-factor authentication](two_factor_authentication.md). diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md new file mode 100644 index 00000000000..0f959b956a5 --- /dev/null +++ b/doc/user/profile/account/two_factor_authentication.md @@ -0,0 +1,215 @@ +# Two-Factor Authentication + +Two-factor Authentication (2FA) provides an additional level of security to your +GitLab account. Once enabled, in addition to supplying your username and +password to login, you'll be prompted for a code generated by an application on +your phone. + +By enabling 2FA, the only way someone other than you can log into your account +is to know your username and password *and* have access to your phone. + +## Overview + +> **Note:** +When you enable 2FA, don't forget to back up your recovery codes. + +In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as +the second factor of authentication. Once enabled, in addition to supplying your username and +password to login, you'll be prompted to activate your U2F device (usually by pressing +a button on it), and it will perform secure authentication on your behalf. + +The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend +that you set up both methods of two-factor authentication, so you can still access your account +from other browsers. + +## Enabling 2FA + +There are two ways to enable two-factor authentication: via a mobile application +or a U2F device. + +### Enable 2FA via mobile application + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-factor Authentication**. + +![Two-factor setup](img/2fa.png) + +**On your phone:** + +1. Install a compatible application. We recommend [Google Authenticator] +\(proprietary\) or [FreeOTP] \(open source\). +1. In the application, add a new entry in one of two ways: + * Scan the code with your phone's camera to add the entry automatically. + * Enter the details provided to add the entry manually. + +**In GitLab:** + +1. Enter the six-digit pin number from the entry on your phone into the **Pin + code** field. +1. Click **Submit**. + +If the pin you entered was correct, you'll see a message indicating that +Two-Factor Authentication has been enabled, and you'll be presented with a list +of recovery codes. + +### Enable 2FA via U2F device + +> **Notes:** +- GitLab officially only supports [Yubikey] U2F devices. +- Support for U2F devices was added in GitLab 8.8. + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-Factor Authentication**. +1. Plug in your U2F device. +1. Click on **Setup New U2F Device**. +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device was successfully set up. +Click on **Register U2F Device** to complete the process. + +![Two-Factor U2F Setup](img/2fa_u2f_register.png) + +## Recovery Codes + +> **Note:** +Recovery codes are not generated for U2F devices. + +Should you ever lose access to your phone, you can use one of the ten provided +backup codes to login to your account. We suggest copying or printing them for +storage in a safe place. **Each code can be used only once** to log in to your +account. + +If you lose the recovery codes or just want to generate new ones, you can do so +from the **Profile settings ➔ Account** page where you first enabled 2FA. + +## Logging in with 2FA Enabled + +Logging in with 2FA enabled is only slightly different than a normal login. +Enter your username and password credentials as you normally would, and you'll +be presented with a second prompt, depending on which type of 2FA you've enabled. + +### Log in via mobile application + +Enter the pin from your phone's application or a recovery code to log in. + +![Two-Factor Authentication on sign in via OTP](img/2fa_auth.png) + +### Log in via U2F device + +1. Click **Login via U2F Device** +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device responded to the authentication request. +Click on **Authenticate via U2F Device** to complete the process. + +![Two-Factor Authentication on sign in via U2F device](img/2fa_u2f_authenticate.png) + +## Disabling 2FA + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Disable**, under **Two-Factor Authentication**. + +This will clear all your two-factor authentication registrations, including mobile +applications and U2F devices. + +## Personal access tokens + +When 2FA is enabled, you can no longer use your normal account password to +authenticate with Git over HTTPS on the command line, you must use a personal +access token instead. + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Access Tokens**. +1. Choose a name and expiry date for the token. +1. Click on **Create Personal Access Token**. +1. Save the personal access token somewhere safe. + +When using Git over HTTPS on the command line, enter the personal access token +into the password field. + +## Recovery options + +If you lose your code generation device (such as your mobile phone) and you need +to disable two-factor authentication on your account, you have several options. + +### Use a saved recovery code + +When you enabled two-factor authentication for your account, a series of +recovery codes were generated. If you saved those codes somewhere safe, you +may use one to sign in. + +First, enter your username/email and password on the GitLab sign in page. When +prompted for a two-factor code, enter one of the recovery codes you saved +previously. + +> **Note:** Once a particular recovery code has been used, it cannot be used again. + You may still use the other saved recovery codes at a later time. + +### Generate new recovery codes using SSH + +It's not uncommon for users to forget to save the recovery codes when enabling +two-factor authentication. If you have an SSH key added to your GitLab account, +you can generate a new set of recovery codes using SSH. + +Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to +confirm that you wish to generate new codes. If you choose to continue, any +previously saved codes will be invalidated. + +```bash +$ ssh git@gitlab.example.com 2fa_recovery_codes +Are you sure you want to generate new two-factor recovery codes? +Any existing recovery codes you saved will be invalidated. (yes/no) +yes + +Your two-factor authentication recovery codes are: + +119135e5a3ebce8e +11f6v2a498810dcd +3924c7ab2089c902 +e79a3398bfe4f224 +34bd7b74adbc8861 +f061691d5107df1a +169bf32a18e63e7f +b510e7422e81c947 +20dbed24c5e74663 +df9d3b9403b9c9f0 + +During sign in, use one of the codes above when prompted for +your two-factor code. Then, visit your Profile Settings and add +a new device so you do not lose access to your account again. +``` + +Next, go to the GitLab sign in page and enter your username/email and password. +When prompted for a two-factor code, enter one of the recovery codes obtained +from the command line output. + +> **Note:** After signing in, you should immediately visit your **Profile Settings + -> Account** to set up two-factor authentication with a new device. + +### Ask a GitLab administrator to disable two-factor on your account + +If the above two methods are not possible, you may ask a GitLab global +administrator to disable two-factor authentication for your account. Please +be aware that this will temporarily leave your account in a less secure state. +You should sign in and re-enable two-factor authentication as soon as possible +after the administrator disables it. + +## Note to GitLab administrators + +You need to take special care to that 2FA keeps working after +[restoring a GitLab backup](../../../raketasks/backup_restore.md). + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en +[FreeOTP]: https://fedorahosted.org/freeotp/ +[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md new file mode 100644 index 00000000000..1760b182114 --- /dev/null +++ b/doc/user/project/issues/confidential_issues.md @@ -0,0 +1,68 @@ +# Confidential issues + +> [Introduced][ce-3282] in GitLab 8.6. + +Confidential issues are issues visible only to members of a project with +[sufficient permissions](#permissions-and-access-to-confidential-issues). +Confidential issues can be used by open source projects and companies alike to +keep security vulnerabilities private or prevent surprises from leaking out. + +## Making an issue confidential + +You can make an issue confidential either by creating a new issue or editing +an existing one. + +When you create a new issue, a checkbox right below the text area is available +to mark the issue as confidential. Check that box and hit the **Submit issue** +button to create the issue. For existing issues, edit them, check the +confidential checkbox and hit **Save changes**. + +![Creating a new confidential issue](img/confidential_issues_create.png) + +## Making an issue non-confidential + +To make an issue non-confidential, all you have to do is edit it and unmark +the confidential checkbox. Once you save the issue, it will gain the default +visibility level you have chosen for your project. + +Every change from regular to confidential and vice versa, is indicated by a +system note in the issue's comments. + +![Confidential issues system notes](img/confidential_issues_system_notes.png) + +## Indications of a confidential issue + +>**Note:** If you don't have [enough permissions](#permissions-and-access-to-confidential-issues), +you won't be able to see the confidential issues at all. + +There are a few things that visually separate a confidential issue from a +regular one. In the issues index page view, you can see the eye-slash icon +next to the issues that are marked as confidential. + +![Confidential issues index page](img/confidential_issues_index_page.png) + +--- + +Likewise, while inside the issue, you can see the eye-slash icon right next to +the issue number, but there is also an indicator in the comment area that the +issue you are commenting on is confidential. + +![Confidential issue page](img/confidential_issues_issue_page.png) + +## Permissions and access to confidential issues + +There are two kinds of level access for confidential issues. The general rule +is that confidential issues are visible only to members of a project with at +least [Reporter access][permissions]. However, a guest user can also create +confidential issues, but can only view the ones that they created themselves. + +Confidential issues are also hidden in search results for unprivileged users. +For example, here's what a user with Master and Guest access sees in the +project's search results respectively. + +| Master access | Guest access | +| :-----------: | :----------: | +| ![Confidential issues search master](img/confidential_issues_search_master.png) | ![Confidential issues search guest](img/confidential_issues_search_guest.png) | + +[permissions]: ../../permissions.md#project +[ce-3282]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3282 diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md new file mode 100644 index 00000000000..b516d47ffa3 --- /dev/null +++ b/doc/user/project/issues/due_dates.md @@ -0,0 +1,37 @@ +# Due dates + +> [Introduced][ce-3614] in GitLab 8.7. + +Due dates can be used in issues to keep track of deadlines and make sure +features are shipped on time. Due dates require at least [Reporter permissions][permissions] +to be able to edit them. On the contrary, they can be seen by everybody. + +## Setting a due date + +When creating or editing an issue, you can see the due date field from where +a calendar will appear to help you choose the date you want. To remove it, +select the date text and delete it. + +![Create a due date](img/due_dates_create.png) + +A quicker way to set a due date is via the issue sidebar. Simply expand the +sidebar and select **Edit** to pick a due date or remove the existing one. +Changes are saved immediately. + +![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png) + +## Making use of due dates + +Issues that have a due date can be distinctively seen in the issues index page +with a calendar icon next to them. Issues where the date is past due will have +the icon and the date colored red. You can sort issues by those that are +_Due soon_ or _Due later_ from the dropdown menu in the right. + +![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png) + +Due dates also appear in your [todos list](../../../workflow/todos.md). + +![Issues with due dates in the todos](img/due_dates_todos.png) + +[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614 +[permissions]: ../../permissions.md#project diff --git a/doc/user/project/issues/img/confidential_issues_create.png b/doc/user/project/issues/img/confidential_issues_create.png Binary files differnew file mode 100644 index 00000000000..d259255599d --- /dev/null +++ b/doc/user/project/issues/img/confidential_issues_create.png diff --git a/doc/user/project/issues/img/confidential_issues_index_page.png b/doc/user/project/issues/img/confidential_issues_index_page.png Binary files differnew file mode 100644 index 00000000000..042461e2451 --- /dev/null +++ b/doc/user/project/issues/img/confidential_issues_index_page.png diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png Binary files differnew file mode 100644 index 00000000000..b3568e9303a --- /dev/null +++ b/doc/user/project/issues/img/confidential_issues_issue_page.png diff --git a/doc/user/project/issues/img/confidential_issues_search_guest.png b/doc/user/project/issues/img/confidential_issues_search_guest.png Binary files differnew file mode 100644 index 00000000000..b85de90b4d5 --- /dev/null +++ b/doc/user/project/issues/img/confidential_issues_search_guest.png diff --git a/doc/user/project/issues/img/confidential_issues_search_master.png b/doc/user/project/issues/img/confidential_issues_search_master.png Binary files differnew file mode 100644 index 00000000000..bf2b9428875 --- /dev/null +++ b/doc/user/project/issues/img/confidential_issues_search_master.png diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png Binary files differnew file mode 100644 index 00000000000..4005f9350f7 --- /dev/null +++ b/doc/user/project/issues/img/confidential_issues_system_notes.png diff --git a/doc/user/project/issues/img/due_dates_create.png b/doc/user/project/issues/img/due_dates_create.png Binary files differnew file mode 100644 index 00000000000..d2fe1172bab --- /dev/null +++ b/doc/user/project/issues/img/due_dates_create.png diff --git a/doc/user/project/issues/img/due_dates_edit_sidebar.png b/doc/user/project/issues/img/due_dates_edit_sidebar.png Binary files differnew file mode 100644 index 00000000000..6b37150e7db --- /dev/null +++ b/doc/user/project/issues/img/due_dates_edit_sidebar.png diff --git a/doc/user/project/issues/img/due_dates_issues_index_page.png b/doc/user/project/issues/img/due_dates_issues_index_page.png Binary files differnew file mode 100644 index 00000000000..defcd5eca39 --- /dev/null +++ b/doc/user/project/issues/img/due_dates_issues_index_page.png diff --git a/doc/user/project/issues/img/due_dates_todos.png b/doc/user/project/issues/img/due_dates_todos.png Binary files differnew file mode 100644 index 00000000000..92c9fd4021b --- /dev/null +++ b/doc/user/project/issues/img/due_dates_todos.png diff --git a/doc/user/project/merge_requests/img/merge_conflict_editor.png b/doc/user/project/merge_requests/img/merge_conflict_editor.png Binary files differnew file mode 100644 index 00000000000..6660920c191 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_conflict_editor.png diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md index 4d7225bd820..68c49054e47 100644 --- a/doc/user/project/merge_requests/resolve_conflicts.md +++ b/doc/user/project/merge_requests/resolve_conflicts.md @@ -21,6 +21,18 @@ request into the source branch, resolving the conflicts using the options chosen. If the source branch is `feature` and the target branch is `master`, this is similar to performing `git checkout feature; git merge master` locally. +## Merge conflict editor + +> Introduced in GitLab 8.13. + +The merge conflict resolution editor allows for more complex merge conflicts, +which require the user to manually modify a file in order to resolve a conflict, +to be solved right form the GitLab interface. Use the **Edit inline** button +to open the editor. Once you're sure about your changes, hit the +**Commit conflict resolution** button. + +![Merge conflict editor](img/merge_conflict_editor.png) + ## Conflicts available for resolution GitLab allows resolving conflicts in a file where all of the below are true: diff --git a/doc/workflow/README.md b/doc/workflow/README.md index b317bd79ded..0b6f00c6aa4 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -7,6 +7,10 @@ - [Feature branch workflow](workflow.md) - [GitLab Flow](gitlab_flow.md) - [Groups](groups.md) +- Issues - The GitLab Issue Tracker is an advanced and complete tool for + tracking the evolution of a new idea or the process of solving a problem. + - [Confidential issues](../user/project/issues/confidential_issues.md) + - [Due date for issues](../user/project/issues/due_dates.md) - [Issue Board](../user/project/issue_board.md) - [Keyboard shortcuts](shortcuts.md) - [File finder](file_finder.md) 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/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 85360730841..64da7d6b86f 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -38,26 +38,25 @@ module API present key, with: Entities::SSHKey end - # TODO: for 9.0 we should check if params are there with the params block - # grape provides, at this point we'd change behaviour so we can't - # Behaviour now if you don't provide all required params: it renders a - # validation error or two. desc 'Add new deploy key to currently authenticated user' do success Entities::SSHKey end + params do + requires :key, type: String, desc: 'The new deploy key' + requires :title, type: String, desc: 'The name of the deploy key' + end post ":id/#{path}" do - attrs = attributes_for_keys [:title, :key] - attrs[:key].strip! if attrs[:key] + params[:key].strip! # Check for an existing key joined to this project - key = user_project.deploy_keys.find_by(key: attrs[:key]) + key = user_project.deploy_keys.find_by(key: params[:key]) if key present key, with: Entities::SSHKey break end # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) + key = current_user.accessible_deploy_keys.find_by(key: params[:key]) if key user_project.deploy_keys << key present key, with: Entities::SSHKey @@ -65,7 +64,7 @@ module API end # Create a new deploy key - key = DeployKey.new attrs + key = DeployKey.new(declared_params(include_missing: false)) if key.valid? && user_project.deploy_keys << key present key, with: Entities::SSHKey else @@ -105,15 +104,19 @@ module API present key.deploy_key, with: Entities::SSHKey end - desc 'Delete existing deploy key of currently authenticated user' do + desc 'Delete deploy key for a project' do success Key end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' end delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys.find(params[:key_id]) - key.destroy + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + if key + key.destroy + else + not_found!('Deploy Key') + end end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 49c5f0652ab..a1d7b323f4f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -90,6 +90,12 @@ module API MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) end + def find_merge_request_with_access(id, access_level = :read_merge_request) + merge_request = user_project.merge_requests.find(id) + authorize! access_level, merge_request + merge_request + end + def authenticate! unauthorized! unless current_user end diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 07435d78468..bc3d69f6904 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -15,10 +15,8 @@ module API end get ":id/merge_requests/:merge_request_id/versions" do - merge_request = user_project.merge_requests. - find(params[:merge_request_id]) + merge_request = find_merge_request_with_access(params[:merge_request_id]) - authorize! :read_merge_request, merge_request present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff end @@ -34,10 +32,8 @@ module API end get ":id/merge_requests/:merge_request_id/versions/:version_id" do - merge_request = user_project.merge_requests. - find(params[:merge_request_id]) + merge_request = find_merge_request_with_access(params[:merge_request_id]) - authorize! :read_merge_request, merge_request present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e77af4b7a0d..7ffb38e62da 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -118,8 +118,8 @@ module API success Entities::MergeRequest end get path do - merge_request = find_project_merge_request(params[:merge_request_id]) - authorize! :read_merge_request, merge_request + merge_request = find_merge_request_with_access(params[:merge_request_id]) + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end @@ -127,8 +127,8 @@ module API success Entities::RepoCommit end get "#{path}/commits" do - merge_request = find_project_merge_request(params[:merge_request_id]) - authorize! :read_merge_request, merge_request + merge_request = find_merge_request_with_access(params[:merge_request_id]) + present merge_request.commits, with: Entities::RepoCommit end @@ -136,8 +136,8 @@ module API success Entities::MergeRequestChanges end get "#{path}/changes" do - merge_request = find_project_merge_request(params[:merge_request_id]) - authorize! :read_merge_request, merge_request + merge_request = find_merge_request_with_access(params[:merge_request_id]) + present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end @@ -155,8 +155,7 @@ module API :remove_source_branch end put path do - merge_request = find_project_merge_request(params.delete(:merge_request_id)) - authorize! :update_merge_request, merge_request + merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? @@ -235,10 +234,7 @@ module API use :pagination end get "#{path}/comments" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - authorize! :read_merge_request, merge_request - + merge_request = find_merge_request_with_access(params[:merge_request_id]) present paginate(merge_request.notes.fresh), with: Entities::MRNote end @@ -250,8 +246,7 @@ module API requires :note, type: String, desc: 'The text of the comment' end post "#{path}/comments" do - merge_request = find_project_merge_request(params[:merge_request_id]) - authorize! :create_note, merge_request + merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) opts = { note: params[:note], @@ -275,7 +270,7 @@ module API use :pagination end get "#{path}/closes_issues" do - merge_request = find_project_merge_request(params[:merge_request_id]) + merge_request = find_merge_request_with_access(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: issue_entity(user_project), current_user: current_user end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 284e4cf549a..4d2a8f48267 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -70,21 +70,27 @@ module API end post ":id/#{noteables_str}/:noteable_id/notes" do opts = { - note: params[:body], - noteable_type: noteables_str.classify, - noteable_id: params[:noteable_id] + note: params[:body], + noteable_type: noteables_str.classify, + noteable_id: params[:noteable_id] } - if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) - opts[:created_at] = params[:created_at] - end + noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + + if can?(current_user, noteable_read_ability_name(noteable), noteable) + if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) + opts[:created_at] = params[:created_at] + end - note = ::Notes::CreateService.new(user_project, current_user, opts).execute + note = ::Notes::CreateService.new(user_project, current_user, opts).execute - if note.valid? - present note, with: Entities::const_get(note.class.name) + if note.valid? + present note, with: Entities::const_get(note.class.name) + else + not_found!("Note #{note.errors.messages}") + end else - not_found!("Note #{note.errors.messages}") + not_found!("Note") end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 3a9dfbb237c..a0abec49438 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -145,7 +145,7 @@ module API name: :room, type: String, desc: 'Campfire room' - }, + } ], 'custom-issue-tracker' => [ { @@ -534,7 +534,36 @@ module API desc: 'The password of the user' } ] - }.freeze + } + + service_classes = [ + AsanaService, + AssemblaService, + BambooService, + BugzillaService, + BuildkiteService, + BuildsEmailService, + CampfireService, + CustomIssueTrackerService, + DroneCiService, + EmailsOnPushService, + ExternalWikiService, + FlowdockService, + GemnasiumService, + HipchatService, + IrkerService, + JiraService, + KubernetesService, + MattermostSlashCommandsService, + SlackSlashCommandsService, + PipelinesEmailService, + PivotaltrackerService, + PushoverService, + RedmineService, + SlackService, + MattermostService, + TeamcityService, + ].freeze trigger_services = { 'mattermost-slash-commands' => [ @@ -568,6 +597,19 @@ module API services.each do |service_slug, settings| desc "Set #{service_slug} service for project" params do + service_classes.each do |service| + event_names = service.try(:event_names) || [] + event_names.each do |event_name| + services[service.to_param.tr("_", "-")] << { + required: false, + name: event_name.to_sym, + type: String, + desc: ServicesHelper.service_event_description(event_name) + } + end + end + services.freeze + settings.each do |setting| if setting[:required] requires setting[:name], type: setting[:type], desc: setting[:desc] @@ -581,7 +623,7 @@ module API service_params = declared_params(include_missing: false).merge(active: true) if service.update_attributes(service_params) - true + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? else render_api_error!('400 Bad Request', 400) end diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index 10749b34004..e11d7537cc9 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -3,8 +3,8 @@ module API before { authenticate! } subscribable_types = { - 'merge_request' => proc { |id| user_project.merge_requests.find(id) }, - 'merge_requests' => proc { |id| user_project.merge_requests.find(id) }, + 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, + 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, 'issues' => proc { |id| find_project_issue(id) }, 'labels' => proc { |id| find_project_label(id) }, } diff --git a/lib/api/todos.rb b/lib/api/todos.rb index ed8f48aa1e3..9bd077263a7 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -5,7 +5,7 @@ module API before { authenticate! } ISSUABLE_TYPES = { - 'merge_requests' => ->(id) { user_project.merge_requests.find(id) }, + 'merge_requests' => ->(id) { find_merge_request_with_access(id) }, 'issues' => ->(id) { find_project_issue(id) } } 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/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index eee9a64120b..38ac6edc9f1 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -4,8 +4,11 @@ module Gitlab module Build class Factory < Status::Factory def self.extended_statuses - [Status::Build::Stop, Status::Build::Play, - Status::Build::Cancelable, Status::Build::Retryable] + [[Status::Build::Cancelable, + Status::Build::Retryable], + [Status::Build::FailedAllowed, + Status::Build::Play, + Status::Build::Stop]] end def self.common_helpers diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb new file mode 100644 index 00000000000..807afe24bd5 --- /dev/null +++ b/lib/gitlab/ci/status/build/failed_allowed.rb @@ -0,0 +1,27 @@ +module Gitlab + module Ci + module Status + module Build + class FailedAllowed < SimpleDelegator + include Status::Extended + + def label + 'failed (allowed to fail)' + end + + def icon + 'icon_status_warning' + end + + def group + 'failed_with_warnings' + end + + def self.matches?(build, user) + build.failed? && build.allow_failure? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb index ae9ef895df4..15836c699c7 100644 --- a/lib/gitlab/ci/status/factory.rb +++ b/lib/gitlab/ci/status/factory.rb @@ -5,41 +5,46 @@ module Gitlab def initialize(subject, user) @subject = subject @user = user + @status = subject.status || HasStatus::DEFAULT_STATUS end def fabricate! - if extended_status - extended_status.new(core_status) - else + if extended_statuses.none? core_status + else + compound_extended_status end end - def self.extended_statuses - [] + def core_status + Gitlab::Ci::Status + .const_get(@status.capitalize) + .new(@subject, @user) + .extend(self.class.common_helpers) end - def self.common_helpers - Module.new + def compound_extended_status + extended_statuses.inject(core_status) do |status, extended| + extended.new(status) + end end - private + def extended_statuses + return @extended_statuses if defined?(@extended_statuses) - def simple_status - @simple_status ||= @subject.status || :created + groups = self.class.extended_statuses.map do |group| + Array(group).find { |status| status.matches?(@subject, @user) } + end + + @extended_statuses = groups.flatten.compact end - def core_status - Gitlab::Ci::Status - .const_get(simple_status.capitalize) - .new(@subject, @user) - .extend(self.class.common_helpers) + def self.extended_statuses + [] end - def extended_status - @extended ||= self.class.extended_statuses.find do |status| - status.matches?(@subject, @user) - end + def self.common_helpers + Module.new end end end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 16dcb326be9..13c8343b12a 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -4,7 +4,7 @@ module Gitlab module Pipeline class Factory < Status::Factory def self.extended_statuses - [Pipeline::SuccessWithWarnings] + [Status::SuccessWarning] end def self.common_helpers diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb deleted file mode 100644 index 24bf8b869e0..00000000000 --- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Gitlab - module Ci - module Status - module Pipeline - class SuccessWithWarnings < SimpleDelegator - include Status::Extended - - def text - 'passed' - end - - def label - 'passed with warnings' - end - - def icon - 'icon_status_warning' - end - - def group - 'success_with_warnings' - end - - def self.matches?(pipeline, user) - pipeline.success? && pipeline.has_warnings? - end - end - end - end - end -end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb index 689a5dd45bc..4c37f084d07 100644 --- a/lib/gitlab/ci/status/stage/factory.rb +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -3,6 +3,10 @@ module Gitlab module Status module Stage class Factory < Status::Factory + def self.extended_statuses + [Status::SuccessWarning] + end + def self.common_helpers Status::Stage::Common end diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb new file mode 100644 index 00000000000..d4cdab6957a --- /dev/null +++ b/lib/gitlab/ci/status/success_warning.rb @@ -0,0 +1,33 @@ +module Gitlab + module Ci + module Status + ## + # Extended status used when pipeline or stage passed conditionally. + # This means that failed jobs that are allowed to fail were present. + # + class SuccessWarning < SimpleDelegator + include Status::Extended + + def text + 'passed' + end + + def label + 'passed with warnings' + end + + def icon + 'icon_status_warning' + end + + def group + 'success_with_warnings' + end + + def self.matches?(subject, user) + subject.success? && subject.has_warnings? + end + end + end + end +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/email/handler.rb b/lib/gitlab/email/handler.rb index bd3267e2a80..bd2f5d3615e 100644 --- a/lib/gitlab/email/handler.rb +++ b/lib/gitlab/email/handler.rb @@ -1,10 +1,11 @@ require 'gitlab/email/handler/create_note_handler' require 'gitlab/email/handler/create_issue_handler' +require 'gitlab/email/handler/unsubscribe_handler' module Gitlab module Email module Handler - HANDLERS = [CreateNoteHandler, CreateIssueHandler] + HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler] def self.for(mail, mail_key) HANDLERS.find do |klass| diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb index 7cccf465334..3f6ace0311a 100644 --- a/lib/gitlab/email/handler/base_handler.rb +++ b/lib/gitlab/email/handler/base_handler.rb @@ -9,52 +9,13 @@ module Gitlab @mail_key = mail_key end - def message - @message ||= process_message - end - - def author + def can_execute? raise NotImplementedError end - def project + def execute raise NotImplementedError end - - private - - def validate_permission!(permission) - raise UserNotFoundError unless author - raise UserBlockedError if author.blocked? - raise ProjectNotFound unless author.can?(:read_project, project) - raise UserNotAuthorizedError unless author.can?(permission, project) - end - - def process_message - message = ReplyParser.new(mail).execute.strip - add_attachments(message) - end - - def add_attachments(reply) - attachments = Email::AttachmentUploader.new(mail).execute(project) - - reply + attachments.map do |link| - "\n\n#{link[:markdown]}" - end.join - end - - def verify_record!(record:, invalid_exception:, record_name:) - return if record.persisted? - return if record.errors.key?(:commands_only) - - error_title = "The #{record_name} could not be created for the following reasons:" - - msg = error_title + record.errors.full_messages.map do |error| - "\n\n- #{error}" - end.join - - raise invalid_exception, msg - end end end end diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 9f90a3ec2b2..127fae159d5 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -5,6 +5,7 @@ module Gitlab module Email module Handler class CreateIssueHandler < BaseHandler + include ReplyProcessing attr_reader :project_path, :incoming_email_token def initialize(mail, mail_key) diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb index 447c7a6a6b9..d87ba427f4b 100644 --- a/lib/gitlab/email/handler/create_note_handler.rb +++ b/lib/gitlab/email/handler/create_note_handler.rb @@ -1,10 +1,13 @@ require 'gitlab/email/handler/base_handler' +require 'gitlab/email/handler/reply_processing' module Gitlab module Email module Handler class CreateNoteHandler < BaseHandler + include ReplyProcessing + def can_handle? mail_key =~ /\A\w+\z/ end @@ -24,6 +27,8 @@ module Gitlab record_name: 'comment') end + private + def author sent_notification.recipient end @@ -36,8 +41,6 @@ module Gitlab @sent_notification ||= SentNotification.for(mail_key) end - private - def create_note Notes::CreateService.new( project, diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb new file mode 100644 index 00000000000..32c5caf93e8 --- /dev/null +++ b/lib/gitlab/email/handler/reply_processing.rb @@ -0,0 +1,54 @@ +module Gitlab + module Email + module Handler + module ReplyProcessing + private + + def author + raise NotImplementedError + end + + def project + raise NotImplementedError + end + + def message + @message ||= process_message + end + + def process_message + message = ReplyParser.new(mail).execute.strip + add_attachments(message) + end + + def add_attachments(reply) + attachments = Email::AttachmentUploader.new(mail).execute(project) + + reply + attachments.map do |link| + "\n\n#{link[:markdown]}" + end.join + end + + def validate_permission!(permission) + raise UserNotFoundError unless author + raise UserBlockedError if author.blocked? + raise ProjectNotFound unless author.can?(:read_project, project) + raise UserNotAuthorizedError unless author.can?(permission, project) + end + + def verify_record!(record:, invalid_exception:, record_name:) + return if record.persisted? + return if record.errors.key?(:commands_only) + + error_title = "The #{record_name} could not be created for the following reasons:" + + msg = error_title + record.errors.full_messages.map do |error| + "\n\n- #{error}" + end.join + + raise invalid_exception, msg + end + end + end + end +end diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb new file mode 100644 index 00000000000..97d7a8d65ff --- /dev/null +++ b/lib/gitlab/email/handler/unsubscribe_handler.rb @@ -0,0 +1,32 @@ +require 'gitlab/email/handler/base_handler' + +module Gitlab + module Email + module Handler + class UnsubscribeHandler < BaseHandler + def can_handle? + mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/ + end + + def execute + raise SentNotificationNotFoundError unless sent_notification + return unless sent_notification.unsubscribable? + + noteable = sent_notification.noteable + raise NoteableNotFoundError unless noteable + noteable.unsubscribe(sent_notification.recipient) + end + + private + + def sent_notification + @sent_notification ||= SentNotification.for(reply_key) + end + + def reply_key + mail_key.sub(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX, '') + end + end + end + end +end 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/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 801dfde9a36..b91012d6405 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,5 +1,6 @@ module Gitlab module IncomingEmail + UNSUBSCRIBE_SUFFIX = '+unsubscribe'.freeze WILDCARD_PLACEHOLDER = '%{key}'.freeze class << self @@ -18,7 +19,11 @@ module Gitlab end def reply_address(key) - config.address.gsub(WILDCARD_PLACEHOLDER, key) + config.address.sub(WILDCARD_PLACEHOLDER, key) + end + + def unsubscribe_address(key) + config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}") end def key_from_address(address) @@ -49,7 +54,7 @@ module Gitlab return nil unless wildcard_address regex = Regexp.escape(wildcard_address) - regex = regex.gsub(Regexp.escape('%{key}'), "(.+)") + regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)') Regexp.new(regex).freeze end end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 6bdf3db9cb8..db325c00705 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -71,6 +71,14 @@ module Gitlab ) end + def single_commit_result? + commits_count == 1 && total_result_count == 1 + end + + def total_result_count + issues_count + merge_requests_count + milestones_count + notes_count + blobs_count + wiki_blobs_count + commits_count + end + private def blobs @@ -114,7 +122,25 @@ module Gitlab end def commits - @commits ||= project.repository.find_commits_by_message(query) + @commits ||= find_commits(query) + end + + def find_commits(query) + return [] unless Ability.allowed?(@current_user, :download_code, @project) + + commits = find_commits_by_message(query) + commit_by_sha = find_commit_by_sha(query) + commits |= [commit_by_sha] if commit_by_sha + commits + end + + def find_commits_by_message(query) + project.repository.find_commits_by_message(query) + end + + def find_commit_by_sha(query) + key = query.strip + project.repository.commit(key) if Commit.valid_hash?(key) end def project_ids_relation diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 35212992698..c9c65f76f4b 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -43,6 +43,10 @@ module Gitlab @milestones_count ||= milestones.count end + def single_commit_result? + false + end + private def projects diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 6c7e673fb9f..6ce9b229294 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -35,7 +35,9 @@ module Gitlab return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten - access_levels.any? { |access_level| access_level.check_access(user) } + has_access = access_levels.any? { |access_level| access_level.check_access(user) } + + has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref) else user.can?(:push_code, project) end diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index ec2903b7ec6..e55c0d6ac49 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -8,21 +8,31 @@ module Mattermost @user = user end - private - def with_session(&blk) Mattermost::Session.new(user).with_session(&blk) end - def json_get(path, options = {}) + private + + # Should be used in a session manually + def get(session, path, options = {}) + json_response session.get(path, options) + end + + # Should be used in a session manually + def post(session, path, options = {}) + json_response session.post(path, options) + end + + def session_get(path, options = {}) with_session do |session| - json_response session.get(path, options) + get(session, path, options) end end - def json_post(path, options = {}) + def session_post(path, options = {}) with_session do |session| - json_response session.post(path, options) + post(session, path, options) end end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index d1e4bb0eccf..33e450d7f0a 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,7 +1,7 @@ module Mattermost class Command < Client def create(params) - response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", + response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", body: params.to_json) response['token'] diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 784eca6ab5a..09dfd082b3a 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,7 +1,7 @@ module Mattermost class Team < Client def all - json_get('/api/v3/teams/all') + session_get('/api/v3/teams/all') end end end 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/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 2e44b5128b4..a6e708c01e4 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -54,6 +54,7 @@ describe Projects::ServicesController do context 'on successful update' do it 'sets the flash' do expect(service).to receive(:to_param).and_return('hipchat') + expect(service).to receive(:event_names).and_return(HipchatService.event_names) put :update, namespace_id: project.namespace.id, diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb index ee3b17b8bf1..7f557b25ccb 100644 --- a/spec/factories/ci/stages.rb +++ b/spec/factories/ci/stages.rb @@ -3,11 +3,12 @@ FactoryGirl.define do transient do name 'test' status nil + warnings nil pipeline factory: :ci_empty_pipeline end initialize_with do - Ci::Stage.new(pipeline, name: name, status: status) + Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings) end end end 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_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index a5b88812b75..87a8f62687a 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -10,7 +10,7 @@ describe "Admin::Projects", feature: true do end describe "GET /admin/projects" do - let!(:archived_project) { create :project, :public, archived: true } + let!(:archived_project) { create :project, :public, :archived } before do visit admin_projects_path 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/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 30b80aa82b0..78a11ffee99 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do include_examples 'project features apply to issuables', MergeRequest context 'archived issuable' do - let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) } + let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) } let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } let(:access_level) { ProjectFeature::ENABLED } let(:user) { user_in_group } diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 16dcc487812..8a155c3bfc5 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -134,14 +134,14 @@ describe 'Dropdown assignee', js: true, feature: true do click_button 'Assigned to me' end - expect(filtered_search.value).to eq("assignee:#{user.to_reference}") + expect(filtered_search.value).to eq("assignee:#{user.to_reference} ") end it 'fills in the assignee username when the assignee has not been filtered' do click_assignee(user_jacob.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") + expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ") end it 'fills in the assignee username when the assignee has been filtered' do @@ -149,14 +149,14 @@ describe 'Dropdown assignee', js: true, feature: true do click_assignee(user.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user.username}") + expect(filtered_search.value).to eq("assignee:@#{user.username} ") end it 'selects `no assignee`' do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:none") + expect(filtered_search.value).to eq("assignee:none ") end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 464749d01e3..a5d5d9d4c5e 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -121,14 +121,14 @@ describe 'Dropdown author', js: true, feature: true do click_author(user_jacob.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user_jacob.username}") + expect(filtered_search.value).to eq("author:@#{user_jacob.username} ") end it 'fills in the author username when the author has been filtered' do click_author(user.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user.username}") + expect(filtered_search.value).to eq("author:@#{user.username} ") end end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 89c144141c9..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:') @@ -159,7 +169,7 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{bug_label.title}") + expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end it 'fills in the label name when the label is partially filled' do @@ -167,49 +177,49 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{bug_label.title}") + expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") + expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ") end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") + expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ") end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") + expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ") end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") + expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ") end it 'fills in the label name with special characters' do click_label(special_label.title) expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:~#{special_label.title}") + expect(filtered_search.value).to eq("label:~#{special_label.title} ") end it 'selects `no label`' do find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click expect(page).to have_css(js_dropdown_label, visible: false) - expect(filtered_search.value).to eq("label:none") + expect(filtered_search.value).to eq("label:none ") end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index e5a271b663f..134e58ad586 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -127,7 +127,7 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") end it 'fills in the milestone name when the milestone is partially filled' do @@ -135,56 +135,56 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") end it 'fills in the milestone name that contains multiple words' do click_milestone(two_words_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") + expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ") end it 'fills in the milestone name that contains multiple words and is very long' do click_milestone(long_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") + expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ") end it 'fills in the milestone name that contains double quotes' do click_milestone(wont_fix_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") + expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ") end it 'fills in the milestone name with the correct capitalization' do click_milestone(uppercase_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ") end it 'fills in the milestone name with special characters' do click_milestone(special_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") + expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ") end it 'selects `no milestone`' do click_static_milestone('No Milestone') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:none") + expect(filtered_search.value).to eq("milestone:none ") end it 'selects `upcoming milestone`' do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:upcoming") + expect(filtered_search.value).to eq("milestone:upcoming ") end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 1cdac520181..f48a0193545 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -539,7 +539,7 @@ describe 'Filter issues', js: true, feature: true do click_button user2.username end - expect(filtered_search.value).to eq("author:@#{user2.username}") + expect(filtered_search.value).to eq("author:@#{user2.username} ") end it 'changes label' do @@ -551,7 +551,7 @@ describe 'Filter issues', js: true, feature: true do click_button label.name end - expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name}") + expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ") end it 'changes label correctly space is in previous label' do @@ -563,7 +563,7 @@ describe 'Filter issues', js: true, feature: true do click_button label.name end - expect(filtered_search.value).to eq("label:~#{label.name}") + expect(filtered_search.value).to eq("label:~#{label.name} ") end end 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/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb new file mode 100644 index 00000000000..d0bafc6168c --- /dev/null +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do + let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } + + let(:project) { create(:empty_project) } + + background do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after do + FileUtils.rm_rf(export_path, secure: true) + end + + context 'admin user' do + before do + login_as(:admin) + end + + context 'moving the namespace' do + scenario 'removes the export file' do + setup_export_project + + old_export_path = project.export_path.dup + + expect(File).to exist(old_export_path) + + project.namespace.update(path: 'new_path') + + expect(File).not_to exist(old_export_path) + end + end + + context 'deleting the namespace' do + scenario 'removes the export file' do + setup_export_project + + old_export_path = project.export_path.dup + + expect(File).to exist(old_export_path) + + project.namespace.destroy + + expect(File).not_to exist(old_export_path) + end + end + + def setup_export_project + visit edit_namespace_project_path(project.namespace, project) + + expect(page).to have_content('Export project') + + click_link 'Export project' + + visit edit_namespace_project_path(project.namespace, project) + + expect(page).to have_content('Download export') + end + end +end 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/features/search_spec.rb b/spec/features/search_spec.rb index a05b83959fb..0fe5a897565 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -211,4 +211,44 @@ describe "Search", feature: true do end end end + + describe 'search for commits' do + before do + visit search_path(project_id: project.id) + end + + it 'redirects to commit page when search by sha and only commit found' do + fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f' + + click_button 'Search' + + expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f')) + end + + it 'redirects to single commit regardless of query case' do + fill_in 'search', with: '6D394385cf' + + click_button 'Search' + + expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f')) + end + + it 'holds on /search page when the only commit is found by message' do + create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master') + + fill_in 'search', with: 'deadbeef' + click_button 'Search' + + expect(page).to have_current_path('/search', only_path: true) + end + + it 'shows multiple matching commits' do + fill_in 'search', with: 'See merge request' + + click_button 'Search' + click_link 'Commits' + + expect(page).to have_selector('.commit-row-description', count: 9) + end + end end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index abb27c90e0a..a5d14aa19f1 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -36,6 +36,19 @@ feature 'Task Lists', feature: true do MARKDOWN end + let(:nested_tasks_markdown) do + <<-EOT.strip_heredoc + - [ ] Task a + - [x] Task a.1 + - [ ] Task a.2 + - [ ] Task b + + 1. [ ] Task 1 + 1. [ ] Task 1.1 + 1. [x] Task 1.2 + EOT + end + before do Warden.test_mode! @@ -123,6 +136,35 @@ feature 'Task Lists', feature: true do expect(page).to have_content("1 of 1 task completed") end end + + describe 'nested tasks', js: true do + let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } + + before { visit_issue(project, issue) } + + it 'renders' do + expect(page).to have_selector('ul.task-list', count: 2) + expect(page).to have_selector('li.task-list-item', count: 7) + expect(page).to have_selector('ul input[checked]', count: 1) + expect(page).to have_selector('ol input[checked]', count: 1) + end + + it 'solves tasks' do + expect(page).to have_content("2 of 7 tasks completed") + + page.find('li.task-list-item', text: 'Task b').find('input').click + page.find('li.task-list-item ul li.task-list-item', text: 'Task a.2').find('input').click + page.find('li.task-list-item ol li.task-list-item', text: 'Task 1.1').find('input').click + + expect(page).to have_content("5 of 7 tasks completed") + + visit_issue(project, issue) # reload to see new system notes + + expect(page).to have_content('marked the task Task b as complete') + expect(page).to have_content('marked the task Task a.2 as complete') + expect(page).to have_content('marked the task Task 1.1 as complete') + end + end end describe 'for Notes' do @@ -236,7 +278,7 @@ feature 'Task Lists', feature: true do expect(page).to have_content("2 of 6 tasks completed") end end - + describe 'single incomplete task' do let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 88361e27102..e4ba1d2f1c2 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -6,7 +6,7 @@ describe MergeRequestsFinder do let(:project1) { create(:project) } let(:project2) { create(:project, forked_from_project: project1) } - let(:project3) { create(:project, forked_from_project: project1, archived: true) } + let(:project3) { create(:project, :archived, forked_from_project: project1) } let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb index 8488dbd2a16..dea87980e25 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/move_to_project_finder_spec.rb @@ -36,7 +36,7 @@ describe MoveToProjectFinder do it 'does not return archived projects' do reporter_project.team << [user, :reporter] - reporter_project.update_attributes(archived: true) + reporter_project.archive! other_reporter_project = create(:empty_project) other_reporter_project.team << [user, :reporter] 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/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index d0d27ceb4a6..4bd45eb457d 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -31,7 +31,7 @@ it('should add tokenName and tokenValue', () => { gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); - expect(getInputValue()).toBe('label:none'); + expect(getInputValue()).toBe('label:none '); }); }); @@ -45,13 +45,13 @@ it('should replace tokenValue', () => { setInputValue('author:roo'); gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); - expect(getInputValue()).toBe('author:@root'); + expect(getInputValue()).toBe('author:@root '); }); it('should add tokenValues containing spaces', () => { setInputValue('label:~"test'); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); - expect(getInputValue()).toBe('label:~\'"test me"\''); + expect(getInputValue()).toBe('label:~\'"test me"\' '); }); }); }); 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/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index dccb29b5ef6..0c40fca0c1a 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -3,15 +3,23 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Factory do let(:user) { create(:user) } let(:project) { build.project } - - subject { described_class.new(build, user) } - let(:status) { subject.fabricate! } + let(:status) { factory.fabricate! } + let(:factory) { described_class.new(build, user) } before { project.team << [user, :developer] } context 'when build is successful' do let(:build) { create(:ci_build, :success) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable] + end + it 'fabricates a retryable build status' do expect(status).to be_a Gitlab::Ci::Status::Build::Retryable end @@ -26,24 +34,72 @@ describe Gitlab::Ci::Status::Build::Factory do end context 'when build is failed' do - let(:build) { create(:ci_build, :failed) } + context 'when build is not allowed to fail' do + let(:build) { create(:ci_build, :failed) } - it 'fabricates a retryable build status' do - expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable] + end + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + end end - it 'fabricates status with correct details' do - expect(status.text).to eq 'failed' - expect(status.icon).to eq 'icon_status_failed' - expect(status.label).to eq 'failed' - expect(status).to have_details - expect(status).to have_action + context 'when build is allowed to fail' do + let(:build) { create(:ci_build, :failed, :allowed_to_fail) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable, + Gitlab::Ci::Status::Build::FailedAllowed] + end + + it 'fabricates a failed but allowed build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::FailedAllowed + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'icon_status_warning' + expect(status.label).to eq 'failed (allowed to fail)' + expect(status).to have_details + expect(status).to have_action + expect(status.action_title).to include 'Retry' + expect(status.action_path).to include 'retry' + end end end context 'when build is a canceled' do let(:build) { create(:ci_build, :canceled) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Canceled + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable] + end + it 'fabricates a retryable build status' do expect(status).to be_a Gitlab::Ci::Status::Build::Retryable end @@ -60,6 +116,15 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when build is running' do let(:build) { create(:ci_build, :running) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Running + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Cancelable] + end + it 'fabricates a canceable build status' do expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable end @@ -76,6 +141,15 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when build is pending' do let(:build) { create(:ci_build, :pending) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Pending + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Cancelable] + end + it 'fabricates a cancelable build status' do expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable end @@ -92,6 +166,14 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when build is skipped' do let(:build) { create(:ci_build, :skipped) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'does not match extended statuses' do + expect(factory.extended_statuses).to be_empty + end + it 'fabricates a core skipped status' do expect(status).to be_a Gitlab::Ci::Status::Skipped end @@ -109,6 +191,15 @@ describe Gitlab::Ci::Status::Build::Factory do context 'when build is a play action' do let(:build) { create(:ci_build, :playable) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Play] + end + it 'fabricates a core skipped status' do expect(status).to be_a Gitlab::Ci::Status::Build::Play end @@ -119,12 +210,22 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.label).to eq 'manual play action' expect(status).to have_details expect(status).to have_action + expect(status.action_path).to include 'play' end end context 'when build is an environment stop action' do let(:build) { create(:ci_build, :playable, :teardown_environment) } + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Stop] + end + it 'fabricates a core skipped status' do expect(status).to be_a Gitlab::Ci::Status::Build::Stop end diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb new file mode 100644 index 00000000000..20f71459738 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::FailedAllowed do + let(:status) { double('core status') } + let(:user) { double('user') } + + subject do + described_class.new(status) + end + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'returns a warning icon' do + expect(subject.icon).to eq 'icon_status_warning' + end + end + + describe '#label' do + it 'returns information about failed but allowed to fail status' do + expect(subject.label).to eq 'failed (allowed to fail)' + end + end + + describe '#group' do + it 'returns status failed with warnings status group' do + expect(subject.group).to eq 'failed_with_warnings' + end + end + + describe 'action details' do + describe '#has_action?' do + it 'does not decorate action details' do + expect(status).to receive(:has_action?) + + subject.has_action? + end + end + + describe '#action_path' do + it 'does not decorate action path' do + expect(status).to receive(:action_path) + + subject.action_path + end + end + + describe '#action_icon' do + it 'does not decorate action icon' do + expect(status).to receive(:action_icon) + + subject.action_icon + end + end + + describe '#action_title' do + it 'does not decorate action title' do + expect(status).to receive(:action_title) + + subject.action_title + end + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'when build is failed' do + context 'when build is allowed to fail' do + let(:build) { create(:ci_build, :failed, :allowed_to_fail) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not allowed to fail' do + let(:build) { create(:ci_build, :failed) } + + it 'is not a correct match' do + expect(subject).not_to be true + end + end + end + + context 'when build did not fail' do + context 'when build is allowed to fail' do + let(:build) { create(:ci_build, :success, :allowed_to_fail) } + + it 'is not a correct match' do + expect(subject).not_to be true + end + end + + context 'when build is not allowed to fail' do + let(:build) { create(:ci_build, :success) } + + it 'is not a correct match' do + expect(subject).not_to be true + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index f92a1c149bf..bbf9c7c83a3 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -1,24 +1,135 @@ require 'spec_helper' describe Gitlab::Ci::Status::Factory do - subject do - described_class.new(resource, user) + let(:user) { create(:user) } + let(:fabricated_status) { factory.fabricate! } + let(:factory) { described_class.new(resource, user) } + + context 'when object has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |simple_status| + context "when simple core status is #{simple_status}" do + let(:resource) { double('resource', status: simple_status) } + + let(:expected_status) do + Gitlab::Ci::Status.const_get(simple_status.capitalize) + end + + it "fabricates a core status #{simple_status}" do + expect(fabricated_status).to be_a expected_status + end + + it "matches a valid core status for #{simple_status}" do + expect(factory.core_status).to be_a expected_status + end + + it "does not match any extended statuses for #{simple_status}" do + expect(factory.extended_statuses).to be_empty + end + end + end end - let(:user) { create(:user) } + context 'when resource supports multiple extended statuses' do + let(:resource) { double('resource', status: :success) } - let(:status) { subject.fabricate! } + let(:first_extended_status) do + Class.new(SimpleDelegator) do + def first_method + 'first return value' + end - context 'when object has a core status' do - HasStatus::AVAILABLE_STATUSES.each do |core_status| - context "when core status is #{core_status}" do - let(:resource) { double(status: core_status) } + def second_method + 'second return value' + end + + def self.matches?(*) + true + end + end + end - it "fabricates a core status #{core_status}" do - expect(status).to be_a( - Gitlab::Ci::Status.const_get(core_status.capitalize)) + let(:second_extended_status) do + Class.new(SimpleDelegator) do + def first_method + 'decorated return value' end + + def third_method + 'third return value' + end + + def self.matches?(*) + true + end + end + end + + shared_examples 'compound decorator factory' do + it 'fabricates compound decorator' do + expect(fabricated_status.first_method).to eq 'decorated return value' + expect(fabricated_status.second_method).to eq 'second return value' + expect(fabricated_status.third_method).to eq 'third return value' end + + it 'delegates to core status' do + expect(fabricated_status.text).to eq 'passed' + end + + it 'latest matches status becomes a status name' do + expect(fabricated_status.class).to eq second_extended_status + end + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [first_extended_status, second_extended_status] + end + end + + context 'when exclusive statuses are matches' do + before do + allow(described_class).to receive(:extended_statuses) + .and_return([[first_extended_status, second_extended_status]]) + end + + it 'does not fabricate compound decorator' do + expect(fabricated_status.first_method).to eq 'first return value' + expect(fabricated_status.second_method).to eq 'second return value' + expect(fabricated_status).not_to respond_to(:third_method) + end + + it 'delegates to core status' do + expect(fabricated_status.text).to eq 'passed' + end + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses).to eq [first_extended_status] + end + end + + context 'when exclusive statuses are not matched' do + before do + allow(described_class).to receive(:extended_statuses) + .and_return([[first_extended_status], [second_extended_status]]) + end + + it_behaves_like 'compound decorator factory' + end + + context 'when using simplified status grouping' do + before do + allow(described_class).to receive(:extended_statuses) + .and_return([first_extended_status, second_extended_status]) + end + + it_behaves_like 'compound decorator factory' end end end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb index d4a2dc7fcc1..b10a447c27a 100644 --- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -3,29 +3,32 @@ require 'spec_helper' describe Gitlab::Ci::Status::Pipeline::Factory do let(:user) { create(:user) } let(:project) { pipeline.project } - - subject do - described_class.new(pipeline, user) - end - - let(:status) do - subject.fabricate! - end + let(:status) { factory.fabricate! } + let(:factory) { described_class.new(pipeline, user) } before do project.team << [user, :developer] end context 'when pipeline has a core status' do - HasStatus::AVAILABLE_STATUSES.each do |core_status| - context "when core status is #{core_status}" do - let(:pipeline) do - create(:ci_pipeline, status: core_status) + HasStatus::AVAILABLE_STATUSES.each do |simple_status| + context "when core status is #{simple_status}" do + let(:pipeline) { create(:ci_pipeline, status: simple_status) } + + let(:expected_status) do + Gitlab::Ci::Status.const_get(simple_status.capitalize) + end + + it "matches correct core status for #{simple_status}" do + expect(factory.core_status).to be_a expected_status end - it "fabricates a core status #{core_status}" do - expect(status).to be_a( - Gitlab::Ci::Status.const_get(core_status.capitalize)) + it 'does not matche extended statuses' do + expect(factory.extended_statuses).to be_empty + end + + it "fabricates a core status #{simple_status}" do + expect(status).to be_a expected_status end it 'extends core status with common pipeline methods' do @@ -47,13 +50,22 @@ describe Gitlab::Ci::Status::Pipeline::Factory do create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) end + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::SuccessWarning] + end + it 'fabricates extended "success with warnings" status' do - expect(status) - .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings + expect(status).to be_a Gitlab::Ci::Status::SuccessWarning end - it 'extends core status with common pipeline methods' do + it 'extends core status with common pipeline method' do expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}" end end end diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb deleted file mode 100644 index 979160eb9c4..00000000000 --- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do - subject do - described_class.new(double('status')) - end - - describe '#test' do - it { expect(subject.text).to eq 'passed' } - end - - describe '#label' do - it { expect(subject.label).to eq 'passed with warnings' } - end - - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_warning' } - end - - describe '#group' do - it { expect(subject.group).to eq 'success_with_warnings' } - end - - describe '.matches?' do - context 'when pipeline is successful' do - let(:pipeline) do - create(:ci_pipeline, status: :success) - end - - context 'when pipeline has warnings' do - before do - allow(pipeline).to receive(:has_warnings?).and_return(true) - end - - it 'is a correct match' do - expect(described_class.matches?(pipeline, double)).to eq true - end - end - - context 'when pipeline does not have warnings' do - it 'does not match' do - expect(described_class.matches?(pipeline, double)).to eq false - end - end - end - - context 'when pipeline is not successful' do - let(:pipeline) do - create(:ci_pipeline, status: :skipped) - end - - context 'when pipeline has warnings' do - before do - allow(pipeline).to receive(:has_warnings?).and_return(true) - end - - it 'does not match' do - expect(described_class.matches?(pipeline, double)).to eq false - end - end - - context 'when pipeline does not have warnings' do - it 'does not match' do - expect(described_class.matches?(pipeline, double)).to eq false - end - end - end - end -end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 6f8721d30c2..bbb40e2c1ab 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -43,4 +43,25 @@ describe Gitlab::Ci::Status::Stage::Factory do end end end + + context 'when stage has warnings' do + let(:stage) do + build(:ci_stage, name: 'test', status: :success, pipeline: pipeline) + end + + before do + create(:ci_build, :allowed_to_fail, :failed, + stage: 'test', pipeline: stage.pipeline) + end + + it 'fabricates extended "success with warnings" status' do + expect(status) + .to be_a Gitlab::Ci::Status::SuccessWarning + end + + it 'extends core status with common stage method' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}" + end + end end diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb new file mode 100644 index 00000000000..7e2269397c6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::SuccessWarning do + subject do + described_class.new(double('status')) + end + + describe '#test' do + it { expect(subject.text).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed with warnings' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_warning' } + end + + describe '#group' do + it { expect(subject.group).to eq 'success_with_warnings' } + end + + describe '.matches?' do + let(:matchable) { double('matchable') } + + context 'when matchable subject is successful' do + before do + allow(matchable).to receive(:success?).and_return(true) + end + + context 'when matchable subject has warnings' do + before do + allow(matchable).to receive(:has_warnings?).and_return(true) + end + + it 'is a correct match' do + expect(described_class.matches?(matchable, double)).to eq true + end + end + + context 'when matchable subject does not have warnings' do + before do + allow(matchable).to receive(:has_warnings?).and_return(false) + end + + it 'does not match' do + expect(described_class.matches?(matchable, double)).to eq false + end + end + end + + context 'when matchable subject is not successful' do + before do + allow(matchable).to receive(:success?).and_return(false) + end + + context 'when matchable subject has warnings' do + before do + allow(matchable).to receive(:has_warnings?).and_return(true) + end + + it 'does not match' do + expect(described_class.matches?(matchable, double)).to eq false + end + end + + context 'when matchable subject does not have warnings' do + it 'does not match' do + expect(described_class.matches?(matchable, double)).to eq false + end + end + end + end +end 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/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb index 19298e261e3..9d806fc524d 100644 --- a/spec/lib/gitlab/email/email_shared_blocks.rb +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -18,7 +18,7 @@ shared_context :email_shared_context do end end -shared_examples :email_shared_examples do +shared_examples :reply_processing_shared_examples do context "when the user could not be found" do before do user.destroy diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index cb3651e3845..08897a4c310 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do include_context :email_shared_context - it_behaves_like :email_shared_examples + it_behaves_like :reply_processing_shared_examples before do stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 48660d1dd1b..cebbeff50cf 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -3,7 +3,7 @@ require_relative '../email_shared_blocks' describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do include_context :email_shared_context - it_behaves_like :email_shared_examples + it_behaves_like :reply_processing_shared_examples before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb new file mode 100644 index 00000000000..a444257754b --- /dev/null +++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do + include_context :email_shared_context + + before do + stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo') + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}+unsubscribe") } + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:noteable) { create(:issue, project: project) } + + let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + + context 'when notification concerns a commit' do + let(:commit) { create(:commit, project: project) } + let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) } + + it 'handler does not raise an error' do + expect { receiver.execute }.not_to raise_error + end + end + + context 'user is unsubscribed' do + it 'leaves user unsubscribed' do + expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false) + end + end + + context 'user is subscribed' do + before do + noteable.subscribe(user) + end + + it 'unsubscribes user from notable' do + expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false) + end + end + + context 'when the noteable could not be found' do + before do + noteable.destroy + end + + it 'raises a NoteableNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context 'when no sent notification for the mail key could be found' do + let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } + + it 'raises a SentNotificationNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) + end + end +end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 1dcf2c0668b..7e951e3fcdd 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do end end + describe 'self.supports_wildcard?' do + context 'address contains the wildard placeholder' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'confirms that wildcard is supported' do + expect(described_class.supports_wildcard?).to be_truthy + end + end + + context "address doesn't contain the wildcard placeholder" do + before do + stub_incoming_email_setting(address: 'replies@example.com') + end + + it 'returns that wildcard is not supported' do + expect(described_class.supports_wildcard?).to be_falsey + end + end + + context 'address is not set' do + before do + stub_incoming_email_setting(address: nil) + end + + it 'returns that wildard is not supported' do + expect(described_class.supports_wildcard?).to be_falsey + end + end + end + + context 'self.unsubscribe_address' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'returns the address with interpolated reply key and unsubscribe suffix' do + expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com') + end + end + context "self.reply_address" do before do stub_incoming_email_setting(address: "replies+%{key}@example.com") diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 14ee386dba6..d94eb52f838 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -178,4 +178,119 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.objects('notes')).not_to include note end end + + # Examples for commit access level test + # + # params: + # * search_phrase + # * commit + # + shared_examples 'access restricted commits' do + context 'when project is internal' do + let(:project) { create(:project, :internal) } + + it 'does not search if user is not authenticated' do + commits = described_class.new(nil, project, search_phrase).objects('commits') + + expect(commits).to be_empty + end + + it 'searches if user is authenticated' do + commits = described_class.new(user, project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + end + + context 'when project is private' do + let!(:creator) { create(:user, username: 'private-project-author') } + let!(:private_project) { create(:project, :private, creator: creator, namespace: creator.namespace) } + let(:team_master) do + user = create(:user, username: 'private-project-master') + private_project.team << [user, :master] + user + end + let(:team_reporter) do + user = create(:user, username: 'private-project-reporter') + private_project.team << [user, :reporter] + user + end + + it 'does not show commit to stranger' do + commits = described_class.new(nil, private_project, search_phrase).objects('commits') + + expect(commits).to be_empty + end + + context 'team access' do + it 'shows commit to creator' do + commits = described_class.new(creator, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'shows commit to master' do + commits = described_class.new(team_master, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'shows commit to reporter' do + commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits') + + expect(commits).to contain_exactly commit + end + end + end + end + + describe 'commit search' do + context 'by commit message' do + let(:project) { create(:project, :public) } + let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') } + let(:message) { 'Sorry, I did a mistake' } + + it 'finds commit by message' do + commits = described_class.new(user, project, message).objects('commits') + + expect(commits).to contain_exactly commit + end + + it 'handles when no commit match' do + commits = described_class.new(user, project, 'not really an existing description').objects('commits') + + expect(commits).to be_empty + end + + it_behaves_like 'access restricted commits' do + let(:search_phrase) { message } + let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') } + end + end + + context 'by commit hash' do + let(:project) { create(:project, :public) } + let(:commit) { project.repository.commit('0b4bc9a') } + commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + commit_hashes.each do |type, commit_hash| + it "shows commit by #{type} hash id" do + commits = described_class.new(user, project, commit_hash).objects('commits') + + expect(commits).to contain_exactly commit + end + end + + it 'handles not existing commit hash correctly' do + commits = described_class.new(user, project, 'deadbeef').objects('commits') + + expect(commits).to be_empty + end + + it_behaves_like 'access restricted commits' do + let(:search_phrase) { '0b4bc9a49' } + let(:commit) { project.repository.commit('0b4bc9a') } + end + end + end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index d3c3b800b94..369e55f61f1 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do end describe 'push to protected branch' do - let(:branch) { create :protected_branch, project: project } + let(:branch) { create :protected_branch, project: project, name: "test" } + let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } it 'returns true if user is a master' do project.team << [user, :master] @@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do expect(access.can_push_to_branch?(branch.name)).to be_falsey end + + it 'returns true if branch does not exist and user has permission to merge' do + project.team << [user, :developer] + + expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy + end end describe 'push to protected branch if allowed for developers' do 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/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index d1aee27057a..2bdd611aeed 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -122,55 +122,80 @@ describe Ci::Pipeline, models: true do end end - describe '#stages' do + describe 'pipeline stages' do before do - create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success') - create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed') - create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running') - create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success') - end - - subject { pipeline.stages } - - context 'stages list' do - it 'returns ordered list of stages' do - expect(subject.map(&:name)).to eq(%w[build test deploy]) + create(:commit_status, pipeline: pipeline, + stage: 'build', + name: 'linux', + stage_idx: 0, + status: 'success') + + create(:commit_status, pipeline: pipeline, + stage: 'build', + name: 'mac', + stage_idx: 0, + status: 'failed') + + create(:commit_status, pipeline: pipeline, + stage: 'deploy', + name: 'staging', + stage_idx: 2, + status: 'running') + + create(:commit_status, pipeline: pipeline, + stage: 'test', + name: 'rspec', + stage_idx: 1, + status: 'success') + end + + describe '#stages' do + subject { pipeline.stages } + + context 'stages list' do + it 'returns ordered list of stages' do + expect(subject.map(&:name)).to eq(%w[build test deploy]) + end end - end - it 'returns a valid number of stages' do - expect(pipeline.stages_count).to eq(3) - end + context 'stages with statuses' do + let(:statuses) do + subject.map { |stage| [stage.name, stage.status] } + end - it 'returns a valid names of stages' do - expect(pipeline.stages_name).to eq(['build', 'test', 'deploy']) - end + it 'returns list of stages with correct statuses' do + expect(statuses).to eq([['build', 'failed'], + ['test', 'success'], + ['deploy', 'running']]) + end - context 'stages with statuses' do - let(:statuses) do - subject.map do |stage| - [stage.name, stage.status] + context 'when commit status is retried' do + before do + create(:commit_status, pipeline: pipeline, + stage: 'build', + name: 'mac', + stage_idx: 0, + status: 'success') + end + + it 'ignores the previous state' do + expect(statuses).to eq([['build', 'success'], + ['test', 'success'], + ['deploy', 'running']]) + end end end + end - it 'returns list of stages with statuses' do - expect(statuses).to eq([['build', 'failed'], - ['test', 'success'], - ['deploy', 'running'] - ]) + describe '#stages_count' do + it 'returns a valid number of stages' do + expect(pipeline.stages_count).to eq(3) end + end - context 'when build is retried' do - before do - create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success') - end - - it 'ignores the previous state' do - expect(statuses).to eq([['build', 'success'], - ['test', 'success'], - ['deploy', 'running'] - ]) - end + describe '#stages_name' do + it 'returns a valid names of stages' do + expect(pipeline.stages_name).to eq(['build', 'test', 'deploy']) end end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 742bedb37e4..c4a9743a4e2 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -142,6 +142,78 @@ describe Ci::Stage, models: true do end end + describe '#success?' do + context 'when stage is successful' do + before do + create_job(:ci_build, status: :success) + create_job(:generic_commit_status, status: :success) + end + + it 'is successful' do + expect(stage).to be_success + end + end + + context 'when stage is not successful' do + before do + create_job(:ci_build, status: :failed) + create_job(:generic_commit_status, status: :success) + end + + it 'is not successful' do + expect(stage).not_to be_success + end + end + end + + describe '#has_warnings?' do + context 'when stage has warnings' do + context 'when using memoized warnings flag' do + context 'when there are warnings' do + let(:stage) { build(:ci_stage, warnings: true) } + + it 'has memoized warnings' do + expect(stage).not_to receive(:statuses) + expect(stage).to have_warnings + end + end + + context 'when there are no warnings' do + let(:stage) { build(:ci_stage, warnings: false) } + + it 'has memoized warnings' do + expect(stage).not_to receive(:statuses) + expect(stage).not_to have_warnings + end + end + end + + context 'when calculating warnings from statuses' do + before do + create(:ci_build, :failed, :allowed_to_fail, + stage: stage_name, pipeline: pipeline) + end + + it 'has warnings calculated from statuses' do + expect(stage).to receive(:statuses).and_call_original + expect(stage).to have_warnings + end + end + end + + context 'when stage does not have warnings' do + before do + create(:ci_build, :success, stage: stage_name, + pipeline: pipeline) + end + + it 'does not have warnings calculated from statuses' do + expect(stage).to receive(:statuses).and_call_original + expect(stage).not_to have_warnings + end + end + end + def create_job(type, status: 'success', stage: stage_name) create(type, pipeline: pipeline, stage: stage, status: status) end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0d425ab7fd4..b2202f0fd44 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -351,4 +351,22 @@ eos expect(commit).not_to be_work_in_progress end end + + describe '.valid_hash?' do + it 'checks hash contents' do + expect(described_class.valid_hash?('abcdef01239ABCDEF')).to be true + expect(described_class.valid_hash?("abcdef01239ABCD\nEF")).to be false + expect(described_class.valid_hash?(' abcdef01239ABCDEF ')).to be false + expect(described_class.valid_hash?('Gabcdef01239ABCDEF')).to be false + expect(described_class.valid_hash?('gabcdef01239ABCDEF')).to be false + expect(described_class.valid_hash?('-abcdef01239ABCDEF')).to be false + end + + it 'checks hash length' do + expect(described_class.valid_hash?('a' * 6)).to be false + expect(described_class.valid_hash?('a' * 7)).to be true + expect(described_class.valid_hash?('a' * 40)).to be true + expect(described_class.valid_hash?('a' * 41)).to be false + end + end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 4d0f51fe82a..dbfe3cd2d36 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -219,4 +219,10 @@ describe HasStatus do end end end + + describe '::DEFAULT_STATUS' do + it 'is a status created' do + expect(described_class::DEFAULT_STATUS).to eq 'created' + end + end end 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/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index b556135532f..30443534cca 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -68,4 +68,14 @@ describe Group, 'Routable' do end end end + + describe '.member_descendants' do + let!(:user) { create(:user) } + let!(:nested_group) { create(:group, parent: group) } + + before { group.add_owner(user) } + subject { described_class.member_descendants(user.id) } + + it { is_expected.to eq([nested_group]) } + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 45fe927202b..63ffdf70958 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -269,6 +269,12 @@ describe Group, models: true do it 'returns the canonical URL' do expect(group.web_url).to include("groups/#{group.name}") end + + context 'nested group' do + let(:nested_group) { create(:group, :nested) } + + it { expect(nested_group.web_url).to include("groups/#{nested_group.full_path}") } + end end describe 'nested group' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 600538ff5f4..8d613a88ca0 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -5,6 +5,8 @@ describe Namespace, models: true do it { is_expected.to have_many :projects } it { is_expected.to have_many :project_statistics } + it { is_expected.to belong_to :parent } + it { is_expected.to have_many :children } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } @@ -117,6 +119,7 @@ describe Namespace, models: true do new_path = @namespace.path + "_new" allow(@namespace).to receive(:path_was).and_return(@namespace.path) allow(@namespace).to receive(:path).and_return(new_path) + expect(@namespace).to receive(:remove_exports!) expect(@namespace.move_dir).to be_truthy end @@ -139,11 +142,17 @@ describe Namespace, models: true do let!(:project) { create(:project, namespace: namespace) } let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } - before { namespace.destroy } - it "removes its dirs when deleted" do + namespace.destroy + expect(File.exist?(path)).to be(false) end + + it 'removes the exports folder' do + expect(namespace).to receive(:remove_exports!) + + namespace.destroy + end end describe '.find_by_path_or_name' do @@ -182,17 +191,31 @@ describe Namespace, models: true do it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } end - describe '#parents' do + describe '#ancestors' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } let(:deep_nested_group) { create(:group, parent: nested_group) } let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - it 'returns the correct parents' do - expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) - expect(deep_nested_group.parents).to eq([group, nested_group]) - expect(nested_group.parents).to eq([group]) - expect(group.parents).to eq([]) + it 'returns the correct ancestors' do + expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.ancestors).to eq([group, nested_group]) + expect(nested_group.ancestors).to eq([group]) + expect(group.ancestors).to eq([]) + end + end + + describe '#descendants' do + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct descendants' do + expect(very_deep_nested_group.descendants.to_a).to eq([]) + expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group]) + expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group]) + expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group]) end end end 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/route_spec.rb b/spec/models/route_spec.rb index 8481a9bef16..dd2a5109abc 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -14,7 +14,7 @@ describe Route, models: true do it { is_expected.to validate_uniqueness_of(:path) } end - describe '#rename_children' do + describe '#rename_descendants' do let!(:nested_group) { create(:group, path: "test", parent: group) } let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) } let!(:similar_group) { create(:group, path: 'gitlab-org') } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ca3d4ff0aa9..0adfc30fe58 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 @@ -1350,6 +1363,39 @@ describe User, models: true do end end + describe '#nested_groups' do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + + before do + group.add_owner(user) + + # Add more data to ensure method does not include wrong groups + create(:group).add_owner(create(:user)) + end + + it { expect(user.nested_groups).to eq([nested_group]) } + end + + describe '#nested_projects' do + let!(:user) { create(:user) } + let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:project) { create(:project, namespace: group) } + let!(:nested_project) { create(:project, namespace: nested_group) } + + before do + group.add_owner(user) + + # Add more data to ensure method does not include wrong projects + other_project = create(:project, namespace: create(:group, :nested)) + other_project.add_developer(create(:user)) + end + + it { expect(user.nested_projects).to eq([nested_project]) } + end + describe '#refresh_authorized_projects', redis: true do let(:project1) { create(:empty_project) } let(:project2) { create(:empty_project) } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 5c14db067a8..766234d7104 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -73,19 +73,14 @@ describe API::DeployKeys, api: true do post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } expect(response).to have_http_status(400) - expect(json_response['message']['key']).to eq([ - 'can\'t be blank', - 'is invalid' - ]) + expect(json_response['error']).to eq('key is missing') end it 'should not create a key without title' do post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' expect(response).to have_http_status(400) - expect(json_response['message']['title']).to eq([ - 'can\'t be blank' - ]) + expect(json_response['error']).to eq('title is missing') end it 'should create new ssh key' do 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/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 6f20ac49269..71a7994e544 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -627,6 +627,17 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(issue.title) expect(json_response.first['id']).to eq(issue.id) end + + it 'returns 403 if the user has no access to the merge request' do + project = create(:empty_project, :private) + merge_request = create(:merge_request, :simple, source_project: project) + guest = create(:user) + project.team << [guest, :guest] + + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) + + expect(response).to have_http_status(403) + end end describe 'POST :id/merge_requests/:merge_request_id/subscription' do @@ -648,6 +659,15 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end end describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do @@ -669,6 +689,15 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end end describe 'Time tracking' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0f8d054b31e..0353ebea9e5 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -264,6 +264,18 @@ describe API::Notes, api: true do end end + context 'when user does not have access to read the noteable' do + it 'responds with 404' do + project = create(:empty_project, :private) { |p| p.add_guest(user) } + issue = create(:issue, :confidential, project: project) + + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'Foo' + + expect(response).to have_http_status(404) + end + end + context 'when user does not have access to create noteable' do let(:private_issue) { create(:issue, project: create(:empty_project, :private)) } diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 39c9e0505d1..776dc655650 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -6,7 +6,7 @@ describe API::Services, api: true do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:user2) { create(:user) } - let(:project) {create(:empty_project, creator_id: user.id, namespace: user.namespace) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } Service.available_services_names.each do |service| describe "PUT /projects/:id/services/#{service.dasherize}" do @@ -16,6 +16,15 @@ describe API::Services, api: true do put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs expect(response).to have_http_status(200) + + current_service = project.services.first + event = current_service.event_names.empty? ? "foo" : current_service.event_names.first + state = current_service[event] || false + + put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), service_attrs + + expect(response).to have_http_status(200) + expect(project.services.first[event]).not_to eq(state) unless event == "foo" end it "returns if required fields missing" do diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 6fe695626ca..56dc017ce54 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -183,12 +183,25 @@ describe API::Todos, api: true do expect(response.status).to eq(404) end + + it 'returns an error if the issuable is not accessible' do + guest = create(:user) + project_1.team << [guest, :guest] + + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest) + + if issuable_type == 'merge_requests' + expect(response).to have_http_status(403) + else + expect(response).to have_http_status(404) + end + end end describe 'POST :id/issuable_type/:issueable_id/todo' do context 'for an issue' do it_behaves_like 'an issuable', 'issues' do - let(:issuable) { create(:issue, author: author_1, project: project_1) } + let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) } end end diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 72978846e93..28b485e4b15 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -13,7 +13,12 @@ describe 'cycle analytics events' do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) - 3.times { create_cycle } + 3.times do |count| + Timecop.freeze(Time.now + count.days) do + create_cycle + end + end + deploy_master login_as(user) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 00d0e20f47c..314ea670a71 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -106,23 +106,46 @@ describe MergeRequests::RefreshService, services: true do context 'push to fork repo source branch' do let(:refresh_service) { service.new(@fork_project, @user) } - before do - allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs - end - it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update', @oldrev) + context 'open fork merge request' do + before do + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it 'executes hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@fork_merge_request, 'update', @oldrev) + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } + it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + context 'closed fork merge request' do + before do + @fork_merge_request.close! + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it 'do not execute hooks with update action' do + expect(refresh_service).not_to have_received(:execute_hooks) + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@fork_merge_request).to be_closed } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } + end end context 'push to fork repo target branch' do 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' diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index 49867aa5cc4..a3724b801b3 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do end shared_examples 'an unsubscribeable thread' do + it_behaves_like 'an unsubscribeable thread with incoming address without %{key}' + + it 'has a List-Unsubscribe header in the correct format' do + is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ + is_expected.to have_header 'List-Unsubscribe', /mailto/ + is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/ + end + + it { is_expected.to have_body_text /unsubscribe/ } +end + +shared_examples 'an unsubscribeable thread with incoming address without %{key}' do + include_context 'reply-by-email is enabled with incoming address without %{key}' + it 'has a List-Unsubscribe header in the correct format' do is_expected.to have_header 'List-Unsubscribe', /unsubscribe/ - is_expected.to have_header 'List-Unsubscribe', /^<.+>$/ + is_expected.not_to have_header 'List-Unsubscribe', /mailto/ + is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/ end it { is_expected.to have_body_text /unsubscribe/ } diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index ad1c783df4d..1b6c33248c9 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -33,6 +33,30 @@ shared_examples 'a Taskable' do end end + describe 'with nested tasks' do + before do + subject.description = <<-EOT.strip_heredoc + - [ ] Task a + - [x] Task a.1 + - [ ] Task a.2 + - [ ] Task b + + 1. [ ] Task 1 + 1. [ ] Task 1.1 + 1. [ ] Task 1.2 + 1. [x] Task 2 + 1. [x] Task 2.1 + EOT + end + + it 'returns the correct task status' do + expect(subject.task_status).to match('3 of') + expect(subject.task_status).to match('9 tasks completed') + expect(subject.task_status_short).to match('3/') + expect(subject.task_status_short).to match('9 tasks') + end + end + describe 'with an incomplete task' do before do subject.description = <<-EOT.strip_heredoc |