diff options
40 files changed, 537 insertions, 278 deletions
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 7d588e8eee6..1ec950494ff 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -74,7 +74,9 @@ case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); - new gl.IssuableBulkActions(); + new gl.IssuableBulkActions({ + prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_' + }); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:issues:show': @@ -144,10 +146,6 @@ new ZenMode(); new MergedButtons(); break; - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': new gl.Activities(); break; diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6 new file mode 100644 index 00000000000..70a2d765abd --- /dev/null +++ b/app/assets/javascripts/extensions/object.js.es6 @@ -0,0 +1,26 @@ +/* eslint-disable no-restricted-syntax */ + +// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +if (typeof Object.assign !== 'function') { + Object.assign = function assign(target, ...args) { + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 0; index < args.length; index += 1) { + const nextSource = args[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (const nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 245383438d1..90010b9fd54 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -67,14 +67,15 @@ // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - var _a, _y, regexp, match; + var _a, _y, regexp, match, atSymbols; + atSymbols = Object.keys(this.app.controllers).join('|'); subtext = subtext.split(' ').pop(); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi'); + regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); match = regexp.exec(subtext); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 68a345d83f9..57dabfe05e4 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -23,7 +23,6 @@ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; $inputContainer = this.input.parent(); $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - this.indeterminateIds = []; $clearButton.on('click', (function(_this) { // Clear click return function(e) { @@ -348,12 +347,12 @@ $el = $(this); selected = self.rowClicked($el); if (self.options.clicked) { - self.options.clicked(selected, $el, e); + self.options.clicked(selected[0], $el, e, selected[1]); } // Update label right after all modifications in dropdown has been done if (self.options.toggleLabel) { - self.updateLabel(selected, $el, self); + self.updateLabel(selected[0], $el, self); } $el.trigger('blur'); @@ -444,12 +443,6 @@ this.resetRows(); this.addArrowKeyEvent(); - if (this.options.setIndeterminateIds) { - this.options.setIndeterminateIds.call(this); - } - if (this.options.setActiveIds) { - this.options.setActiveIds.call(this); - } // Makes indeterminate items effective if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { this.parseData(this.fullData); @@ -483,11 +476,6 @@ if (this.options.filterable) { $input.blur().val(""); } - // Triggering 'keyup' will re-render the dropdown which is not always required - // specially if we want to keep the state of the dropdown needed for bulk-assignment - if (!this.options.persistWhenHide) { - $input.trigger("input"); - } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); } @@ -620,7 +608,8 @@ }; GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value; + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; + fieldName = this.options.fieldName; isInput = $(this.el).is('input'); if (this.renderedData) { @@ -641,7 +630,7 @@ el.addClass(ACTIVE_CLASS); } - return selectedObject; + return [selectedObject]; } field = []; @@ -659,6 +648,7 @@ } if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { if (isInput) { @@ -668,6 +658,7 @@ } } } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); if (field && field.length && value == null) { @@ -677,6 +668,7 @@ this.addInput(fieldName, value, selectedObject); } } else { + isMarking = true; if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); if (!isInput) { @@ -697,7 +689,7 @@ } } - return selectedObject; + return [selectedObject, isMarking]; }; GitLabDropdown.prototype.focusTextInput = function() { diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index b174eb2ff96..1c10a7445bb 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -144,6 +144,9 @@ const $issuesOtherFilters = $('.issues-other-filters'); const $issuesBulkUpdate = $('.issues_bulk_update'); + this.issuableBulkActions.willUpdateLabels = false; + this.issuableBulkActions.setOriginalDropdownData(); + if ($checkedIssues.length > 0) { let ids = $.map($checkedIssues, function(value) { return $(value).data('id'); @@ -155,7 +158,6 @@ $updateIssuesIds.val([]); $issuesBulkUpdate.hide(); $issuesOtherFilters.show(); - this.issuableBulkActions.willUpdateLabels = false; } return true; }, diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index ad25104152c..1c8e5dede6f 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -5,9 +5,10 @@ ((global) => { class IssuableBulkActions { - constructor({ container, form, issues } = {}) { - this.container = container || $('.content'), + constructor({ container, form, issues, prefixId } = {}) { + this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); + this.$labelDropdown = this.form.find('.js-label-select'); this.issues = issues || this.getElement('.issues-list .issue'); this.form.data('bulkActions', this); this.willUpdateLabels = false; @@ -16,10 +17,6 @@ Issuable.initChecks(); } - getElement(selector) { - return this.container.find(selector); - } - bindEvents() { return this.form.off('submit').on('submit', this.onFormSubmit.bind(this)); } @@ -73,10 +70,7 @@ getUnmarkedIndeterminedLabels() { const result = []; - const labelsToKeep = []; - - this.getElement('.labels-filter .is-indeterminate') - .each((i, el) => labelsToKeep.push($(el).data('labelId'))); + const labelsToKeep = this.$labelDropdown.data('indeterminate'); this.getLabelsFromSelection().forEach((id) => { if (labelsToKeep.indexOf(id) === -1) { @@ -106,45 +100,65 @@ } }; if (this.willUpdateLabels) { - this.getLabelsToApply().map(function(id) { - return formData.update.add_label_ids.push(id); - }); - this.getLabelsToRemove().map(function(id) { - return formData.update.remove_label_ids.push(id); - }); + formData.update.add_label_ids = this.$labelDropdown.data('marked'); + formData.update.remove_label_ids = this.$labelDropdown.data('unmarked'); } return formData; } - getLabelsToApply() { - const labelIds = []; - const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]'); - $labels.each(function(k, label) { - if (label) { - return labelIds.push(parseInt($(label).val())); - } - }); - return labelIds; + setOriginalDropdownData() { + let $labelSelect = $('.bulk-update .js-label-select'); + $labelSelect.data('common', this.getOriginalCommonIds()); + $labelSelect.data('marked', this.getOriginalMarkedIds()); + $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds()); } + // From issuable's initial bulk selection + getOriginalCommonIds() { + let labelIds = []; - /** - * Returns Label IDs that will be removed from issue selection - * @return {Array} Array of labels IDs - */ + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); + }); + return _.intersection.apply(this, labelIds); + } - getLabelsToRemove() { - const result = []; - const indeterminatedLabels = this.getUnmarkedIndeterminedLabels(); - const labelsToApply = this.getLabelsToApply(); - indeterminatedLabels.map(function(id) { - // We need to exclude label IDs that will be applied - // By not doing this will cause issues from selection to not add labels at all - if (labelsToApply.indexOf(id) === -1) { - return result.push(id); - } + // From issuable's initial bulk selection + getOriginalMarkedIds() { + var labelIds = []; + this.getElement('.selected_issue:checked').each((i, el) => { + labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); - return result; + return _.intersection.apply(_, labelIds); + } + + // From issuable's initial bulk selection + getOriginalIndeterminateIds() { + let uniqueIds = []; + let labelIds = []; + let issuableLabels = []; + + // Collect unique label IDs for all checked issues + this.getElement('.selected_issue:checked').each((i, el) => { + issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); + issuableLabels.forEach((labelId) => { + // Store unique IDs + if (uniqueIds.indexOf(labelId) === -1) { + uniqueIds.push(labelId); + } + }); + // Store array of IDs per issuable + labelIds.push(issuableLabels); + }); + // Add uniqueIds to add it as argument for _.intersection + labelIds.unshift(uniqueIds); + // Return IDs that are present but not in all selected issueables + return _.difference(uniqueIds, _.intersection.apply(this, labelIds)); + } + + getElement(selector) { + this.scopeEl = this.scopeEl || $('.content'); + return this.scopeEl.find(selector); } } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index d29a5edb9ad..6853d6b9db2 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -8,8 +8,9 @@ var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container; $dropdown = $(dropdown); + $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); namespacePath = $dropdown.data('namespace-path'); projectPath = $dropdown.data('project-path'); @@ -125,7 +126,7 @@ }); }); }; - return $dropdown.glDropdown({ + $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { return $.ajax({ @@ -172,33 +173,40 @@ }); }, renderRow: function(label, instance) { - var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing; + var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { - indeterminate = instance.indeterminateIds; - active = instance.activeIds; + indeterminate = $dropdown.data('indeterminate') || []; + marked = $dropdown.data('marked') || []; + if (indeterminate.indexOf(label.id) !== -1) { selectedClass.push('is-indeterminate'); } - if (active.indexOf(label.id) !== -1) { + + if (marked.indexOf(label.id) !== -1) { // Remove is-indeterminate class if the item will be marked as active i = selectedClass.indexOf('is-indeterminate'); if (i !== -1) { selectedClass.splice(i, 1); } selectedClass.push('is-active'); - // Add input manually - instance.addInput(this.fieldName, label.id); } - } - if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) { - selectedClass.push('is-active'); - } - if ($dropdown.hasClass('js-multiselect') && removesAll) { - selectedClass.push('dropdown-clear-active'); + } else { + if (this.id(label)) { + dropdownName = $dropdown.data('fieldName'); + dropdownValue = this.id(label).toString().replace(/'/g, '\\\''); + + if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) { + selectedClass.push('is-active'); + } + } + + if ($dropdown.hasClass('js-multiselect') && removesAll) { + selectedClass.push('dropdown-clear-active'); + } } if (label.duplicate) { spacing = 100 / label.color.length; @@ -234,7 +242,6 @@ // Return generated html return $li.html($a).prop('outerHTML'); }, - persistWhenHide: $dropdown.data('persistWhenHide'), search: { fields: ['title'] }, @@ -313,18 +320,15 @@ } } } - if ($dropdown.hasClass('js-filter-bulk-update')) { - // If we are persisting state we need the classes - if (!this.options.persistWhenHide) { - return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass(); - } - } }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(label, $el, e) { + clicked: function(label, $el, e, isMarking) { var isIssueIndex, isMRIndex, page; - _this.enableBulkLabelDropdown(); + + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { $dropdown.parent() @@ -333,12 +337,11 @@ } if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + _this.enableBulkLabelDropdown(); + _this.setDropdownData($dropdown, isMarking, this.id(label)); return; } - page = $('body').data('page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = page === 'projects:merge_requests:index'; if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; @@ -400,17 +403,10 @@ } } }, - setIndeterminateIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.indeterminateIds = _this.getIndeterminateIds(); - } - }, - setActiveIds: function() { - if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - return this.activeIds = _this.getActiveIds(); - } - } }); + + // Set dropdown data + _this.setOriginalDropdownData($dropdownContainer, $dropdown); }); this.bindEvents(); } @@ -423,34 +419,9 @@ if ($('.selected_issue:checked').length) { return; } - // Remove inputs - $('.issues_bulk_update .labels-filter input[type="hidden"]').remove(); - // Also restore button text return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label'); }; - LabelsSelect.prototype.getIndeterminateIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.flatten(label_ids); - }; - - LabelsSelect.prototype.getActiveIds = function() { - var label_ids; - label_ids = []; - $('.selected_issue:checked').each(function(i, el) { - var issue_id; - issue_id = $(el).data('id'); - return label_ids.push($("#issue_" + issue_id).data('labels')); - }); - return _.intersection.apply(_, label_ids); - }; - LabelsSelect.prototype.enableBulkLabelDropdown = function() { var issuableBulkActions; if ($('.selected_issue:checked').length) { @@ -459,8 +430,59 @@ } }; - return LabelsSelect; + LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { + var i, markedIds, unmarkedIds, indeterminateIds; + var issuableBulkActions = $('.bulk-update').data('bulkActions'); + + markedIds = $dropdown.data('marked') || []; + unmarkedIds = $dropdown.data('unmarked') || []; + indeterminateIds = $dropdown.data('indeterminate') || []; + + if (isMarking) { + markedIds.push(value); + + i = indeterminateIds.indexOf(value); + if (i > -1) { + indeterminateIds.splice(i, 1); + } + + i = unmarkedIds.indexOf(value); + if (i > -1) { + unmarkedIds.splice(i, 1); + } + } else { + // If marked item (not common) is unmarked + i = markedIds.indexOf(value); + if (i > -1) { + markedIds.splice(i, 1); + } + + // If an indeterminate item is being unmarked + if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + + // If a marked item is being unmarked + // (a marked item could also be a label that is present in all selection) + if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) { + unmarkedIds.push(value); + } + } + $dropdown.data('marked', markedIds); + $dropdown.data('unmarked', unmarkedIds); + $dropdown.data('indeterminate', indeterminateIds); + }; + + LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) { + var labels = []; + $container.find('[name="label_name[]"]').map(function() { + return labels.push(this.value); + }); + $dropdown.data('marked', labels); + }; + + return LabelsSelect; })(); }).call(this); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ecd540793d0..11adf2568a1 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -188,7 +188,6 @@ &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; - outline: 0; } &.dropdown-menu-empty-link { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 54f73171fd4..48354cdbefb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -88,8 +88,24 @@ module Ci end # ref can't be HEAD or SHA, can only be branch/tag name + scope :latest, ->(ref = nil) do + max_id = unscope(:select) + .select("max(#{quoted_table_name}.id)") + .group(:ref, :sha) + + if ref + where(id: max_id, ref: ref) + else + where(id: max_id) + end + end + + def self.latest_status(ref = nil) + latest(ref).status + end + def self.latest_successful_for(ref) - where(ref: ref).order(id: :desc).success.first + success.latest(ref).first end def self.truncate_sha(sha) diff --git a/app/models/commit.rb b/app/models/commit.rb index 1831cc7e175..69cfc47f5bf 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -228,13 +228,9 @@ class Commit def status(ref = nil) @statuses ||= {} - if @statuses.key?(ref) - @statuses[ref] - elsif ref - @statuses[ref] = pipelines.where(ref: ref).status - else - @statuses[ref] = pipelines.status - end + return @statuses[ref] if @statuses.key?(ref) + + @statuses[ref] = pipelines.latest_status(ref) end def revert_branch_name @@ -270,7 +266,7 @@ class Commit @merged_merge_request_hash ||= Hash.new do |hash, user| hash[user] = merged_merge_request_no_cache(user) end - + @merged_merge_request_hash[current_user] end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 75d847d5bee..240ddabec36 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,13 +1,13 @@ module Ci class ImageForBuildService def execute(project, opts) - sha = opts[:sha] || ref_sha(project, opts[:ref]) - + ref = opts[:ref] + sha = opts[:sha] || ref_sha(project, ref) pipelines = project.pipelines.where(sha: sha) - pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(pipelines.status) + image_name = image_for_status(pipelines.latest_status(ref)) image_path = Rails.root.join('public/ci', image_name) + OpenStruct.new(path: image_path, name: image_name) end diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index ea95e91eada..e13f404fee2 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -32,7 +32,7 @@ - if params[:project_id].present? = hidden_field_tag(:project_id, params[:project_id]) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } }) .filter-item.inline - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) @@ -42,12 +42,12 @@ - if params[:type].present? = hidden_field_tag(:type, params[:type]) = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) + data: { data: todo_types_options, default_label: 'Type' } }) .filter-item.inline.actions-filter - if params[:action_id].present? = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) + data: { data: todo_actions_options, default_label: 'Action' } }) .pull-right .dropdown.inline.prepend-left-10 %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'} diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 959c796ecec..b3c43286a50 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,4 @@ -%li{ class: mr_css_classes(merge_request) } +%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } - if @bulk_edit .issue-check = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a0e12fb3f38..baa1ade5eee 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,6 +1,2 @@ -.content-block.oneline-block - = icon("sort-amount-desc") - Most recent commits displayed first - %ol#commits-list.list-unstyled = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml new file mode 100644 index 00000000000..66970c2a9a5 --- /dev/null +++ b/changelogs/unreleased/24824-dropdown-items-focus.yml @@ -0,0 +1,4 @@ +--- +title: Add focus state to dropdown items +merge_request: +author: diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml new file mode 100644 index 00000000000..cc7c2604824 --- /dev/null +++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml @@ -0,0 +1,4 @@ +--- +title: Improve bulk assignment for issuables +merge_request: +author: diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml new file mode 100644 index 00000000000..531b0f83099 --- /dev/null +++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml @@ -0,0 +1,4 @@ +--- +title: Improve help message for issue create slash command +merge_request: 7850 +author: diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml new file mode 100644 index 00000000000..c6a92547c5c --- /dev/null +++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml @@ -0,0 +1,4 @@ +--- +title: Added go back anchor on error pages. +merge_request: 8087 +author: diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml new file mode 100644 index 00000000000..5d0adb04ef3 --- /dev/null +++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml @@ -0,0 +1,4 @@ +--- +title: 25617 Fix placeholder color of todo filters +merge_request: +author: diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml new file mode 100644 index 00000000000..11250643a23 --- /dev/null +++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml @@ -0,0 +1,4 @@ +--- +title: Move admin active tab spinach tests to rspec +merge_request: 8037 +author: Semyon Pupkov diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml new file mode 100644 index 00000000000..754af641add --- /dev/null +++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary commits order message +merge_request: 8004 +author: diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml new file mode 100644 index 00000000000..bbd7a217493 --- /dev/null +++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml @@ -0,0 +1,4 @@ +--- +title: Show commit status from latest pipeline +merge_request: 7333 +author: diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 6fcbf6f1642..1a7c13a29b4 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -65,7 +65,7 @@ the administrator console. ### Step 3. Create a new custom slash command in Mattermost -Now that you have enabled the custom slash commands in Mattermost and opened +Now that you have enabled custom slash commands in Mattermost and opened the Mattermost slash commands service in GitLab, it's time to copy these values in a new slash command. @@ -128,20 +128,16 @@ GitLab using the Mattermost commands. ## Available slash commands -The available slash commands so far are: +The available slash commands are: | Command | Description | Example | | ------- | ----------- | ------- | -| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` | -| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` | -| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` | +| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | -To see a list of available commands that can interact with GitLab, type the -trigger word followed by `help`: - -``` -/my-project help -``` +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> ![Mattermost bot available commands](img/mattermost_bot_available_commands.png) diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature deleted file mode 100644 index f5bb06dea7d..00000000000 --- a/features/admin/active_tab.feature +++ /dev/null @@ -1,54 +0,0 @@ -@admin -Feature: Admin Active Tab - Background: - Given I sign in as an admin - - Scenario: On Admin Home - Given I visit admin page - Then the active main tab should be Overview - And no other main tabs should be active - - Scenario: On Admin Projects - Given I visit admin projects page - Then the active main tab should be Overview - And the active sub tab should be Projects - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Groups - Given I visit admin groups page - Then the active main tab should be Overview - And the active sub tab should be Groups - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Users - Given I visit admin users page - Then the active main tab should be Overview - And the active sub tab should be Users - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Logs - Given I visit admin logs page - Then the active main tab should be Monitoring - And the active sub tab should be Logs - And no other main tabs should be active - And no other sub tabs should be active - - Scenario: On Admin Messages - Given I visit admin messages page - Then the active main tab should be Messages - And no other main tabs should be active - - Scenario: On Admin Hooks - Given I visit admin hooks page - Then the active main tab should be Hooks - And no other main tabs should be active - - Scenario: On Admin Resque - Given I visit admin Resque page - Then the active main tab should be Monitoring - And the active sub tab should be Resque - And no other main tabs should be active - And no other sub tabs should be active diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb deleted file mode 100644 index 9b1689a8198..00000000000 --- a/features/steps/admin/active_tab.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Overview' do - ensure_active_main_tab('Overview') - end - - step 'the active sub tab should be Projects' do - ensure_active_sub_tab('Projects') - end - - step 'the active sub tab should be Groups' do - ensure_active_sub_tab('Groups') - end - - step 'the active sub tab should be Users' do - ensure_active_sub_tab('Users') - end - - step 'the active main tab should be Hooks' do - ensure_active_main_tab('Hooks') - end - - step 'the active main tab should be Monitoring' do - ensure_active_main_tab('Monitoring') - end - - step 'the active sub tab should be Resque' do - ensure_active_sub_tab('Background Jobs') - end - - step 'the active sub tab should be Logs' do - ensure_active_sub_tab('Logs') - end - - step 'the active main tab should be Messages' do - ensure_active_main_tab('Messages') - end -end diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb index 50aa45e5406..b762d85b6e5 100644 --- a/lib/gitlab/badge/build/status.rb +++ b/lib/gitlab/badge/build/status.rb @@ -20,8 +20,8 @@ module Gitlab def status @project.pipelines - .where(sha: @sha, ref: @ref) - .status || 'unknown' + .where(sha: @sha) + .latest_status(@ref) || 'unknown' end def metadata diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 1dba85c1b51..cefb6775db8 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -8,7 +8,7 @@ module Gitlab end def self.help_message - 'issue new <title>\n<description>' + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' end def self.allowed?(project, user) diff --git a/public/404.html b/public/404.html index 11b29d09a82..b3b3a0fa3f3 100644 --- a/public/404.html +++ b/public/404.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure the address is correct and that the page hasn't moved.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/422.html b/public/422.html index 9bd7cb4b7c8..119e54ad8bd 100644 --- a/public/422.html +++ b/public/422.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Make sure you have access to the thing you tried to change.</p> <p>Please contact your GitLab administrator if you think this is a mistake.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/500.html b/public/500.html index f92e8839f8d..226ef3c40ea 100644 --- a/public/500.html +++ b/public/500.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/502.html b/public/502.html index c2be4f130a9..f037b81bace 100644 --- a/public/502.html +++ b/public/502.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/public/503.html b/public/503.html index 8850ffce362..f946a087871 100644 --- a/public/503.html +++ b/public/503.html @@ -46,6 +46,14 @@ margin: 40px auto; } + a { + line-height: 100px; + font-weight: normal; + color: #4A8BEE; + font-size: 18px; + text-decoration: none; + } + .container { margin: auto 20px; } @@ -63,6 +71,7 @@ <hr /> <p>Try refreshing the page, or going back and attempting the action again.</p> <p>Please contact your GitLab administrator if this problem persists.</p> + <a href="javascript:history.back()">Go back</a> </div> </body> </html> diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb new file mode 100644 index 00000000000..f2eecc5b552 --- /dev/null +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +RSpec.describe 'admin active tab' do + before do + login_as :admin + end + + shared_examples 'page has active tab' do |title| + it "activates #{title} tab" do + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) + expect(page.find('.layout-nav li.active')).to have_content(title) + end + end + + shared_examples 'page has active sub tab' do |title| + it "activates #{title} sub tab" do + expect(page).to have_selector('.sub-nav li.active', count: 1) + expect(page.find('.sub-nav li.active')).to have_content(title) + end + end + + context 'on home page' do + before do + visit admin_root_path + end + + it_behaves_like 'page has active tab', 'Overview' + end + + context 'on projects' do + before do + visit admin_namespaces_projects_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Projects' + end + + context 'on groups' do + before do + visit admin_groups_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Groups' + end + + context 'on users' do + before do + visit admin_users_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Users' + end + + context 'on logs' do + before do + visit admin_logs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Logs' + end + + context 'on messages' do + before do + visit admin_broadcast_messages_path + end + + it_behaves_like 'page has active tab', 'Messages' + end + + context 'on hooks' do + before do + visit admin_hooks_path + end + + it_behaves_like 'page has active tab', 'Hooks' + end + + context 'on background jobs' do + before do + visit admin_background_jobs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Background Jobs' + end +end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index bc2c087c9b9..832757b24d4 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do let!(:issue2) { create(:issue, project: project, title: "Issue 2") } let!(:bug) { create(:label, project: project, title: 'bug') } let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:wontfix) { create(:label, project: project, title: 'wontfix') } context 'as an allowed user', js: true do before do @@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' end end + + # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877 + context 'unmarking common label' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'applies label from filtered results' do + check 'check_all_issues' + + page.within('.issues_bulk_update') do + click_button 'Labels' + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') + + click_link 'bug' + find('.dropdown-input-field', visible: true).set('wontfix') + click_link 'wontfix' + end + + update_issues + + page.within '.issues-holder' do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue1.id}")).to have_content 'wontfix' + + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'wontfix' + end + end + end end context 'as a guest' do @@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do - click_button 'Label' + click_button 'Labels' wait_for_ajax items.map do |item| click_link item diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index cd0512a37e6..da64827b377 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -89,4 +89,12 @@ feature 'GFM autocomplete', feature: true, js: true do end end end + + it 'doesnt open autocomplete after non-word character' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("@#{user.username[0..2]}!") + end + + expect(page).not_to have_selector('.atwho-view') + end end diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 new file mode 100644 index 00000000000..3b71c255b30 --- /dev/null +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -0,0 +1,25 @@ +/*= require extensions/object */ + +describe('Object extensions', () => { + describe('assign', () => { + it('merges source object into target object', () => { + const targetObj = {}; + const sourceObj = { + foo: 'bar', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('bar'); + }); + + it('merges object with the same properties', () => { + const targetObj = { + foo: 'bar', + }; + const sourceObj = { + foo: 'baz', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('baz'); + }); + }); +}); diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 38eebb2a176..70f03021d36 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do new_build.success! end - it 'reports the compound status' do - expect(badge.status).to eq 'failed' + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index e78ae14b737..52dd41065e9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do end end + shared_context 'with some outdated pipelines' do + before do + create_pipeline(:canceled, 'ref', 'A') + create_pipeline(:success, 'ref', 'A') + create_pipeline(:failed, 'ref', 'B') + create_pipeline(:skipped, 'feature', 'C') + end + + def create_pipeline(status, ref, sha) + create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + end + end + + describe '.latest' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:pipelines) { described_class.latest.all } + + it 'returns the latest pipeline for the same ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed', 'skipped') + end + end + + context 'when ref is specified' do + let(:pipelines) { described_class.latest('ref').all } + + it 'returns the latest pipeline for ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed') + end + end + end + + describe '.latest_status' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:latest_status) { described_class.latest_status } + + it 'returns the latest status for the same ref and different sha' do + expect(latest_status).to eq(described_class.latest.status) + expect(latest_status).to eq('failed') + end + end + + context 'when ref is specified' do + let(:latest_status) { described_class.latest_status('ref') } + + it 'returns the latest status for ref and different sha' do + expect(latest_status).to eq(described_class.latest_status('ref')) + expect(latest_status).to eq('failed') + end + end + end + describe '#status' do let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0935fc0561c..74b50d2908d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -213,23 +213,19 @@ eos end describe '#status' do - context 'without arguments for compound status' do - shared_examples 'giving the status from pipeline' do - it do - expect(commit.status).to eq(Ci::Pipeline.status) - end - end - - context 'with pipelines' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: commit.sha) + context 'without ref argument' do + before do + %w[success failed created pending].each do |status| + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: status) end - - it_behaves_like 'giving the status from pipeline' end - context 'without pipelines' do - it_behaves_like 'giving the status from pipeline' + it 'gives compound status from latest pipelines' do + expect(commit.status).to eq(Ci::Pipeline.latest_status) + expect(commit.status).to eq('pending') end end @@ -255,8 +251,9 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives compound status if ref is nil' do - expect(commit.status(nil)).to eq(commit.status) + it 'gives compound status from latest pipelines if ref is nil' do + expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) + expect(commit.status(nil)).to eq('failed') end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 893c6827a91..7d5ecfbaa64 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -272,7 +272,7 @@ describe Group, models: true do end describe 'nested group' do - subject { create(:group, :nested) } + subject { build(:group, :nested) } it { is_expected.to be_valid } it { expect(subject.parent).to be_kind_of(Group) } |