Commit a148b99d authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Merge branch 'master' into 4310-security-reports

* master: (109 commits)
  Fix JIRA not working when a trailing slash is included
  Allow project to be set up to push to and pull from same mirror
  Remove tap and use simplified method call
  Remove tap and use simplified method call
  Replace $.post in edit blob with axios
  Resolve conflict in qa/qa/page/project/show.rb
  Add changelog entry
  Add test for `updateIssueOrder`
  Import mock data from file
  Mock data for related issues app
  Update issue order in store upon reorder success
  Fix an order dependency in a spec
  Close and do not reload MR diffs when source branch is deleted
  Don't allow Repository#log with limit zero
  Replace $.post in awards handler with axios
  Fix broken test
  Fix MR revert check when no merged_at is present
  Removed typo check for `unaproved` in sast:container report
  Disable MR check out button when source branch is deleted
  normalize headers correctly i18n flash message
  ...
parents 5f9d0084 c61a917f
...@@ -344,6 +344,7 @@ setup-test-env: ...@@ -344,6 +344,7 @@ setup-test-env:
expire_in: 7d expire_in: 7d
paths: paths:
- tmp/tests - tmp/tests
- config/secrets.yml
rspec-pg geo: *rspec-metadata-pg-geo rspec-pg geo: *rspec-metadata-pg-geo
......
...@@ -337,7 +337,7 @@ group :development, :test do ...@@ -337,7 +337,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' 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) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -329,7 +329,7 @@ GEM ...@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-license (1.0.0) gitlab-license (1.0.0)
gitlab-markup (1.6.3) gitlab-markup (1.6.3)
gitlab-styles (2.3.1) gitlab-styles (2.3.2)
rubocop (~> 0.51) rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19) rubocop-rspec (~> 1.19)
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { s__ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; 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 animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -441,13 +443,15 @@ class AwardsHandler { ...@@ -441,13 +443,15 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) { if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton); this.userAuthored($emojiButton);
} else { } else {
$.post(awardUrl, { axios.post(awardUrl, {
name: emoji, name: emoji,
}, (data) => { })
.then(({ data }) => {
if (data.ok) { if (data.ok) {
callback(); callback();
} }
}).fail(() => new Flash('Something went wrong on our end.')); })
.catch(() => flash(s__('Something went wrong on our end.')));
} }
} }
......
/* global ace */ /* global ace */
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob { export default class EditBlob {
...@@ -56,12 +59,14 @@ export default class EditBlob { ...@@ -56,12 +59,14 @@ export default class EditBlob {
if (paneId === '#preview') { if (paneId === '#preview') {
this.$toggleButton.hide(); this.$toggleButton.hide();
return $.post(currentLink.data('preview-url'), { axios.post(currentLink.data('preview-url'), {
content: this.editor.getValue(), content: this.editor.getValue(),
}, (response) => { })
currentPane.empty().append(response); .then(({ data }) => {
return currentPane.renderGFM(); currentPane.empty().append(data);
}); currentPane.renderGFM();
})
.catch(() => createFlash(__('An error occurred previewing the blob')));
} }
this.$toggleButton.show(); this.$toggleButton.show();
......
...@@ -87,6 +87,9 @@ export default { ...@@ -87,6 +87,9 @@ export default {
isEditForm() { isEditForm() {
return this.currentPage === 'edit'; return this.currentPage === 'edit';
}, },
isVisible() {
return this.currentPage !== '';
},
buttonText() { buttonText() {
if (this.isNewForm) { if (this.isNewForm) {
return 'Create board'; return 'Create board';
...@@ -181,14 +184,14 @@ export default { ...@@ -181,14 +184,14 @@ export default {
<template> <template>
<modal <modal
v-show="currentPage" v-show="isVisible"
modal-dialog-class="board-config-modal" modal-dialog-class="board-config-modal"
:hide-footer="readonly" :hide-footer="readonly"
:title="title" :title="title"
:primary-button-label="buttonText" :primary-button-label="buttonText"
:kind="buttonKind" :kind="buttonKind"
:submit-disabled="submitDisabled" :submit-disabled="submitDisabled"
@toggle="cancel" @cancel="cancel"
@submit="submit" @submit="submit"
> >
<template slot="body"> <template slot="body">
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue'; import applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/** /**
* Cluster page has 2 separate parts: * Cluster page has 2 separate parts:
...@@ -48,12 +49,9 @@ export default class Clusters { ...@@ -48,12 +49,9 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
}); });
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
this.showToken = this.showToken.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.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success'); this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating'); this.creatingContainer = document.querySelector('.js-cluster-creating');
...@@ -63,6 +61,7 @@ export default class Clusters { ...@@ -63,6 +61,7 @@ export default class Clusters {
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications();
if (this.store.state.status !== 'created') { if (this.store.state.status !== 'created') {
...@@ -101,13 +100,11 @@ export default class Clusters { ...@@ -101,13 +100,11 @@ export default class Clusters {
} }
addListeners() { addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
} }
removeListeners() { removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
} }
...@@ -151,11 +148,6 @@ export default class Clusters { ...@@ -151,11 +148,6 @@ export default class Clusters {
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); 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() { showToken() {
const type = this.tokenField.getAttribute('type'); const type = this.tokenField.getAttribute('type');
......
import Flash from '../flash'; import Flash from '../flash';
import { s__ } from '../locale'; import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import ClustersService from './services/clusters_service'; 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');
};
/** export default () => {
* Toggles checked class for the given button const clusterList = document.querySelector('.js-clusters-list');
* @param {HTMLElement} button // The empty state won't have a clusterList
*/ if (clusterList) {
const toggleValue = (button) => { setupToggleButtons(
button.classList.toggle('is-checked'); 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.'));
});
}));
}
/* 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 */ /* 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 { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare { export default class Compare {
constructor(opts) { constructor(opts) {
...@@ -41,17 +42,14 @@ export default class Compare { ...@@ -41,17 +42,14 @@ export default class Compare {
} }
getTargetProject() { getTargetProject() {
return $.ajax({ $('.mr_target_commit').empty();
url: this.opts.targetProjectUrl,
data: { return axios.get(this.opts.targetProjectUrl, {
target_project_id: $("input[name='merge_request[target_project_id]']").val() params: {
}, target_project_id: $("input[name='merge_request[target_project_id]']").val(),
beforeSend: function() {
return $('.mr_target_commit').empty();
}, },
success: function(html) { }).then(({ data }) => {
return $('.js-target-branch-dropdown .dropdown-content').html(html); $('.js-target-branch-dropdown .dropdown-content').html(data);
}
}); });
} }
...@@ -68,22 +66,19 @@ export default class Compare { ...@@ -68,22 +66,19 @@ export default class Compare {
}); });
} }
static sendAjax(url, loading, target, data) { static sendAjax(url, loading, target, params) {
var $target; const $target = $(target);
$target = $(target);
return $.ajax({ loading.show();
url: url, $target.empty();
data: data,
beforeSend: function() { return axios.get(url, {
loading.show(); params,
return $target.empty(); }).then(({ data }) => {
}, loading.hide();
success: function(html) { $target.html(data);
loading.hide(); const className = '.' + $target[0].className.replace(' ', '.');
$target.html(html); localTimeAgo($('.js-timeago', className));
var className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
}
}); });
} }
} }
/* 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 */ /* 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() { export default function initCompareAutocomplete() {
$('.js-compare-dropdown').each(function() { $('.js-compare-dropdown').each(function() {
...@@ -10,15 +13,14 @@ export default function initCompareAutocomplete() { ...@@ -10,15 +13,14 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({ $dropdown.glDropdown({
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ axios.get($dropdown.data('refsUrl'), {
url: $dropdown.data('refs-url'), params: {
data: {
ref: $dropdown.data('ref'), ref: $dropdown.data('ref'),
search: term, search: term,
} },
}).done(function(refs) { }).then(({ data }) => {
return callback(refs); callback(data);
}); }).catch(() => flash(__('Error fetching refs')));
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash'; import Flash from './flash';
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
...@@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown { ...@@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown {
} }
checkAbilityToCreateBranch() { checkAbilityToCreateBranch() {
return $.ajax({ this.setUnavailableButtonState();
type: 'GET',
dataType: 'json', axios.get(this.canCreatePath)
url: this.canCreatePath, .then(({ data }) => {
beforeSend: () => this.setUnavailableButtonState(), this.setUnavailableButtonState(false);
})
.done((data) => { if (data.can_create_branch) {
this.setUnavailableButtonState(false); this.available();
this.enable();
if (data.can_create_branch) {
this.available(); if (!this.droplabInitialized) {
this.enable(); this.droplabInitialized = true;
this.initDroplab();
if (!this.droplabInitialized) { this.bindEvents();
this.droplabInitialized = true; }
this.initDroplab(); } else if (data.has_related_branch) {
this.bindEvents(); this.hide();
} }
} else if (data.has_related_branch) { })
this.hide(); .catch(() => {
} this.unavailable();
}).fail(() => { this.disable();
this.unavailable(); Flash('Failed to check if a new branch can be created.');
this.disable(); });
new Flash('Failed to check if a new branch can be created.');
});
} }
createBranch() { createBranch() {
return $.ajax({ this.isCreatingBranch = true;
method: 'POST',
dataType: 'json', return axios.post(this.createBranchPath)
url: this.createBranchPath, .then(({ data }) => {
beforeSend: () => (this.isCreatingBranch = true), this.branchCreated = true;
}) window.location.href = data.url;
.done((data) => { })
this.branchCreated = true; .catch(() => Flash('Failed to create a branch for this issue. Please try again.'));
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
} }
createMergeRequest() { createMergeRequest() {
return $.ajax({ this.isCreatingMergeRequest = true;
method: 'POST',
dataType: 'json', return axios.post(this.createMrPath)
url: this.createMrPath, .then(({ data }) => {
beforeSend: () => (this.isCreatingMergeRequest = true), this.mergeRequestCreated = true;
}) window.location.href = data.url;
.done((data) => { })
this.mergeRequestCreated = true; .catch(() => Flash('Failed to create Merge Request. Please try again.'));
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
} }
disable() { disable() {
...@@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown { ...@@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') { getRef(ref, target = 'all') {
if (!ref) return false; if (!ref) return false;
return $.ajax({ return axios.get(this.refsPath + ref)
method: 'GET', .then(({ data }) => {
dataType: 'json', const branches = data[Object.keys(data)[0]];
url: this.refsPath + ref, const tags = data[Object.keys(data)[1]];
beforeSend: () => { let result;
this.isGettingRef = true;
}, if (target === 'branch') {
}) result = CreateMergeRequestDropdown.findByValue(branches, ref);
.always(() => { } else {
this.isGettingRef = false; result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
}) CreateMergeRequestDropdown.findByValue(tags, ref, true);
.done((data) => { this.suggestedRef = result;
const branches = data[Object.keys(data)[0]]; }
const tags = data[Object.keys(data)[1]];
let result;
if (target === 'branch') { this.isGettingRef = false;
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
return this.updateInputState(target, ref, result); return this.updateInputState(target, ref, result);
}) })
.fail(() => { .catch(() => {
this.unavailable(); this.unavailable();
this.disable(); this.disable();
new Flash('Failed to get ref.'); new Flash('Failed to get ref.');
return false; this.isGettingRef = false;
});
return false;
});
} }
getTargetData(target) { getTargetData(target) {
...@@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown { ...@@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown {
xhr = this.createBranch(); xhr = this.createBranch();
} }
xhr.fail(() => { xhr.catch(() => {
this.isCreatingMergeRequest = false; this.isCreatingMergeRequest = false;
this.isCreatingBranch = false; this.isCreatingBranch = false;
});
xhr.always(() => this.enable()); this.enable();
});
this.disable(); this.disable();
} }
......
...@@ -2,6 +2,7 @@ import Dropzone from 'dropzone'; ...@@ -2,6 +2,7 @@ import Dropzone from 'dropzone';
import _ from 'underscore'; import _ from 'underscore';
import './preview_markdown'; import './preview_markdown';
import csrf from './lib/utils/csrf'; import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
...@@ -235,25 +236,21 @@ export default function dropzoneInput(form) { ...@@ -235,25 +236,21 @@ export default function dropzoneInput(form) {
uploadFile = (item, filename) => { uploadFile = (item, filename) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', item, filename); formData.append('file', item, filename);
return $.ajax({
url: uploadsPath, showSpinner();
type: 'POST', closeAlertMessage();
data: formData,
dataType: 'json', axios.post(uploadsPath, formData)
processData: false, .then(({ data }) => {
contentType: false, const md = data.link.markdown;
headers: csrf.headers,
beforeSend: () => {
showSpinner();
return closeAlertMessage();
},
success: (e, text, response) => {
const md = response.responseJSON.link.markdown;
insertToTextArea(filename, md); insertToTextArea(filename, md);
}, closeSpinner();
error: response => showError(response.responseJSON.message), })
complete: () => closeSpinner(), .catch((e) => {
}); showError(e.response.data.message);
closeSpinner();
});
}; };
updateAttachingMessage = (files, messageContainer) => { updateAttachingMessage = (files, messageContainer) => {
......
/* global dateFormat */ /* global dateFormat */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import axios from './lib/utils/axios_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
...@@ -125,37 +126,30 @@ class DueDateSelect { ...@@ -125,37 +126,30 @@ class DueDateSelect {
} }
submitSelectedDate(isDropdown) { submitSelectedDate(isDropdown) {
return $.ajax({ const selectedDateValue = this.datePayload[this.abilityName].due_date;
type: 'PUT', const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
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';
this.$loading.removeClass('hidden').fadeIn(); this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide(); this.$selectbox.hide();
} }
this.$value.css('display', ''); this.$value.css('display', '');
this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
this.$sidebarValue.html(this.displayedDate); this.$sidebarValue.html(this.displayedDate);
return selectedDateValue.length ? $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden'); return axios.put(this.issueUpdateURL, this.datePayload)
}, .then(() => {
}).done(() => { if (isDropdown) {
if (isDropdown) { this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.trigger('loaded.gl.dropdown'); this.$dropdown.dropdown('toggle');
this.$dropdown.dropdown('toggle'); }
} return this.$loading.fadeOut();
return this.$loading.fadeOut(); });
});
} }
} }
......
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
/** /**
* Makes search request for content when user types a value in the search input. * Makes search request for content when user types a value in the search input.
...@@ -54,32 +55,26 @@ export default class FilterableList { ...@@ -54,32 +55,26 @@ export default class FilterableList {
this.listFilterElement.removeEventListener('input', this.debounceFilter); this.listFilterElement.removeEventListener('input', this.debounceFilter);
} }
filterResults(queryData) { filterResults(params) {
if (this.isBusy) { if (this.isBusy) {
return false; return false;
} }
$(this.listHolderElement).fadeTo(250, 0.5); $(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({ this.isBusy = true;
url: this.getFilterEndpoint(),
data: queryData, return axios.get(this.getFilterEndpoint(), {
type: 'GET', params,
dataType: 'json', }).then((res) => {
context: this, this.onFilterSuccess(res, params);
complete: this.onFilterComplete, this.onFilterComplete();
beforeSend: () => { }).catch(() => this.onFilterComplete());
this.isBusy = true;
},
success: (response, textStatus, xhr) => {
this.onFilterSuccess(response, xhr, queryData);
},
});
} }
onFilterSuccess(response, xhr, queryData) { onFilterSuccess(response, queryData) {
if (response.html) { if (response.data.html) {
this.listHolderElement.innerHTML = response.html; this.listHolderElement.innerHTML = response.data.html;
} }
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
......
import FilterableList from '~/filterable_list'; import FilterableList from '~/filterable_list';
import eventHub from './event_hub'; 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 { export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
...@@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList { ...@@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList {
this.form.querySelector(`[name="${this.filterInputField}"]`).value = ''; this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
} }
onFilterSuccess(data, xhr, queryData) { onFilterSuccess(res, queryData) {
const currentPath = this.getPagePath(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({ window.history.replaceState({
page: currentPath, page: currentPath,
}, document.title, currentPath); }, document.title, currentPath);
eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
eventHub.$emit('updatePagination', paginationData); eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
} }
} }
...@@ -26,6 +26,9 @@ ...@@ -26,6 +26,9 @@
'currentBranchId', 'currentBranchId',
'rightPanelCollapsed', 'rightPanelCollapsed',
]), ]),
isCommitInfoShown() {
return this.rightPanelCollapsed || this.fileList.length;
},
}, },
methods: { methods: {
toggleCollapsed() { toggleCollapsed() {
...@@ -36,7 +39,11 @@ ...@@ -36,7 +39,11 @@
</script> </script>
<template> <template>
<div class="multi-file-commit-list"> <div
:class="{
'multi-file-commit-list': isCommitInfoShown
}"
>
<list-collapsed <list-collapsed
v-if="rightPanelCollapsed" v-if="rightPanelCollapsed"
/> />
...@@ -54,12 +61,6 @@ ...@@ -54,12 +61,6 @@
/> />
</li> </li>
</ul> </ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template> </template>
</div> </div>
</template> </template>
...@@ -23,6 +23,14 @@ ...@@ -23,6 +23,14 @@
type: String, type: String,
required: true, required: true,
}, },
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -94,6 +102,9 @@ ...@@ -94,6 +102,9 @@
</div> </div>
</template> </template>
</div> </div>
<ide-contextbar/> <ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div> </div>
</template> </template>
...@@ -10,6 +10,16 @@ ...@@ -10,6 +10,16 @@
icon, icon,
panelResizer, panelResizer,
}, },
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
data() { data() {
return { return {
width: 290, width: 290,
...@@ -46,6 +56,11 @@ ...@@ -46,6 +56,11 @@
collapsed: !this.rightPanelCollapsed, collapsed: !this.rightPanelCollapsed,
}); });
}, },
toggleFullbarCollapsed() {
if (this.rightPanelCollapsed) {
this.toggleCollapsed();
}
},
resizingStarted() { resizingStarted() {
this.setResizingStatus(true); this.setResizingStatus(true);
}, },
...@@ -63,8 +78,11 @@ ...@@ -63,8 +78,11 @@
'is-collapsed': rightPanelCollapsed, 'is-collapsed': rightPanelCollapsed,
}" }"
:style="panelStyle" :style="panelStyle"
@click="toggleFullbarCollapsed"
> >
<div class="multi-file-commit-panel-section"> <div
class="multi-file-commit-panel-section"
>
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header"
:class="{ :class="{
...@@ -75,16 +93,20 @@ ...@@ -75,16 +93,20 @@
class="multi-file-commit-panel-header-title" class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed" v-if="!rightPanelCollapsed"
> >
<icon <div
name="list-bulleted" v-if="changedFiles.length"
:size="18" >
/> <icon
Staged name="list-bulleted"
:size="18"
/>
Staged
</div>
</div> </div>
<button <button
type="button" type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn" class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed" @click.stop="toggleCollapsed"
> >
<icon <icon
:name="currentIcon" :name="currentIcon"
...@@ -92,7 +114,10 @@ ...@@ -92,7 +114,10 @@
/> />
</button> </button>
</header> </header>
<repo-commit-section /> <repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div> </div>
<panel-resizer <panel-resizer
:size.sync="width" :size.sync="width"
......
...@@ -14,6 +14,16 @@ export default { ...@@ -14,6 +14,16 @@ export default {
directives: { directives: {
tooltip, tooltip,
}, },
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
data() { data() {
return { return {
showNewBranchModal: false, showNewBranchModal: false,
...@@ -27,6 +37,7 @@ export default { ...@@ -27,6 +37,7 @@ export default {
'currentProjectId', 'currentProjectId',
'currentBranchId', 'currentBranchId',
'rightPanelCollapsed', 'rightPanelCollapsed',
'lastCommitMsg',
]), ]),
...mapGetters([ ...mapGetters([
'changedFiles', 'changedFiles',
...@@ -37,6 +48,9 @@ export default { ...@@ -37,6 +48,9 @@ export default {
commitMessageCount() { commitMessageCount() {
return this.commitMessage.length; return this.commitMessage.length;
}, },
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -101,7 +115,12 @@ export default { ...@@ -101,7 +115,12 @@ export default {
</script> </script>
<template> <template>
<div class="multi-file-commit-panel-section"> <div
class="multi-file-commit-panel-section"
:class="{
'multi-file-commit-empty-state-container': !changedFiles.length
}"
>
<modal <modal
v-if="showNewBranchModal" v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
...@@ -118,54 +137,92 @@ you started editing. Would you like to create a new branch?`)" ...@@ -118,54 +137,92 @@ you started editing. Would you like to create a new branch?`)"
:collapsed="rightPanelCollapsed" :collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed" @toggleCollapsed="toggleCollapsed"
/> />
<form <template
class="form-horizontal multi-file-commit-form" v-if="changedFiles.length"
@submit.prevent="tryCommit"
v-if="!rightPanelCollapsed"
> >
<div class="multi-file-commit-fieldset"> <form
<textarea class="form-horizontal multi-file-commit-form"
class="form-control multi-file-commit-message" @submit.prevent="tryCommit"
name="commit-message" v-if="!rightPanelCollapsed"
v-model="commitMessage" >
placeholder="Commit message" <div class="multi-file-commit-fieldset">
> <textarea
</textarea> class="form-control multi-file-commit-message"
name="commit-message"
v-model="commitMessage"
placeholder="Commit message"
>
</textarea>
</div>
<div class="multi-file-commit-fieldset">
<label
v-tooltip
title="Create a new merge request with these changes"
data-container="body"
data-placement="top"
>
<input
type="checkbox"
v-model="startNewMR"
/>
{{ __('Merge Request') }}
</label>
<button
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
>
<i
v-if="submitCommitsLoading"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading"
>
</i>
{{ __('Commit') }}
</button>
<div
class="multi-file-commit-message-count"
>
{{ commitMessageCount }}
</div>
</div>
</form>
</template>
<div
v-else-if="!rightPanelCollapsed"
class="row js-empty-state"
>
<div class="col-xs-10 col-xs-offset-1">
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
</div> </div>
<div class="multi-file-commit-fieldset"> <div class="col-xs-10 col-xs-offset-1">
<label <div
v-tooltip class="text-content text-center"
title="Create a new merge request with these changes" v-if="!lastCommitMsg"
data-container="body"
data-placement="top"
>
<input
type="checkbox"
v-model="startNewMR"
/>
Merge Request
</label>
<button
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
> >
<i <h4>
v-if="submitCommitsLoading" {{ __('No changes') }}
class="js-commit-loading-icon fa fa-spinner fa-spin" </h4>
aria-hidden="true" <p>
aria-label="loading" {{ __('Edit files in the editor and commit changes here') }}
> </p>
</i> </div>
Commit
</button>
<div <div
class="multi-file-commit-message-count" class="text-content text-center"
v-else
> >
{{ commitMessageCount }} <h4>
{{ __('All changes are committed') }}
</h4>
<p>
{{ lastCommitMsg }}
</p>
</div> </div>
</div> </div>
</form> </div>
</div> </div>
</template> </template>
...@@ -18,6 +18,8 @@ function initIde(el) { ...@@ -18,6 +18,8 @@ function initIde(el) {
return createElement('ide', { return createElement('ide', {
props: { props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath, emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
}, },
}); });
}, },
......
...@@ -110,15 +110,7 @@ export const commitChanges = ( ...@@ -110,15 +110,7 @@ export const commitChanges = (
if (data.stats) { if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`; commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
} }
commit(types.SET_LAST_COMMIT_MSG, commitMsg);
flash(
commitMsg,
'notice',
document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) { if (newMr) {
dispatch('discardAllChanges'); dispatch('discardAllChanges');
......
...@@ -3,6 +3,7 @@ export const TOGGLE_LOADING = 'TOGGLE_LOADING'; ...@@ -3,6 +3,7 @@ export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL'; export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT'; export const SET_ROOT = 'SET_ROOT';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
......
...@@ -63,6 +63,11 @@ export default { ...@@ -63,6 +63,11 @@ export default {
updatedAt: lastCommit.commit.authored_date, updatedAt: lastCommit.commit.authored_date,
}); });
}, },
[types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) {
Object.assign(state, {
lastCommitMsg,
});
},
...projectMutations, ...projectMutations,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
......
...@@ -8,6 +8,7 @@ export default () => ({ ...@@ -8,6 +8,7 @@ export default () => ({
endpoints: {}, endpoints: {},
isRoot: false, isRoot: false,
isInitialRoot: false, isInitialRoot: false,
lastCommitMsg: '',
lastCommitPath: '', lastCommitPath: '',
loading: false, loading: false,
onTopOfBranch: false, onTopOfBranch: false,
...@@ -18,6 +19,6 @@ export default () => ({ ...@@ -18,6 +19,6 @@ export default () => ({
trees: {}, trees: {},
projects: {}, projects: {},
leftPanelCollapsed: false, leftPanelCollapsed: false,
rightPanelCollapsed: true, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
}); });
...@@ -115,9 +115,12 @@ export default { ...@@ -115,9 +115,12 @@ export default {
reordered(event) { reordered(event) {
this.removeDraggingCursor(); this.removeDraggingCursor();
const { beforeId, afterId } = this.getBeforeAfterId(event.item); const { beforeId, afterId } = this.getBeforeAfterId(event.item);
const { oldIndex, newIndex } = event;
this.$emit('saveReorder', { this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10), issueId: parseInt(event.item.dataset.key, 10),
oldIndex,
newIndex,
afterId, afterId,
beforeId, beforeId,
}); });
......
...@@ -176,7 +176,7 @@ export default { ...@@ -176,7 +176,7 @@ export default {
Flash('An error occurred while fetching issues.'); Flash('An error occurred while fetching issues.');
}); });
}, },
saveIssueOrder({ issueId, beforeId, afterId }) { saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) {
const issueToReorder = _.find(this.state.relatedIssues, issue => issue.id === issueId); const issueToReorder = _.find(this.state.relatedIssues, issue => issue.id === issueId);
if (issueToReorder) { if (issueToReorder) {
...@@ -184,7 +184,14 @@ export default { ...@@ -184,7 +184,14 @@ export default {
endpoint: issueToReorder.relation_path, endpoint: issueToReorder.relation_path,
move_before_id: beforeId, move_before_id: beforeId,
move_after_id: afterId, move_after_id: afterId,
}).catch(() => { })
.then(res => res.json())
.then((res) => {
if (!res.message) {
this.store.updateIssueOrder(oldIndex, newIndex);
}
})
.catch(() => {
Flash('An error occurred while reordering issues.'); Flash('An error occurred while reordering issues.');
}); });
} }
......
...@@ -16,6 +16,13 @@ class RelatedIssuesStore { ...@@ -16,6 +16,13 @@ class RelatedIssuesStore {
this.state.relatedIssues = this.state.relatedIssues.filter(issue => issue.id !== idToRemove); this.state.relatedIssues = this.state.relatedIssues.filter(issue => issue.id !== idToRemove);
} }
updateIssueOrder(oldIndex, newIndex) {
if (this.state.relatedIssues.length > 0) {
const updatedIssue = this.state.relatedIssues.splice(oldIndex, 1)[0];
this.state.relatedIssues.splice(newIndex, 0, updatedIssue);
}
}
setPendingReferences(issues) { setPendingReferences(issues) {
this.state.pendingReferences = issues; this.state.pendingReferences = issues;
} }
......
...@@ -22,3 +22,11 @@ axios.interceptors.response.use((config) => { ...@@ -22,3 +22,11 @@ axios.interceptors.response.use((config) => {
}); });
export default axios; export default axios;
/**
* @return The adapter that axios uses for dispatching requests. This may be overwritten in tests.
*
* @see https://github.com/axios/axios/tree/master/lib/adapters
* @see https://github.com/ctimmerm/axios-mock-adapter/blob/v1.12.0/src/index.js#L39
*/
export const getDefaultAdapter = () => axios.defaults.adapter;
...@@ -84,7 +84,7 @@ export default { ...@@ -84,7 +84,7 @@ export default {
return !this.showLess || (index < this.defaultRenderCount && this.showLess); return !this.showLess || (index < this.defaultRenderCount && this.showLess);
}, },
avatarUrl(user) { avatarUrl(user) {
return user.avatar || user.avatar_url; return user.avatar || user.avatar_url || gon.default_avatar_url;
}, },
assigneeUrl(user) { assigneeUrl(user) {
return `${this.rootPath}${user.username}`; return `${this.rootPath}${user.username}`;
......
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));
});
}
...@@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -492,7 +492,7 @@ function UsersSelect(currentUser, els, options = {}) {
renderRow: function(user) { renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, username; var avatar, img, listClosingTags, listWithName, listWithUserName, username;
username = user.username ? "@" + user.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; let selected = false;
...@@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (user.beforeDivider != null) { if (user.beforeDivider != null) {
`<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`;
} else { } else {
if (avatar) { img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
}
} }
return ` return `
......
...@@ -76,6 +76,7 @@ export default { ...@@ -76,6 +76,7 @@ export default {
<a <a
href="#modal_merge_info" href="#modal_merge_info"
data-toggle="modal" data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm inline"> class="btn btn-sm inline">
Check out branch Check out branch
</a> </a>
......
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetFailedToMerge',
props: {
mr: { type: Object, required: true },
},
data() {
return {
timer: 10,
isRefreshing: false,
};
},
mounted() {
setInterval(() => {
this.updateTimer();
}, 1000);
},
created() {
eventHub.$emit('DisablePolling');
},
computed: {
timerText() {
return this.timer > 1 ? `${this.timer} seconds` : 'a second';
},
},
methods: {
refresh() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('EnablePolling');
},
updateTimer() {
this.timer = this.timer - 1;
if (this.timer === 0) {
this.refresh();
}
},
},
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<template v-if="isRefreshing">
<status-icon status="loading" />
<span class="media-body bold js-refresh-label">
Refreshing now
</span>
</template>
<template v-else>
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
class="has-error-message"
v-if="mr.mergeError">
{{mr.mergeError}}.
</span>
<span v-else>Merge failed.</span>
<span
:class="{ 'has-custom-error': mr.mergeError }">
Refreshing in {{timerText}} to show the updated status...
</span>
</span>
<button
@click="refresh"
class="btn btn-default btn-xs js-refresh-button"
type="button">
Refresh now
</button>
</div>
</template>
</div>
`,
};
<script>
import { n__ } from '~/locale';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetFailedToMerge',
components: {
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
},
data() {
return {
timer: 10,
isRefreshing: false,
};
},
computed: {
timerText() {
return n__(
'Refreshing in a second to show the updated status...',
'Refreshing in %d seconds to show the updated status...',
this.timer,
);
},
},
mounted() {
setInterval(() => {
this.updateTimer();
}, 1000);
},
created() {
eventHub.$emit('DisablePolling');
},
methods: {
refresh() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('EnablePolling');
},
updateTimer() {
this.timer = this.timer - 1;
if (this.timer === 0) {
this.refresh();
}
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<template v-if="isRefreshing">
<status-icon status="loading" />
<span class="media-body bold js-refresh-label">
{{ s__("mrWidget|Refreshing now") }}
</span>
</template>
<template v-else>
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
<span
class="has-error-message"
v-if="mr.mergeError"
>
{{ mr.mergeError }}.
</span>
<span v-else>
{{ s__("mrWidget|Merge failed.") }}
</span>
<span
:class="{ 'has-custom-error': mr.mergeError }"
>
{{ timerText }}
</span>
</span>
<button
@click="refresh"
class="btn btn-default btn-xs js-refresh-button"
type="button"
>
{{ s__("mrWidget|Refresh now") }}
</button>
</div>
</template>
</div>
</template>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
statusIcon,
},
data() {
return {
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
canRemoveSourceBranch() {
const { shouldRemoveSourceBranch, canRemoveSourceBranch,
mergeUserId, currentUserId } = this.mr;
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
},
},
methods: {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
removeSourceBranch() {
const options = {
sha: this.mr.sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
};
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options)
.then(res => res.data)
.then((data) => {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
.catch(() => {
this.isRemovingSourceBranch = 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">
<h4 class="flex-container-block">
<span class="append-right-10">
Set by
<mr-widget-author :author="mr.setToMWPSBy" />
to be merged automatically when the pipeline succeeds
</span>
<a
v-if="mr.canCancelAutomaticMerge"
@click.prevent="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-xs btn-default js-cancel-auto-merge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Cancel automatic merge
</a>
</h4>
<section class="mr-info-list">
<p>The changes will be merged into
<a
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}
</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed
</p>
<p
v-else
class="flex-container-block"
>
<span class="append-right-10">
The source branch will not be removed
</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
@click.prevent="removeSourceBranch"
role="button"
class="btn btn-xs btn-default js-remove-source-branch"
href="#">
<i
v-if="isRemovingSourceBranch"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Remove source branch
</a>
</p>
</section>
</div>
</div>
`,
};
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
components: {
mrWidgetAuthor,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
service: {
type: Object,
required: true,
default: () => ({}),
},
},
data() {
return {
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
canRemoveSourceBranch() {
const {
shouldRemoveSourceBranch,
canRemoveSourceBranch,
mergeUserId,
currentUserId,
} = this.mr;
return !shouldRemoveSourceBranch &&
canRemoveSourceBranch &&
mergeUserId === currentUserId;
},
},
methods: {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
Flash('Something went wrong. Please try again.');
});
},
removeSourceBranch() {
const options = {
sha: this.mr.sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
};
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options)
.then(res => res.data)
.then((data) => {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
.catch(() => {
this.isRemovingSourceBranch = false;
Flash('Something went wrong. Please try again.');
});
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<h4 class="flex-container-block">
<span class="append-right-10">
{{ s__("mrWidget|Set by") }}
<mr-widget-author :author="mr.setToMWPSBy" />
{{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }}
</span>
<a
v-if="mr.canCancelAutomaticMerge"
@click.prevent="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-xs btn-default js-cancel-auto-merge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
aria-hidden="true"
>
</i>
{{ s__("mrWidget|Cancel automatic merge") }}
</a>
</h4>
<section class="mr-info-list">
<p>
{{ s__("mrWidget|The changes will be merged into") }}
<a
:href="mr.targetBranchPath"
class="label-branch"
>
{{ mr.targetBranch }}
</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
{{ s__("mrWidget|The source branch will be removed") }}
</p>
<p
v-else
class="flex-container-block"
>
<span class="append-right-10">
{{ s__("mrWidget|The source branch will not be removed") }}
</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
@click.prevent="removeSourceBranch"
role="button"
class="btn btn-xs btn-default js-remove-source-branch"
href="#"
>
<i
v-if="isRemovingSourceBranch"
class="fa fa-spinner fa-spin"
aria-hidden="true"
>
</i>
{{ s__("mrWidget|Remove source branch") }}
</a>
</p>
</section>
</div>
</div>
</template>
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>
`,
};
<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>
...@@ -16,8 +16,8 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; ...@@ -16,8 +16,8 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; 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'; 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 ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WipState } from './components/states/mr_widget_wip';
...@@ -31,7 +31,7 @@ export { default as SHAMismatchState } from './components/states/mr_widget_sha_m ...@@ -31,7 +31,7 @@ export { default as SHAMismatchState } from './components/states/mr_widget_sha_m
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
export { default as CheckingState } from './components/states/mr_widget_checking.vue'; export { default as CheckingState } from './components/states/mr_widget_checking.vue';
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
width: 100%; width: 100%;
} }
$image-widths: 250 306 394 430; $image-widths: 80 250 306 394 430;
@each $width in $image-widths { @each $width in $image-widths {
&.svg-#{$width} { &.svg-#{$width} {
img, img,
......
...@@ -355,6 +355,11 @@ table.table tr td.multi-file-table-name { ...@@ -355,6 +355,11 @@ table.table tr td.multi-file-table-name {
flex: 1; flex: 1;
} }
.multi-file-commit-empty-state-container {
align-items: center;
justify-content: center;
}
.multi-file-commit-panel-header { .multi-file-commit-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -381,7 +386,7 @@ table.table tr td.multi-file-table-name { ...@@ -381,7 +386,7 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel-header-title { .multi-file-commit-panel-header-title {
display: flex; display: flex;
flex: 1; flex: 1;
padding: $gl-btn-padding; padding: 0 $gl-btn-padding;
svg { svg {
margin-right: $gl-btn-padding; margin-right: $gl-btn-padding;
......
...@@ -89,7 +89,7 @@ module ApplicationHelper ...@@ -89,7 +89,7 @@ module ApplicationHelper
end end
def default_avatar def default_avatar
'no_avatar.png' asset_path('no_avatar.png')
end end
def last_commit(project) def last_commit(project)
......
...@@ -476,7 +476,7 @@ module Ci ...@@ -476,7 +476,7 @@ module Ci
if cache && project.jobs_cache_index if cache && project.jobs_cache_index
cache = cache.merge( cache = cache.merge(
key: "#{cache[:key]}:#{project.jobs_cache_index}") key: "#{cache[:key]}_#{project.jobs_cache_index}")
end end
[cache] [cache]
......
...@@ -1016,13 +1016,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -1016,13 +1016,13 @@ class MergeRequest < ActiveRecord::Base
merged_at = metrics&.merged_at merged_at = metrics&.merged_at
notes_association = notes_with_associations 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 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) notes_association = notes_association.where('created_at >= ?', cutoff)
end end
......
...@@ -576,6 +576,9 @@ class Project < ActiveRecord::Base ...@@ -576,6 +576,9 @@ class Project < ActiveRecord::Base
RepositoryForkWorker.perform_async(id, RepositoryForkWorker.perform_async(id,
forked_from_project.repository_storage_path, forked_from_project.repository_storage_path,
forked_from_project.disk_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 else
RepositoryImportWorker.perform_async(self.id) RepositoryImportWorker.perform_async(self.id)
end end
......
...@@ -2,7 +2,7 @@ class EmailsOnPushService < Service ...@@ -2,7 +2,7 @@ class EmailsOnPushService < Service
boolean_accessor :send_from_committer_email boolean_accessor :send_from_committer_email
boolean_accessor :disable_diffs boolean_accessor :disable_diffs
prop_accessor :recipients prop_accessor :recipients
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :valid_recipients?
def title def title
'Emails on push' 'Emails on push'
......
...@@ -4,7 +4,7 @@ class IrkerService < Service ...@@ -4,7 +4,7 @@ class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels prop_accessor :recipients, :channels
boolean_accessor :colorize_messages boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :valid_recipients?
before_validation :get_channels before_validation :get_channels
......
...@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService ...@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService
username: self.username, username: self.username,
password: self.password, password: self.password,
site: URI.join(url, '/').to_s, site: URI.join(url, '/').to_s,
context_path: url.path, context_path: url.path.chomp('/'),
auth_type: :basic, auth_type: :basic,
read_timeout: 120, read_timeout: 120,
use_cookies: true, use_cookies: true,
......
class PipelinesEmailService < Service class PipelinesEmailService < Service
prop_accessor :recipients prop_accessor :recipients
boolean_accessor :notify_only_broken_pipelines boolean_accessor :notify_only_broken_pipelines
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :valid_recipients?
def initialize_properties def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true } self.properties ||= { notify_only_broken_pipelines: true }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
# and implement a set of methods # and implement a set of methods
class Service < ActiveRecord::Base class Service < ActiveRecord::Base
include Sortable include Sortable
include Importable
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, false default_value_for :active, false
...@@ -301,4 +303,8 @@ class Service < ActiveRecord::Base ...@@ -301,4 +303,8 @@ class Service < ActiveRecord::Base
project.cache_has_external_wiki project.cache_has_external_wiki
end end
end end
def valid_recipients?
activated? && !importing?
end
end end
...@@ -9,7 +9,8 @@ module MergeRequests ...@@ -9,7 +9,8 @@ module MergeRequests
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
close_merge_requests close_upon_missing_source_branch_ref
post_merge_manually_merged
reload_merge_requests reload_merge_requests
reset_merge_when_pipeline_succeeds reset_merge_when_pipeline_succeeds
mark_pending_todos_done mark_pending_todos_done
...@@ -30,11 +31,22 @@ module MergeRequests ...@@ -30,11 +31,22 @@ module MergeRequests
private 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 # Collect open merge requests that target same branch we push into
# and close if push to master include last commit from merge request # 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 # We need this to close(as merged) merge requests that were merged into
# target branch manually # target branch manually
def close_merge_requests def post_merge_manually_merged
commit_ids = @commits.map(&:id) 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 = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select(&:diff_head_commit)
......
...@@ -5,16 +5,16 @@ ...@@ -5,16 +5,16 @@
= markdown(current_application_settings.help_page_text) = markdown(current_application_settings.help_page_text)
%hr %hr
- unless current_application_settings.help_page_hide_commercial_content? %h1
%h1 GitLab
GitLab Enterprise Edition
Enterprise Edition - if user_signed_in?
- if user_signed_in? %span= Gitlab::VERSION
%span= Gitlab::VERSION %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ee', Gitlab::REVISION)
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ee', Gitlab::REVISION) = version_status_badge
- if current_application_settings.version_check_enabled %hr
= version_status_badge
- unless current_application_settings.help_page_hide_commercial_content?
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
%br %br
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide', force_same_domain: true = webpack_bundle_tag 'ide', force_same_domain: true
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} } #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
.text-center .text-center
= icon('spinner spin 2x') = icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...') %h2.clgray= _('Loading the GitLab IDE...')
...@@ -14,5 +14,5 @@ ...@@ -14,5 +14,5 @@
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
.pull-right .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') } #{ _('Create merge request') }
...@@ -12,11 +12,12 @@ ...@@ -12,11 +12,12 @@
.table-section.section-10 .table-section.section-10
.table-mobile-header{ role: "rowheader" } .table-mobile-header{ role: "rowheader" }
.table-mobile-content .table-mobile-content
%button{ type: "button", %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?, disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } 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") = icon("spinner spin", class: "loading-icon")
%span.toggle-icon %span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
......
...@@ -10,13 +10,12 @@ ...@@ -10,13 +10,12 @@
= s_('ClusterIntegration|Cluster integration is enabled for this project.') = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else - else
= s_('ClusterIntegration|Cluster integration is disabled for this project.') = s_('ClusterIntegration|Cluster integration is disabled for this project.')
%label.append-bottom-10 %label.append-bottom-10.js-cluster-enable-toggle-area
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button', %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"), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) } disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon %span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = 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') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description, = 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...", placeholder: "Write a comment or drag your files here...",
supports_quick_actions: supports_quick_actions supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
......
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
%span.append-right-10 %span.append-right-10
- if issuable.new_record? - 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 - else
= form.submit 'Save changes', class: 'btn btn-save' = form.submit 'Save changes', class: 'btn btn-save'
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%div{ class: div_class } %div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true, = 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?) - if issuable.respond_to?(:work_in_progress?)
%p.help-block %p.help-block
......
...@@ -20,7 +20,11 @@ class RepositoryImportWorker ...@@ -20,7 +20,11 @@ class RepositoryImportWorker
# to those importers to mark the import process as complete. # to those importers to mark the import process as complete.
return if service.async? 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 project.after_import
...@@ -37,4 +41,8 @@ class RepositoryImportWorker ...@@ -37,4 +41,8 @@ class RepositoryImportWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false false
end end
def fail_import(project, message)
project.mark_import_as_failed(message)
end
end end
require_relative "../lib/gitlab/upgrader"
Gitlab::Upgrader.new.execute
---
title: Update behavior of MR widgets that require pipeline artifacts to allow jobs
with multiple artifacts
merge_request: 4203
author:
type: changed
---
title: Remove unaproved typo check in sast:container report
merge_request:
author:
type: other
---
title: Improve Geo Disaster Recovery docs for systems in multi-secondary configurations
merge_request: 4285
author:
type: fixed
---
title: Preserve updated issue order to store when reorder is completed
merge_request: 4278
author:
type: fixed
---
title: Allow project to be set up to push to and pull from same mirror
merge_request:
author:
type: fixed
---
title: 'Geo: Don''t attempt to schedule a repository sync for downed Gitaly shards'
merge_request:
author:
type: changed
---
title: Fix default avatar icon missing when Gravatar is disabled
merge_request: 16681
author: Felix Geyer
type: fixed
---
title: Disable MR check out button when source branch is deleted
merge_request: 16631
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fixes destination already exists, and some particular service errors on Import/Export
error
merge_request: 16714
author:
type: fixed
---
title: Fix version information not showing on help page if commercial content display
was disabled.
merge_request: 16743
author:
type: fixed
---
title: 'Fix cache clear bug withg using : on Windows'
merge_request: 16740
author:
type: fixed
---
title: Close and do not reload MR diffs when source branch is deleted
merge_request:
author:
type: fixed
---
title: Return more consistent values for merge_status on MR APIs
merge_request:
author:
type: fixed
---
title: Fix JIRA not working when a trailing slash is included
merge_request:
author:
type: fixed
...@@ -8,6 +8,7 @@ require 'elasticsearch/rails/instrumentation' ...@@ -8,6 +8,7 @@ require 'elasticsearch/rails/instrumentation'
module Gitlab module Gitlab
class Application < Rails::Application 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/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues') require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state') require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
......
...@@ -190,3 +190,26 @@ synchronization operations. ...@@ -190,3 +190,26 @@ synchronization operations.
While FDW is available in older versions of Postgres, we needed to bump the While FDW is available in older versions of Postgres, we needed to bump the
minimum required version to 9.6 as this includes many performance improvements minimum required version to 9.6 as this includes many performance improvements
to the FDW implementation. to the FDW implementation.
### Refeshing the Foreign Tables
Whenever the database schema changes on the primary, the secondary will need to refresh
its foreign tables by running the following:
```sh
bundle exec rake geo:db:refresh_foreign_tables
```
Failure to do this will prevent the secondary from functioning properly. The
secondary will generate error messages, as the following PostgreSQL error:
```
ERROR: relation "gitlab_secondary.ci_job_artifacts" does not exist at character 323
STATEMENT: SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '"gitlab_secondary"."ci_job_artifacts"'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
```
...@@ -237,7 +237,7 @@ The following guide assumes that: ...@@ -237,7 +237,7 @@ The following guide assumes that:
cat ~gitlab-psql/data/server.crt cat ~gitlab-psql/data/server.crt
``` ```
Copy the output into a file on your local computer called `server.crt`. You Copy the output into a clipboard or into a local file. You
will need it when setting up the secondary! The certificate is not sensitive will need it when setting up the secondary! The certificate is not sensitive
data. data.
...@@ -267,12 +267,6 @@ because we have not yet configured the secondary server. This is the next step. ...@@ -267,12 +267,6 @@ because we have not yet configured the secondary server. This is the next step.
### Step 3. Configure the secondary server ### Step 3. Configure the secondary server
1. From your **local machine**, copy `server.crt` to the secondary:
```
scp server.crt secondary.geo.example.com:
```
1. SSH into your GitLab **secondary** server and login as root: 1. SSH into your GitLab **secondary** server and login as root:
``` ```
...@@ -292,6 +286,13 @@ because we have not yet configured the secondary server. This is the next step. ...@@ -292,6 +286,13 @@ because we have not yet configured the secondary server. This is the next step.
that, if a firewall is present, the secondary is permitted to connect to the that, if a firewall is present, the secondary is permitted to connect to the
primary on port 5432. primary on port 5432.
1. Create a file `server.crt` in the secondary server, with the content you got on the last step of the primary setup:
```
editor server.crt
```
1. Set up PostgreSQL TLS verification on the secondary 1. Set up PostgreSQL TLS verification on the secondary
Install the `server.crt` file: Install the `server.crt` file:
...@@ -311,8 +312,8 @@ because we have not yet configured the secondary server. This is the next step. ...@@ -311,8 +312,8 @@ because we have not yet configured the secondary server. This is the next step.
``` ```
When prompted enter the password you set in the first step for the When prompted enter the password you set in the first step for the
`gitlab_replicator` user. If all worked correctly, you should see the `gitlab_replicator` user. If all worked correctly, you should see
database prompt. the list of primary's databases.
A failure to connect here indicates that the TLS configuration is incorrect. A failure to connect here indicates that the TLS configuration is incorrect.
Ensure that the contents of `~gitlab-psql/data/server.crt` on the primary Ensure that the contents of `~gitlab-psql/data/server.crt` on the primary
......
...@@ -9,18 +9,17 @@ fail-over with minimal effort, in a disaster situation. ...@@ -9,18 +9,17 @@ fail-over with minimal effort, in a disaster situation.
See [current limitations](README.md#current-limitations) for more information. See [current limitations](README.md#current-limitations) for more information.
### Step 1. Promoting a secondary geo replica ## Promoting secondary Geo replica in single-secondary configuration
> **Warning:** Disaster Recovery does not yet support systems with multiple We don't currently provide an automated way to promote a Geo replica and do a
> secondary geo replicas (e.g. one primary and two or more secondaries).
We don't currently provide an automated way to promote a geo replica and do a
fail-over, but you can do it manually if you have `root` access to the machine. fail-over, but you can do it manually if you have `root` access to the machine.
This process promotes a secondary Geo replica to a primary. To regain This process promotes a secondary Geo replica to a primary. To regain
geographical redundancy as quickly as possible, you should add a new secondary geographical redundancy as quickly as possible, you should add a new secondary
immediately after following these instructions. immediately after following these instructions.
### Step 1. Promoting a secondary Geo replica
1. SSH into your **primary** to stop and disable GitLab. 1. SSH into your **primary** to stop and disable GitLab.
```bash ```bash
...@@ -124,10 +123,17 @@ secondary domain, like changing Git remotes and API URLs. ...@@ -124,10 +123,17 @@ secondary domain, like changing Git remotes and API URLs.
If you updated the DNS records for the primary domain, these changes may If you updated the DNS records for the primary domain, these changes may
not have yet propagated depending on the previous DNS records TTL. not have yet propagated depending on the previous DNS records TTL.
### Step 3. (Optional) Add secondary geo replicas to a promoted primary ### Step 3. (Optional) Add secondary Geo replicas to a promoted primary
Promoting a secondary to primary using the process above does not enable Promoting a secondary to primary using the process above does not enable
GitLab Geo on the new primary. GitLab Geo on the new primary.
To bring a new secondary online, follow the [GitLab Geo setup instructions]( To bring a new secondary online, follow the [GitLab Geo setup instructions](
README.md#setup-instructions). README.md#setup-instructions).
## Promoting secondary Geo replica in multi-secondary configurations
Disaster Recovery does not yet support systems with multiple
secondary Geo replicas (e.g. one primary and two or more secondaries). We are
working on it, see [#4284](https://gitlab.com/gitlab-org/gitlab-ee/issues/4284)
for details.
...@@ -169,15 +169,11 @@ For Omnibus GitLab packages: ...@@ -169,15 +169,11 @@ For Omnibus GitLab packages:
1. [Reconfigure GitLab] for the changes to take effect 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, This example can be used for a bucket in Amsterdam (AMS3).
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).
1. For example, with [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/), 1. Add the following to `/etc/gitlab/gitlab.rb`:
this example configuration can be used for a bucket in Amsterdam (AMS3):
```ruby ```ruby
gitlab_rails['backup_upload_connection'] = { gitlab_rails['backup_upload_connection'] = {
...@@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3): ...@@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3):
'region' => 'ams3', 'region' => 'ams3',
'aws_access_key_id' => 'AKIAKIAKI', 'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123', 'aws_secret_access_key' => 'secret123',
'aws_signature_version' => 2,
'endpoint' => 'https://ams3.digitaloceanspaces.com' 'endpoint' => 'https://ams3.digitaloceanspaces.com'
} }
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' 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): ...@@ -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 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: For installations from source:
...@@ -494,7 +496,7 @@ more of the following options: ...@@ -494,7 +496,7 @@ more of the following options:
- `BACKUP=timestamp_of_backup` - Required if more than one backup exists. - `BACKUP=timestamp_of_backup` - Required if more than one backup exists.
Read what the [backup timestamp is about](#backup-timestamp). 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 ### Restore for installation from source
......
...@@ -38,10 +38,7 @@ In order for the report to show in the merge request, you need to specify a ...@@ -38,10 +38,7 @@ In order for the report to show in the merge request, you need to specify a
`codeclimate.json` as an artifact. GitLab will then check this file and show `codeclimate.json` as an artifact. GitLab will then check this file and show
the information inside the merge request. the information inside the merge request.
`codeclimate.json` needs to be the only artifact file for the job. If you try >**Note:**
to also include other files, like Code Climate's HTML report, it will break the
Code Climate display in the merge request.
If the Code Climate report doesn't have anything to compare to, no information If the Code Climate report doesn't have anything to compare to, no information
will be displayed in the merge request area. That is the case when you add the will be displayed in the merge request area. That is the case when you add the
`codequality` job in your `.gitlab-ci.yml` for the very first time. `codequality` job in your `.gitlab-ci.yml` for the very first time.
......
...@@ -90,10 +90,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -90,10 +90,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dockerReport.vulnerabilities = parsedVulnerabilities || []; this.dockerReport.vulnerabilities = parsedVulnerabilities || [];
// There is a typo in the original repo: const unapproved = data.unapproved || [];
// https://github.com/arminc/clair-scanner/pull/39/files
// Fix this when the above PR is accepted
const unapproved = data.unapproved || data.unaproved || [];
// Approved can be calculated by subtracting unapproved from vulnerabilities. // Approved can be calculated by subtracting unapproved from vulnerabilities.
this.dockerReport.approved = parsedVulnerabilities this.dockerReport.approved = parsedVulnerabilities
......
...@@ -63,7 +63,7 @@ module EE ...@@ -63,7 +63,7 @@ module EE
private private
def has_artifact?(name) def has_artifact?(name)
options.dig(:artifacts, :paths) == [name] && options.dig(:artifacts, :paths)&.include?(name) &&
artifacts_metadata? artifacts_metadata?
end end
end end
......
...@@ -334,12 +334,6 @@ module EE ...@@ -334,12 +334,6 @@ module EE
repository.async_remove_remote(::Repository::MIRROR_REMOTE) repository.async_remove_remote(::Repository::MIRROR_REMOTE)
end end
def import_url_availability
if remote_mirrors.find_by(url: import_url)
errors.add(:import_url, 'is already in use by a remote mirror')
end
end
def username_only_import_url def username_only_import_url
bare_url = read_attribute(:import_url) bare_url = read_attribute(:import_url)
return bare_url unless ::Gitlab::UrlSanitizer.valid?(bare_url) return bare_url unless ::Gitlab::UrlSanitizer.valid?(bare_url)
......
...@@ -17,8 +17,6 @@ class RemoteMirror < ActiveRecord::Base ...@@ -17,8 +17,6 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? }
validates :url, addressable_url: true, if: :url_changed? validates :url, addressable_url: true, if: :url_changed?
before_save :set_new_remote_name, if: :mirror_url_changed? before_save :set_new_remote_name, if: :mirror_url_changed?
...@@ -174,14 +172,6 @@ class RemoteMirror < ActiveRecord::Base ...@@ -174,14 +172,6 @@ class RemoteMirror < ActiveRecord::Base
end end
end end
def url_availability
return unless project
if project.import_url == url && project.mirror?
errors.add(:url, 'is already in use')
end
end
def reset_fields def reset_fields
update_columns( update_columns(
last_error: nil, last_error: nil,
......
...@@ -3,6 +3,11 @@ module Geo ...@@ -3,6 +3,11 @@ module Geo
include ApplicationWorker include ApplicationWorker
include CronjobQueue include CronjobQueue
HEALTHY_SHARD_CHECKS = [
Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
def perform def perform
return unless Gitlab::Geo.geo_database_configured? return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary? return unless Gitlab::Geo.secondary?
...@@ -17,10 +22,13 @@ module Geo ...@@ -17,10 +22,13 @@ module Geo
end end
def healthy_shards def healthy_shards
Gitlab::HealthChecks::FsShardsCheck # For now, we need to perform both Gitaly and direct filesystem checks to ensure
.readiness # the shard is healthy. We take the intersection of the successful checks
.select(&:success) # as the healthy shards.
.map { |check| check.labels[:shard] } HEALTHY_SHARD_CHECKS.map(&:readiness)
.map { |check_result| check_result.select(&:success) }
.inject(:&)
.map { |check_result| check_result.labels[:shard] }
.compact .compact
.uniq .uniq
end end
......
...@@ -575,7 +575,15 @@ module API ...@@ -575,7 +575,15 @@ module API
expose :work_in_progress?, as: :work_in_progress expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :merge_when_pipeline_succeeds 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 :diff_head_sha, as: :sha
expose :merge_commit_sha expose :merge_commit_sha
expose :user_notes_count expose :user_notes_count
......
...@@ -42,9 +42,7 @@ module Gitlab ...@@ -42,9 +42,7 @@ module Gitlab
end end
def load_blame_by_shelling_out def load_blame_by_shelling_out
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) @repo.shell_blame(@sha, @path)
# Read in binary mode to ensure ASCII-8BIT
IO.popen(cmd, 'rb') {|io| io.read }
end end
def process_raw_blame(output) def process_raw_blame(output)
......
...@@ -19,6 +19,8 @@ module Gitlab ...@@ -19,6 +19,8 @@ module Gitlab
cmd_output = "" cmd_output = ""
cmd_status = 0 cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
stdout.set_encoding(Encoding::ASCII_8BIT)
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
......
...@@ -468,9 +468,13 @@ module Gitlab ...@@ -468,9 +468,13 @@ module Gitlab
} }
options = default_options.merge(options) options = default_options.merge(options)
options[:limit] ||= 0
options[:offset] ||= 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| gitaly_migrate(:find_commits) do |is_enabled|
if is_enabled if is_enabled
gitaly_commit_client.find_commits(options) gitaly_commit_client.find_commits(options)
...@@ -614,11 +618,11 @@ module Gitlab ...@@ -614,11 +618,11 @@ module Gitlab
if is_enabled if is_enabled
gitaly_ref_client.find_ref_name(sha, ref_path) gitaly_ref_client.find_ref_name(sha, ref_path)
else 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] # Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 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 end
end end
...@@ -887,8 +891,7 @@ module Gitlab ...@@ -887,8 +891,7 @@ module Gitlab
"delete #{ref}\x00\x00" "delete #{ref}\x00\x00"
end end
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] message, status = run_git(%w[update-ref --stdin -z]) do |stdin|
message, status = popen(command, path) do |stdin|
stdin.write(instructions.join) stdin.write(instructions.join)
end end
...@@ -1409,6 +1412,11 @@ module Gitlab ...@@ -1409,6 +1412,11 @@ module Gitlab
end end
end end
def shell_blame(sha, path)
output, _status = run_git(%W(blame -p #{sha} -- #{path}))
output
end
private private
def shell_write_ref(ref_path, ref, old_ref) def shell_write_ref(ref_path, ref, old_ref)
...@@ -1433,6 +1441,12 @@ module Gitlab ...@@ -1433,6 +1441,12 @@ module Gitlab
def run_git(args, chdir: path, env: {}, nice: false, &block) def run_git(args, chdir: path, env: {}, nice: false, &block)
cmd = [Gitlab.config.git.bin_path, *args] cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice 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 circuit_breaker.perform do
popen(cmd, chdir, env, &block) popen(cmd, chdir, env, &block)
end end
...@@ -1624,7 +1638,7 @@ module Gitlab ...@@ -1624,7 +1638,7 @@ module Gitlab
offset_in_ruby = use_follow_flag && options[:offset].present? offset_in_ruby = use_follow_flag && options[:offset].present?
limit += offset if offset_in_ruby 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 << "--max-count=#{limit}"
cmd << '--format=%H' cmd << '--format=%H'
cmd << "--skip=#{offset}" unless offset_in_ruby cmd << "--skip=#{offset}" unless offset_in_ruby
...@@ -1640,7 +1654,7 @@ module Gitlab ...@@ -1640,7 +1654,7 @@ module Gitlab
cmd += Array(options[:path]) cmd += Array(options[:path])
end 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 = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) } lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
...@@ -1678,18 +1692,23 @@ module Gitlab ...@@ -1678,18 +1692,23 @@ module Gitlab
end end
def alternate_object_directories 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? if relative_paths.any?
relative_paths.map { |d| File.join(path, d) } relative_paths.map { |d| File.join(path, d) }
else else
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES) absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) }
.flatten
.compact
.flat_map { |d| d.split(File::PATH_SEPARATOR) }
end end
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 # Get the content of a blob for a given commit. If the blob is a commit
# (for submodules) then return the blob's OID. # (for submodules) then return the blob's OID.
def blob_content(commit, blob_name) def blob_content(commit, blob_name)
...@@ -1833,13 +1852,13 @@ module Gitlab ...@@ -1833,13 +1852,13 @@ module Gitlab
def count_commits_by_shelling_out(options) def count_commits_by_shelling_out(options)
cmd = count_commits_shelling_command(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) process_count_commits_raw_output(raw_output, options)
end end
def count_commits_shelling_command(options) 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 << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count] cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
...@@ -1884,20 +1903,17 @@ module Gitlab ...@@ -1884,20 +1903,17 @@ module Gitlab
return [] return []
end end
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref})
cmd += %w(-r) raw_output, _status = run_git(cmd)
cmd += %w(--full-tree)
cmd += %w(--full-name)
cmd += %W(-- #{actual_ref})
raw_output = IO.popen(cmd, &:read).split("\n").map do |f| lines = raw_output.split("\n").map do |f|
stuff, path = f.split("\t") stuff, path = f.split("\t")
_mode, type, _sha = stuff.split(" ") _mode, type, _sha = stuff.split(" ")
path if type == "blob" path if type == "blob"
# Contain only blob type # Contain only blob type
end end
raw_output.compact lines.compact
end end
# Returns true if the given ref name exists # Returns true if the given ref name exists
......
...@@ -19,8 +19,13 @@ module Gitlab ...@@ -19,8 +19,13 @@ module Gitlab
def error(error) def error(error)
error_out(error.message, caller[0].dup) error_out(error.message, caller[0].dup)
@errors << error.message @errors << error.message
# Debug: # 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 end
private private
......
...@@ -5,7 +5,17 @@ module Gitlab ...@@ -5,7 +5,17 @@ module Gitlab
module Popen module Popen
extend self 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) unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings" raise "System commands must be given as an array of strings"
end end
...@@ -18,18 +28,21 @@ module Gitlab ...@@ -18,18 +28,21 @@ module Gitlab
FileUtils.mkdir_p(path) FileUtils.mkdir_p(path)
end end
cmd_output = "" cmd_stdout = ''
cmd_status = 0 cmd_stderr = ''
cmd_status = nil
start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
cmd_output << stdout.read cmd_stdout = stdout.read
cmd_output << stderr.read cmd_stderr = stderr.read
cmd_status = wait_thr.value.exitstatus cmd_status = wait_thr.value
end end
[cmd_output, cmd_status] Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
end end
end end
end end
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
# please require all dependencies below: # 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 Gitlab
module Redis module Redis
......
...@@ -5,9 +5,15 @@ module DeliverNever ...@@ -5,9 +5,15 @@ module DeliverNever
end end
end end
module MuteNotifications
def new_note(note)
end
end
module Gitlab module Gitlab
class Seeder class Seeder
def self.quiet def self.quiet
mute_notifications
mute_mailer mute_mailer
SeedFu.quiet = true SeedFu.quiet = true
...@@ -18,6 +24,10 @@ module Gitlab ...@@ -18,6 +24,10 @@ module Gitlab
puts "\nOK".color(:green) puts "\nOK".color(:green)
end end
def self.mute_notifications
NotificationService.prepend(MuteNotifications)
end
def self.mute_mailer def self.mute_mailer
ActionMailer::MessageDelivery.prepend(DeliverNever) ActionMailer::MessageDelivery.prepend(DeliverNever)
end end
......
require 'rainbow/ext/string' require 'rainbow/ext/string'
require 'gitlab/utils/strong_memoize' require 'gitlab/utils/strong_memoize'
# rubocop:disable Rails/Output
module Gitlab module Gitlab
TaskFailedError = Class.new(StandardError) TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError) TaskAbortedByUserError = Class.new(StandardError)
...@@ -96,11 +97,9 @@ module Gitlab ...@@ -96,11 +97,9 @@ module Gitlab
end end
def gid_for(group_name) def gid_for(group_name)
begin Etc.getgrnam(group_name).gid
Etc.getgrnam(group_name).gid rescue ArgumentError # no group
rescue ArgumentError # no group "group #{group_name} doesn't exist"
"group #{group_name} doesn't exist"
end
end end
def gitlab_user def gitlab_user
......
require_relative "popen"
require_relative "version_info"
module Gitlab module Gitlab
class Upgrader class Upgrader
def execute def execute
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
## See installation.md#using-https for additional HTTPS configuration details. ## See installation.md#using-https for additional HTTPS configuration details.
upstream gitlab-workhorse { 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; server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
} }
...@@ -110,6 +112,8 @@ server { ...@@ -110,6 +112,8 @@ server {
error_page 502 /502.html; error_page 502 /502.html;
error_page 503 /503.html; error_page 503 /503.html;
location ~ ^/(404|422|500|502|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; root /home/git/gitlab/public;
internal; internal;
} }
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
## See installation.md#using-https for additional HTTPS configuration details. ## See installation.md#using-https for additional HTTPS configuration details.
upstream gitlab-workhorse { 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; server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
} }
...@@ -160,6 +162,8 @@ server { ...@@ -160,6 +162,8 @@ server {
error_page 502 /502.html; error_page 502 /502.html;
error_page 503 /503.html; error_page 503 /503.html;
location ~ ^/(404|422|500|502|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; root /home/git/gitlab/public;
internal; internal;
} }
......
require 'tasks/gitlab/task_helpers'
module SystemCheck module SystemCheck
module Helpers module Helpers
include ::Gitlab::TaskHelpers include ::Gitlab::TaskHelpers
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment