diff options
author | Phil Hughes <me@iamphill.com> | 2017-10-30 11:26:25 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-10-30 11:26:25 +0000 |
commit | 4b80cde2c803ead14ca34fc71991b4a7b458ab00 (patch) | |
tree | ea2af9942b23991c73f5f3dea31952b7382a0cc2 | |
parent | 47c906eb57d95eacd336aa89541f8a7071b746bf (diff) | |
parent | 6919f6361c3cfa87a38cd7eb391ade70823c9106 (diff) | |
download | gitlab-ce-4b80cde2c803ead14ca34fc71991b4a7b458ab00.tar.gz |
Merge branch 'master' into multi-file-editor-vuex
132 files changed, 2018 insertions, 524 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83e41a11e52..9b2ee157193 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ - [Workflow labels](#workflow-labels) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) + - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) -- [Implement design & UI elements](#implement-design--ui-elements) +- [Implement design & UI elements](#implement-design-ui-elements) - [Issue tracker](#issue-tracker) - [Issue triaging](#issue-triaging) - [Feature proposals](#feature-proposals) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 4d2d4db7c0e..0f28bd233ac 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ +/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */ + import AccessorUtilities from './lib/utils/accessor'; -window.Autosave = (function() { - function Autosave(field, key, resource) { +export default class Autosave { + constructor(field, key, resource) { this.field = field; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.resource = resource; @@ -12,14 +13,10 @@ window.Autosave = (function() { this.key = 'autosave/' + key; this.field.data('autosave', this); this.restore(); - this.field.on('input', (function(_this) { - return function() { - return _this.save(); - }; - })(this)); + this.field.on('input', () => this.save()); } - Autosave.prototype.restore = function() { + restore() { var text; if (!this.isLocalStorageAvailable) return; @@ -40,9 +37,9 @@ window.Autosave = (function() { field.dispatchEvent(event); } } - }; + } - Autosave.prototype.save = function() { + save() { var text; text = this.field.val(); @@ -51,15 +48,11 @@ window.Autosave = (function() { } return this.reset(); - }; + } - Autosave.prototype.reset = function() { + reset() { if (!this.isLocalStorageAvailable) return; return window.localStorage.removeItem(this.key); - }; - - return Autosave; -})(); - -export default window.Autosave; + } +} diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 3f083655f95..184665f395c 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit; + this.cantEdit = cantEdit.filter(i => typeof i === 'string'); + this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object'); } updateObject(path) { @@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { this.filteredSearchInput.dispatchEvent(new Event('input')); } - canEdit(tokenName) { - return this.cantEdit.indexOf(tokenName) === -1; + canEdit(tokenName, tokenValue) { + if (this.cantEdit.includes(tokenName)) return false; + return this.cantEditWithValue.findIndex(token => token.name === tokenName && + token.value === tokenValue) === -1; } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index ea82958e80d..798d7e0d147 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = { }, state: {}, detail: { - issue: {} + issue: {}, }, moving: { issue: {}, - list: {} + list: {}, }, create () { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); - this.detail = { issue: {} }; + this.detail = { + issue: {}, + }; }, addList (listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f20162c48e9..970e83c0ecb 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -13,7 +13,7 @@ import GroupLabelSubscription from './group_label_subscription'; /* global LineHighlighter */ import BuildArtifacts from './build_artifacts'; import CILintEditor from './ci_lint_editor'; -/* global GroupsSelect */ +import groupsSelect from './groups_select'; /* global Search */ /* global Admin */ /* global NamespaceSelects */ @@ -414,7 +414,7 @@ import Diff from './diff'; break; case 'projects:project_members:index': memberExpirationDate('.js-access-expiration-date-groups'); - new GroupsSelect(); + groupsSelect(); memberExpirationDate(); new Members(); new UsersSelect(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 8d711e3213c..cf8a9b0402b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -147,6 +147,16 @@ class DropdownUtils { return dataValue !== null; } + static getVisualTokenValues(visualToken) { + const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); + let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); + if (tokenName === 'label' && tokenValue) { + // remove leading symbol and wrapping quotes + tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); + } + return { tokenName, tokenValue }; + } + // Determines the full search query (visual tokens + input) static getSearchQuery(untilInput = false) { const container = FilteredSearchContainer.container; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 7b233842d5a..69c57f923b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -185,8 +185,8 @@ class FilteredSearchManager { if (e.keyCode === 8 || e.keyCode === 46) { const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); - const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); + const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial(); @@ -336,8 +336,8 @@ class FilteredSearchManager { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { - const tokenKey = t.querySelector('.name').textContent.trim(); - canClearToken = this.canEdit && this.canEdit(tokenKey); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t); + canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue); } if (canClearToken) { @@ -469,7 +469,7 @@ class FilteredSearchManager { } hasFilteredSearch = true; - const canEdit = this.canEdit && this.canEdit(sanitizedKey); + const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); gl.FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index d2f92929b8a..6139e81fe6d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -38,21 +38,14 @@ class FilteredSearchVisualTokens { } static createVisualTokenElementHTML(canEdit = true) { - let removeTokenMarkup = ''; - if (canEdit) { - removeTokenMarkup = ` - <div class="remove-token" role="button"> - <i class="fa fa-close"></i> - </div> - `; - } - return ` - <div class="selectable" role="button"> + <div class="${canEdit ? 'selectable' : 'hidden'}" role="button"> <div class="name"></div> <div class="value-container"> <div class="value"></div> - ${removeTokenMarkup} + <div class="remove-token" role="button"> + <i class="fa fa-close"></i> + </div> </div> </div> `; diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 90ca70289ab..a69a0bde17b 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,121 +1,86 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, - camelcase, one-var-declaration-per-line, quotes, object-shorthand, - prefer-arrow-callback, comma-dangle, consistent-return, yoda, - prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, - promise/catch-or-return */ import Api from './api'; import { normalizeCRLFHeaders } from './lib/utils/common_utils'; -var slice = [].slice; +export default function groupsSelect() { + // Needs to be accessible in rspec + window.GROUP_SELECT_PER_PAGE = 20; + $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { + const $select = $(this); + const allAvailable = $select.data('all-available'); + const skipGroups = $select.data('skip-groups') || []; + $select.select2({ + placeholder: 'Search for a group', + multiple: $select.hasClass('multiselect'), + minimumInputLength: 0, + ajax: { + url: Api.buildUrl(Api.groupsPath), + dataType: 'json', + quietMillis: 250, + transport(params) { + return $.ajax(params) + .then((data, status, xhr) => { + const results = data || []; -window.GroupsSelect = (function() { - function GroupsSelect() { - $('.ajax-groups-select').each((function(_this) { - const self = _this; - - return function(i, select) { - var all_available, skip_groups; - const $select = $(select); - all_available = $select.data('all-available'); - skip_groups = $select.data('skip-groups') || []; - - $select.select2({ - placeholder: "Search for a group", - multiple: $select.hasClass('multiselect'), - minimumInputLength: 0, - ajax: { - url: Api.buildUrl(Api.groupsPath), - dataType: 'json', - quietMillis: 250, - transport: function (params) { - $.ajax(params).then((data, status, xhr) => { - const results = data || []; - - const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); - const currentPage = parseInt(headers['X-PAGE'], 10) || 0; - const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; - const more = currentPage < totalPages; - - return { - results, - pagination: { - more, - }, - }; - }).then(params.success).fail(params.error); - }, - data: function (search, page) { - return { - search, - page, - per_page: GroupsSelect.PER_PAGE, - all_available, - }; - }, - results: function (data, page) { - if (data.length) return { results: [] }; - - const groups = data.length ? data : data.results || []; - const more = data.pagination ? data.pagination.more : false; - const results = groups.filter(group => skip_groups.indexOf(group.id) === -1); + const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; return { results, - page, - more, + pagination: { + more, + }, }; - }, - }, - initSelection: function(element, callback) { - var id; - id = $(element).val(); - if (id !== "") { - return Api.group(id, callback); - } - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return self.formatResult.apply(self, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return self.formatSelection.apply(self, args); - }, - dropdownCssClass: "ajax-groups-dropdown select2-infinite", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; - } - }); - - self.dropdown = document.querySelector('.select2-infinite .select2-results'); - - $select.on('select2-loaded', self.forceOverflow.bind(self)); - }; - })(this)); - } - - GroupsSelect.prototype.formatResult = function(group) { - var avatar; - if (group.avatar_url) { - avatar = group.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>"; - }; - - GroupsSelect.prototype.formatSelection = function(group) { - return group.full_name; - }; + }) + .then(params.success) + .fail(params.error); + }, + data(search, page) { + return { + search, + page, + per_page: window.GROUP_SELECT_PER_PAGE, + all_available: allAvailable, + }; + }, + results(data, page) { + if (data.length) return { results: [] }; - GroupsSelect.prototype.forceOverflow = function (e) { - this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; - }; + const groups = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); - GroupsSelect.PER_PAGE = 20; + return { + results, + page, + more, + }; + }, + }, + // eslint-disable-next-line consistent-return + initSelection(element, callback) { + const id = $(element).val(); + if (id !== '') { + return Api.group(id, callback); + } + }, + formatResult(object) { + return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; + }, + formatSelection(object) { + return object.full_name; + }, + dropdownCssClass: 'ajax-groups-dropdown select2-infinite', + // we do not want to escape markup since we are displaying html in results + escapeMarkup(m) { + return m; + }, + }); - return GroupsSelect; -})(); + $select.on('select2-loaded', () => { + const dropdown = document.querySelector('.select2-infinite .select2-results'); + dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; + }); + }); +} diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index cd2562bc6a9..26dd946984d 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,8 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ -/* global Autosave */ import Pikaday from 'pikaday'; +import Autosave from './autosave'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 84602cf9207..1e52963b1dd 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label'; (function() { this.LabelsSelect = (function() { - function LabelsSelect(els) { + function LabelsSelect(els, options = {}) { var _this, $els; _this = this; @@ -58,6 +58,7 @@ import CreateLabelDropdown from './create_label'; labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); labelNoneHTMLTemplate = '<span class="no-value">None</span>'; } + const handleClick = options.handleClick; $sidebarLabelTooltip.tooltip(); @@ -316,9 +317,9 @@ import CreateLabelDropdown from './create_label'; }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const label = options.selectedObj; + clicked: function(clickEvent) { + const { $el, e, isMarking } = clickEvent; + const label = clickEvent.selectedObj; var isIssueIndex, isMRIndex, page, boardsModel; var fadeOutLoader = () => { @@ -391,6 +392,10 @@ import CreateLabelDropdown from './create_label'; .then(fadeOutLoader) .catch(fadeOutLoader); } + else if (handleClick) { + e.preventDefault(); + handleClick(label); + } else { if ($dropdown.hasClass('js-multiselect')) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 52715fba43f..7f045338417 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -41,7 +41,6 @@ import './behaviors/'; import './activities'; import './admin'; import './aside'; -import './autosave'; import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; import './commits'; @@ -55,7 +54,6 @@ import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; import './gl_form'; -import './groups_select'; import './header'; import './importer_status'; import './issuable_index'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index e7d5325a509..74e5a4f1cea 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,7 +5,7 @@ import _ from 'underscore'; (function() { this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject, els) { + function MilestoneSelect(currentProject, els, options = {}) { var _this, $els; if (currentProject != null) { _this = this; @@ -136,19 +136,26 @@ import _ from 'underscore'; }, opened: function(e) { const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar')) { + if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; } $('a.is-active', $el).removeClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e } = options; - let selected = options.selectedObj; + clicked: function(clickEvent) { + const { $el, e } = clickEvent; + let selected = clickEvent.selectedObj; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; if (!selected) return; + + if (options.handleClick) { + e.preventDefault(); + options.handleClick(selected); + return; + } + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 5a6868be444..ab101a56db8 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -5,7 +5,7 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, class-methods-use-this */ -/* global Autosave */ + /* global ResolveService */ /* global mrRefreshWidgetUrl */ @@ -20,7 +20,7 @@ import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; -import './autosave'; +import Autosave from './autosave'; import TaskList from './task_list'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 2ce52e4538a..ad384a1cc36 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,10 +1,9 @@ <script> - /* global Autosave */ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; import autosize from 'vendor/autosize'; import Flash from '../../flash'; - import '../../autosave'; + import Autosave from '../../autosave'; import TaskList from '../../task_list'; import * as constants from '../constants'; import eventHub from '../event_hub'; diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index baf43190d9e..0f13221b81e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -9,8 +9,8 @@ import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteForm from './issue_note_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; export default { diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index aecd1f957e5..5c9119644e3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -5,10 +5,10 @@ import * as constants from '../constants'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; - import issueSystemNote from './issue_system_note.vue'; + import systemNote from '../../vue_shared/components/notes/system_note.vue'; import issueCommentForm from './issue_comment_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { @@ -37,7 +37,7 @@ components: { issueNote, issueDiscussion, - issueSystemNote, + systemNote, issueCommentForm, loadingIcon, placeholderNote, @@ -68,7 +68,7 @@ } return placeholderNote; } else if (note.individual_note) { - return note.notes[0].system ? issueSystemNote : issueNote; + return note.notes[0].system ? systemNote : issueNote; } return issueDiscussion; diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 5843b97f225..a008171beda 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -1,5 +1,4 @@ -/* globals Autosave */ -import '../../autosave'; +import Autosave from '../../autosave'; export default { methods: { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index a0883b32593..759cc9925f4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -6,7 +6,7 @@ import _ from 'underscore'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; -function UsersSelect(currentUser, els) { +function UsersSelect(currentUser, els, options = {}) { var $els; this.users = this.users.bind(this); this.user = this.user.bind(this); @@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) { } } + const { handleClick } = options; + $els = $(els); if (!els) { @@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) { } if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if (handleClick) { + e.preventDefault(); + handleClick(user, isMarking); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { return Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index b8a96b23012..be37dd87de9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -286,6 +286,7 @@ export default { <input id="remove-source-branch-input" v-model="removeSourceBranch" + class="js-remove-source-branch-checkbox" :disabled="isRemoveSourceBranchButtonDisabled" type="checkbox"/> Remove source branch </label> @@ -311,8 +312,8 @@ export default { </button> </template> <template v-else> - <span class="bold"> - The pipeline for this merge request has not succeeded yet + <span class="bold js-resolve-mr-widget-items-message"> + You can only merge once the items above are resolved </span> </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 15581d5c2a0..494fe4468d9 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -18,6 +18,12 @@ required: false, default: false, }, + + class: { + type: String, + required: false, + default: '', + }, }, computed: { @@ -25,7 +31,7 @@ return this.inline ? 'span' : 'div'; }, cssClass() { - return `fa-${this.size}x`; + return `fa-${this.size}x ${this.class}`.trim(); }, }, }; diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 6921d91372f..e467ca56704 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -1,9 +1,26 @@ <script> + /** + * Common component to render a placeholder note and user information. + * + * This component needs to be used with a vuex store. + * That vuex store needs to have a `getUserData` getter that contains + * { + * path: String, + * avatar_url: String, + * name: String, + * username: String, + * } + * + * @example + * <placeholder-note + * :note="{body: 'This is a note'}" + * /> + */ import { mapGetters } from 'vuex'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { - name: 'issuePlaceholderNote', + name: 'placeholderNote', props: { note: { type: Object, diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index 80a8ef56a83..d805fea8006 100644 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -1,4 +1,12 @@ <script> + /** + * Common component to render a placeholder system note. + * + * @example + * <placeholder-system-note + * :note="{ body: 'Commands are being applied'}" + * /> + */ export default { name: 'placeholderSystemNote', props: { diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 0cfb6522e77..98f8f32557d 100644 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -1,6 +1,24 @@ <script> + /** + * Common component to render a system note, icon and user information. + * + * This component needs to be used with a vuex store. + * That vuex store needs to have a `targetNoteHash` getter + * + * @example + * <system-note + * :note="{ + * id: String, + * author: Object, + * createdAt: String, + * note_html: String, + * system_note_icon_name: String + * }" + * /> + */ import { mapGetters } from 'vuex'; - import issueNoteHeader from './issue_note_header.vue'; + import issueNoteHeader from '../../../notes/components/issue_note_header.vue'; + import { spriteIcon } from '../../../lib/utils/common_utils'; export default { name: 'systemNote', @@ -24,7 +42,7 @@ return this.targetNoteHash === this.noteAnchorId; }, iconHtml() { - return gl.utils.spriteIcon(this.note.system_note_icon_name); + return spriteIcon(this.note.system_note_icon_name); }, }, }; @@ -46,7 +64,8 @@ :author="note.author" :created-at="note.created_at" :note-id="note.id" - :action-text-html="note.note_html" /> + :action-text-html="note.note_html" + /> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 9e8c10bdc1a..fc6421fecb9 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -5,17 +5,27 @@ export default { props: { title: { type: String, - required: true, + required: false, }, text: { type: String, required: false, }, + hideFooter: { + type: Boolean, + required: false, + default: false, + }, kind: { type: String, required: false, default: 'primary', }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, closeKind: { type: String, required: false, @@ -30,6 +40,11 @@ export default { type: String, required: true, }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, }, computed: { @@ -57,43 +72,57 @@ export default { </script> <template> -<div - class="modal popup-dialog" - role="dialog" - tabindex="-1"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" - class="close" - @click="close" - aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - <h4 class="modal-title">{{this.title}}</h4> - </div> - <div class="modal-body"> - <slot name="body" :text="text"> - <p>{{text}}</p> - </slot> - </div> - <div class="modal-footer"> - <button - type="button" - class="btn" - :class="btnCancelKindClass" - @click="close"> - {{ closeButtonLabel }} - </button> - <button - type="button" - class="btn" - :class="btnKindClass" - @click="emitSubmit(true)"> - {{ primaryButtonLabel }} - </button> +<div class="modal-open"> + <div + class="modal popup-dialog" + role="dialog" + tabindex="-1" + > + <div + :class="modalDialogClass" + class="modal-dialog" + role="document" + > + <div class="modal-content"> + <div class="modal-header"> + <slot name="header"> + <h4 class="modal-title pull-left"> + {{this.title}} + </h4> + <button + type="button" + class="close pull-right" + @click="close" + aria-label="Close" + > + <span aria-hidden="true">×</span> + </button> + </slot> + </div> + <div class="modal-body"> + <slot name="body" :text="text"> + <p>{{this.text}}</p> + </slot> + </div> + <div class="modal-footer" v-if="!hideFooter"> + <button + type="button" + class="btn pull-left" + :class="btnCancelKindClass" + @click="close"> + {{ closeButtonLabel }} + </button> + <button + type="button" + class="btn pull-right" + :class="btnKindClass" + @click="emitSubmit(true)"> + {{ primaryButtonLabel }} + </button> + </div> </div> </div> </div> + <div class="modal-backdrop fade in" /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 95898d54cf7..dc32e783258 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -12,12 +12,14 @@ :img-alt="tooltipText" :img-size="20" :tooltip-text="tooltipText" - tooltip-placement="top" + :tooltip-placement="top" + :username="username" /> */ import userAvatarImage from './user_avatar_image.vue'; +import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarLink', @@ -60,6 +62,22 @@ export default { required: false, default: 'top', }, + username: { + type: String, + required: false, + default: '', + }, + }, + computed: { + shouldShowUsername() { + return this.username.length > 0; + }, + avatarTooltipText() { + return this.shouldShowUsername ? '' : this.tooltipText; + }, + }, + directives: { + tooltip, }, }; </script> @@ -73,8 +91,13 @@ export default { :img-alt="imgAlt" :css-classes="imgCssClasses" :size="imgSize" - :tooltip-text="tooltipText" + :tooltip-text="avatarTooltipText" + :tooltip-placement="tooltipPlacement" + /><span + v-if="shouldShowUsername" + v-tooltip + :title="tooltipText" :tooltip-placement="tooltipPlacement" - /> + >{{username}}</span> </a> </template> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 96f9dda26c4..1cfd7ef01a8 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -4,6 +4,9 @@ .cred { color: $common-red; } .cgreen { color: $common-green; } .cdark { color: $common-gray-dark; } +.text-secondary { + color: $gl-text-color-secondary; +} /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a9d804e735d..63697fd38a7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -37,6 +37,7 @@ .dropdown-menu-nav { @include set-visible; display: block; + min-height: 40px; @media (max-width: $screen-xs-max) { width: 100%; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b2847c348eb..0d80a85d521 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -65,7 +65,7 @@ display: flex; flex: 1; -webkit-flex: 1; - padding-left: 30px; + padding-left: 12px; position: relative; margin-bottom: 0; } @@ -221,10 +221,6 @@ box-shadow: 0 0 4px $search-input-focus-shadow-color; } - &.focus .fa-filter { - color: $common-gray-dark; - } - gl-emoji { display: inline-block; font-family: inherit; @@ -251,13 +247,6 @@ } } - .fa-filter { - position: absolute; - top: 10px; - left: 10px; - color: $gray-darkest; - } - .fa-times { right: 10px; color: $gray-darkest; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 1cebd02df48..d218fb6d702 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -42,3 +42,11 @@ body.modal-open { width: 98%; } } + +.modal.popup-dialog { + display: block; +} + +.modal-body { + background-color: $modal-body-bg; +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 3ea77eb7a43..a23131e0818 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -164,3 +164,36 @@ $pre-border-color: $border-color; $table-bg-accent: $gray-light; $zindex-popover: 900; + +//== Modals +// +//## + +//** Padding applied to the modal body +$modal-inner-padding: $gl-padding; + +//** Padding applied to the modal title +$modal-title-padding: $gl-padding; +//** Modal title line-height +// $modal-title-line-height: $line-height-base + +//** Background color of modal content area +$modal-content-bg: $gray-light; +$modal-body-bg: $white-light; +//** Modal content border color +// $modal-content-border-color: rgba(0,0,0,.2) +//** Modal content border color **for IE8** +// $modal-content-fallback-border-color: #999 + +//** Modal backdrop background color +// $modal-backdrop-bg: #000 +//** Modal backdrop opacity +// $modal-backdrop-opacity: .5 +//** Modal header border color +// $modal-header-border-color: #e5e5e5 +//** Modal footer border color +// $modal-footer-border-color: $modal-header-border-color + +// $modal-lg: 900px +// $modal-md: 600px +// $modal-sm: 300px diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 7c5f3f1126a..019f3a2ec45 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,16 +1,3 @@ -.modal.popup-dialog { - display: block; - background-color: $black-transparent; - z-index: 2100; - - @media (min-width: $screen-md-min) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - } -} - .project-refs-form, .project-refs-target-form { display: inline-block; diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 80ab681ed87..bc0948cd3fb 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController users_almost_there_path end - def after_confirmation_path_for(_resource_name, resource) + def after_confirmation_path_for(resource_name, resource) # incoming resource can either be a :user or an :email if signed_in?(:user) after_sign_in(resource) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 572915a4930..38f379dbf4f 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -57,6 +57,10 @@ class HelpController < ApplicationController def shortcuts end + def instance_configuration + @instance_configuration = InstanceConfiguration.new + end + def ui @user = User.new(id: 0, name: 'John Doe', username: '@johndoe') end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 7f03ce07dec..f28df83d5a5 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html do @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) + @merged_branch_names = + repository.merged_branch_names(@branches.map(&:name)) # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 Gitlab::GitalyClient.allow_n_plus_1_calls do @max_commits = @branches.reduce(0) do |memo, branch| diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b7a108a0ebd..fe1334c0cfe 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_action :authorize_update_issue!, only: [:update, :move] + before_action :authorize_update_issue!, only: [:edit, :update, :move] # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController respond_with(@issue) end + def edit + respond_with(@issue) + end + def show @noteable = @issue @note = @project.notes.new(noteable: @issue) @@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) respond_to do |format| + format.html do + recaptcha_check_with_fallback { render :edit } + end + format.json do render_issue_json end diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 533076585c0..852eac3647d 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -23,7 +23,7 @@ class BranchesFinder def filter_by_name(branches) if search - branches.select { |branch| branch.name.include?(search) } + branches.select { |branch| branch.name.upcase.include?(search.upcase) } else branches end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 7112c6ee470..c4a621160af 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -20,17 +20,6 @@ module BoardsHelper project_issues_path(@project) end - def current_board_json - board = @board || @boards.first - - board.to_json( - only: [:id, :name, :milestone_id], - include: { - milestone: { only: [:title] } - } - ) - end - def board_base_url project_boards_path(@project) end diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb new file mode 100644 index 00000000000..cee319f20bc --- /dev/null +++ b/app/helpers/instance_configuration_helper.rb @@ -0,0 +1,18 @@ +module InstanceConfigurationHelper + def instance_configuration_cell_html(value, &block) + return '-' unless value.to_s.presence + + block_given? ? yield(value) : value + end + + def instance_configuration_host(host) + @instance_configuration_host ||= instance_configuration_cell_html(host).capitalize + end + + # Value must be in bytes + def instance_configuration_human_size_cell(value) + instance_configuration_cell_html(value) do |v| + number_to_human_size(v, strip_insignificant_zeros: true, significant: false) + end + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index cf3ce3c9e54..ca65e81f27a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -249,9 +249,7 @@ module Ci end def commit - @commit ||= project.commit(sha) - rescue - nil + @commit ||= project.commit_by(oid: sha) end def branch? diff --git a/app/models/email.rb b/app/models/email.rb index 384f38f2db7..2da8b050149 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -14,6 +14,8 @@ class Email < ActiveRecord::Base devise :confirmable self.reconfirmable = false # currently email can't be changed, no need to reconfirm + delegate :username, to: :user + def email=(value) write_attribute(:email, value.downcase.strip) end diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb new file mode 100644 index 00000000000..b30b707e5fe --- /dev/null +++ b/app/models/instance_configuration.rb @@ -0,0 +1,71 @@ +require 'resolv' + +class InstanceConfiguration + SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze + SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze + CACHE_KEY = 'instance_configuration'.freeze + EXPIRATION_TIME = 24.hours + + def settings + @configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do + { ssh_algorithms_hashes: ssh_algorithms_hashes, + host: host, + gitlab_pages: gitlab_pages, + gitlab_ci: gitlab_ci }.deep_symbolize_keys + end + end + + private + + def ssh_algorithms_hashes + SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact + end + + def host + Settings.gitlab.host + end + + def gitlab_pages + Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host)) + end + + def resolv_dns(dns) + Resolv.getaddress(dns) + rescue Resolv::ResolvError + end + + def gitlab_ci + Settings.gitlab_ci + .to_h + .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes, + default: 100.megabytes }) + end + + def ssh_algorithm_file(algorithm) + File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub") + end + + def ssh_algorithm_hashes(algorithm) + content = ssh_algorithm_file_content(algorithm) + return unless content.present? + + { name: algorithm, + md5: ssh_algorithm_md5(content), + sha256: ssh_algorithm_sha256(content) } + end + + def ssh_algorithm_file_content(algorithm) + file = ssh_algorithm_file(algorithm) + return unless File.exist?(file) + + File.read(file) + end + + def ssh_algorithm_md5(ssh_file_content) + OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') + end + + def ssh_algorithm_sha256(ssh_file_content) + OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c3fae16d109..07352db5d2d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -396,6 +396,10 @@ class MergeRequest < ActiveRecord::Base end def merge_ongoing? + # While the MergeRequest is locked, it should present itself as 'merge ongoing'. + # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron. + return true if locked? + !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) end diff --git a/app/models/project.rb b/app/models/project.rb index 4689b588906..7185b4d44fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -540,6 +540,10 @@ class Project < ActiveRecord::Base repository.commit(ref) end + def commit_by(oid:) + repository.commit_by(oid: oid) + end + # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) latest_pipeline = pipelines.latest_successful_for(ref) @@ -553,7 +557,7 @@ class Project < ActiveRecord::Base def merge_base_commit(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id) - repository.commit(sha) if sha + commit_by(oid: sha) if sha end def saved? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 9ee3a533c1e..b487378edd2 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,6 +3,8 @@ class JiraService < IssueTrackerService validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true + validates :username, presence: true, if: :activated? + validates :password, presence: true, if: :activated? prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8ba07173c74..5c0b3338a62 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -153,7 +153,10 @@ class KubernetesService < DeploymentService end def default_namespace - "#{project.path}-#{project.id}" if project.present? + return unless project + + slug = "#{project.path}-#{project.id}".downcase + slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end def build_kubeclient!(api_path: 'api', api_version: 'v1') diff --git a/app/models/repository.rb b/app/models/repository.rb index 4324ea46aac..44a1e9ce529 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -76,6 +76,7 @@ class Repository @full_path = full_path @disk_path = disk_path || full_path @project = project + @commit_cache = {} end def ==(other) @@ -103,18 +104,17 @@ class Repository def commit(ref = 'HEAD') return nil unless exists? + return ref if ref.is_a?(::Commit) - commit = - if ref.is_a?(Gitlab::Git::Commit) - ref - else - Gitlab::Git::Commit.find(raw_repository, ref) - end + find_commit(ref) + end - commit = ::Commit.new(commit, @project) if commit - commit - rescue Rugged::OdbError, Rugged::TreeError - nil + # Finding a commit by the passed SHA + # Also takes care of caching, based on the SHA + def commit_by(oid:) + return @commit_cache[oid] if @commit_cache.key?(oid) + + @commit_cache[oid] = find_commit(oid) end def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) @@ -231,7 +231,7 @@ class Repository # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. def keep_around(sha) - return unless sha && commit(sha) + return unless sha && commit_by(oid: sha) return if kept_around?(sha) @@ -862,22 +862,12 @@ class Repository end def ff_merge(user, source, target_branch, merge_request: nil) - our_commit = rugged.branches[target_branch].target - their_commit = - if source.is_a?(Gitlab::Git::Commit) - source.raw_commit - else - rugged.lookup(source) - end - - raise 'Invalid merge target' if our_commit.nil? - raise 'Invalid merge source' if their_commit.nil? + their_commit_id = commit(source)&.id + raise 'Invalid merge source' if their_commit_id.nil? - with_branch(user, target_branch) do |start_commit| - merge_request&.update(in_progress_merge_commit_sha: their_commit.oid) + merge_request&.update(in_progress_merge_commit_sha: their_commit_id) - their_commit.oid - end + with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) } end def revert( @@ -912,18 +902,27 @@ class Repository end end - def merged_to_root_ref?(branch_name) - branch_commit = commit(branch_name) - root_ref_commit = commit(root_ref) + def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil) + branch = Gitlab::Git::Branch.find(self, branch_or_name) - if branch_commit - same_head = branch_commit.id == root_ref_commit.id - !same_head && ancestor?(branch_commit.id, root_ref_commit.id) + if branch + root_ref_sha = commit(root_ref).sha + same_head = branch.target == root_ref_sha + merged = + if pre_loaded_merged_branches + pre_loaded_merged_branches.include?(branch.name) + else + ancestor?(branch.target, root_ref_sha) + end + + !same_head && merged else nil end end + delegate :merged_branch_names, to: :raw_repository + def merge_base(first_commit_id, second_commit_id) first_commit_id = commit(first_commit_id).try(:id) || first_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id @@ -1031,6 +1030,10 @@ class Repository if instance_variable_defined?(ivar) instance_variable_get(ivar) else + # If the repository doesn't exist and a fallback was specified we return + # that value inmediately. This saves us Rugged/gRPC invocations. + return fallback unless fallback.nil? || exists? + begin value = if memoize_only @@ -1040,8 +1043,9 @@ class Repository end instance_variable_set(ivar, value) rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository - # if e.g. HEAD or the entire repository doesn't exist we want to - # gracefully handle this and not cache anything. + # Even if the above `#exists?` check passes these errors might still + # occur (for example because of a non-existing HEAD). We want to + # gracefully handle this and not cache anything fallback end end @@ -1069,6 +1073,18 @@ class Repository private + # TODO Generice finder, later split this on finders by Ref or Oid + # gitlab-org/gitlab-ce#39239 + def find_commit(oid_or_ref) + commit = if oid_or_ref.is_a?(Gitlab::Git::Commit) + oid_or_ref + else + Gitlab::Git::Commit.find(raw_repository, oid_or_ref) + end + + ::Commit.new(commit, @project) if commit + end + def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1107,12 +1123,12 @@ class Repository def last_commit_for_path_by_gitaly(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) - commit(c) + commit_by(oid: c) end def last_commit_for_path_by_rugged(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path) - commit(sha) + commit_by(oid: sha) end def last_commit_id_for_path_by_shelling_out(sha, path) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 8c5821aa870..156e7b2f078 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -82,16 +82,9 @@ module MergeRequests @merge_request.can_remove_source_branch?(branch_deletion_user) end - # Logs merge error message and cleans `MergeRequest#merge_jid`. - # def handle_merge_error(log_message:, save_message_on_model: false) Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") - - if save_message_on_model - @merge_request.update(merge_error: log_message, merge_jid: nil) - else - clean_merge_jid - end + @merge_request.update(merge_error: log_message) if save_message_on_model end def merge_request_info diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index a1c2f8d0180..5d275967821 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -83,7 +83,7 @@ class SystemHooksService project_id: model.id, owner_name: owner.name, owner_email: owner.respond_to?(:email) ? owner.email : "", - project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase + project_visibility: model.visibility.downcase } end diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index fd2ba9ac1ca..9038c4fbebd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -6,13 +6,13 @@ .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs = nav_link(page: [dashboard_projects_path, root_path]) do - = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects = nav_link(page: starred_dashboard_projects_path) do - = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + = link_to starred_dashboard_projects_path, data: {placement: 'right'} do Starred projects = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do - = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do + = link_to explore_root_path, data: {placement: 'right'} do Explore projects .nav-controls diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index c25eae63eec..d0c2e0b1d69 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -11,6 +11,7 @@ %span= Gitlab::VERSION %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) = version_status_badge + %p.slead GitLab is open source software to collaborate on code. %br @@ -23,6 +24,7 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. + %p= link_to 'Check the current instance configuration ', help_instance_configuration_url %hr .row.prepend-top-default diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml new file mode 100644 index 00000000000..f09e3825a4b --- /dev/null +++ b/app/views/help/instance_configuration.html.haml @@ -0,0 +1,17 @@ +- page_title 'Instance Configuration' +.wiki.documentation + %h1 Instance Configuration + + %p + In this page you will find information about the settings that are used in your current instance. + + = render 'help/instance_configuration/ssh_info' + = render 'help/instance_configuration/gitlab_pages' + = render 'help/instance_configuration/gitlab_ci' + %p + %strong Table of contents + + %ul + = content_for :table_content + + = content_for :settings_content diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml new file mode 100644 index 00000000000..7fa8bd086d4 --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml @@ -0,0 +1,24 @@ +- content_for :table_content do + %li= link_to 'GitLab CI', '#gitlab-ci' + +- content_for :settings_content do + %h2#gitlab-ci + GitLab CI + + %p + Below are the current settings regarding + = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') } + + .table-responsive + %table + %thead + %tr + %th Setting + %th= instance_configuration_host(@instance_configuration.settings[:host]) + %th Default + %tbody + %tr + - artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size] + %td Artifacts maximum size + %td= instance_configuration_human_size_cell(artifacts_size[:value]) + %td= instance_configuration_human_size_cell(artifacts_size[:default]) diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml new file mode 100644 index 00000000000..bdd77730dcc --- /dev/null +++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml @@ -0,0 +1,35 @@ +- gitlab_pages = @instance_configuration.settings[:gitlab_pages] +- content_for :table_content do + %li= link_to 'GitLab Pages', '#gitlab-pages' + +- content_for :settings_content do + %h2#gitlab-pages + GitLab Pages + + %p + Below are the settings for + = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') } + .table-responsive + %table + %thead + %tr + %th Setting + %th= instance_configuration_host(@instance_configuration.settings[:host]) + %tbody + %tr + %td Domain Name + %td + %code= instance_configuration_cell_html(gitlab_pages[:host]) + %tr + %td IP Address + %td + %code= instance_configuration_cell_html(gitlab_pages[:ip_address]) + %tr + %td Port + %td + %code= instance_configuration_cell_html(gitlab_pages[:port]) + %br + + %p + The maximum size of your Pages site is regulated by the artifacts maximum + size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }} diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml new file mode 100644 index 00000000000..987cc61b3f6 --- /dev/null +++ b/app/views/help/instance_configuration/_ssh_info.html.haml @@ -0,0 +1,27 @@ +- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes] +- if ssh_info.any? + - content_for :table_content do + %li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints' + + - content_for :settings_content do + %h2#ssh-host-keys-fingerprints + SSH host keys fingerprints + + %p + Below are the fingerprints for the current instance SSH host keys. + + .table-responsive + %table + %thead + %tr + %th Algorithm + %th MD5 + %th SHA256 + %tbody + - ssh_info.each do |algorithm| + %tr + %td= algorithm[:name] + %td + %code= instance_configuration_cell_html(algorithm[:md5]) + %td + %code= instance_configuration_cell_html(algorithm[:sha256]) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 49101d1efa4..6e02ae6c9cc 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,3 +1,4 @@ +- merged = local_assigns.fetch(:merged, false) - commit = @repository.commit(branch.dereferenced_target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) @@ -12,7 +13,7 @@ - if branch.name == @repository.root_ref %span.label.label-primary default - - elsif @repository.merged_to_root_ref? branch.name + - elsif merged %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } = s_('Branches|merged') @@ -47,7 +48,7 @@ target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), branch_name: branch.name, - is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } + is_merged: ("true" if merged) } } = icon("trash-o") - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 7d9645d79e6..aade310236e 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -38,7 +38,7 @@ - if @branches.any? %ul.content-list.all-branches - @branches.each do |branch| - = render "projects/branches/branch", branch: branch + = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names) = paginate @branches, theme: 'gitlab' - else .nothing-here-block diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml new file mode 100644 index 00000000000..1b7d878c38c --- /dev/null +++ b/app/views/projects/issues/edit.html.haml @@ -0,0 +1,7 @@ +- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" + +%h3.page-title + Edit Issue ##{@issue.iid} +%hr + += render "form" diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 161b1c9fd72..fabb17c7340 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,6 @@ %ul.tokens-container.list-unstyled %li.input-token %input.form-control.filtered-search{ search_filter_input_options(type) } - = icon('filter') #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { action: 'submit' } } diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb index 7843179d77c..a396c0f27b2 100644 --- a/app/workers/stuck_merge_jobs_worker.rb +++ b/app/workers/stuck_merge_jobs_worker.rb @@ -23,7 +23,7 @@ class StuckMergeJobsWorker merge_requests = MergeRequest.where(id: completed_ids) merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged) - merge_requests.where(merge_commit_sha: nil).update_all(state: :opened) + merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil) Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}") end diff --git a/changelogs/unreleased/32318-filter-icon.yml b/changelogs/unreleased/32318-filter-icon.yml new file mode 100644 index 00000000000..71e7c2c4dac --- /dev/null +++ b/changelogs/unreleased/32318-filter-icon.yml @@ -0,0 +1,5 @@ +--- +title: Remove filter icon from search bar +merge_request: +author: +type: other diff --git a/changelogs/unreleased/35199-case-insensitive-branches-search.yml b/changelogs/unreleased/35199-case-insensitive-branches-search.yml new file mode 100644 index 00000000000..da2729e9e55 --- /dev/null +++ b/changelogs/unreleased/35199-case-insensitive-branches-search.yml @@ -0,0 +1,5 @@ +--- +title: Case insensitive search for branches +merge_request: 14995 +author: George Andrinopoulos +type: fixed diff --git a/changelogs/unreleased/38178-fl-mr-notes-components.yml b/changelogs/unreleased/38178-fl-mr-notes-components.yml new file mode 100644 index 00000000000..244ccfb3071 --- /dev/null +++ b/changelogs/unreleased/38178-fl-mr-notes-components.yml @@ -0,0 +1,6 @@ +--- +title: Moves placeholders components into shared folder with documentation. Makes + them easier to reuse in MR and Snippets comments +merge_request: +author: +type: other diff --git a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml new file mode 100644 index 00000000000..7de65f5c3f6 --- /dev/null +++ b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml @@ -0,0 +1,5 @@ +--- +title: Update default disabled merge request widget message to reflect a general failure +merge_request: 14960 +author: +type: changed diff --git a/changelogs/unreleased/39366-email-confirmation-fails.yml b/changelogs/unreleased/39366-email-confirmation-fails.yml new file mode 100644 index 00000000000..a5568670c70 --- /dev/null +++ b/changelogs/unreleased/39366-email-confirmation-fails.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix bug preventing secondary emails from being confirmed' +merge_request: 15010 +author: +type: fixed diff --git a/changelogs/unreleased/39419-remove-overzealous-tooltips.yml b/changelogs/unreleased/39419-remove-overzealous-tooltips.yml new file mode 100644 index 00000000000..d6cf60bebfa --- /dev/null +++ b/changelogs/unreleased/39419-remove-overzealous-tooltips.yml @@ -0,0 +1,5 @@ +--- +title: Remove overzealous tooltips in projects page tabs +merge_request: 15017 +author: +type: removed diff --git a/changelogs/unreleased/39441-bring-edit-form-back.yml b/changelogs/unreleased/39441-bring-edit-form-back.yml new file mode 100644 index 00000000000..025417e4da9 --- /dev/null +++ b/changelogs/unreleased/39441-bring-edit-form-back.yml @@ -0,0 +1,5 @@ +--- +title: Fix editing issue description in mobile view +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39495-fix-bitbucket-login.yml b/changelogs/unreleased/39495-fix-bitbucket-login.yml new file mode 100644 index 00000000000..b48d557108b --- /dev/null +++ b/changelogs/unreleased/39495-fix-bitbucket-login.yml @@ -0,0 +1,5 @@ +--- +title: Fix bitbucket login +merge_request: 15051 +author: +type: fixed diff --git a/changelogs/unreleased/api-configure-jira.yml b/changelogs/unreleased/api-configure-jira.yml new file mode 100644 index 00000000000..3ac52d573b0 --- /dev/null +++ b/changelogs/unreleased/api-configure-jira.yml @@ -0,0 +1,5 @@ +--- +title: Validate username/pw for Jiraservice, require them in the API +merge_request: 15025 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml b/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml new file mode 100644 index 00000000000..a17ed51c9b8 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Use the correct visibility attribute for projects in system hooks +merge_request: 15065 +author: +type: fixed diff --git a/changelogs/unreleased/feature-ssh_host_fingerprint.yml b/changelogs/unreleased/feature-ssh_host_fingerprint.yml new file mode 100644 index 00000000000..04f9fd1d6ed --- /dev/null +++ b/changelogs/unreleased/feature-ssh_host_fingerprint.yml @@ -0,0 +1,5 @@ +--- +title: Automatic configuration settings page +merge_request: 13850 +author: Francisco Lopez +type: added diff --git a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml new file mode 100644 index 00000000000..0847b5f6733 --- /dev/null +++ b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken wiki pages that link to a wiki file +merge_request: 15019 +author: +type: fixed diff --git a/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml b/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml new file mode 100644 index 00000000000..fe945e822fd --- /dev/null +++ b/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml @@ -0,0 +1,5 @@ +--- +title: Fix widget of locked merge requests not being presented +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/mr-14642.yml b/changelogs/unreleased/mr-14642.yml new file mode 100644 index 00000000000..048cc79e323 --- /dev/null +++ b/changelogs/unreleased/mr-14642.yml @@ -0,0 +1,6 @@ +--- +title: Auto Devops kubernetes default namespace is now correctly built out of gitlab + project group-name +merge_request: 14642 +author: Mircea Danila Dumitrescu +type: fixed diff --git a/changelogs/unreleased/use-git-branch-merged.yml b/changelogs/unreleased/use-git-branch-merged.yml new file mode 100644 index 00000000000..24ec226250c --- /dev/null +++ b/changelogs/unreleased/use-git-branch-merged.yml @@ -0,0 +1,5 @@ +--- +title: Improve branch listing page performance +merge_request: 14729 +author: +type: performance diff --git a/changelogs/unreleased/zj-commit-cache.yml b/changelogs/unreleased/zj-commit-cache.yml new file mode 100644 index 00000000000..e3afe0ea7ef --- /dev/null +++ b/changelogs/unreleased/zj-commit-cache.yml @@ -0,0 +1,5 @@ +--- +title: Cache commits fetched from the repository +merge_request: +author: +type: performance diff --git a/config/routes/help.rb b/config/routes/help.rb index d53822da9ec..2ea8bfd7aed 100644 --- a/config/routes/help.rb +++ b/config/routes/help.rb @@ -1,4 +1,5 @@ -get 'help' => 'help#index' -get 'help/shortcuts' => 'help#shortcuts' -get 'help/ui' => 'help#ui' -get 'help/*path' => 'help#show', as: :help_page +get 'help' => 'help#index' +get 'help/shortcuts' => 'help#shortcuts' +get 'help/ui' => 'help#ui' +get 'help/instance_configuration' => 'help#instance_configuration' +get 'help/*path' => 'help#show', as: :help_page diff --git a/doc/api/services.md b/doc/api/services.md index 49b87a4228c..6c8f196fd5c 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -478,8 +478,8 @@ PUT /projects/:id/services/jira | --------- | ---- | -------- | ----------- | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | -| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | -| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | +| `username` | string | yes | The username of the user created to be used with GitLab/JIRA. | +| `password` | string | yes | The password of the user created to be used with GitLab/JIRA. | | `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md new file mode 100644 index 00000000000..932a44f65e4 --- /dev/null +++ b/doc/development/ee_features.md @@ -0,0 +1,382 @@ +# Guidelines for implementing Enterprise Edition feature + +- **Write the code and the tests.**: As with any code, EE features should have + good test coverage to prevent regressions. +- **Write documentation.**: Add documentation to the `doc/` directory. Describe + the feature and include screenshots, if applicable. +- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the + [EE features list][ee-features-list]. + +## Act as CE when unlicensed + +Since the implementation of [GitLab CE features to work with unlicensed EE instance][ee-as-ce] +GitLab Enterprise Edition should work like GitLab Community Edition +when no license is active. So EE features always should be guarded by +`project.feature_available?` or `group.feature_available?` (or +`License.feature_available?` if it is a system-wide feature). + +CE specs should remain untouched as much as possible and extra specs +should be added for EE. Licensed features can be stubbed using the +spec helper `stub_licensed_features` in `EE::LicenseHelpers`. + +[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500 + +## Separation of EE code + +We want a [single code base][] eventually, but before we reach the goal, +we still need to merge changes from GitLab CE to EE. To help us get there, +we should make sure that we no longer edit CE files in place in order to +implement EE features. + +Instead, all EE codes should be put inside the `ee/` top-level directory, and +tests should be put inside `spec/ee/`. We don't use `ee/spec` for now due to +technical limitation. The rest of codes should be as close as to the CE files. + +[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454 + +### EE-only features + +If the feature being developed is not present in any form in CE, we don't +need to put the codes under `EE` namespace. For example, an EE model could +go into: `ee/app/models/awesome.rb` using `Awesome` as the class name. This +is applied not only to models. Here's a list of other examples: + +- `ee/app/controllers/foos_controller.rb` +- `ee/app/finders/foos_finder.rb` +- `ee/app/helpers/foos_helper.rb` +- `ee/app/mailers/foos_mailer.rb` +- `ee/app/models/foo.rb` +- `ee/app/policies/foo_policy.rb` +- `ee/app/serializers/foo_entity.rb` +- `ee/app/serializers/foo_serializer.rb` +- `ee/app/services/foo/create_service.rb` +- `ee/app/validators/foo_attr_validator.rb` +- `ee/app/workers/foo_worker.rb` + +### EE features based on CE features + +For features that build on existing CE features, write a module in the +`EE` namespace and `prepend` it in the CE class. This makes conflicts +less likely to happen during CE to EE merges because only one line is +added to the CE class - the `prepend` line. + +Since the module would require an `EE` namespace, the file should also be +put in an `ee/` sub-directory. For example, we want to extend the user model +in EE, so we have a module called `::EE::User` put inside +`ee/app/models/ee/user.rb`. + +This is also not just applied to models. Here's a list of other examples: + +- `ee/app/controllers/ee/foos_controller.rb` +- `ee/app/finders/ee/foos_finder.rb` +- `ee/app/helpers/ee/foos_helper.rb` +- `ee/app/mailers/ee/foos_mailer.rb` +- `ee/app/models/ee/foo.rb` +- `ee/app/policies/ee/foo_policy.rb` +- `ee/app/serializers/ee/foo_entity.rb` +- `ee/app/serializers/ee/foo_serializer.rb` +- `ee/app/services/ee/foo/create_service.rb` +- `ee/app/validators/ee/foo_attr_validator.rb` +- `ee/app/workers/ee/foo_worker.rb` + +#### Overriding CE methods + +To override a method present in the CE codebase, use `prepend`. It +lets you override a method in a class with a method from a module, while +still having access the class's implementation with `super`. + +There are a few gotchas with it: + +- you should always add a `raise NotImplementedError unless defined?(super)` + guard clause in the "overrider" method to ensure that if the method gets + renamed in CE, the EE override won't be silently forgotten. +- when the "overrider" would add a line in the middle of the CE + implementation, you should refactor the CE method and split it in + smaller methods. Or create a "hook" method that is empty in CE, + and with the EE-specific implementation in EE. +- when the original implementation contains a guard clause (e.g. + `return unless condition`), we cannot easily extend the behaviour by + overriding the method, because we can't know when the overridden method + (i.e. calling `super` in the overriding method) would want to stop early. + In this case, we shouldn't just override it, but update the original method + to make it call the other method we want to extend, like a [template method + pattern](https://en.wikipedia.org/wiki/Template_method_pattern). + For example, given this base: + ``` ruby + class Base + def execute + return unless enabled? + + # ... + # ... + end + end + ``` + Instead of just overriding `Base#execute`, we should update it and extract + the behaviour into another method: + ``` ruby + class Base + def execute + return unless enabled? + + do_something + end + + private + + def do_something + # ... + # ... + end + end + ``` + Then we're free to override that `do_something` without worrying about the + guards: + ``` ruby + module EE::Base + def do_something + # Follow the above pattern to call super and extend it + end + end + ``` + This would require updating CE first, or make sure this is back ported to CE. + +When prepending, place them in the `ee/` specific sub-directory, and +wrap class or module in `module EE` to avoid naming conflicts. + +For example to override the CE implementation of +`ApplicationController#after_sign_out_path_for`: + +```ruby +def after_sign_out_path_for(resource) + current_application_settings.after_sign_out_path.presence || new_user_session_path +end +``` + +Instead of modifying the method in place, you should add `prepend` to +the existing file: + +```ruby +class ApplicationController < ActionController::Base + prepend EE::ApplicationController + # ... + + def after_sign_out_path_for(resource) + current_application_settings.after_sign_out_path.presence || new_user_session_path + end + + # ... +end +``` + +And create a new file in the `ee/` sub-directory with the altered +implementation: + +```ruby +module EE + class ApplicationController + def after_sign_out_path_for(resource) + raise NotImplementedError unless defined?(super) + + if Gitlab::Geo.secondary? + Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state) + else + super + end + end + end +end +``` + +#### Use self-descriptive wrapper methods + +When it's not possible/logical to modify the implementation of a +method. Wrap it in a self-descriptive method and use that method. + +For example, in CE only an `admin` is allowed to access all private +projects/groups, but in EE also an `auditor` has full private +access. It would be incorrect to override the implementation of +`User#admin?`, so instead add a method `full_private_access?` to +`app/models/users.rb`. The implementation in CE will be: + +```ruby +def full_private_access? + admin? +end +``` + +In EE, the implementation `ee/app/models/ee/users.rb` would be: + +```ruby +def full_private_access? + raise NotImplementedError unless defined?(super) + super || auditor? +end +``` + +In `lib/gitlab/visibility_level.rb` this method is used to return the +allowed visibilty levels: + +```ruby +def levels_for_user(user = nil) + if user.full_private_access? + [PRIVATE, INTERNAL, PUBLIC] + elsif # ... +end +``` + +See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for +full implementation details. + +[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373 +[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199 + +### Code in `app/controllers/` + +In controllers, the most common type of conflict is with `before_action` that +has a list of actions in CE but EE adds some actions to that list. + +The same problem often occurs for `params.require` / `params.permit` calls. + +**Mitigations** + +Separate CE and EE actions/keywords. For instance for `params.require` in +`ProjectsController`: + +```ruby +def project_params + params.require(:project).permit(project_params_attributes) +end + +# Always returns an array of symbols, created however best fits the use case. +# It _should_ be sorted alphabetically. +def project_params_attributes + %i[ + description + name + path + ] +end + +``` + +In the `EE::ProjectsController` module: + +```ruby +def project_params_attributes + super + project_params_attributes_ee +end + +def project_params_attributes_ee + %i[ + approvals_before_merge + approver_group_ids + approver_ids + ... + ] +end +``` + +### Code in `app/models/` + +EE-specific models should `extend EE::Model`. + +For example, if EE has a specific `Tanuki` model, you would +place it in `ee/app/models/ee/tanuki.rb`. + +### Code in `app/views/` + +It's a very frequent problem that EE is adding some specific view code in a CE +view. For instance the approval code in the project's settings page. + +**Mitigations** + +Blocks of code that are EE-specific should be moved to partials. This +avoids conflicts with big chunks of HAML code that that are not fun to +resolve when you add the indentation to the equation. + +EE-specific views should be placed in `ee/app/views/ee/`, using extra +sub-directories if appropriate. + +### Code in `lib/` + +Place EE-specific logic in the top-level `EE` module namespace. Namespace the +class beneath the `EE` module just as you would normally. + +For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place +EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`. + +### Code in `spec/` + +When you're testing EE-only features, avoid adding examples to the +existing CE specs. Also do no change existing CE examples, since they +should remain working as-is when EE is running without a license. + +Instead place EE specs in the `spec/ee/spec` folder. + +## JavaScript code in `assets/javascripts/` + +To separate EE-specific JS-files we can also move the files into an `ee` folder. + +For example there can be an +`app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an +EE counterpart +`ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`. + +That way we can create a separate webpack bundle in `webpack.config.js`: + +```javascript + protected_branches: '~/protected_branches', + ee_protected_branches: 'ee/protected_branches/protected_branches_bundle.js', +``` + +With the separate bundle in place, we can decide which bundle to load inside the +view, using the `page_specific_javascript_bundle_tag` helper. + +```haml +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('protected_branches') +``` + +## SCSS code in `assets/stylesheets` + +To separate EE-specific styles in SCSS files, if a component you're adding styles for +is limited to only EE, it is better to have a separate SCSS file in appropriate directory +within `app/assets/stylesheets`. + +In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill, +e.g. a text style of some component is different for EE. In such cases, +styles are usually kept in stylesheet that is common for both CE and EE, and it is wise +to isolate such ruleset from rest of CE rules (along with adding comment describing the same) +to avoid conflicts during CE to EE merge. + +#### Bad +```scss +.section-body { + .section-title { + background: $gl-header-color; + } + + &.ee-section-body { + .section-title { + background: $gl-header-color-cyan; + } + } +} +``` + +#### Good +```scss +.section-body { + .section-title { + background: $gl-header-color; + } +} + +/* EE-specific styles */ +.section-body.ee-section-body { + .section-title { + background: $gl-header-color-cyan; + } +} +``` diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md index bf01fe51dc3..5cc014419ad 100644 --- a/doc/gitlab-basics/add-merge-request.md +++ b/doc/gitlab-basics/add-merge-request.md @@ -3,31 +3,28 @@ Merge requests are useful to integrate separate changes that you've made to a project, on different branches. This is a brief guide on how to create a merge request. For more information, check the -[merge requests documentation](../user/project/merge_requests.md). +[merge requests documentation](../user/project/merge_requests/index.md). --- 1. Before you start, you should have already [created a branch](create-branch.md) and [pushed your changes](basic-git-commands.md) to GitLab. - -1. You can then go to the project where you'd like to merge your changes and - click on the **Merge requests** tab. - - ![Merge requests](img/project_navbar.png) - +1. Go to the project where you'd like to merge your changes and click on the + **Merge requests** tab. 1. Click on **New merge request** on the right side of the screen. - - ![New Merge Request](img/merge_request_new.png) - -1. Select a source branch and click on the **Compare branches and continue** button. +1. From there on, you have the option to select the source branch and the target + branch you'd like to compare to. The default target project is the upstream + repository, but you can choose to compare across any of its forks. ![Select a branch](img/merge_request_select_branch.png) +1. When ready, click on the **Compare branches and continue** button. 1. At a minimum, add a title and a description to your merge request. Optionally, select a user to review your merge request and to accept or close it. You may also select a milestone and labels. ![New merge request page](img/merge_request_page.png) -1. When ready, click on the **Submit merge request** button. Your merge request - will be ready to be approved and published. +1. When ready, click on the **Submit merge request** button. + +Your merge request will be ready to be approved and merged. diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png Binary files differdeleted file mode 100644 index 6fcd7bebada..00000000000 --- a/doc/gitlab-basics/img/merge_request_new.png +++ /dev/null diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png Binary files differindex 9f6b93943a9..57ea0e65f34 100644 --- a/doc/gitlab-basics/img/merge_request_select_branch.png +++ b/doc/gitlab-basics/img/merge_request_select_branch.png diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png Binary files differdeleted file mode 100644 index be6f38ede32..00000000000 --- a/doc/gitlab-basics/img/project_navbar.png +++ /dev/null diff --git a/doc/integration/saml.md b/doc/integration/saml.md index b5b245c626f..3ae98adc465 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. -## External Groups +## Marking Users as External based on SAML Groups >**Note:** This setting is only available on GitLab 8.7 and above. -SAML login includes support for external groups. You can define in the SAML -settings which groups, to which your users belong in your IdP, you wish to be -marked as [external](../user/permissions.md). +SAML login includes support for automatically identifying whether a user should +be considered an [external](../user/permissions.md) user based on the user's group +membership in the SAML identity provider. This feature **does not** allow you to +automatically add users to GitLab [Groups](../user/group/index.md), it simply +allows you to mark users as External if they are members of certain groups in the +Identity Provider. ### Requirements diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 6289fcf3c2b..4b2e042251b 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -34,7 +34,6 @@ With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium) - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter) - [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter) -- Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter) - Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter) ## Use cases diff --git a/lib/api/services.rb b/lib/api/services.rb index 2cbd0517dc3..1e4f7c29633 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -313,13 +313,13 @@ module API desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' }, { - required: false, + required: true, name: :username, type: String, desc: 'The username of the user created to be used with GitLab/JIRA' }, { - required: false, + required: true, name: :password, type: String, desc: 'The password of the user created to be used with GitLab/JIRA' diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb index c53882787f1..3487e099381 100644 --- a/lib/gitlab/git/branch.rb +++ b/lib/gitlab/git/branch.rb @@ -3,6 +3,14 @@ module Gitlab module Git class Branch < Ref + def self.find(repo, branch_name) + if branch_name.is_a?(Gitlab::Git::Branch) + branch_name + else + repo.find_branch(branch_name) + end + end + def initialize(repository, name, target, target_commit) super(repository, name, target, target_commit) end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 1957c254c28..23ae37ff71e 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -72,7 +72,8 @@ module Gitlab decorate(repo, commit) if commit rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, - Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository + Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository, + Rugged::OdbError, Rugged::TreeError nil end diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb index c327e9b1616..f302b852b35 100644 --- a/lib/gitlab/git/hooks_service.rb +++ b/lib/gitlab/git/hooks_service.rb @@ -8,7 +8,7 @@ module Gitlab def execute(pusher, repository, oldrev, newrev, ref) @repository = repository @gl_id = pusher.gl_id - @gl_username = pusher.name + @gl_username = pusher.username @oldrev = oldrev @newrev = newrev @ref = ref diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 59a54b48ed9..fc8af38d4d9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -166,7 +166,7 @@ module Gitlab end def local_branches(sort_by: nil) - gitaly_migrate(:local_branches) do |is_enabled| + gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_ref_client.local_branches(sort_by: sort_by) else @@ -511,6 +511,10 @@ module Gitlab gitaly_commit_client.ancestor?(from, to) end + def merged_branch_names(branch_names = []) + Set.new(git_merged_branch_names(branch_names)) + end + # Return an array of Diff objects that represent the diff # between +from+ and +to+. See Diff::filter_diff_options for the allowed # diff options. The +options+ hash can also include :break_rewrites to @@ -745,6 +749,16 @@ module Gitlab nil end + def ff_merge(user, source_sha, target_branch) + OperationService.new(user, self).with_branch(target_branch) do |our_commit| + raise ArgumentError, 'Invalid merge target' unless our_commit + + source_sha + end + rescue Rugged::ReferenceError + raise ArgumentError, 'Invalid merge source' + end + def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) OperationService.new(user, self).with_branch( branch_name, @@ -1180,6 +1194,13 @@ module Gitlab sort_branches(branches, sort_by) end + def git_merged_branch_names(branch_names = []) + lines = run_git(['branch', '--merged', root_ref] + branch_names) + .first.lines + + lines.map(&:strip) + end + def log_using_shell?(options) options[:path].present? || options[:disable_walk] || diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index e7b2f52a552..f01d5c96fc8 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -8,6 +8,7 @@ module Gitlab { name: name, email: email, message: message } end end + PageBlob = Struct.new(:name) def self.default_ref 'master' @@ -80,7 +81,15 @@ module Gitlab end def preview_slug(title, format) - gollum_wiki.preview_page(title, '', format).url_path + # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid + # using Rugged through a Gollum::Wiki instance + page_class = Gollum::Page + page = page_class.new(nil) + ext = page_class.format_to_ext(format.to_sym) + name = page_class.cname(title) + '.' + ext + blob = PageBlob.new(name) + page.populate(blob) + page.url_path end private diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb index 527f2a44dea..84335aca4bc 100644 --- a/lib/gitlab/git/wiki_file.rb +++ b/lib/gitlab/git/wiki_file.rb @@ -1,7 +1,7 @@ module Gitlab module Git class WikiFile - attr_reader :mime_type, :raw_data, :name + attr_reader :mime_type, :raw_data, :name, :path # This class is meant to be serializable so that it can be constructed # by Gitaly and sent over the network to GitLab. @@ -13,6 +13,7 @@ module Gitlab @mime_type = gollum_file.mime_type @raw_data = gollum_file.raw_data @name = gollum_file.name + @path = gollum_file.path end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 42b59c106e2..8998c4b1a83 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -215,10 +215,6 @@ module Gitlab ).exec end - def matching_merge_request?(newrev, branch_name) - Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? - end - def deploy_key actor if deploy_key? end diff --git a/lib/omni_auth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb index 5a7d67c2390..ce1bdfe6ee4 100644 --- a/lib/omni_auth/strategies/bitbucket.rb +++ b/lib/omni_auth/strategies/bitbucket.rb @@ -36,6 +36,10 @@ module OmniAuth email_response = access_token.get('api/2.0/user/emails').parsed @emails ||= email_response && email_response['values'] || nil end + + def callback_url + options[:redirect_uri] || (full_host + script_name + callback_path) + end end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 6f48f091a20..aecdfb50759 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -557,6 +557,29 @@ describe Projects::IssuesController do end end end + + describe 'GET #edit' do + it_behaves_like 'restricted action', success: 200 + + def go(id:) + get :edit, + namespace_id: project.namespace.to_param, + project_id: project, + id: id + end + end + + describe 'PUT #update' do + it_behaves_like 'restricted action', success: 302 + + def go(id:) + put :update, + namespace_id: project.namespace.to_param, + project_id: project, + id: id, + issue: { title: 'New title' } + end + end end describe 'POST #create' do diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index f1ad5dd84ff..1604a2da485 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -46,7 +46,7 @@ describe Projects::PipelinesController do context 'when performing gitaly calls', :request_store do it 'limits the Gitaly requests' do - expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10) + expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8) end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index c2b59239af9..cf38066dedc 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -119,7 +119,7 @@ FactoryGirl.define do finished_at nil end - factory :ci_build_tag do + trait :tag do tag true end diff --git a/spec/factories/instance_configuration.rb b/spec/factories/instance_configuration.rb new file mode 100644 index 00000000000..406c7c3caf1 --- /dev/null +++ b/spec/factories/instance_configuration.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :instance_configuration do + skip_create + end +end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index c2674ce2d11..ccf63f3ffa4 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -38,6 +38,8 @@ FactoryGirl.define do active true properties( url: 'https://jira.example.com', + username: 'jira_user', + password: 'my-secret-password', project_key: 'jira-key' ) end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 8ce470fc288..2db6f9a2982 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -218,15 +218,54 @@ describe 'New/edit issue', :js do context 'edit issue' do before do - visit project_issue_path(project, issue) - page.within('.content .issuable-actions') do - click_on 'Edit' + visit edit_project_issue_path(project, issue) + end + + it 'allows user to update issue' do + expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible + + page.within '.js-user-search' do + expect(page).to have_content user.name + end + + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end end end it 'description has autocomplete' do - find_field('issue-description').native.send_keys('') - fill_in 'issue-description', with: '@' + find('#issue_description').native.send_keys('') + fill_in 'issue_description', with: '@' expect(page).to have_selector('.atwho-view') end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 25e99774575..d4fd3a50008 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Issues', :js do +describe 'Issues' do include DropzoneHelper include IssueHelpers include SortingHelper @@ -24,15 +24,109 @@ describe 'Issues', :js do end before do - visit project_issue_path(project, issue) - page.within('.content .issuable-actions') do - find('.issuable-edit').click - end - find('.issue-details .content-block .js-zen-enter').click + visit edit_project_issue_path(project, issue) + find('.js-zen-enter').click end it 'opens new issue popup' do - expect(page).to have_content(issue.description) + expect(page).to have_content("Issue ##{issue.iid}") + end + end + + describe 'Editing issue assignee' do + let!(:issue) do + create(:issue, + author: user, + assignees: [user], + project: project) + end + + it 'allows user to select unassigned', :js do + visit edit_project_issue_path(project, issue) + + expect(page).to have_content "Assignee #{user.name}" + + first('.js-user-search').click + click_link 'Unassigned' + + click_button 'Save changes' + + page.within('.assignee') do + expect(page).to have_content 'No assignee - assign yourself' + end + + expect(issue.reload.assignees).to be_empty + end + end + + describe 'due date', :js do + context 'on new form' do + before do + visit new_project_issue_path(project) + end + + it 'saves with due date' do + date = Date.today.at_beginning_of_month + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click + + page.within '.pika-single' do + click_button date.day + end + + expect(find('#issuable-due-date').value).to eq date.to_s + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + + context 'on edit form' do + let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) } + + before do + visit edit_project_issue_path(project, issue) + end + + it 'saves with due date' do + date = Date.today.at_beginning_of_month + + expect(find('#issuable-due-date').value).to eq date.to_s + + date = date.tomorrow + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click + + page.within '.pika-single' do + click_button date.day + end + + expect(find('#issuable-due-date').value).to eq date.to_s + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + + it 'warns about version conflict' do + issue.update(title: "New title") + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + + click_button 'Save changes' + + expect(page).to have_content 'Someone edited the issue the same time you did' + end end end diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb index 3b368f8e25d..63b5df5a8f5 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -149,7 +149,7 @@ feature 'Project > Members > Share with Group', :js do create(:group).add_owner(master) visit project_settings_members_path(project) - execute_script 'GroupsSelect.PER_PAGE = 1;' + execute_script 'GROUP_SELECT_PER_PAGE = 1;' open_select2 '#link_group_id' end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index d70cf1527e7..a7928857b7d 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -181,6 +181,21 @@ describe "Internal Project Access" do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/issues/:id/edit" do + let(:issue) { create(:issue, project: project) } + subject { edit_project_issue_path(project, issue) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index ea130606545..a4396b20afd 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -181,6 +181,21 @@ describe "Private Project Access" do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/issues/:id/edit" do + let(:issue) { create(:issue, project: project) } + subject { edit_project_issue_path(project, issue) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index d15f5af66c9..fccdeb0e5b7 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -394,6 +394,21 @@ describe "Public Project Access" do it { is_expected.to be_allowed_for(:visitor) } end + describe "GET /:project_path/issues/:id/edit" do + let(:issue) { create(:issue, project: project) } + subject { edit_project_issue_path(project, issue) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 91f34973ba5..9e3f2c69606 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -46,6 +46,15 @@ describe BranchesFinder do expect(result.count).to eq(1) end + it 'filters branches by name ignoring letter case' do + branches_finder = described_class.new(repository, { search: 'FiX' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('fix') + expect(result.count).to eq(1) + end + it 'does not find any branch with that name' do branches_finder = described_class.new(repository, { search: 'random' }) diff --git a/spec/fixtures/ssh_host_example_key.pub b/spec/fixtures/ssh_host_example_key.pub new file mode 100644 index 00000000000..6bac42b3ad0 --- /dev/null +++ b/spec/fixtures/ssh_host_example_key.pub @@ -0,0 +1 @@ +random content diff --git a/spec/helpers/instance_configuration_helper_spec.rb b/spec/helpers/instance_configuration_helper_spec.rb new file mode 100644 index 00000000000..5d716b9191d --- /dev/null +++ b/spec/helpers/instance_configuration_helper_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe InstanceConfigurationHelper do + describe '#instance_configuration_cell_html' do + describe 'if not block is passed' do + it 'returns the parameter if present' do + expect(helper.instance_configuration_cell_html('gitlab')).to eq('gitlab') + end + + it 'returns "-" if the parameter is blank' do + expect(helper.instance_configuration_cell_html(nil)).to eq('-') + expect(helper.instance_configuration_cell_html('')).to eq('-') + end + end + + describe 'if a block is passed' do + let(:upcase_block) { ->(value) { value.upcase } } + + it 'returns the result of the block' do + expect(helper.instance_configuration_cell_html('gitlab', &upcase_block)).to eq('GITLAB') + expect(helper.instance_configuration_cell_html('gitlab') { |v| v.upcase }).to eq('GITLAB') + end + + it 'returns "-" if the parameter is blank' do + expect(helper.instance_configuration_cell_html(nil, &upcase_block)).to eq('-') + expect(helper.instance_configuration_cell_html(nil) { |v| v.upcase }).to eq('-') + expect(helper.instance_configuration_cell_html('', &upcase_block)).to eq('-') + end + end + + it 'boolean are valid values to display' do + expect(helper.instance_configuration_cell_html(true)).to eq(true) + expect(helper.instance_configuration_cell_html(false)).to eq(false) + end + end + + describe '#instance_configuration_human_size_cell' do + it 'returns "-" if the parameter is blank' do + expect(helper.instance_configuration_human_size_cell(nil)).to eq('-') + expect(helper.instance_configuration_human_size_cell('')).to eq('-') + end + + it 'accepts the value in bytes' do + expect(helper.instance_configuration_human_size_cell(1024)).to eq('1 KB') + end + + it 'returns the value in human size readable format' do + expect(helper.instance_configuration_human_size_cell(1048576)).to eq('1 MB') + end + end +end diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js deleted file mode 100644 index d508a49f710..00000000000 --- a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import Vue from 'vue'; -import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue'; - -describe('issue placeholder system note component', () => { - let mountComponent; - beforeEach(() => { - const PlaceholderSystemNote = Vue.extend(placeholderSystemNote); - - mountComponent = props => new PlaceholderSystemNote({ - propsData: { - note: { - body: props, - }, - }, - }).$mount(); - }); - - it('should render system note placeholder with plain text', () => { - const vm = mountComponent('This is a placeholder'); - - expect(vm.$el.tagName).toEqual('LI'); - expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index d7019ea408b..df3d29ee1f9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -43,6 +43,10 @@ describe('MRWidgetReadyToMerge', () => { vm = createComponent(); }); + afterEach(() => { + vm.$destroy(); + }); + describe('props', () => { it('should have props', () => { const { mr, service } = readyToMergeComponent.props; @@ -495,6 +499,48 @@ describe('MRWidgetReadyToMerge', () => { }); }); + describe('Merge controls', () => { + describe('when allowed to merge', () => { + beforeEach(() => { + vm = createComponent({ + mr: { isMergeAllowed: true }, + }); + }); + + it('shows remove source branch checkbox', () => { + expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeDefined(); + }); + + it('shows modify commit message button', () => { + expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); + }); + + it('does not show message about needing to resolve items', () => { + expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull(); + }); + }); + + describe('when not allowed to merge', () => { + beforeEach(() => { + vm = createComponent({ + mr: { isMergeAllowed: false }, + }); + }); + + it('does not show remove source branch checkbox', () => { + expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull(); + }); + + it('does not show modify commit message button', () => { + expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); + }); + + it('shows message to resolve all items before being allowed to merge', () => { + expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined(); + }); + }); + }); + describe('Commit message area', () => { it('when using merge commits, should show "Modify commit message" button', () => { const customVm = createComponent({ diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js index 6e5275087f3..ba8ab0b2cd7 100644 --- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue'; +import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import store from '~/notes/stores'; -import { userDataMock } from '../mock_data'; +import { userDataMock } from '../../../notes/mock_data'; describe('issue placeholder system note component', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js new file mode 100644 index 00000000000..7b8e6c330c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('placeholder system note component', () => { + let PlaceholderSystemNote; + let vm; + + beforeEach(() => { + PlaceholderSystemNote = Vue.extend(placeholderSystemNote); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render system note placeholder with plain text', () => { + vm = mountComponent(PlaceholderSystemNote, { + note: { body: 'This is a placeholder' }, + }); + + expect(vm.$el.tagName).toEqual('LI'); + expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); + }); +}); diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js index c317ce32716..36aaf0a6c2e 100644 --- a/spec/javascripts/notes/components/issue_system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import issueSystemNote from '~/notes/components/issue_system_note.vue'; +import issueSystemNote from '~/vue_shared/components/notes/system_note.vue'; import store from '~/notes/stores'; describe('issue system note', () => { @@ -33,6 +33,10 @@ describe('issue system note', () => { }).$mount(); }); + afterEach(() => { + vm.$destroy(); + }); + it('should render a list item with correct id', () => { expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); }); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 52e450e9ba5..8450ad9dbcb 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () { imgCssClasses: 'myextraavatarclass', tooltipText: 'tooltip text', tooltipPlacement: 'bottom', + username: 'username', }; const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); @@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () { expect(this.userAvatarLink[key]).toBeDefined(); }); }); + + describe('no username', function () { + beforeEach(function (done) { + this.userAvatarLink.username = ''; + + Vue.nextTick(done); + }); + + it('should only render image tag in link', function () { + const childElements = this.userAvatarLink.$el.childNodes; + expect(childElements[0].tagName).toBe('IMG'); + + // Vue will render the hidden component as <!----> + expect(childElements[1].tagName).toBeUndefined(); + }); + + it('should render avatar image tooltip', function () { + expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText); + }); + }); + + describe('username', function () { + it('should not render avatar image tooltip', function () { + expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(''); + }); + + it('should render username prop in <span>', function () { + expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username); + }); + + it('should render text tooltip for <span>', function () { + expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText); + }); + + it('should render text tooltip placement for <span>', function () { + expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement); + }); + }); }); diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index 97d612e6347..ca76d6f0881 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do context 'linking internal images' do it 'creates img tag if image exists' do - file = Gollum::File.new(project_wiki.wiki) - expect(file).to receive(:path).and_return('images/image.jpg') - expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file) + gollum_file_double = double('Gollum::File', + mime_type: 'image/jpeg', + name: 'images/image.jpg', + path: 'images/image.jpg', + raw_data: '') + wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(wiki_file) tag = '[[images/image.jpg]]' doc = filter("See #{tag}", project_wiki: project_wiki) diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 318a7b7a332..708870060e7 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -7,6 +7,38 @@ describe Gitlab::Git::Branch, seed_helper: true do it { is_expected.to be_kind_of Array } + describe '.find' do + subject { described_class.find(repository, branch) } + + before do + allow(repository).to receive(:find_branch).with(branch) + .and_call_original + end + + context 'when finding branch via branch name' do + let(:branch) { 'master' } + + it 'returns a branch object' do + expect(subject).to be_a(described_class) + expect(subject.name).to eq(branch) + + expect(repository).to have_received(:find_branch).with(branch) + end + end + + context 'when the branch is already a branch' do + let(:commit) { repository.commit('master') } + let(:branch) { described_class.new(repository, 'master', commit.sha, commit) } + + it 'returns a branch object' do + expect(subject).to be_a(described_class) + expect(subject).to eq(branch) + + expect(repository).not_to have_received(:find_branch).with(branch) + end + end + end + describe '#size' do subject { super().size } it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index 51e4e3fdad1..3ed3feb4c74 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,24 +1,26 @@ require 'spec_helper' describe Gitlab::Git::HooksService, seed_helper: true do - let(:user) { Gitlab::Git::User.new('janedoe', 'Jane Doe', 'janedoe@example.com', 'user-456') } + let(:gl_id) { 'user-456' } + let(:gl_username) { 'janedoe' } + let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:service) { described_class.new } - - before do - @blankrev = Gitlab::Git::BLANK_SHA - @oldrev = SeedRepo::Commit::PARENT_ID - @newrev = SeedRepo::Commit::ID - @ref = 'refs/heads/feature' - end + let(:blankrev) { Gitlab::Git::BLANK_SHA } + let(:oldrev) { SeedRepo::Commit::PARENT_ID } + let(:newrev) { SeedRepo::Commit::ID } + let(:ref) { 'refs/heads/feature' } describe '#execute' do context 'when receive hooks were successful' do - it 'calls post-receive hook' do - hook = double(trigger: [true, nil]) + let(:hook) { double(:hook) } + + it 'calls all three hooks' do expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) + expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref) + .exactly(3).times.and_return([true, nil]) - service.execute(user, repository, @blankrev, @newrev, @ref) { } + service.execute(user, repository, blankrev, newrev, ref) { } end end @@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, blankrev, newrev, ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end @@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, blankrev, newrev, ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index b2d2f770e3d..b161d25b96c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1135,6 +1135,32 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#merged_branch_names' do + context 'when branch names are passed' do + it 'only returns the names we are asking' do + names = repository.merged_branch_names(%w[merge-test]) + + expect(names).to contain_exactly('merge-test') + end + + it 'does not return unmerged branch names' do + names = repository.merged_branch_names(%w[feature]) + + expect(names).to be_empty + end + end + + context 'when no branch names are specified' do + it 'returns all merged branch names' do + names = repository.merged_branch_names + + expect(names).to include('merge-test') + expect(names).to include('fix-mode') + expect(names).not_to include('feature') + end + end + end + describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } let(:not_existed_branch) { repository.ls_files("not_existed_branch") } @@ -1564,6 +1590,60 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#ff_merge' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:user) { build(:user) } + let(:target_branch) { 'test-ff-target-branch' } + + before do + repository.create_branch(target_branch, branch_head) + end + + after do + ensure_seeds + end + + subject { repository.ff_merge(user, source_sha, target_branch) } + + it 'performs a ff_merge' do + expect(subject.newrev).to eq(source_sha) + expect(subject.repo_created).to be(false) + expect(subject.branch_created).to be(false) + + expect(repository.commit(target_branch).id).to eq(source_sha) + end + + context 'with a non-existing target branch' do + subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'with a non-existing source commit' do + let(:source_sha) { 'f001' } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when the source sha is not a descendant of the branch head' do + let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + + it "doesn't perform the ff_merge" do + expect { subject }.to raise_error(Gitlab::Git::CommitError) + + expect(repository.commit(target_branch).id).to eq(branch_head) + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index b32dd31ae6d..47eb0717c0c 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -40,4 +40,12 @@ describe Email do expect(user.emails.confirmed.count).to eq 1 end end + + describe 'delegation' do + let(:user) { create(:user) } + + it 'delegates to :user' do + expect(build(:email, user: user).username).to eq user.username + end + end end diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb new file mode 100644 index 00000000000..8548fff5c76 --- /dev/null +++ b/spec/models/instance_configuration_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +RSpec.describe InstanceConfiguration do + context 'without cache' do + describe '#settings' do + describe '#ssh_algorithms_hashes' do + let(:md5) { '54:e0:f8:70:d6:4f:4c:b1:b3:02:44:77:cf:cd:0d:fc' } + let(:sha256) { '9327f0d15a48c4d9f6a3aee65a1825baf9a3412001c98169c5fd022ac27762fc' } + + it 'does not return anything if file does not exist' do + stub_pub_file(exist: false) + + expect(subject.settings[:ssh_algorithms_hashes]).to be_empty + end + + it 'does not return anything if file is empty' do + stub_pub_file + + allow(File).to receive(:read).and_return('') + + expect(subject.settings[:ssh_algorithms_hashes]).to be_empty + end + + it 'returns the md5 and sha256 if file valid and exists' do + stub_pub_file + + result = subject.settings[:ssh_algorithms_hashes].select { |o| o[:md5] == md5 && o[:sha256] == sha256 } + + expect(result.size).to eq(InstanceConfiguration::SSH_ALGORITHMS.size) + end + + def stub_pub_file(exist: true) + path = 'spec/fixtures/ssh_host_example_key.pub' + path << 'random' unless exist + allow(subject).to receive(:ssh_algorithm_file).and_return(Rails.root.join(path)) + end + end + + describe '#host' do + it 'returns current instance host' do + allow(Settings.gitlab).to receive(:host).and_return('exampledomain') + + expect(subject.settings[:host]).to eq(Settings.gitlab.host) + end + end + + describe '#gitlab_pages' do + let(:gitlab_pages) { subject.settings[:gitlab_pages] } + it 'returns Settings.pages' do + gitlab_pages.delete(:ip_address) + + expect(gitlab_pages).to eq(Settings.pages.symbolize_keys) + end + + it 'returns the Gitlab\'s pages host ip address' do + expect(gitlab_pages.keys).to include(:ip_address) + end + + it 'returns the ip address as nil if the domain is invalid' do + allow(Settings.pages).to receive(:host).and_return('exampledomain') + + expect(gitlab_pages[:ip_address]).to eq nil + end + + it 'returns the ip address of the domain' do + allow(Settings.pages).to receive(:host).and_return('localhost') + + expect(gitlab_pages[:ip_address]).to eq('127.0.0.1').or eq('::1') + end + end + + describe '#gitlab_ci' do + let(:gitlab_ci) { subject.settings[:gitlab_ci] } + it 'returns Settings.gitalb_ci' do + gitlab_ci.delete(:artifacts_max_size) + + expect(gitlab_ci).to eq(Settings.gitlab_ci.symbolize_keys) + end + + it 'returns the key artifacts_max_size' do + expect(gitlab_ci.keys).to include(:artifacts_max_size) + end + end + end + end + + context 'with cache', :use_clean_rails_memory_store_caching do + it 'caches settings content' do + expect(Rails.cache.read(described_class::CACHE_KEY)).to be_nil + + settings = subject.settings + + expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(settings) + end + + describe 'cached settings' do + before do + subject.settings + end + + it 'expires after EXPIRATION_TIME' do + allow(Time).to receive(:now).and_return(Time.now + described_class::EXPIRATION_TIME) + Rails.cache.cleanup + + expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(nil) + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 73e038b61ed..476a2697605 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -86,7 +86,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do - project.repository.raw_repository.delete_branch(subject.target_branch) + project.repository.rm_branch(subject.author, subject.target_branch) end it 'returns nil' do @@ -1388,7 +1388,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do - subject.project.repository.raw_repository.delete_branch(subject.target_branch) + subject.project.repository.rm_branch(subject.author, subject.target_branch) end it 'returns nil' do @@ -1460,6 +1460,12 @@ describe MergeRequest do end describe '#merge_ongoing?' do + it 'returns true when the merge request is locked' do + merge_request = build_stubbed(:merge_request, state: :locked) + + expect(merge_request.merge_ongoing?).to be(true) + end + it 'returns true when merge_id, MR is not merged and it has no running job' do merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo') allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 63bf131cfc5..ad22fb2a386 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -24,6 +24,8 @@ describe JiraService do end it { is_expected.not_to validate_presence_of(:url) } + it { is_expected.not_to validate_presence_of(:username) } + it { is_expected.not_to validate_presence_of(:password) } end context 'validating urls' do @@ -54,6 +56,18 @@ describe JiraService do expect(service).not_to be_valid end + it 'is not valid when username is missing' do + service.username = nil + + expect(service).not_to be_valid + end + + it 'is not valid when password is missing' do + service.password = nil + + expect(service).not_to be_valid + end + it 'is valid when api url is a valid url' do service.api_url = 'http://jira.test.com/api' diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 2298dcab55f..00de536a18b 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -99,45 +99,34 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe '#actual_namespace' do subject { service.actual_namespace } - it "returns the default namespace" do - is_expected.to eq(service.send(:default_namespace)) - end - - context 'when namespace is specified' do - before do - service.namespace = 'my-namespace' + shared_examples 'a correctly formatted namespace' do + it 'returns a valid Kubernetes namespace name' do + expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex) + expect(subject).to eq(expected_namespace) end + end - it "returns the user-namespace" do - is_expected.to eq('my-namespace') - end + it_behaves_like 'a correctly formatted namespace' do + let(:expected_namespace) { service.send(:default_namespace) } end - context 'when service is not assigned to project' do + context 'when the project path contains forbidden characters' do before do - service.project = nil + project.path = '-a_Strange.Path--forSure' end - it "does not return namespace" do - is_expected.to be_nil + it_behaves_like 'a correctly formatted namespace' do + let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" } end end - end - - describe '#actual_namespace' do - subject { service.actual_namespace } - - it "returns the default namespace" do - is_expected.to eq(service.send(:default_namespace)) - end context 'when namespace is specified' do before do service.namespace = 'my-namespace' end - it "returns the user-namespace" do - is_expected.to eq('my-namespace') + it_behaves_like 'a correctly formatted namespace' do + let(:expected_namespace) { 'my-namespace' } end end @@ -146,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do service.project = nil end - it "does not return namespace" do + it 'does not return namespace' do is_expected.to be_nil end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 39d188f18af..d7c07676911 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -299,6 +299,24 @@ describe Repository do it { is_expected.to be_falsey } end + + context 'when pre-loaded merged branches are provided' do + using RSpec::Parameterized::TableSyntax + + where(:branch, :pre_loaded, :expected) do + 'not-merged-branch' | ['branch-merged'] | false + 'branch-merged' | ['not-merged-branch'] | false + 'branch-merged' | ['branch-merged'] | true + 'not-merged-branch' | ['not-merged-branch'] | false + 'master' | ['master'] | false + end + + with_them do + subject { repository.merged_to_root_ref?(branch, pre_loaded) } + + it { is_expected.to eq(expected) } + end + end end describe '#can_be_merged?' do @@ -2110,19 +2128,41 @@ describe Repository do end describe '#cache_method_output', :use_clean_rails_memory_store_caching do + let(:fallback) { 10 } + context 'with a non-existing repository' do - let(:value) do - repository.cache_method_output(:cats, fallback: 10) do - raise Rugged::ReferenceError + let(:project) { create(:project) } # No repository + + subject do + repository.cache_method_output(:cats, fallback: fallback) do + repository.cats_call_stub end end - it 'returns a fallback value' do - expect(value).to eq(10) + it 'returns the fallback value' do + expect(subject).to eq(fallback) + end + + it 'avoids calling the original method' do + expect(repository).not_to receive(:cats_call_stub) + + subject + end + end + + context 'with a method throwing a non-existing-repository error' do + subject do + repository.cache_method_output(:cats, fallback: fallback) do + raise Gitlab::Git::Repository::NoRepository + end + end + + it 'returns the fallback value' do + expect(subject).to eq(fallback) end it 'does not cache the data' do - value + subject expect(repository.instance_variable_defined?(:@cats)).to eq(false) expect(repository.send(:cache).exist?(:cats)).to eq(false) @@ -2238,4 +2278,24 @@ describe Repository do end end end + + describe 'commit cache' do + set(:project) { create(:project, :repository) } + + it 'caches based on SHA' do + # Gets the commit oid, and warms the cache + oid = project.commit.id + + expect(Gitlab::Git::Commit).not_to receive(:find).once + + project.commit_by(oid: oid) + end + + it 'caches nil values' do + expect(Gitlab::Git::Commit).to receive(:find).once + + project.commit_by(oid: '1' * 40) + project.commit_by(oid: '1' * 40) + end + end end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 5fd76fce7df..47f4ccd4887 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -385,7 +385,7 @@ describe API::Runner do end context 'when job is made for tag' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } it 'sets branch as ref_type' do request_job @@ -436,8 +436,8 @@ describe API::Runner do end context 'when project and pipeline have multiple jobs' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } before do @@ -458,7 +458,7 @@ describe API::Runner do end context 'when pipeline have jobs with artifacts' do - let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } before do @@ -478,8 +478,8 @@ describe API::Runner do end context 'when explicit dependencies are defined' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) do create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', stage: 'deploy', stage_idx: 1, @@ -502,8 +502,8 @@ describe API::Runner do end context 'when dependencies is an empty array' do - let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:empty_dependencies_job) do create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', stage: 'deploy', stage_idx: 1, diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index d1043f99b5a..ac196e92601 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -12,55 +12,6 @@ describe MergeRequests::MergeService do end describe '#execute' do - context 'MergeRequest#merge_jid' do - let(:service) do - described_class.new(project, user, commit_message: 'Awesome message') - end - - before do - merge_request.update_column(:merge_jid, 'hash-123') - end - - it 'is cleaned when no error is raised' do - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is cleaned when expected error is raised' do - allow(service).to receive(:commit).and_raise(described_class::MergeError) - - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is cleaned when merge request is not mergeable' do - allow(merge_request).to receive(:mergeable?).and_return(false) - - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is cleaned when no source is found' do - allow(merge_request).to receive(:diff_head_sha).and_return(nil) - - service.execute(merge_request) - - expect(merge_request.reload.merge_jid).to be_nil - end - - it 'is not cleaned when unexpected error is raised' do - service = described_class.new(project, user, commit_message: 'Awesome message') - allow(service).to receive(:commit).and_raise(StandardError) - - expect { service.execute(merge_request) }.to raise_error(StandardError) - - expect(merge_request.reload.merge_jid).to be_present - end - end - context 'valid params' do let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 8b5d9187785..8f7aea533dc 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -63,6 +63,12 @@ describe SystemHooksService do :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end + + it 'includes the correct project visibility level' do + data = event_data(project, :create) + + expect(data[:project_visibility]).to eq('private') + end end context 'event names' do diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index 0b5f66597fd..88a7aeba461 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -6,6 +6,8 @@ module JiraServiceHelper properties = { title: "JIRA tracker", url: JIRA_URL, + username: 'jira-user', + password: 'my-secret-password', project_key: "JIRA", jira_issue_transition_id: '1' } diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb index 50a1d4a56e2..1490287681b 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/update_invalid_issuable.rb @@ -25,13 +25,11 @@ shared_examples 'update invalid issuable' do |klass| .and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) end - if klass == MergeRequest - it 'renders edit when format is html' do - put :update, params + it 'renders edit when format is html' do + put :update, params - expect(response).to render_template(:edit) - expect(assigns[:conflict]).to be_truthy - end + expect(response).to render_template(:edit) + expect(assigns[:conflict]).to be_truthy end it 'renders json error message when format is json' do @@ -44,17 +42,16 @@ shared_examples 'update invalid issuable' do |klass| end end - if klass == MergeRequest - context 'when updating an invalid issuable' do - before do - params[:merge_request][:title] = "" - end + context 'when updating an invalid issuable' do + before do + key = klass == Issue ? :issue : :merge_request + params[key][:title] = "" + end - it 'renders edit when merge request is invalid' do - put :update, params + it 'renders edit when merge request is invalid' do + put :update, params - expect(response).to render_template(:edit) - end + expect(response).to render_template(:edit) end end end diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index c030129559e..0a78606171d 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -25,6 +25,14 @@ describe 'help/index' do end end + describe 'instance configuration link' do + it 'is visible to guests' do + render + + expect(rendered).to have_link(nil, help_instance_configuration_url) + end + end + def stub_user(user = double) allow(view).to receive(:user_signed_in?).and_return(user) end diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb new file mode 100644 index 00000000000..f30b5881fde --- /dev/null +++ b/spec/views/help/instance_configuration.html.haml_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe 'help/instance_configuration' do + describe 'General Sections:' do + let(:instance_configuration) { build(:instance_configuration)} + let(:settings) { instance_configuration.settings } + let(:ssh_settings) { settings[:ssh_algorithms_hashes] } + + before do + assign(:instance_configuration, instance_configuration) + end + + it 'has links to several sections' do + render + + expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_link(nil, '#gitlab-pages') + expect(rendered).to have_link(nil, '#gitlab-ci') + end + + it 'has several sections' do + render + + expect(rendered).to have_css('h2#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_css('h2#gitlab-pages') + expect(rendered).to have_css('h2#gitlab-ci') + end + end +end diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb index a5ad78393c9..f8b55e873df 100644 --- a/spec/workers/stuck_merge_jobs_worker_spec.rb +++ b/spec/workers/stuck_merge_jobs_worker_spec.rb @@ -12,8 +12,13 @@ describe StuckMergeJobsWorker do worker.perform - expect(mr_with_sha.reload).to be_merged - expect(mr_without_sha.reload).to be_opened + mr_with_sha.reload + mr_without_sha.reload + + expect(mr_with_sha).to be_merged + expect(mr_without_sha).to be_opened + expect(mr_with_sha.merge_jid).to be_present + expect(mr_without_sha.merge_jid).to be_nil end it 'updates merge request to opened when locked but has not been merged' do |