diff options
131 files changed, 1623 insertions, 892 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c1d78ef2d48..be18520b876 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -321,6 +321,7 @@ setup-test-env: expire_in: 7d paths: - tmp/tests + - config/secrets.yml rspec-pg 0 27: *rspec-metadata-pg rspec-pg 1 27: *rspec-metadata-pg diff --git a/CHANGELOG.md b/CHANGELOG.md index 248c85304a9..5fc97c06f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.4.2 (2018-01-30) + +### Fixed (6 changes) + +- Fix copy/paste on iOS devices due to a bug in webkit. !15804 +- Fix missing "allow users to request access" option in public project permissions. !16485 +- Fix encoding issue when counting commit count. !16637 +- Fixes destination already exists, and some particular service errors on Import/Export error. !16714 +- Fix cache clear bug withg using : on Windows. !16740 +- Use has_table_privilege for TRIGGER on PostgreSQL. + +### Changed (1 change) + +- Vendor Auto DevOps template with DAST security checks enabled. !16691 + + ## 10.4.1 (2018-01-24) ### Fixed (4 changes) @@ -325,7 +325,7 @@ group :development, :test do gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'rspec_profiling', '~> 0.0.5' gem 'rspec-set', '~> 0.1.3' - gem 'rspec-parameterized' + gem 'rspec-parameterized', require: false # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5532888d179..1a3c8f42469 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -304,7 +304,7 @@ GEM mime-types (>= 1.16) posix-spawn (~> 0.3) gitlab-markup (1.6.3) - gitlab-styles (2.3.1) + gitlab-styles (2.3.2) rubocop (~> 0.51) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) 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/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index b37988a674d..a25f7fb3dcd 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,5 +1,8 @@ /* global ace */ +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; export default class EditBlob { @@ -56,12 +59,14 @@ export default class EditBlob { if (paneId === '#preview') { this.$toggleButton.hide(); - return $.post(currentLink.data('preview-url'), { + axios.post(currentLink.data('preview-url'), { content: this.editor.getValue(), - }, (response) => { - currentPane.empty().append(response); - return currentPane.renderGFM(); - }); + }) + .then(({ data }) => { + currentPane.empty().append(data); + currentPane.renderGFM(); + }) + .catch(() => createFlash(__('An error occurred previewing the blob'))); } this.$toggleButton.show(); diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 637d0dbde23..4dddb6eb0d6 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -14,6 +14,7 @@ import { import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import applications from './components/applications.vue'; +import setupToggleButtons from '../toggle_buttons'; /** * Cluster page has 2 separate parts: @@ -48,12 +49,9 @@ export default class Clusters { installPrometheusEndpoint: installPrometheusPath, }); - this.toggle = this.toggle.bind(this); this.installApplication = this.installApplication.bind(this); this.showToken = this.showToken.bind(this); - this.toggleButton = document.querySelector('.js-toggle-cluster'); - this.toggleInput = document.querySelector('.js-toggle-input'); this.errorContainer = document.querySelector('.js-cluster-error'); this.successContainer = document.querySelector('.js-cluster-success'); this.creatingContainer = document.querySelector('.js-cluster-creating'); @@ -63,6 +61,7 @@ export default class Clusters { this.tokenField = document.querySelector('.js-cluster-token'); initSettingsPanels(); + setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); if (this.store.state.status !== 'created') { @@ -101,13 +100,11 @@ export default class Clusters { } addListeners() { - this.toggleButton.addEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); } removeListeners() { - this.toggleButton.removeEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); } @@ -151,11 +148,6 @@ export default class Clusters { this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); } - toggle() { - this.toggleButton.classList.toggle('is-checked'); - this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString()); - } - showToken() { const type = this.tokenField.getAttribute('type'); diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 6844d1dbd83..2e3ad244375 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,58 +1,20 @@ import Flash from '../flash'; import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; import ClustersService from './services/clusters_service'; -/** - * Toggles loading and disabled classes. - * @param {HTMLElement} button - */ -const toggleLoadingButton = (button) => { - if (button.getAttribute('disabled')) { - button.removeAttribute('disabled'); - } else { - button.setAttribute('disabled', true); - } - - button.classList.toggle('is-loading'); -}; -/** - * Toggles checked class for the given button - * @param {HTMLElement} button - */ -const toggleValue = (button) => { - button.classList.toggle('is-checked'); +export default () => { + const clusterList = document.querySelector('.js-clusters-list'); + // The empty state won't have a clusterList + if (clusterList) { + setupToggleButtons( + document.querySelector('.js-clusters-list'), + (value, toggle) => + ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) + .catch((err) => { + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + throw err; + }), + ); + } }; - -/** - * Handles toggle buttons in the cluster's table. - * - * When the user clicks the toggle button for each cluster, it: - * - toggles the button - * - shows a loading and disables button - * - Makes a put request to the given endpoint - * Once we receive the response, either: - * 1) Show updated status in case of successfull response - * 2) Show initial status in case of failed response - */ -export default function setClusterTableToggles() { - document.querySelectorAll('.js-toggle-cluster-list') - .forEach(button => button.addEventListener('click', (e) => { - const toggleButton = e.currentTarget; - const endpoint = toggleButton.getAttribute('data-endpoint'); - - toggleValue(toggleButton); - toggleLoadingButton(toggleButton); - - const value = toggleButton.classList.contains('is-checked'); - - ClustersService.updateCluster(endpoint, { cluster: { enabled: value } }) - .then(() => { - toggleLoadingButton(toggleButton); - }) - .catch(() => { - toggleLoadingButton(toggleButton); - toggleValue(toggleButton); - Flash(s__('ClusterIntegration|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/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 025e38ea99a..5afae93724b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -76,7 +76,13 @@ .then(data => this.store.storeDeploymentData(data)) .catch(() => new Flash('Error getting deployment information.')), ]) - .then(() => { this.showEmptyState = false; }) + .then(() => { + if (this.store.groups.length < 1) { + this.state = 'noData'; + return; + } + this.showEmptyState = false; + }) .catch(() => { this.state = 'unableToConnect'; }); }, diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index 87d1975d5ad..56cd60c583b 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -34,16 +34,23 @@ svgUrl: this.emptyGettingStartedSvgPath, title: 'Get started with performance monitoring', description: `Stay updated about the performance and health -of your environment by configuring Prometheus to monitor your deployments.`, + of your environment by configuring Prometheus to monitor your deployments.`, buttonText: 'Configure Prometheus', }, loading: { svgUrl: this.emptyLoadingSvgPath, title: 'Waiting for performance data', description: `Creating graphs uses the data from the Prometheus server. -If this takes a long time, ensure that data is available.`, + If this takes a long time, ensure that data is available.`, buttonText: 'View documentation', }, + noData: { + svgUrl: this.emptyUnableToConnectSvgPath, + title: 'No data found', + description: `You are connected to the Prometheus server, but there is currently + no data to display.`, + buttonText: 'Configure Prometheus', + }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: 'Unable to connect to Prometheus server', 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/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js new file mode 100644 index 00000000000..974dc3ee052 --- /dev/null +++ b/app/assets/javascripts/toggle_buttons.js @@ -0,0 +1,61 @@ +import $ from 'jquery'; +import Flash from './flash'; +import { __ } from './locale'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; + +/* + example HAML: + ``` + %button.js-project-feature-toggle.project-feature-toggle{ type: "button", + class: "#{'is-checked' if enabled?}", + 'aria-label': _('Toggle Cluster') } + %input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? } + ``` +*/ + +function updatetoggle(toggle, isOn) { + toggle.classList.toggle('is-checked', isOn); +} + +function onToggleClicked(toggle, input, clickCallback) { + const previousIsOn = convertPermissionToBoolean(input.value); + + // Visually change the toggle and start loading + updatetoggle(toggle, !previousIsOn); + toggle.setAttribute('disabled', true); + toggle.classList.toggle('is-loading', true); + + Promise.resolve(clickCallback(!previousIsOn, toggle)) + .then(() => { + // Actually change the input value + input.setAttribute('value', !previousIsOn); + }) + .catch(() => { + // Revert the visuals if something goes wrong + updatetoggle(toggle, previousIsOn); + }) + .then(() => { + // Remove the loading indicator in any case + toggle.removeAttribute('disabled'); + toggle.classList.toggle('is-loading', false); + + $(input).trigger('trigger-change'); + }) + .catch(() => { + Flash(__('Something went wrong when toggling the button')); + }); +} + +export default function setupToggleButtons(container, clickCallback = () => {}) { + const toggles = container.querySelectorAll('.js-project-feature-toggle'); + + toggles.forEach((toggle) => { + const input = toggle.querySelector('.js-project-feature-toggle-input'); + const isOn = convertPermissionToBoolean(input.value); + + // Get the visible toggle in sync with the hidden input + updatetoggle(toggle, isOn); + + toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback)); + }); +} 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/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js deleted file mode 100644 index 7f8d78cab73..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ /dev/null @@ -1,139 +0,0 @@ -import Flash from '../../../flash'; -import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import statusIcon from '../mr_widget_status_icon.vue'; -import eventHub from '../../event_hub'; - -export default { - name: 'MRWidgetMerged', - props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, - }, - data() { - return { - isMakingRequest: false, - }; - }, - directives: { - tooltip, - }, - components: { - 'mr-widget-author-and-time': mrWidgetAuthorTime, - loadingIcon, - statusIcon, - }, - computed: { - shouldShowRemoveSourceBranch() { - const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; - - return !sourceBranchRemoved && canRemoveSourceBranch && - !this.isMakingRequest && !isRemovingSourceBranch; - }, - shouldShowSourceBranchRemoving() { - const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr; - return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest); - }, - shouldShowMergedButtons() { - const { canRevertInCurrentMR, canCherryPickInCurrentMR, revertInForkPath, - cherryPickInForkPath } = this.mr; - - return canRevertInCurrentMR || canCherryPickInCurrentMR || - revertInForkPath || cherryPickInForkPath; - }, - }, - methods: { - removeSourceBranch() { - this.isMakingRequest = true; - this.service.removeSourceBranch() - .then(res => res.data) - .then((data) => { - if (data.message === 'Branch was removed') { - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isMakingRequest = false; - }); - } - }) - .catch(() => { - this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line - }); - }, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="success" /> - <div class="media-body"> - <div class="space-children"> - <mr-widget-author-and-time - actionText="Merged by" - :author="mr.metrics.mergedBy" - :date-title="mr.metrics.mergedAt" - :date-readable="mr.metrics.readableMergedAt" /> - <a - v-if="mr.canRevertInCurrentMR" - v-tooltip - class="btn btn-close btn-xs" - href="#modal-revert-commit" - data-toggle="modal" - data-container="body" - title="Revert this merge request in a new merge request"> - Revert - </a> - <a - v-else-if="mr.revertInForkPath" - v-tooltip - class="btn btn-close btn-xs" - data-method="post" - :href="mr.revertInForkPath" - title="Revert this merge request in a new merge request"> - Revert - </a> - <a - v-if="mr.canCherryPickInCurrentMR" - v-tooltip - class="btn btn-default btn-xs" - href="#modal-cherry-pick-commit" - data-toggle="modal" - data-container="body" - title="Cherry-pick this merge request in a new merge request"> - Cherry-pick - </a> - <a - v-else-if="mr.cherryPickInForkPath" - v-tooltip - class="btn btn-default btn-xs" - data-method="post" - :href="mr.cherryPickInForkPath" - title="Cherry-pick this merge request in a new merge request"> - Cherry-pick - </a> - </div> - <section class="mr-info-list"> - <p> - The changes were merged into - <span class="label-branch"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> - </span> - </p> - <p v-if="mr.sourceBranchRemoved">The source branch has been removed</p> - <p v-if="shouldShowRemoveSourceBranch" class="space-children"> - <span>You can remove source branch now</span> - <button - @click="removeSourceBranch" - :disabled="isMakingRequest" - type="button" - class="btn btn-xs btn-default js-remove-branch-button"> - Remove Source Branch - </button> - </p> - <p v-if="shouldShowSourceBranchRemoving"> - <loading-icon inline /> - <span>The source branch is being removed</span> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue new file mode 100644 index 00000000000..a92e0b3c124 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -0,0 +1,192 @@ +<script> + import Flash from '~/flash'; + import tooltip from '~/vue_shared/directives/tooltip'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import { s__, __ } from '~/locale'; + import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; + import statusIcon from '../mr_widget_status_icon.vue'; + import eventHub from '../../event_hub'; + + export default { + name: 'MRWidgetMerged', + directives: { + tooltip, + }, + components: { + mrWidgetAuthorTime, + loadingIcon, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + service: { + type: Object, + required: true, + default: () => ({}), + }, + }, + data() { + return { + isMakingRequest: false, + }; + }, + computed: { + shouldShowRemoveSourceBranch() { + const { + sourceBranchRemoved, + isRemovingSourceBranch, + canRemoveSourceBranch, + } = this.mr; + + return !sourceBranchRemoved && + canRemoveSourceBranch && + !this.isMakingRequest && + !isRemovingSourceBranch; + }, + shouldShowSourceBranchRemoving() { + const { + sourceBranchRemoved, + isRemovingSourceBranch, + } = this.mr; + return !sourceBranchRemoved && + (isRemovingSourceBranch || this.isMakingRequest); + }, + shouldShowMergedButtons() { + const { + canRevertInCurrentMR, + canCherryPickInCurrentMR, + revertInForkPath, + cherryPickInForkPath, + } = this.mr; + + return canRevertInCurrentMR || + canCherryPickInCurrentMR || + revertInForkPath || + cherryPickInForkPath; + }, + revertTitle() { + return s__('mrWidget|Revert this merge request in a new merge request'); + }, + cherryPickTitle() { + return s__('mrWidget|Cherry-pick this merge request in a new merge request'); + }, + revertLabel() { + return s__('mrWidget|Revert'); + }, + cherryPickLabel() { + return s__('mrWidget|Cherry-pick'); + }, + }, + methods: { + removeSourceBranch() { + this.isMakingRequest = true; + + this.service.removeSourceBranch() + .then(res => res.data) + .then((data) => { + if (data.message === 'Branch was removed') { + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isMakingRequest = false; + }); + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash(__('Something went wrong. Please try again.')); + }); + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon status="success" /> + <div class="media-body"> + <div class="space-children"> + <mr-widget-author-time + :action-text="s__('mrWidget|Merged by')" + :author="mr.metrics.mergedBy" + :date-title="mr.metrics.mergedAt" + :date-readable="mr.metrics.readableMergedAt" + /> + <a + v-if="mr.canRevertInCurrentMR" + v-tooltip + class="btn btn-close btn-xs" + href="#modal-revert-commit" + data-toggle="modal" + data-container="body" + :title="revertTitle" + > + {{ revertLabel }} + </a> + <a + v-else-if="mr.revertInForkPath" + v-tooltip + class="btn btn-close btn-xs" + data-method="post" + :href="mr.revertInForkPath" + :title="revertTitle" + > + {{ revertLabel }} + </a> + <a + v-if="mr.canCherryPickInCurrentMR" + v-tooltip + class="btn btn-default btn-xs" + href="#modal-cherry-pick-commit" + data-toggle="modal" + data-container="body" + :title="cherryPickTitle" + > + {{ cherryPickLabel }} + </a> + <a + v-else-if="mr.cherryPickInForkPath" + v-tooltip + class="btn btn-default btn-xs" + data-method="post" + :href="mr.cherryPickInForkPath" + :title="cherryPickTitle" + > + {{ cherryPickLabel }} + </a> + </div> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes were merged into") }} + <span class="label-branch"> + <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> + </span> + </p> + <p v-if="mr.sourceBranchRemoved"> + {{ s__("mrWidget|The source branch has been removed") }} + </p> + <p + v-if="shouldShowRemoveSourceBranch" + class="space-children" + > + <span>{{ s__("mrWidget|You can remove source branch now") }}</span> + <button + @click="removeSourceBranch" + :disabled="isMakingRequest" + type="button" + class="btn btn-xs btn-default js-remove-branch-button" + > + {{ s__("mrWidget|Remove Source Branch") }} + </button> + </p> + <p v-if="shouldShowSourceBranchRemoving"> + <loading-icon :inline="true" /> + <span> + {{ s__("mrWidget|The source branch is being removed") }} + </span> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index b930aca6877..2917090e073 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -16,7 +16,7 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; -export { default as MergedState } from './components/states/mr_widget_merged'; +export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue'; 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/ci/build.rb b/app/models/ci/build.rb index df67fb243ad..6ced5fb0e24 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -466,7 +466,7 @@ module Ci if cache && project.jobs_cache_index cache = cache.merge( - key: "#{cache[:key]}:#{project.jobs_cache_index}") + key: "#{cache[:key]}_#{project.jobs_cache_index}") end [cache] 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/models/project.rb b/app/models/project.rb index e19873f64ce..d0d0fd6e093 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -568,6 +568,9 @@ class Project < ActiveRecord::Base RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, forked_from_project.disk_path) + elsif gitlab_project_import? + # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved. + RepositoryImportWorker.set(retry: false).perform_async(self.id) else RepositoryImportWorker.perform_async(self.id) end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 1a236e232f9..b604d860a87 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,7 +2,7 @@ class EmailsOnPushService < Service boolean_accessor :send_from_committer_email boolean_accessor :disable_diffs prop_accessor :recipients - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? def title 'Emails on push' diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 19357f90810..27bdf708c80 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -4,7 +4,7 @@ class IrkerService < Service prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :recipients, :channels boolean_accessor :colorize_messages - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? before_validation :get_channels diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2be35b6ea9d..23147d7f666 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -43,7 +43,7 @@ class JiraService < IssueTrackerService username: self.username, password: self.password, site: URI.join(url, '/').to_s, - context_path: url.path, + context_path: url.path.chomp('/'), auth_type: :basic, read_timeout: 120, use_cookies: true, diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 6a3118a11b8..9c7b58dead5 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,7 +1,7 @@ class PipelinesEmailService < Service prop_accessor :recipients boolean_accessor :notify_only_broken_pipelines - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? def initialize_properties self.properties ||= { notify_only_broken_pipelines: true } diff --git a/app/models/service.rb b/app/models/service.rb index 96a064697f0..369cae2e85f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,6 +2,8 @@ # and implement a set of methods class Service < ActiveRecord::Base include Sortable + include Importable + serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize default_value_for :active, false @@ -295,4 +297,8 @@ class Service < ActiveRecord::Base project.cache_has_external_wiki end end + + def valid_recipients? + activated? && !importing? + end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 9f05535d4d4..262622f8bd0 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -9,7 +9,8 @@ module MergeRequests Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) # Be sure to close outstanding MRs before reloading them to avoid generating an # empty diff during a manual merge - close_merge_requests + close_upon_missing_source_branch_ref + post_merge_manually_merged reload_merge_requests reset_merge_when_pipeline_succeeds mark_pending_todos_done @@ -29,11 +30,22 @@ module MergeRequests private + def close_upon_missing_source_branch_ref + # MergeRequest#reload_diff ignores not opened MRs. This means it won't + # create an `empty` diff for `closed` MRs without a source branch, keeping + # the latest diff state as the last _valid_ one. + merge_requests_for_source_branch.reject(&:source_branch_exists?).each do |mr| + MergeRequests::CloseService + .new(mr.target_project, @current_user) + .execute(mr) + end + end + # Collect open merge requests that target same branch we push into # and close if push to master include last commit from merge request # We need this to close(as merged) merge requests that were merged into # target branch manually - def close_merge_requests + def post_merge_manually_merged commit_ids = @commits.map(&:id) merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a merge_requests = merge_requests.select(&:diff_head_commit) diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index b8692009225..fdd72ead2cb 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -5,15 +5,16 @@ = markdown_field(current_application_settings, :help_page_text) %hr -- unless current_application_settings.help_page_hide_commercial_content? - %h1 - GitLab - Community Edition - - if user_signed_in? - %span= Gitlab::VERSION - %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) - = version_status_badge +%h1 + GitLab + Community Edition + - if user_signed_in? + %span= Gitlab::VERSION + %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) + = version_status_badge + %hr +- unless current_application_settings.help_page_hide_commercial_content? %p.slead GitLab is open source software to collaborate on code. %br 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/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml index 3943dfc0856..20ee8086f93 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/projects/clusters/_cluster.html.haml @@ -12,11 +12,12 @@ .table-section.section-10 .table-mobile-header{ role: "rowheader" } .table-mobile-content - %button{ type: "button", - class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", + %button.js-project-feature-toggle.project-feature-toggle{ type: "button", + class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", "aria-label": s_("ClusterIntegration|Toggle Cluster"), disabled: !cluster.can_toggle_cluster?, data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } + %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? } = icon("spinner spin", class: "loading-icon") %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml index 9d593ffc021..0af6e6e0577 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -10,13 +10,12 @@ = s_('ClusterIntegration|Cluster integration is enabled for this project.') - else = s_('ClusterIntegration|Cluster integration is disabled for this project.') - %label.append-bottom-10 - = field.hidden_field :enabled, { class: 'js-toggle-input'} - + %label.append-bottom-10.js-cluster-enable-toggle-area %button{ type: 'button', - class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", + class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", "aria-label": s_("ClusterIntegration|Toggle Cluster"), disabled: !can?(current_user, :update_cluster, @cluster) } + = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'} %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 467f19b4c56..55e45a5e954 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -32,5 +32,5 @@ = icon("pencil") - if can?(current_user, :admin_project, @project) - = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do + = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do = icon("trash-o") 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/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 31e2798c36b..d79b5ee5346 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -20,7 +20,11 @@ class RepositoryImportWorker # to those importers to mark the import process as complete. return if service.async? - raise result[:message] if result[:status] == :error + if result[:status] == :error + fail_import(project, result[:message]) if project.gitlab_project_import? + + raise result[:message] + end project.after_import end @@ -33,4 +37,8 @@ class RepositoryImportWorker Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") false end + + def fail_import(project, message) + project.mark_import_as_failed(message) + end end diff --git a/bin/upgrade.rb b/bin/upgrade.rb deleted file mode 100755 index a5caecf8526..00000000000 --- a/bin/upgrade.rb +++ /dev/null @@ -1,3 +0,0 @@ -require_relative "../lib/gitlab/upgrader" - -Gitlab::Upgrader.new.execute diff --git a/changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml b/changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml deleted file mode 100644 index f4c44983736..00000000000 --- a/changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix copy/paste on iOS devices due to a bug in webkit -merge_request: 15804 -author: -type: fixed 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/41802-add-space-to-edit-delete-tag-btns.yml b/changelogs/unreleased/41802-add-space-to-edit-delete-tag-btns.yml new file mode 100644 index 00000000000..f23a6452b0d --- /dev/null +++ b/changelogs/unreleased/41802-add-space-to-edit-delete-tag-btns.yml @@ -0,0 +1,5 @@ +--- +title: Adds spacing between edit and delete tag btn in tag list +merge_request: 16757 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml b/changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml deleted file mode 100644 index 38684cd3c44..00000000000 --- a/changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix missing "allow users to request access" option in public project permissions -merge_request: 16485 -author: -type: fixed diff --git a/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml b/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml deleted file mode 100644 index c64bee9126e..00000000000 --- a/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix encoding issue when counting commit count -merge_request: 16637 -author: -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/changelogs/unreleased/cs-fix-commercial-content-check.yml b/changelogs/unreleased/cs-fix-commercial-content-check.yml new file mode 100644 index 00000000000..fec80e3ecd2 --- /dev/null +++ b/changelogs/unreleased/cs-fix-commercial-content-check.yml @@ -0,0 +1,6 @@ +--- +title: Fix version information not showing on help page if commercial content display + was disabled. +merge_request: 16743 +author: +type: fixed diff --git a/changelogs/unreleased/fix-postgresql-table-grant.yml b/changelogs/unreleased/fix-postgresql-table-grant.yml deleted file mode 100644 index 1c6559f6f73..00000000000 --- a/changelogs/unreleased/fix-postgresql-table-grant.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use has_table_privilege for TRIGGER on PostgreSQL -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml b/changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml new file mode 100644 index 00000000000..1cffb213f23 --- /dev/null +++ b/changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml @@ -0,0 +1,5 @@ +--- +title: Close and do not reload MR diffs when source branch is deleted +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml new file mode 100644 index 00000000000..3854985e576 --- /dev/null +++ b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml @@ -0,0 +1,5 @@ +--- +title: Return more consistent values for merge_status on MR APIs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-jira-trailing-slash.yml b/changelogs/unreleased/sh-fix-jira-trailing-slash.yml new file mode 100644 index 00000000000..786f6cd3727 --- /dev/null +++ b/changelogs/unreleased/sh-fix-jira-trailing-slash.yml @@ -0,0 +1,5 @@ +--- +title: Fix JIRA not working when a trailing slash is included +merge_request: +author: +type: fixed diff --git a/config/application.rb b/config/application.rb index 2067428ff62..751307de975 100644 --- a/config/application.rb +++ b/config/application.rb @@ -6,6 +6,7 @@ Bundler.require(:default, Rails.env) module Gitlab class Application < Rails::Application + require_dependency Rails.root.join('lib/gitlab/redis/wrapper') require_dependency Rails.root.join('lib/gitlab/redis/cache') require_dependency Rails.root.join('lib/gitlab/redis/queues') require_dependency Rails.root.join('lib/gitlab/redis/shared_state') diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb index d3cf9e453d0..612cca3dfbd 100644 --- a/config/initializers/grape_route_helpers_fix.rb +++ b/config/initializers/grape_route_helpers_fix.rb @@ -1,5 +1,21 @@ if defined?(GrapeRouteHelpers) module GrapeRouteHelpers + module AllRoutes + # Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment. + # + # Without the following fix, when two helper methods are the same, but have different arguments + # (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2)) + # if the helper method with the least number of arguments is defined first (because the route was defined first) + # then it will shadow the longer route. + # + # The fix is to sort descending by amount of arguments + def decorated_routes + @decorated_routes ||= all_routes + .map { |r| DecoratedRoute.new(r) } + .sort_by { |r| -r.dynamic_path_segments.count } + end + end + class DecoratedRoute # GrapeRouteHelpers gem tries to parse the versions # from a string, not supporting Grape `version` array definition. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 76f33b765d3..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: @@ -494,7 +496,7 @@ more of the following options: - `BACKUP=timestamp_of_backup` - Required if more than one backup exists. Read what the [backup timestamp is about](#backup-timestamp). -- `force=yes` - Do not ask if the authorized_keys file should get regenerated. +- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed. ### Restore for installation from source diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 7b9a80a234b..cb222697f32 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -507,7 +507,15 @@ module API expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_pipeline_succeeds - expose :merge_status + + # Ideally we should deprecate `MergeRequest#merge_status` exposure and + # use `MergeRequest#mergeable?` instead (boolean). + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more + # information. + expose :merge_status do |merge_request| + merge_request.check_if_can_be_merged + merge_request.merge_status + end expose :diff_head_sha, as: :sha expose :merge_commit_sha expose :user_notes_count 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..3a7930154e5 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -468,9 +468,13 @@ module Gitlab } options = default_options.merge(options) - options[:limit] ||= 0 options[:offset] ||= 0 + limit = options[:limit] + if limit == 0 || !limit.is_a?(Integer) + raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}") + end + gitaly_migrate(:find_commits) do |is_enabled| if is_enabled gitaly_commit_client.find_commits(options) @@ -614,11 +618,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 +891,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 +1412,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 +1441,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 +1638,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 +1654,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 +1692,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 +1852,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 +1903,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/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index d03cbc880fd..b34cafc6876 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -19,8 +19,13 @@ module Gitlab def error(error) error_out(error.message, caller[0].dup) @errors << error.message + # Debug: - Rails.logger.error(error.backtrace.join("\n")) + if error.backtrace + Rails.logger.error("Import/Export backtrace: #{error.backtrace.join("\n")}") + else + Rails.logger.error("No backtrace found") + end end private diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index 4bc5cda8cb5..b9832a724c4 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -5,7 +5,17 @@ module Gitlab module Popen extend self - def popen(cmd, path = nil, vars = {}) + Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration) + + # Returns [stdout + stderr, status] + def popen(cmd, path = nil, vars = {}, &block) + result = popen_with_detail(cmd, path, vars, &block) + + [result.stdout << result.stderr, result.status&.exitstatus] + end + + # Returns Result + def popen_with_detail(cmd, path = nil, vars = {}) unless cmd.is_a?(Array) raise "System commands must be given as an array of strings" end @@ -18,18 +28,21 @@ module Gitlab FileUtils.mkdir_p(path) end - cmd_output = "" - cmd_status = 0 + cmd_stdout = '' + cmd_stderr = '' + cmd_status = nil + start = Time.now + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| yield(stdin) if block_given? stdin.close - cmd_output << stdout.read - cmd_output << stderr.read - cmd_status = wait_thr.value.exitstatus + cmd_stdout = stdout.read + cmd_stderr = stderr.read + cmd_status = wait_thr.value end - [cmd_output, cmd_status] + Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start) end end end diff --git a/lib/gitlab/popen/runner.rb b/lib/gitlab/popen/runner.rb new file mode 100644 index 00000000000..f44035a48bb --- /dev/null +++ b/lib/gitlab/popen/runner.rb @@ -0,0 +1,46 @@ +module Gitlab + module Popen + class Runner + attr_reader :results + + def initialize + @results = [] + end + + def run(commands, &block) + commands.each do |cmd| + # yield doesn't support blocks, so we need to use a block variable + block.call(cmd) do # rubocop:disable Performance/RedundantBlockCall + cmd_result = Gitlab::Popen.popen_with_detail(cmd) + + results << cmd_result + + cmd_result + end + end + end + + def all_success_and_clean? + all_success? && all_stderr_empty? + end + + def all_success? + results.all? { |result| result.status.success? } + end + + def all_stderr_empty? + results.all? { |result| result.stderr.empty? } + end + + def failed_results + results.reject { |result| result.status.success? } + end + + def warned_results + results.select do |result| + result.status.success? && !result.stderr.empty? + end + end + end + end +end diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb index 9bf019b72e6..a991933e910 100644 --- a/lib/gitlab/redis/cache.rb +++ b/lib/gitlab/redis/cache.rb @@ -1,5 +1,5 @@ # please require all dependencies below: -require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper) +require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present? module Gitlab module Redis 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/tasks/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb index c1182af1014..34bee6fecbe 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/gitlab/task_helpers.rb @@ -1,6 +1,7 @@ require 'rainbow/ext/string' require 'gitlab/utils/strong_memoize' +# rubocop:disable Rails/Output module Gitlab TaskFailedError = Class.new(StandardError) TaskAbortedByUserError = Class.new(StandardError) @@ -96,11 +97,9 @@ module Gitlab end def gid_for(group_name) - begin - Etc.getgrnam(group_name).gid - rescue ArgumentError # no group - "group #{group_name} doesn't exist" - end + Etc.getgrnam(group_name).gid + rescue ArgumentError # no group + "group #{group_name} doesn't exist" end def gitlab_user diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index 3b64cb32afa..d545f2f95f1 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -1,6 +1,3 @@ -require_relative "popen" -require_relative "version_info" - module Gitlab class Upgrader def execute 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/system_check/helpers.rb b/lib/system_check/helpers.rb index c42ae4fe4c4..914ed794601 100644 --- a/lib/system_check/helpers.rb +++ b/lib/system_check/helpers.rb @@ -1,5 +1,3 @@ -require 'tasks/gitlab/task_helpers' - module SystemCheck module Helpers include ::Gitlab::TaskHelpers diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake index 7ad2b2e4d39..b1e012e70c5 100644 --- a/lib/tasks/flay.rake +++ b/lib/tasks/flay.rake @@ -1,6 +1,6 @@ desc 'Code duplication analyze via flay' task :flay do - output = `bundle exec flay --mass 35 app/ lib/gitlab/` + output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}` if output.include? "Similar code found" puts output diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 2383bcf954b..24e37f6c6cc 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -4,7 +4,7 @@ namespace :gitlab do namespace :backup do # Create backup of GitLab system desc "GitLab | Create a backup of the GitLab system" - task create: :environment do + task create: :gitlab_environment do warn_user_is_not_gitlab configure_cron_mode @@ -25,7 +25,7 @@ namespace :gitlab do # Restore backup of GitLab system desc 'GitLab | Restore a previously created backup' - task restore: :environment do + task restore: :gitlab_environment do warn_user_is_not_gitlab configure_cron_mode @@ -73,7 +73,7 @@ namespace :gitlab do end namespace :repo do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping repositories ...".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("repositories") @@ -84,7 +84,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring repositories ...".color(:blue) Backup::Repository.new.restore $progress.puts "done".color(:green) @@ -92,7 +92,7 @@ namespace :gitlab do end namespace :db do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping database ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("db") @@ -103,7 +103,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring database ... ".color(:blue) Backup::Database.new.restore $progress.puts "done".color(:green) @@ -111,7 +111,7 @@ namespace :gitlab do end namespace :builds do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping builds ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("builds") @@ -122,7 +122,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring builds ... ".color(:blue) Backup::Builds.new.restore $progress.puts "done".color(:green) @@ -130,7 +130,7 @@ namespace :gitlab do end namespace :uploads do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping uploads ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("uploads") @@ -141,7 +141,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring uploads ... ".color(:blue) Backup::Uploads.new.restore $progress.puts "done".color(:green) @@ -149,7 +149,7 @@ namespace :gitlab do end namespace :artifacts do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping artifacts ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") @@ -160,7 +160,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring artifacts ... ".color(:blue) Backup::Artifacts.new.restore $progress.puts "done".color(:green) @@ -168,7 +168,7 @@ namespace :gitlab do end namespace :pages do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping pages ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("pages") @@ -179,7 +179,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring pages ... ".color(:blue) Backup::Pages.new.restore $progress.puts "done".color(:green) @@ -187,7 +187,7 @@ namespace :gitlab do end namespace :lfs do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping lfs objects ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("lfs") @@ -198,7 +198,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring lfs objects ... ".color(:blue) Backup::Lfs.new.restore $progress.puts "done".color(:green) @@ -206,7 +206,7 @@ namespace :gitlab do end namespace :registry do - task create: :environment do + task create: :gitlab_environment do $progress.puts "Dumping container registry images ... ".color(:blue) if Gitlab.config.registry.enabled @@ -221,7 +221,7 @@ namespace :gitlab do end end - task restore: :environment do + task restore: :gitlab_environment do $progress.puts "Restoring container registry images ... ".color(:blue) if Gitlab.config.registry.enabled diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index a584eb97cf5..e05a3aad824 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -1,7 +1,3 @@ -# Temporary hack, until we migrate all checks to SystemCheck format -require 'system_check' -require 'system_check/helpers' - namespace :gitlab do desc 'GitLab | Check the configuration of GitLab and its environment' task check: %w{gitlab:gitlab_shell:check @@ -12,7 +8,7 @@ namespace :gitlab do namespace :app do desc 'GitLab | Check the configuration of the GitLab Rails app' - task check: :environment do + task check: :gitlab_environment do warn_user_is_not_gitlab checks = [ @@ -43,7 +39,7 @@ namespace :gitlab do namespace :gitlab_shell do desc "GitLab | Check the configuration of GitLab Shell" - task check: :environment do + task check: :gitlab_environment do warn_user_is_not_gitlab start_checking "GitLab Shell" @@ -251,7 +247,7 @@ namespace :gitlab do namespace :sidekiq do desc "GitLab | Check the configuration of Sidekiq" - task check: :environment do + task check: :gitlab_environment do warn_user_is_not_gitlab start_checking "Sidekiq" @@ -310,7 +306,7 @@ namespace :gitlab do namespace :incoming_email do desc "GitLab | Check the configuration of Reply by email" - task check: :environment do + task check: :gitlab_environment do warn_user_is_not_gitlab if Gitlab.config.incoming_email.enabled @@ -333,7 +329,7 @@ namespace :gitlab do end namespace :ldap do - task :check, [:limit] => :environment do |_, args| + task :check, [:limit] => :gitlab_environment do |_, args| # Only show up to 100 results because LDAP directories can be very big. # This setting only affects the `rake gitlab:check` script. args.with_defaults(limit: 100) @@ -389,7 +385,7 @@ namespace :gitlab do namespace :repo do desc "GitLab | Check the integrity of the repositories managed by GitLab" - task check: :environment do + task check: :gitlab_environment do puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red) Rake::Task["gitlab:git:fsck"].execute end @@ -397,7 +393,7 @@ namespace :gitlab do namespace :orphans do desc 'Gitlab | Check for orphaned namespaces and repositories' - task check: :environment do + task check: :gitlab_environment do warn_user_is_not_gitlab checks = [ SystemCheck::Orphans::NamespaceCheck, @@ -408,7 +404,7 @@ namespace :gitlab do end desc 'GitLab | Check for orphaned namespaces in the repositories path' - task check_namespaces: :environment do + task check_namespaces: :gitlab_environment do warn_user_is_not_gitlab checks = [SystemCheck::Orphans::NamespaceCheck] @@ -416,7 +412,7 @@ namespace :gitlab do end desc 'GitLab | Check for orphaned repositories in the repositories path' - task check_repositories: :environment do + task check_repositories: :gitlab_environment do warn_user_is_not_gitlab checks = [SystemCheck::Orphans::RepositoryCheck] @@ -426,7 +422,7 @@ namespace :gitlab do namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" - task :check_repos, [:username] => :environment do |t, args| + task :check_repos, [:username] => :gitlab_environment do |t, args| username = args[:username] || prompt("Check repository integrity for username? ".color(:blue)) user = User.find_by(username: username) if user diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index ab601b0d66b..5a53eac0897 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -5,7 +5,7 @@ namespace :gitlab do HASHED_REPOSITORY_NAME = '@hashed'.freeze desc "GitLab | Cleanup | Clean namespaces" - task dirs: :environment do + task dirs: :gitlab_environment do warn_user_is_not_gitlab remove_flag = ENV['REMOVE'] @@ -49,7 +49,7 @@ namespace :gitlab do end desc "GitLab | Cleanup | Clean repositories" - task repos: :environment do + task repos: :gitlab_environment do warn_user_is_not_gitlab move_suffix = "+orphaned+#{Time.now.to_i}" @@ -78,7 +78,7 @@ namespace :gitlab do end desc "GitLab | Cleanup | Block users that have been removed in LDAP" - task block_removed_ldap_users: :environment do + task block_removed_ldap_users: :gitlab_environment do warn_user_is_not_gitlab block_flag = ENV['BLOCK'] @@ -109,7 +109,7 @@ namespace :gitlab do # released. So likely this should only be run once on gitlab.com # Faulty refs are moved so they are kept around, else some features break. desc 'GitLab | Cleanup | Remove faulty deployment refs' - task move_faulty_deployment_refs: :environment do + task move_faulty_deployment_refs: :gitlab_environment do projects = Project.where(id: Deployment.select(:project_id).distinct) projects.find_each do |project| diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index 3f5dd2ae3b3..cb4f7e5c8a8 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :git do desc "GitLab | Git | Repack" - task repack: :environment do + task repack: :gitlab_environment do failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo") if failures.empty? puts "Done".color(:green) @@ -11,7 +11,7 @@ namespace :gitlab do end desc "GitLab | Git | Run garbage collection on all repos" - task gc: :environment do + task gc: :gitlab_environment do failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting") if failures.empty? puts "Done".color(:green) @@ -21,7 +21,7 @@ namespace :gitlab do end desc "GitLab | Git | Prune all repos" - task prune: :environment do + task prune: :gitlab_environment do failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune") if failures.empty? puts "Done".color(:green) @@ -31,7 +31,7 @@ namespace :gitlab do end desc 'GitLab | Git | Check all repos integrity' - task fsck: :environment do + task fsck: :gitlab_environment do failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo| check_config_lock(repo) check_ref_locks(repo) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index a2e68c0471b..107ff1d8aeb 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :gitaly do desc "GitLab | Install or upgrade gitaly" - task :install, [:dir, :repo] => :environment do |t, args| + task :install, [:dir, :repo] => :gitlab_environment do |t, args| require 'toml' warn_user_is_not_gitlab @@ -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 diff --git a/lib/tasks/gitlab/helpers.rake b/lib/tasks/gitlab/helpers.rake index b0a24790c4a..14d1125a03d 100644 --- a/lib/tasks/gitlab/helpers.rake +++ b/lib/tasks/gitlab/helpers.rake @@ -1,8 +1,6 @@ -require 'tasks/gitlab/task_helpers' - # Prevent StateMachine warnings from outputting during a cron task StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] -namespace :gitlab do +task gitlab_environment: :environment do extend SystemCheck::Helpers end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index e9fb6a008b0..45e9a1a1c72 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :env do desc "GitLab | Show information about GitLab and its environment" - task info: :environment do + task info: :gitlab_environment do # check if there is an RVM environment rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s) # check Ruby version diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 05fcb8e3da5..1d903c81358 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -1,6 +1,6 @@ namespace :gitlab do desc "GitLab | Setup production application" - task setup: :environment do + task setup: :gitlab_environment do setup_db end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 12ae4199b69..844664b12d4 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :shell do desc "GitLab | Install or upgrade gitlab-shell" - task :install, [:repo] => :environment do |t, args| + task :install, [:repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab default_version = Gitlab::Shell.version_required @@ -58,12 +58,12 @@ namespace :gitlab do end desc "GitLab | Setup gitlab-shell" - task setup: :environment do + task setup: :gitlab_environment do setup end desc "GitLab | Build missing projects" - task build_missing_projects: :environment do + task build_missing_projects: :gitlab_environment do Project.find_each(batch_size: 1000) do |project| path_to_repo = project.repository.path_to_repo if File.exist?(path_to_repo) @@ -80,7 +80,7 @@ namespace :gitlab do end desc 'Create or repair repository hooks symlink' - task create_hooks: :environment do + task create_hooks: :gitlab_environment do warn_user_is_not_gitlab puts 'Creating/Repairing hooks symlinks for all repositories' diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index 308ffb0e284..b917a293095 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :workhorse do desc "GitLab | Install or upgrade gitlab-workhorse" - task :install, [:dir, :repo] => :environment do |t, args| + task :install, [:dir, :repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab unless args.dir.present? diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake index ad2d034b0b4..5c0cc4990fc 100644 --- a/lib/tasks/haml-lint.rake +++ b/lib/tasks/haml-lint.rake @@ -2,5 +2,14 @@ unless Rails.env.production? require 'haml_lint/rake_task' require 'haml_lint/inline_javascript' + # Workaround for warnings from parser/current + # TODO: Remove this after we update parser gem + task :haml_lint do + require 'parser' + def Parser.warn(*args) + puts(*args) # static-analysis ignores stdout if status is 0 + end + end + HamlLint::RakeTask.new end diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index c996537cfbe..31cbd651edb 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -1,16 +1,14 @@ -require Rails.root.join('lib/gitlab/database') -require Rails.root.join('lib/gitlab/database/migration_helpers') -require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') -require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') -require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes') -require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like') -require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes') -require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like') -require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb') -require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb') - desc 'GitLab | Sets up PostgreSQL' task setup_postgresql: :environment do + require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes') + require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes') + require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes') + require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like') + require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes') + require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like') + require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb') + require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb') + NamespacesProjectsPathLowerIndexes.new.up AddUsersLowerUsernameEmailIndexes.new.up AddLowerPathIndexToRoutes.new.up @@ -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/scripts/static-analysis b/scripts/static-analysis index 96d08287ded..b3895292ab4 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -1,6 +1,29 @@ #!/usr/bin/env ruby -require ::File.expand_path('../lib/gitlab/popen', __dir__) +# We don't have auto-loading here +require_relative '../lib/gitlab/popen' +require_relative '../lib/gitlab/popen/runner' + +def emit_warnings(static_analysis) + static_analysis.warned_results.each do |result| + puts + puts "**** #{result.cmd.join(' ')} had the following warnings:" + puts + puts result.stderr + puts + end +end + +def emit_errors(static_analysis) + static_analysis.failed_results.each do |result| + puts + puts "**** #{result.cmd.join(' ')} failed with the following error:" + puts + puts result.stdout + puts result.stderr + puts + end +end tasks = [ %w[bundle exec rake config_lint], @@ -17,18 +40,16 @@ tasks = [ %w[scripts/lint-rugged] ] -failed_tasks = tasks.reduce({}) do |failures, task| - start = Time.now - puts - puts "$ #{task.join(' ')}" +static_analysis = Gitlab::Popen::Runner.new - output, status = Gitlab::Popen.popen(task) - puts "==> Finished in #{Time.now - start} seconds" +static_analysis.run(tasks) do |cmd, &run| puts + puts "$ #{cmd.join(' ')}" - failures[task.join(' ')] = output unless status.zero? + result = run.call - failures + puts "==> Finished in #{result.duration} seconds" + puts end puts @@ -36,17 +57,20 @@ puts '===================================================' puts puts -if failed_tasks.empty? +if static_analysis.all_success_and_clean? puts 'All static analyses passed successfully.' +elsif static_analysis.all_success? + puts 'All static analyses passed successfully, but we have warnings:' + puts + + emit_warnings(static_analysis) + + exit 2 else puts 'Some static analyses failed:' - failed_tasks.each do |failed_task, output| - puts - puts "**** #{failed_task} failed with the following error:" - puts - puts output - end + emit_warnings(static_analysis) + emit_errors(static_analysis) exit 1 end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index a28b8905b65..62a2ec55b00 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -194,7 +194,7 @@ describe 'Commits' do end it 'includes the committed_date for each commit' do - commits = project.repository.commits(branch_name) + commits = project.repository.commits(branch_name, limit: 40) commits.each do |commit| expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}") diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 69e4c9f04a1..89d3bd24b89 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -17,12 +17,15 @@ feature 'Editing file blob', :js do sign_in(user) end - def edit_and_commit + def edit_and_commit(commit_changes: true) wait_for_requests find('.js-edit-blob').click find('#editor') execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') - click_button 'Commit changes' + + if commit_changes + click_button 'Commit changes' + end end context 'from MR diff' do @@ -39,13 +42,26 @@ feature 'Editing file blob', :js do context 'from blob file path' do before do visit project_blob_path(project, tree_join(branch, file_path)) - edit_and_commit end it 'updates content' do + edit_and_commit + expect(page).to have_content 'successfully committed' expect(page).to have_content 'NextFeature' end + + it 'previews content' do + edit_and_commit(commit_changes: false) + click_link 'Preview changes' + wait_for_requests + + old_line_count = page.all('.line_holder.old').size + new_line_count = page.all('.line_holder.new').size + + expect(old_line_count).to be > 0 + expect(new_line_count).to be > 0 + end end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 8953b30bebf..94bde723e2f 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do context 'when user disables the cluster' do before do - page.find(:css, '.js-toggle-cluster').click + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click page.within('#cluster-integration') { click_button 'Save changes' } end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index a519b9f9c7e..b9ab434c259 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -62,7 +62,7 @@ feature 'User Cluster', :js do context 'when user disables the cluster' do before do - page.find(:css, '.js-toggle-cluster').click + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click fill_in 'cluster_name', with: 'dev-cluster' page.within('#cluster-integration') { click_button 'Save changes' } end diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index eae2910a8f6..497a50bebe4 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -37,13 +37,13 @@ feature 'Clusters', :js do context 'inline update of cluster' do it 'user can update cluster' do - expect(page).to have_selector('.js-toggle-cluster-list') + expect(page).to have_selector('.js-project-feature-toggle') end context 'with sucessfull request' do it 'user sees updated cluster' do expect do - page.find('.js-toggle-cluster-list').click + page.find('.js-project-feature-toggle').click wait_for_requests end.to change { cluster.reload.enabled } @@ -57,7 +57,7 @@ feature 'Clusters', :js do expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false } - page.find('.js-toggle-cluster-list').click + page.find('.js-project-feature-toggle').click expect(page).to have_content('Something went wrong on our end.') expect(page).to have_selector('.is-checked') 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/initializers/grape_route_helpers_fix_spec.rb b/spec/initializers/grape_route_helpers_fix_spec.rb new file mode 100644 index 00000000000..2cf5924128f --- /dev/null +++ b/spec/initializers/grape_route_helpers_fix_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' +require_relative '../../config/initializers/grape_route_helpers_fix' + +describe 'route shadowing' do + include GrapeRouteHelpers::NamedRouteMatcher + + it 'does not occur' do + path = api_v4_projects_merge_requests_path(id: 1) + expect(path).to eq('/api/v4/projects/1/merge_requests') + + path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3) + expect(path).to eq('/api/v4/projects/1/merge_requests/3') + end +end diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index f5be9ea0fb2..7b38f6b7855 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -23,16 +23,24 @@ describe('Clusters', () => { }); describe('toggle', () => { - it('should update the button and the input field on click', () => { - cluster.toggleButton.click(); + it('should update the button and the input field on click', (done) => { + const toggleButton = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle'); + const toggleInput = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle-input'); - expect( - cluster.toggleButton.classList, - ).not.toContain('is-checked'); + toggleButton.click(); - expect( - cluster.toggleInput.getAttribute('value'), - ).toEqual('false'); + getSetTimeoutPromise() + .then(() => { + expect( + toggleButton.classList, + ).not.toContain('is-checked'); + + expect( + toggleInput.getAttribute('value'), + ).toEqual('false'); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js deleted file mode 100644 index 0a8b63ed5b4..00000000000 --- a/spec/javascripts/clusters/clusters_index_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import setClusterTableToggles from '~/clusters/clusters_index'; -import { setTimeout } from 'core-js/library/web/timers'; - -describe('Clusters table', () => { - preloadFixtures('clusters/index_cluster.html.raw'); - let mock; - - beforeEach(() => { - loadFixtures('clusters/index_cluster.html.raw'); - mock = new MockAdapter(axios); - setClusterTableToggles(); - }); - - describe('update cluster', () => { - it('renders loading state while request is made', () => { - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - - expect(button.classList).toContain('is-loading'); - expect(button.getAttribute('disabled')).toEqual('true'); - }); - - afterEach(() => { - mock.restore(); - }); - - it('shows updated state after sucessfull request', (done) => { - mock.onPut().reply(200, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - button.click(); - - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).not.toContain('is-checked'); - done(); - }, 0); - }); - - it('shows inital state after failed request', (done) => { - mock.onPut().reply(500, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).toContain('is-checked'); - done(); - }, 0); - }); - }); -}); diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index d26ea3febe8..8e74c4f859c 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle expect(response).to be_success store_frontend_fixture(response, example.description) end - - context 'rendering non-empty state' do - before do - cluster - end - - it 'clusters/index_cluster.html.raw' do |example| - get :index, - namespace_id: namespace, - project_id: project - - expect(response).to be_success - store_frontend_fixture(response, example.description) - end - end end 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/toggle_buttons_spec.js b/spec/javascripts/toggle_buttons_spec.js new file mode 100644 index 00000000000..205e396d682 --- /dev/null +++ b/spec/javascripts/toggle_buttons_spec.js @@ -0,0 +1,120 @@ +import setupToggleButtons from '~/toggle_buttons'; +import getSetTimeoutPromise from './helpers/set_timeout_promise_helper'; + +function generateMarkup(isChecked = true) { + return ` + <button type="button" class="${isChecked ? 'is-checked' : ''} js-project-feature-toggle"> + <input type="hidden" class="js-project-feature-toggle-input" value="${isChecked}" /> + </button> + `; +} + +function setupFixture(isChecked, clickCallback) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = generateMarkup(isChecked); + + setupToggleButtons(wrapper, clickCallback); + + return wrapper; +} + +describe('ToggleButtons', () => { + describe('when input value is true', () => { + it('should initialize as checked', () => { + const wrapper = setupFixture(true); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }); + + it('should toggle to unchecked when clicked', (done) => { + const wrapper = setupFixture(true); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when input value is false', () => { + it('should initialize as unchecked', () => { + const wrapper = setupFixture(false); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }); + + it('should toggle to checked when clicked', (done) => { + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }) + .then(done) + .catch(done.fail); + }); + }); + + it('should emit `trigger-change` event', (done) => { + const changeSpy = jasmine.createSpy('changeEventHandler'); + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + const input = wrapper.querySelector('.js-project-feature-toggle-input'); + + $(input).on('trigger-change', changeSpy); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(changeSpy).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('clickCallback', () => { + it('should show loading indicator while waiting', (done) => { + const isChecked = true; + const clickCallback = (newValue, toggleButton) => { + const input = toggleButton.querySelector('.js-project-feature-toggle-input'); + + expect(newValue).toEqual(false); + + // Check for the loading state + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(true); + expect(toggleButton.disabled).toEqual(true); + expect(input.value).toEqual('true'); + + // After the callback finishes, check that the loading state is gone + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(false); + expect(toggleButton.disabled).toEqual(false); + expect(input.value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }; + + const wrapper = setupFixture(isChecked, clickCallback); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + }); + }); +}); 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/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index 2dc3b72ea40..43a989393ba 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -1,108 +1,99 @@ import Vue from 'vue'; -import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged'; +import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; - -const targetBranch = 'foo'; - -const createComponent = () => { - const Component = Vue.extend(mergedComponent); - const mr = { - isRemovingSourceBranch: false, - cherryPickInForkPath: false, - canCherryPickInCurrentMR: true, - revertInForkPath: false, - canRevertInCurrentMR: true, - canRemoveSourceBranch: true, - sourceBranchRemoved: true, - metrics: { - mergedBy: {}, - mergedAt: 'mergedUpdatedAt', - readableMergedAt: '', - closedBy: {}, - closedAt: 'mergedUpdatedAt', - readableClosedAt: '', - }, - updatedAt: 'mrUpdatedAt', - targetBranch, - }; - - const service = { - removeSourceBranch() {}, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr, service }, - }); -}; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetMerged', () => { - describe('props', () => { - it('should have props', () => { - const { mr, service } = mergedComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); - }); - }); - - describe('components', () => { - it('should have components added', () => { - expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + let vm; + const targetBranch = 'foo'; + + beforeEach(() => { + const Component = Vue.extend(mergedComponent); + const mr = { + isRemovingSourceBranch: false, + cherryPickInForkPath: false, + canCherryPickInCurrentMR: true, + revertInForkPath: false, + canRevertInCurrentMR: true, + canRemoveSourceBranch: true, + sourceBranchRemoved: true, + metrics: { + mergedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + closedBy: {}, + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableClosedAt: '', + }, + updatedAt: 'mergedUpdatedAt', + targetBranch, + }; + + const service = { + removeSourceBranch() {}, + }; + + spyOn(eventHub, '$emit'); + + vm = mountComponent(Component, { mr, service }); }); - describe('data', () => { - it('should have default data', () => { - const data = mergedComponent.data(); - - expect(data.isMakingRequest).toBeFalsy(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('shouldShowRemoveSourceBranch', () => { - it('should correct value when fields changed', () => { - const vm = createComponent(); + it('returns true when sourceBranchRemoved is false', () => { vm.mr.sourceBranchRemoved = false; - expect(vm.shouldShowRemoveSourceBranch).toBeTruthy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(true); + }); + it('returns false wehn sourceBranchRemoved is true', () => { vm.mr.sourceBranchRemoved = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns false when canRemoveSourceBranch is false', () => { vm.mr.sourceBranchRemoved = false; vm.mr.canRemoveSourceBranch = false; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns false when is making request', () => { vm.mr.canRemoveSourceBranch = true; vm.isMakingRequest = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + }); + it('returns true when all are true', () => { vm.mr.isRemovingSourceBranch = true; vm.mr.canRemoveSourceBranch = true; vm.isMakingRequest = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); }); }); + describe('shouldShowSourceBranchRemoving', () => { it('should correct value when fields changed', () => { - const vm = createComponent(); vm.mr.sourceBranchRemoved = false; - expect(vm.shouldShowSourceBranchRemoving).toBeFalsy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(false); vm.mr.sourceBranchRemoved = true; - expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); + expect(vm.shouldShowRemoveSourceBranch).toEqual(false); vm.mr.sourceBranchRemoved = false; vm.isMakingRequest = true; - expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(true); vm.isMakingRequest = false; vm.mr.isRemovingSourceBranch = true; - expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); + expect(vm.shouldShowSourceBranchRemoving).toEqual(true); }); }); }); @@ -110,8 +101,6 @@ describe('MRWidgetMerged', () => { describe('methods', () => { describe('removeSourceBranch', () => { it('should set flag and call service then request main component to update the widget', (done) => { - const vm = createComponent(); - spyOn(eventHub, '$emit'); spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => { resolve({ data: { @@ -123,7 +112,7 @@ describe('MRWidgetMerged', () => { vm.removeSourceBranch(); setTimeout(() => { const args = eventHub.$emit.calls.argsFor(0); - expect(vm.isMakingRequest).toBeTruthy(); + expect(vm.isMakingRequest).toEqual(true); expect(args[0]).toEqual('MRWidgetUpdateRequested'); expect(args[1]).not.toThrow(); done(); @@ -132,53 +121,50 @@ describe('MRWidgetMerged', () => { }); }); - describe('template', () => { - let vm; - let el; + it('has merged by information', () => { + expect(vm.$el.textContent).toContain('Merged by'); + expect(vm.$el.textContent).toContain('Administrator'); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders branch information', () => { + expect(vm.$el.textContent).toContain('The changes were merged into'); + expect(vm.$el.textContent).toContain(targetBranch); + }); - it('should have correct elements', () => { - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('.js-mr-widget-author')).toBeDefined(); - expect(el.innerText).toContain('The changes were merged into'); - expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch has been removed'); - expect(el.innerText).toContain('Revert'); - expect(el.innerText).toContain('Cherry-pick'); - expect(el.innerText).not.toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch is being removed'); - }); + it('renders information about branch being removed', () => { + expect(vm.$el.textContent).toContain('The source branch has been removed'); + }); - it('should not show source branch removed text', (done) => { - vm.mr.sourceBranchRemoved = false; + it('shows revert and cherry-pick buttons', () => { + expect(vm.$el.textContent).toContain('Revert'); + expect(vm.$el.textContent).toContain('Cherry-pick'); + }); - Vue.nextTick(() => { - expect(el.innerText).toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch has been removed'); - done(); - }); + it('should not show source branch removed text', (done) => { + vm.mr.sourceBranchRemoved = false; + + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('You can remove source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + done(); }); + }); - it('should show source branch removing text', (done) => { - vm.mr.isRemovingSourceBranch = true; - vm.mr.sourceBranchRemoved = false; + it('should show source branch removing text', (done) => { + vm.mr.isRemovingSourceBranch = true; + vm.mr.sourceBranchRemoved = false; - Vue.nextTick(() => { - expect(el.innerText).toContain('The source branch is being removed'); - expect(el.innerText).not.toContain('You can remove source branch now'); - expect(el.innerText).not.toContain('The source branch has been removed'); - done(); - }); + Vue.nextTick(() => { + expect(vm.$el.innerText).toContain('The source branch is being removed'); + expect(vm.$el.innerText).not.toContain('You can remove source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + done(); }); + }); - it('should use mergedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('mergedUpdatedAt'); - }); + it('should use mergedEvent mergedAt as tooltip title', () => { + expect( + vm.$el.querySelector('time').getAttribute('title'), + ).toBe('Jan 24, 2018 1:02pm GMT+0000'); }); }); diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 3db04d99855..935d1df6dad 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -981,6 +981,16 @@ describe Gitlab::Git::Repository, seed_helper: true do end end end + + context 'limit validation' do + where(:limit) do + [0, nil, '', 'foo'] + end + + with_them do + it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } + end + end end describe "#rugged_commits_between" do diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 4cf33778d15..b6c1f0c81cb 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -7096,7 +7096,7 @@ "project_id": 5, "created_at": "2016-06-14T15:01:51.232Z", "updated_at": "2016-06-14T15:01:51.232Z", - "active": false, + "active": true, "properties": { }, diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 08e5bbbd400..5804c45871e 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -164,6 +164,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') end + it 'saves the properties for a service' do + expect(saved_project_json['services'].first['properties']).to eq('one' => 'value') + end + it 'has project feature' do project_feature = saved_project_json['project_feature'] expect(project_feature).not_to be_empty @@ -279,7 +283,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do commit_id: ci_build.pipeline.sha) create(:event, :created, target: milestone, project: project, author: user) - create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') + create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project) diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb new file mode 100644 index 00000000000..2e2cb4ca28f --- /dev/null +++ b/spec/lib/gitlab/popen/runner_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +describe Gitlab::Popen::Runner do + subject { described_class.new } + + describe '#run' do + it 'runs the command and returns the result' do + run_command + + expect(Gitlab::Popen).to have_received(:popen_with_detail) + end + end + + describe '#all_success_and_clean?' do + it 'returns true when exit status is 0 and stderr is empty' do + run_command + + expect(subject).to be_all_success_and_clean + end + + it 'returns false when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject).not_to be_all_success_and_clean + end + + it 'returns false when exit stderr has something' do + run_command(stderr: 'stderr') + + expect(subject).not_to be_all_success_and_clean + end + end + + describe '#all_success?' do + it 'returns true when exit status is 0' do + run_command + + expect(subject).to be_all_success + end + + it 'returns false when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject).not_to be_all_success + end + + it 'returns true' do + run_command(stderr: 'stderr') + + expect(subject).to be_all_success + end + end + + describe '#all_stderr_empty?' do + it 'returns true when stderr is empty' do + run_command + + expect(subject).to be_all_stderr_empty + end + + it 'returns true when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject).to be_all_stderr_empty + end + + it 'returns false when exit stderr has something' do + run_command(stderr: 'stderr') + + expect(subject).not_to be_all_stderr_empty + end + end + + describe '#failed_results' do + it 'returns [] when everything is passed' do + run_command + + expect(subject.failed_results).to be_empty + end + + it 'returns the result when exit status is not 0' do + result = run_command(exitstatus: 1) + + expect(subject.failed_results).to contain_exactly(result) + end + + it 'returns [] when exit stderr has something' do + run_command(stderr: 'stderr') + + expect(subject.failed_results).to be_empty + end + end + + describe '#warned_results' do + it 'returns [] when everything is passed' do + run_command + + expect(subject.warned_results).to be_empty + end + + it 'returns [] when exit status is not 0' do + run_command(exitstatus: 1) + + expect(subject.warned_results).to be_empty + end + + it 'returns the result when exit stderr has something' do + result = run_command(stderr: 'stderr') + + expect(subject.warned_results).to contain_exactly(result) + end + end + + def run_command( + command: 'command', + stdout: 'stdout', + stderr: '', + exitstatus: 0, + status: double(exitstatus: exitstatus, success?: exitstatus.zero?), + duration: 0.1) + + result = + Gitlab::Popen::Result.new(command, stdout, stderr, status, duration) + + allow(Gitlab::Popen) + .to receive(:popen_with_detail) + .and_return(result) + + subject.run([command]) do |cmd, &run| + expect(cmd).to eq(command) + + cmd_result = run.call + + expect(cmd_result).to eq(result) + end + + subject.results.first + end +end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index b145ca36f26..1dbead16d5b 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -1,11 +1,23 @@ require 'spec_helper' -describe 'Gitlab::Popen' do +describe Gitlab::Popen do let(:path) { Rails.root.join('tmp').to_s } before do @klass = Class.new(Object) - @klass.send(:include, Gitlab::Popen) + @klass.send(:include, described_class) + end + + describe '.popen_with_detail' do + subject { @klass.new.popen_with_detail(cmd) } + + let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1);$stderr.puts(2);exit(3)] } + + it { expect(subject.cmd).to eq(cmd) } + it { expect(subject.stdout).to eq("1\n") } + it { expect(subject.stderr).to eq("2\n") } + it { expect(subject.status.exitstatus).to eq(3) } + it { expect(subject.duration).to be_kind_of(Numeric) } end context 'zero status' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 45a606c1ea8..f5b3b4a9fc5 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -277,7 +277,7 @@ describe Ci::Build do allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) end - it { is_expected.to be_an(Array).and all(include(key: "key:1")) } + it { is_expected.to be_an(Array).and all(include(key: "key_1")) } end context 'when project does not have jobs_cache_index' do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index f8a98b43e46..959383ff0b7 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -228,7 +228,7 @@ eos it { expect(data).to be_a(Hash) } it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') } it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') } - it { expect(data[:added]).to eq(["bar/branch-test.txt"]) } + it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") } it { expect(data[:modified]).to eq([]) } it { expect(data[:removed]).to eq([]) } end @@ -532,8 +532,8 @@ eos let(:commit2) { merge_request1.merge_request_diff.commits.first } it 'returns merge_requests that introduced that commit' do - expect(commit1.merge_requests).to eq([merge_request1, merge_request2]) - expect(commit2.merge_requests).to eq([merge_request1]) + expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2) + expect(commit2.merge_requests).to contain_exactly(merge_request1) end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 429b6615131..eb9690df313 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) @@ -1529,7 +1539,7 @@ describe MergeRequest do expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1) end - it "executs diff cache service" do + it "executes diff cache service" do expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject) subject.reload_diff diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index c9b3c6cf602..1eaaadf56c5 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -3,6 +3,29 @@ require 'spec_helper' describe JiraService do include Gitlab::Routing + describe '#options' do + let(:service) do + described_class.new( + project: build_stubbed(:project), + active: true, + username: 'username', + password: 'test', + jira_issue_transition_id: 24, + url: 'http://jira.test.com/path/' + ) + end + + it 'sets the URL properly' do + # jira-ruby gem parses the URI and handles trailing slashes + # fine: https://github.com/sumoheavy/jira-ruby/blob/v1.4.1/lib/jira/http_client.rb#L59 + expect(service.options[:site]).to eq('http://jira.test.com/') + end + + it 'leaves out trailing slashes in context' do + expect(service.options[:context_path]).to eq('/path') + end + end + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7c61c6b7299..d4070b320ed 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -222,20 +222,20 @@ describe Repository do it 'sets follow when path is a single path' do expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice - repository.commits('master', path: 'README.md') - repository.commits('master', path: ['README.md']) + repository.commits('master', limit: 1, path: 'README.md') + repository.commits('master', limit: 1, path: ['README.md']) end it 'does not set follow when path is multiple paths' do expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original - repository.commits('master', path: ['README.md', 'CHANGELOG']) + repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG']) end it 'does not set follow when there are no paths' do expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original - repository.commits('master') + repository.commits('master', limit: 1) end end @@ -455,7 +455,7 @@ describe Repository do expect do repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) newdir = repository.tree('master', 'newdir') expect(newdir.path).to eq('newdir') @@ -469,7 +469,7 @@ describe Repository do repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'patch', start_branch_name: 'master', start_project: forked_project) - end.to change { repository.commits('master').count }.by(0) + end.to change { repository.count_commits(ref: 'master') }.by(0) expect(repository.branch_exists?('patch')).to be_truthy expect(forked_project.repository.branch_exists?('patch')).to be_falsy @@ -486,7 +486,7 @@ describe Repository do message: 'Add newdir', branch_name: 'master', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -502,7 +502,7 @@ describe Repository do repository.create_file(user, 'NEWCHANGELOG', 'Changelog!', message: 'Create changelog', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) blob = repository.blob_at('master', 'NEWCHANGELOG') @@ -514,7 +514,7 @@ describe Repository do repository.create_file(user, 'new_dir/new_file.txt', 'File!', message: 'Create new_file with new_dir', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) expect(repository.tree('master', 'new_dir').path).to eq('new_dir') expect(repository.blob_at('master', 'new_dir/new_file.txt').data).to eq('File!') @@ -538,7 +538,7 @@ describe Repository do branch_name: 'master', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -554,7 +554,7 @@ describe Repository do repository.update_file(user, 'CHANGELOG', 'Changelog!', message: 'Update changelog', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) blob = repository.blob_at('master', 'CHANGELOG') @@ -567,7 +567,7 @@ describe Repository do branch_name: 'master', previous_path: 'LICENSE', message: 'Changes filename') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) files = repository.ls_files('master') @@ -584,7 +584,7 @@ describe Repository do message: 'Update README', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit @@ -599,7 +599,7 @@ describe Repository do expect do repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master') - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) expect(repository.blob_at('master', 'README')).to be_nil end @@ -610,7 +610,7 @@ describe Repository do repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master', author_email: author_email, author_name: author_name) - end.to change { repository.commits('master').count }.by(1) + end.to change { repository.count_commits(ref: 'master') }.by(1) last_commit = repository.commit diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 34db50dc082..ff5f207487b 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -62,7 +62,7 @@ describe API::Commits do context "since optional parameter" do it "returns project commits since provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) after = commits.second.created_at get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user) @@ -73,7 +73,7 @@ describe API::Commits do end it 'include correct pagination headers' do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) after = commits.second.created_at commit_count = project.repository.count_commits(ref: 'master', after: after).to_s @@ -87,12 +87,12 @@ describe API::Commits do context "until optional parameter" do it "returns project commits until provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 20) before = commits.second.created_at get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user) - if commits.size >= 20 + if commits.size == 20 expect(json_response.size).to eq(20) else expect(json_response.size).to eq(commits.size - 1) @@ -103,7 +103,7 @@ describe API::Commits do end it 'include correct pagination headers' do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) before = commits.second.created_at commit_count = project.repository.count_commits(ref: 'master', before: before).to_s @@ -181,7 +181,7 @@ describe API::Commits do let(:page) { 3 } it 'returns the third 5 commits' do - commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first expect(json_response.size).to eq(per_page) expect(json_response.first['id']).to eq(commit.id) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 43218755f4f..13db40d21a5 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/123/issues/#{issue.iid}/move", user), + post api("/projects/12345/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_gitlab_http_status(404) @@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - to_project_id: 123 + to_project_id: 12345 expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 8e2982f1a5d..14dd9da119d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -198,6 +198,8 @@ describe API::MergeRequests do create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + expect do get api("/projects/#{project.id}/merge_requests", user) end.not_to exceed_query_limit(control) diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 34c543bffe8..9ef3b859001 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -36,7 +36,7 @@ describe API::V3::Commits do context "since optional parameter" do it "returns project commits since provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 2) since = commits.second.created_at get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) @@ -49,12 +49,12 @@ describe API::V3::Commits do context "until optional parameter" do it "returns project commits until provided parameter" do - commits = project.repository.commits("master") + commits = project.repository.commits("master", limit: 20) before = commits.second.created_at get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) - if commits.size >= 20 + if commits.size == 20 expect(json_response.size).to eq(20) else expect(json_response.size).to eq(commits.size - 1) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7a01d3dd698..7c3374c6113 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -55,11 +55,12 @@ describe MergeRequests::RefreshService do before do allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs end it 'executes hooks with update action' do + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + expect(refresh_service).to have_received(:execute_hooks) .with(@merge_request, 'update', old_rev: @oldrev) @@ -72,6 +73,26 @@ describe MergeRequests::RefreshService do expect(@build_failed_todo).to be_done expect(@fork_build_failed_todo).to be_done end + + context 'when source branch ref does not exists' do + before do + DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) + end + + it 'closes MRs without source branch ref' do + expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') } + .to change { @merge_request.reload.state } + .from('opened') + .to('closed') + + expect(@fork_merge_request.reload).to be_open + end + + it 'does not change the merge request diff' do + expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') } + .not_to change { @merge_request.reload.merge_request_diff } + end + end end context 'when pipeline exists for the source branch' do diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index 923c8080e6c..2197bc9d853 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -1,6 +1,5 @@ require 'action_dispatch/testing/test_request' require 'fileutils' -require 'gitlab/popen' module JavaScriptFixturesHelpers include Gitlab::Popen 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) diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb index fae5ec35c47..e9322ec4931 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/tasks/gitlab/task_helpers_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'tasks/gitlab/task_helpers' class TestHelpersTest include Gitlab::TaskHelpers diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 7274a9f00f9..2b1a617ee62 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -49,9 +49,22 @@ describe RepositoryImportWorker do expect do subject.perform(project.id) - end.to raise_error(StandardError, error) + end.to raise_error(RuntimeError, error) expect(project.reload.import_jid).not_to be_nil end + + it 'updates the error on Import/Export' do + error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + + project.update_attributes(import_jid: '123', import_type: 'gitlab_project') + expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) + + expect do + subject.perform(project.id) + end.to raise_error(RuntimeError, error) + + expect(project.reload.import_error).not_to be_nil + end end context 'when using an asynchronous importer' do |