diff options
44 files changed, 481 insertions, 291 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 622764107ad..d9341837149 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,8 +1,10 @@ /* eslint-disable class-methods-use-this */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { s__ } from './locale'; import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; -import Flash from './flash'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -441,13 +443,15 @@ class AwardsHandler { if (this.isUserAuthored($emojiButton)) { this.userAuthored($emojiButton); } else { - $.post(awardUrl, { + axios.post(awardUrl, { name: emoji, - }, (data) => { + }) + .then(({ data }) => { if (data.ok) { callback(); } - }).fail(() => new Flash('Something went wrong on our end.')); + }) + .catch(() => flash(s__('Something went wrong on our end.'))); } } diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 144caf1d278..e2a008e8904 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ import { localTimeAgo } from './lib/utils/datetime_utility'; +import axios from './lib/utils/axios_utils'; export default class Compare { constructor(opts) { @@ -41,17 +42,14 @@ export default class Compare { } getTargetProject() { - return $.ajax({ - url: this.opts.targetProjectUrl, - data: { - target_project_id: $("input[name='merge_request[target_project_id]']").val() - }, - beforeSend: function() { - return $('.mr_target_commit').empty(); + $('.mr_target_commit').empty(); + + return axios.get(this.opts.targetProjectUrl, { + params: { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), }, - success: function(html) { - return $('.js-target-branch-dropdown .dropdown-content').html(html); - } + }).then(({ data }) => { + $('.js-target-branch-dropdown .dropdown-content').html(data); }); } @@ -68,22 +66,19 @@ export default class Compare { }); } - static sendAjax(url, loading, target, data) { - var $target; - $target = $(target); - return $.ajax({ - url: url, - data: data, - beforeSend: function() { - loading.show(); - return $target.empty(); - }, - success: function(html) { - loading.hide(); - $target.html(html); - var className = '.' + $target[0].className.replace(' ', '.'); - localTimeAgo($('.js-timeago', className)); - } + static sendAjax(url, loading, target, params) { + const $target = $(target); + + loading.show(); + $target.empty(); + + return axios.get(url, { + params, + }).then(({ data }) => { + loading.hide(); + $target.html(data); + const className = '.' + $target[0].className.replace(' ', '.'); + localTimeAgo($('.js-timeago', className)); }); } } diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index e633ef8a29e..59899e97be1 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,4 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; export default function initCompareAutocomplete() { $('.js-compare-dropdown').each(function() { @@ -10,15 +13,14 @@ export default function initCompareAutocomplete() { const $filterInput = $('input[type="search"]', $dropdownContainer); $dropdown.glDropdown({ data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { + axios.get($dropdown.data('refsUrl'), { + params: { ref: $dropdown.data('ref'), search: term, - } - }).done(function(refs) { - return callback(refs); - }); + }, + }).then(({ data }) => { + callback(data); + }).catch(() => flash(__('Error fetching refs'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index bc23a72762f..482d83621e2 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; @@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown { } checkAbilityToCreateBranch() { - return $.ajax({ - type: 'GET', - dataType: 'json', - url: this.canCreatePath, - beforeSend: () => this.setUnavailableButtonState(), - }) - .done((data) => { - this.setUnavailableButtonState(false); - - if (data.can_create_branch) { - this.available(); - this.enable(); - - if (!this.droplabInitialized) { - this.droplabInitialized = true; - this.initDroplab(); - this.bindEvents(); + this.setUnavailableButtonState(); + + axios.get(this.canCreatePath) + .then(({ data }) => { + this.setUnavailableButtonState(false); + + if (data.can_create_branch) { + this.available(); + this.enable(); + + if (!this.droplabInitialized) { + this.droplabInitialized = true; + this.initDroplab(); + this.bindEvents(); + } + } else if (data.has_related_branch) { + this.hide(); } - } else if (data.has_related_branch) { - this.hide(); - } - }).fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to check if a new branch can be created.'); - }); + }) + .catch(() => { + this.unavailable(); + this.disable(); + Flash('Failed to check if a new branch can be created.'); + }); } createBranch() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createBranchPath, - beforeSend: () => (this.isCreatingBranch = true), - }) - .done((data) => { - this.branchCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create a branch for this issue. Please try again.')); + this.isCreatingBranch = true; + + return axios.post(this.createBranchPath) + .then(({ data }) => { + this.branchCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create a branch for this issue. Please try again.')); } createMergeRequest() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createMrPath, - beforeSend: () => (this.isCreatingMergeRequest = true), - }) - .done((data) => { - this.mergeRequestCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create Merge Request. Please try again.')); + this.isCreatingMergeRequest = true; + + return axios.post(this.createMrPath) + .then(({ data }) => { + this.mergeRequestCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create Merge Request. Please try again.')); } disable() { @@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; - return $.ajax({ - method: 'GET', - dataType: 'json', - url: this.refsPath + ref, - beforeSend: () => { - this.isGettingRef = true; - }, - }) - .always(() => { - this.isGettingRef = false; - }) - .done((data) => { - const branches = data[Object.keys(data)[0]]; - const tags = data[Object.keys(data)[1]]; - let result; + return axios.get(this.refsPath + ref) + .then(({ data }) => { + const branches = data[Object.keys(data)[0]]; + const tags = data[Object.keys(data)[1]]; + let result; + + if (target === 'branch') { + result = CreateMergeRequestDropdown.findByValue(branches, ref); + } else { + result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || + CreateMergeRequestDropdown.findByValue(tags, ref, true); + this.suggestedRef = result; + } - if (target === 'branch') { - result = CreateMergeRequestDropdown.findByValue(branches, ref); - } else { - result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || - CreateMergeRequestDropdown.findByValue(tags, ref, true); - this.suggestedRef = result; - } + this.isGettingRef = false; - return this.updateInputState(target, ref, result); - }) - .fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to get ref.'); + return this.updateInputState(target, ref, result); + }) + .catch(() => { + this.unavailable(); + this.disable(); + new Flash('Failed to get ref.'); - return false; - }); + this.isGettingRef = false; + + return false; + }); } getTargetData(target) { @@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown { xhr = this.createBranch(); } - xhr.fail(() => { + xhr.catch(() => { this.isCreatingMergeRequest = false; this.isCreatingBranch = false; - }); - xhr.always(() => this.enable()); + this.enable(); + }); this.disable(); } diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 550dbdda922..ba89e5726fa 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,7 @@ import Dropzone from 'dropzone'; import _ from 'underscore'; import './preview_markdown'; import csrf from './lib/utils/csrf'; +import axios from './lib/utils/axios_utils'; Dropzone.autoDiscover = false; @@ -235,25 +236,21 @@ export default function dropzoneInput(form) { uploadFile = (item, filename) => { const formData = new FormData(); formData.append('file', item, filename); - return $.ajax({ - url: uploadsPath, - type: 'POST', - data: formData, - dataType: 'json', - processData: false, - contentType: false, - headers: csrf.headers, - beforeSend: () => { - showSpinner(); - return closeAlertMessage(); - }, - success: (e, text, response) => { - const md = response.responseJSON.link.markdown; + + showSpinner(); + closeAlertMessage(); + + axios.post(uploadsPath, formData) + .then(({ data }) => { + const md = data.link.markdown; + insertToTextArea(filename, md); - }, - error: response => showError(response.responseJSON.message), - complete: () => closeSpinner(), - }); + closeSpinner(); + }) + .catch((e) => { + showError(e.response.data.message); + closeSpinner(); + }); }; updateAttachingMessage = (files, messageContainer) => { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index ada985913bb..bd4c58b7cb1 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,6 +1,7 @@ /* global dateFormat */ import Pikaday from 'pikaday'; +import axios from './lib/utils/axios_utils'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; class DueDateSelect { @@ -125,37 +126,30 @@ class DueDateSelect { } submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - this.$loading.removeClass('hidden').fadeIn(); + this.$loading.removeClass('hidden').fadeIn(); - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } - this.$value.css('display', ''); - this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); - this.$sidebarValue.html(this.displayedDate); + this.$value.css('display', ''); + this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); + this.$sidebarValue.html(this.displayedDate); - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - }, - }).done(() => { - if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); - } - return this.$loading.fadeOut(); - }); + $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length); + + return axios.put(this.issueUpdateURL, this.datePayload) + .then(() => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); } } diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 9e91f72b2ea..a10f027de53 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; /** * Makes search request for content when user types a value in the search input. @@ -54,32 +55,26 @@ export default class FilterableList { this.listFilterElement.removeEventListener('input', this.debounceFilter); } - filterResults(queryData) { + filterResults(params) { if (this.isBusy) { return false; } $(this.listHolderElement).fadeTo(250, 0.5); - return $.ajax({ - url: this.getFilterEndpoint(), - data: queryData, - type: 'GET', - dataType: 'json', - context: this, - complete: this.onFilterComplete, - beforeSend: () => { - this.isBusy = true; - }, - success: (response, textStatus, xhr) => { - this.onFilterSuccess(response, xhr, queryData); - }, - }); + this.isBusy = true; + + return axios.get(this.getFilterEndpoint(), { + params, + }).then((res) => { + this.onFilterSuccess(res, params); + this.onFilterComplete(); + }).catch(() => this.onFilterComplete()); } - onFilterSuccess(response, xhr, queryData) { - if (response.html) { - this.listHolderElement.innerHTML = response.html; + onFilterSuccess(response, queryData) { + if (response.data.html) { + this.listHolderElement.innerHTML = response.data.html; } // Change url so if user reload a page - search results are saved diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 2db233b09da..31d56d15c23 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,6 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; -import { getParameterByName } from '../lib/utils/common_utils'; +import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { @@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList { this.form.querySelector(`[name="${this.filterInputField}"]`).value = ''; } - onFilterSuccess(data, xhr, queryData) { + onFilterSuccess(res, queryData) { const currentPath = this.getPagePath(queryData); - const paginationData = { - 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), - 'X-Page': xhr.getResponseHeader('X-Page'), - 'X-Total': xhr.getResponseHeader('X-Total'), - 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'), - 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'), - 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), - }; - window.history.replaceState({ page: currentPath, }, document.title, currentPath); - eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); - eventHub.$emit('updatePagination', paginationData); + eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); + eventHub.$emit('updatePagination', normalizeHeaders(res.headers)); } } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js index 7e5feac622c..643877b9d47 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.js @@ -84,7 +84,7 @@ export default { return !this.showLess || (index < this.defaultRenderCount && this.showLess); }, avatarUrl(user) { - return user.avatar || user.avatar_url; + return user.avatar || user.avatar_url || gon.default_avatar_url; }, assigneeUrl(user) { return `${this.rootPath}${user.username}`; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index f249bd036d6..ab108906732 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) { renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, username; username = user.username ? "@" + user.username : ""; - avatar = user.avatar_url ? user.avatar_url : false; + avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; let selected = false; @@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) { if (user.beforeDivider != null) { `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; } else { - if (avatar) { - img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; - } + img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; } return ` diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index 85bfd03a3cf..de6e5149a87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -76,6 +76,7 @@ export default { <a href="#modal_merge_info" data-toggle="modal" + :disabled="mr.sourceBranchRemoved" class="btn btn-sm inline"> Check out branch </a> diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d13407a06c8..6530327698b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -89,7 +89,7 @@ module ApplicationHelper end def default_avatar - 'no_avatar.png' + asset_path('no_avatar.png') end def last_commit(project) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4accb08eaf9..f6d4843abc3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -989,13 +989,13 @@ class MergeRequest < ActiveRecord::Base merged_at = metrics&.merged_at notes_association = notes_with_associations - # It is not guaranteed that Note#created_at will be strictly later than - # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this - # comparison, as will a HA environment if clocks are not *precisely* - # synchronized. Add a minute's leeway to compensate for both possibilities - cutoff = merged_at - 1.minute - if merged_at + # It is not guaranteed that Note#created_at will be strictly later than + # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this + # comparison, as will a HA environment if clocks are not *precisely* + # synchronized. Add a minute's leeway to compensate for both possibilities + cutoff = merged_at - 1.minute + notes_association = notes_association.where('created_at >= ?', cutoff) end diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 56eecece54c..6f5eb828902 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -14,5 +14,5 @@ #{time_ago_with_tooltip(event.created_at)} .pull-right - = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm" do + = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do #{ _('Create merge request') } diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index f65bb6a29e6..38e9899ca4b 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -15,7 +15,7 @@ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render 'projects/zen', f: form, attr: :description, - classes: 'note-textarea', + classes: 'note-textarea qa-issuable-form-description', placeholder: "Write a comment or drag your files here...", supports_quick_actions: supports_quick_actions = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index bb02dfa0d3a..79021a08719 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -65,7 +65,7 @@ %span.append-right-10 - if issuable.new_record? - = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create qa-issuable-create-button' - else = form.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index 64826d41d60..e81639f35ea 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -6,7 +6,7 @@ %div{ class: div_class } = form.text_field :title, required: true, maxlength: 255, autofocus: true, - autocomplete: 'off', class: 'form-control pad' + autocomplete: 'off', class: 'form-control pad qa-issuable-form-title' - if issuable.respond_to?(:work_in_progress?) %p.help-block diff --git a/changelogs/unreleased/34733-fix-default-avatar-when-gravatar-disabled.yml b/changelogs/unreleased/34733-fix-default-avatar-when-gravatar-disabled.yml new file mode 100644 index 00000000000..0791847b64d --- /dev/null +++ b/changelogs/unreleased/34733-fix-default-avatar-when-gravatar-disabled.yml @@ -0,0 +1,5 @@ +--- +title: Fix default avatar icon missing when Gravatar is disabled +merge_request: 16681 +author: Felix Geyer +type: fixed diff --git a/changelogs/unreleased/42255-disable-mr-checkout-button-when-source-branch-deleted.yml b/changelogs/unreleased/42255-disable-mr-checkout-button-when-source-branch-deleted.yml new file mode 100644 index 00000000000..bd7e0d3a1b0 --- /dev/null +++ b/changelogs/unreleased/42255-disable-mr-checkout-button-when-source-branch-deleted.yml @@ -0,0 +1,5 @@ +--- +title: Disable MR check out button when source branch is deleted +merge_request: 16631 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 85db8f40a28..bbd2d214fe4 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -169,15 +169,11 @@ For Omnibus GitLab packages: 1. [Reconfigure GitLab] for the changes to take effect -#### Digital Ocean Spaces and other S3-compatible providers +#### Digital Ocean Spaces -Not all S3 providers are fully-compatible with the Fog library. For example, -if you see `411 Length Required` errors after attempting to upload, you may -need to downgrade the `aws_signature_version` value from the default value to -2 [due to this issue](https://github.com/fog/fog-aws/issues/428). +This example can be used for a bucket in Amsterdam (AMS3). -1. For example, with [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/), -this example configuration can be used for a bucket in Amsterdam (AMS3): +1. Add the following to `/etc/gitlab/gitlab.rb`: ```ruby gitlab_rails['backup_upload_connection'] = { @@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3): 'region' => 'ams3', 'aws_access_key_id' => 'AKIAKIAKI', 'aws_secret_access_key' => 'secret123', - 'aws_signature_version' => 2, 'endpoint' => 'https://ams3.digitaloceanspaces.com' } gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' @@ -193,6 +188,13 @@ this example configuration can be used for a bucket in Amsterdam (AMS3): 1. [Reconfigure GitLab] for the changes to take effect +#### Other S3 Providers + +Not all S3 providers are fully-compatible with the Fog library. For example, +if you see `411 Length Required` errors after attempting to upload, you may +need to downgrade the `aws_signature_version` value from the default value to +2 [due to this issue](https://github.com/fog/fog-aws/issues/428). + --- For installations from source: diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 31effdba292..6d6ed065f79 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -42,9 +42,7 @@ module Gitlab end def load_blame_by_shelling_out - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) - # Read in binary mode to ensure ASCII-8BIT - IO.popen(cmd, 'rb') {|io| io.read } + @repo.shell_blame(@sha, @path) end def process_raw_blame(output) diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb index 1ccca13ce2f..e0bd2bbe47b 100644 --- a/lib/gitlab/git/popen.rb +++ b/lib/gitlab/git/popen.rb @@ -19,6 +19,8 @@ module Gitlab cmd_output = "" cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + stdout.set_encoding(Encoding::ASCII_8BIT) + yield(stdin) if block_given? stdin.close diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 638d335b523..64b491517cb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -614,11 +614,11 @@ module Gitlab if is_enabled gitaly_ref_client.find_ref_name(sha, ref_path) else - args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) + args = %W(for-each-ref --count=1 #{ref_path} --contains #{sha}) # Not found -> ["", 0] # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] - popen(args, @path).first.split.last + run_git(args).first.split.last end end end @@ -887,8 +887,7 @@ module Gitlab "delete #{ref}\x00\x00" end - command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - message, status = popen(command, path) do |stdin| + message, status = run_git(%w[update-ref --stdin -z]) do |stdin| stdin.write(instructions.join) end @@ -1409,6 +1408,11 @@ module Gitlab end end + def shell_blame(sha, path) + output, _status = run_git(%W(blame -p #{sha} -- #{path})) + output + end + private def shell_write_ref(ref_path, ref, old_ref) @@ -1433,6 +1437,12 @@ module Gitlab def run_git(args, chdir: path, env: {}, nice: false, &block) cmd = [Gitlab.config.git.bin_path, *args] cmd.unshift("nice") if nice + + object_directories = alternate_object_directories + if object_directories.any? + env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = object_directories.join(File::PATH_SEPARATOR) + end + circuit_breaker.perform do popen(cmd, chdir, env, &block) end @@ -1624,7 +1634,7 @@ module Gitlab offset_in_ruby = use_follow_flag && options[:offset].present? limit += offset if offset_in_ruby - cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log] + cmd = %w[log] cmd << "--max-count=#{limit}" cmd << '--format=%H' cmd << "--skip=#{offset}" unless offset_in_ruby @@ -1640,7 +1650,7 @@ module Gitlab cmd += Array(options[:path]) end - raw_output = IO.popen(cmd) { |io| io.read } + raw_output, _status = run_git(cmd) lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines lines.map! { |c| Rugged::Commit.new(rugged, c.strip) } @@ -1678,18 +1688,23 @@ module Gitlab end def alternate_object_directories - relative_paths = Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact + relative_paths = relative_object_directories if relative_paths.any? relative_paths.map { |d| File.join(path, d) } else - Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES) - .flatten - .compact - .flat_map { |d| d.split(File::PATH_SEPARATOR) } + absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) } end end + def relative_object_directories + Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact + end + + def absolute_object_directories + Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).flatten.compact + end + # Get the content of a blob for a given commit. If the blob is a commit # (for submodules) then return the blob's OID. def blob_content(commit, blob_name) @@ -1833,13 +1848,13 @@ module Gitlab def count_commits_by_shelling_out(options) cmd = count_commits_shelling_command(options) - raw_output = IO.popen(cmd) { |io| io.read } + raw_output, _status = run_git(cmd) process_count_commits_raw_output(raw_output, options) end def count_commits_shelling_command(options) - cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] + cmd = %w[rev-list] cmd << "--after=#{options[:after].iso8601}" if options[:after] cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--max-count=#{options[:max_count]}" if options[:max_count] @@ -1884,20 +1899,17 @@ module Gitlab return [] end - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) - cmd += %w(-r) - cmd += %w(--full-tree) - cmd += %w(--full-name) - cmd += %W(-- #{actual_ref}) + cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref}) + raw_output, _status = run_git(cmd) - raw_output = IO.popen(cmd, &:read).split("\n").map do |f| + lines = raw_output.split("\n").map do |f| stuff, path = f.split("\t") _mode, type, _sha = stuff.split(" ") path if type == "blob" # Contain only blob type end - raw_output.compact + lines.compact end # Returns true if the given ref name exists diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 94a481a0f2e..98f005cb61b 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -5,9 +5,15 @@ module DeliverNever end end +module MuteNotifications + def new_note(note) + end +end + module Gitlab class Seeder def self.quiet + mute_notifications mute_mailer SeedFu.quiet = true @@ -18,6 +24,10 @@ module Gitlab puts "\nOK".color(:green) end + def self.mute_notifications + NotificationService.prepend(MuteNotifications) + end + def self.mute_mailer ActionMailer::MessageDelivery.prepend(DeliverNever) end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 54f51d9d633..0e27a28ea6e 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -17,6 +17,8 @@ ## See installation.md#using-https for additional HTTPS configuration details. upstream gitlab-workhorse { + # Gitlab socket file, + # for Omnibus this would be: unix:/var/opt/gitlab/gitlab-workhorse/socket server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } @@ -110,6 +112,8 @@ server { error_page 502 /502.html; error_page 503 /503.html; location ~ ^/(404|422|500|502|503)\.html$ { + # Location to the Gitlab's public directory, + # for Omnibus this would be: /opt/gitlab/embedded/service/gitlab-rails/public. root /home/git/gitlab/public; internal; } diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index ed8131ef24f..8218d68f9ba 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -21,6 +21,8 @@ ## See installation.md#using-https for additional HTTPS configuration details. upstream gitlab-workhorse { + # Gitlab socket file, + # for Omnibus this would be: unix:/var/opt/gitlab/gitlab-workhorse/socket server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } @@ -160,6 +162,8 @@ server { error_page 502 /502.html; error_page 503 /503.html; location ~ ^/(404|422|500|502|503)\.html$ { + # Location to the Gitlab's public directory, + # for Omnibus this would be: /opt/gitlab/embedded/service/gitlab-rails/public root /home/git/gitlab/public; internal; } diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index a2e68c0471b..aece8893974 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -21,7 +21,11 @@ namespace :gitlab do _, status = Gitlab::Popen.popen(%w[which gmake]) command << (status.zero? ? 'gmake' : 'make') - command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? + if Rails.env.test? + command.push( + 'BUNDLE_FLAGS=--no-deployment', + "BUNDLE_PATH=#{Bundler.bundle_path}") + end Gitlab::SetupHelper.create_gitaly_configuration(args.dir) Dir.chdir(args.dir) do @@ -28,6 +28,7 @@ module QA autoload :Sandbox, 'qa/factory/resource/sandbox' autoload :Group, 'qa/factory/resource/group' autoload :Project, 'qa/factory/resource/project' + autoload :MergeRequest, 'qa/factory/resource/merge_request' autoload :DeployKey, 'qa/factory/resource/deploy_key' autoload :SecretVariable, 'qa/factory/resource/secret_variable' autoload :Runner, 'qa/factory/resource/runner' @@ -130,6 +131,10 @@ module QA autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' end + module MergeRequest + autoload :New, 'qa/page/merge_request/new' + end + module Admin autoload :Settings, 'qa/page/admin/settings' end diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb index d0e85a68237..fc5dc82ce29 100644 --- a/qa/qa/factory/dependency.rb +++ b/qa/qa/factory/dependency.rb @@ -16,20 +16,21 @@ module QA def build! return if overridden? - Builder.new(@signature).fabricate!.tap do |product| + Builder.new(@signature, @factory).fabricate!.tap do |product| @factory.public_send("#{@name}=", product) end end class Builder - def initialize(signature) + def initialize(signature, caller_factory) @factory = signature.factory @block = signature.block + @caller_factory = caller_factory end def fabricate! @factory.fabricate! do |factory| - @block&.call(factory) + @block&.call(factory, @caller_factory) end end end diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb new file mode 100644 index 00000000000..ce04e904aaf --- /dev/null +++ b/qa/qa/factory/resource/merge_request.rb @@ -0,0 +1,49 @@ +require 'securerandom' + +module QA + module Factory + module Resource + class MergeRequest < Factory::Base + attr_accessor :title, + :description, + :source_branch, + :target_branch + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-with-merge-request' + end + + dependency Factory::Repository::Push, as: :target do |push, factory| + push.project = factory.project + push.branch_name = "master:#{factory.target_branch}" + end + + dependency Factory::Repository::Push, as: :source do |push, factory| + push.project = factory.project + push.branch_name = "#{factory.target_branch}:#{factory.source_branch}" + push.file_name = "added_file.txt" + push.file_content = "File Added" + end + + def initialize + @title = 'QA test - merge request' + @description = 'This is a test merge request' + @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" + @target_branch = "master" + end + + def fabricate! + project.visit! + + Page::Project::Show.act { new_merge_request } + + Page::MergeRequest::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.create_merge_request + end + end + end + end + end +end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 81ba80cdbaf..f472e8ccc7e 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -13,16 +13,18 @@ module QA visit current_url end - def wait(css = '.application', time: 60) - Time.now.tap do |start| - while Time.now - start < time - break if page.has_css?(css, wait: 5) + def wait(max: 60, time: 1, reload: true) + start = Time.now - refresh - end + while Time.now - start < max + return true if yield + + sleep(time) + + refresh if reload end - yield if block_given? + false end def scroll_to(selector, text: nil) @@ -40,12 +42,16 @@ module QA page.within(selector) { yield } if block_given? end + def find_element(name) + find(element_selector_css(name)) + end + def click_element(name) find_element(name).click end - def find_element(name) - find(element_selector_css(name)) + def fill_element(name, content) + find_element(name).set(content) end def within_element(name) diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 37ed3b35bce..f23294145dd 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -25,7 +25,12 @@ module QA def go_to_new_subgroup within '.new-project-subgroup' do - find('.dropdown-toggle').click + # May need to click again because it is possible to click the button quicker than the JS is bound + wait(reload: false) do + find('.dropdown-toggle').click + + page.has_css?("li[data-value='new-subgroup']") + end find("li[data-value='new-subgroup']").click end @@ -34,7 +39,12 @@ module QA def go_to_new_project within '.new-project-subgroup' do - find('.dropdown-toggle').click + # May need to click again because it is possible to click the button quicker than the JS is bound + wait(reload: false) do + find('.dropdown-toggle').click + + page.has_css?("li[data-value='new-project']") + end find("li[data-value='new-project']").click end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 9cff2c5c317..95880475ffa 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -10,12 +10,14 @@ module QA view 'app/views/devise/sessions/_new_base.html.haml' do element :login_field, 'text_field :login' - element :passowrd_field, 'password_field :password' + element :password_field, 'password_field :password' element :sign_in_button, 'submit "Sign in"' end def initialize - wait('.application', time: 500) + wait(max: 500) do + page.has_css?('.application') + end end def sign_in_using_credentials diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb new file mode 100644 index 00000000000..ec94ff4ac98 --- /dev/null +++ b/qa/qa/page/merge_request/new.rb @@ -0,0 +1,31 @@ +module QA + module Page + module MergeRequest + class New < Page::Base + view 'app/views/shared/issuable/_form.html.haml' do + element :issuable_create_button + end + + view 'app/views/shared/issuable/form/_title.html.haml' do + element :issuable_form_title + end + + view 'app/views/shared/form_elements/_description.html.haml' do + element :issuable_form_description + end + + def create_merge_request + click_element :issuable_create_button + end + + def fill_title(title) + fill_element :issuable_form_title, title + end + + def fill_description(description) + fill_element :issuable_form_description, description + end + end + end + end +end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 9b1438f76d5..186a4724326 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -4,7 +4,7 @@ module QA class New < Page::Base view 'app/views/projects/_new_project_fields.html.haml' do element :project_namespace_select - element :project_namespace_field, 'select :namespace_id' + element :project_namespace_field, /select :namespace_id.*class: 'select2/ element :project_path, 'text_field :path' element :project_description, 'text_area :description' element :project_create_button, "submit 'Create project'" @@ -13,7 +13,7 @@ module QA def choose_test_namespace click_element :project_namespace_select - first('li', text: Runtime::Namespace.path).click + find('ul.select2-result-sub > li', text: Runtime::Namespace.path).click end def choose_name(name) diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb index c7955124ef3..319cb1045b6 100644 --- a/qa/qa/page/project/settings/common.rb +++ b/qa/qa/page/project/settings/common.rb @@ -17,7 +17,12 @@ module QA def expand_section(name) page.within('#content-body') do page.within('section', text: name) do - click_button 'Expand' unless first('button', text: 'Collapse') + # Because it is possible to click the button before the JS toggle code is bound + wait(reload: false) do + click_button 'Expand' unless first('button', text: 'Collapse') + + page.has_content?('Collapse') + end yield if block_given? end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 5e66e40a0b5..75308ae8a3c 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -3,12 +3,14 @@ module QA module Project class Show < Page::Base view 'app/views/shared/_clone_panel.html.haml' do + element :clone_holder, '.git-clone-holder' element :clone_dropdown element :clone_options_dropdown, '.clone-options-dropdown' + element :project_repository_location, 'text_field_tag :project_clone' end - view 'app/views/shared/_clone_panel.html.haml' do - element :project_repository_location, 'text_field_tag :project_clone' + view 'app/views/projects/_last_push.html.haml' do + element :create_merge_request end view 'app/views/projects/_home_panel.html.haml' do @@ -16,10 +18,15 @@ module QA end def choose_repository_clone_http - click_element :clone_dropdown + wait(reload: false) do + click_element :clone_dropdown - page.within('.clone-options-dropdown') do - click_link('HTTP') + page.within('.clone-options-dropdown') do + click_link('HTTP') + end + + # Ensure git clone textbox was updated to http URI + page.has_css?('.git-clone-holder input#project_clone[value*="http"]') end end @@ -31,6 +38,10 @@ module QA find('.qa-project-name').text end + def new_merge_request + click_element :create_merge_request + end + def wait_for_push sleep 5 refresh diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb new file mode 100644 index 00000000000..fbf9a4d17e5 --- /dev/null +++ b/qa/qa/specs/features/merge_request/create_spec.rb @@ -0,0 +1,17 @@ +module QA + feature 'creates a merge request', :core do + scenario 'user creates a new merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.title = 'This is a merge request' + merge_request.description = 'Great feature' + end + + expect(page).to have_content('This is a merge request') + expect(page).to have_content('Great feature') + expect(page).to have_content('Opened less than a minute ago') + end + end +end diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb index 32405415126..8aaa6665a18 100644 --- a/qa/spec/factory/dependency_spec.rb +++ b/qa/spec/factory/dependency_spec.rb @@ -54,6 +54,19 @@ describe QA::Factory::Dependency do expect(factory).to have_received(:mydep=).with(dependency) end + + context 'when receives a caller factory as block argument' do + let(:dependency) { QA::Factory::Base } + + it 'calls given block with dependency factory and caller factory' do + allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory) + allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any')) + + subject.build! + + expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory) + end + end end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index da0343588ef..f7a4a7afced 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -100,7 +100,7 @@ describe ApplicationHelper do end it 'returns a generic avatar' do - expect(helper.gravatar_icon(user_email)).to match('no_avatar.png') + expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png') end end @@ -110,7 +110,7 @@ describe ApplicationHelper do end it 'returns a generic avatar when email is blank' do - expect(helper.gravatar_icon('')).to match('no_avatar.png') + expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png') end it 'returns a valid Gravatar URL' do diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index b3cbf9aba48..de744739e42 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -26,8 +26,8 @@ describe('Pipelines Table Row', () => { const pipelines = getJSONFixture(jsonFixtureName).pipelines; pipeline = pipelines.find(p => p.user !== null && p.commit !== null); - pipelineWithoutAuthor = pipelines.find(p => p.user == null && p.commit !== null); - pipelineWithoutCommit = pipelines.find(p => p.user == null && p.commit == null); + pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null); + pipelineWithoutCommit = pipelines.find(p => p.user === null && p.commit === null); }); afterEach(() => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 06f89fabf42..93bb83ca8bd 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -48,20 +48,23 @@ describe('MRWidgetHeader', () => { describe('template', () => { let vm; let el; + let mr; const sourceBranchPath = '/foo/bar/mr-widget-refactor'; - const mr = { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - }; beforeEach(() => { + mr = { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`, + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + }; + vm = createComponent(mr); el = vm.$el; }); @@ -82,6 +85,8 @@ describe('MRWidgetHeader', () => { expect(el.textContent).toContain('Check out branch'); expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath); expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath); + + expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBeNull(); }); it('should not have right action links if the MR state is not open', (done) => { @@ -101,5 +106,16 @@ describe('MRWidgetHeader', () => { done(); }); }); + + it('should disable check out branch button if source branch has been removed', (done) => { + vm.mr.sourceBranchRemoved = true; + + Vue.nextTick() + .then(() => { + expect(el.querySelector('a[href="#modal_merge_info"]').getAttribute('disabled')).toBe('disabled'); + done(); + }) + .catch(done.fail); + }); }); }); diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 429b6615131..15f9c68e400 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1064,16 +1064,6 @@ describe MergeRequest do end describe '#can_be_reverted?' do - context 'when there is no merged_at for the MR' do - before do - subject.metrics.update!(merged_at: nil) - end - - it 'returns false' do - expect(subject.can_be_reverted?(nil)).to be_falsey - end - end - context 'when there is no merge_commit for the MR' do before do subject.metrics.update!(merged_at: Time.now.utc) @@ -1097,6 +1087,16 @@ describe MergeRequest do end end + context 'when there is no merged_at for the MR' do + before do + subject.metrics.update!(merged_at: nil) + end + + it 'returns true' do + expect(subject.can_be_reverted?(nil)).to be_truthy + end + end + context 'when there is a revert commit' do let(:current_user) { subject.author } let(:branch) { subject.target_branch } @@ -1127,6 +1127,16 @@ describe MergeRequest do end end + context 'when there is no merged_at for the MR' do + before do + subject.metrics.update!(merged_at: nil) + end + + it 'returns false' do + expect(subject.can_be_reverted?(current_user)).to be_falsey + end + end + context 'when the revert commit is mentioned in a note just before the MR was merged' do before do subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds) diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 6aba86fdc3c..b37d6ac831f 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -76,7 +76,11 @@ describe 'gitlab:gitaly namespace rake task' do end context 'when Rails.env is test' do - let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] } + let(:command) do + %W[make + BUNDLE_FLAGS=--no-deployment + BUNDLE_PATH=#{Bundler.bundle_path}] + end before do allow(Rails.env).to receive(:test?).and_return(true) |
