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:
expire_in: 7d
paths:
- tmp/tests
- config/secrets.yml
rspec-pg geo: *rspec-metadata-pg-geo
......
......@@ -337,7 +337,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
gem 'rspec-parameterized', require: false
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
......
......@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3)
gitlab-license (1.0.0)
gitlab-markup (1.6.3)
gitlab-styles (2.3.1)
gitlab-styles (2.3.2)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
......
/* eslint-disable class-methods-use-this */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { s__ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import Flash from './flash';
import flash from './flash';
import axios from './lib/utils/axios_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
......@@ -441,13 +443,15 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
$.post(awardUrl, {
axios.post(awardUrl, {
name: emoji,
}, (data) => {
})
.then(({ data }) => {
if (data.ok) {
callback();
}
}).fail(() => new Flash('Something went wrong on our end.'));
})
.catch(() => flash(s__('Something went wrong on our end.')));
}
}
......
/* global ace */
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
......@@ -56,12 +59,14 @@ export default class EditBlob {
if (paneId === '#preview') {
this.$toggleButton.hide();
return $.post(currentLink.data('preview-url'), {
axios.post(currentLink.data('preview-url'), {
content: this.editor.getValue(),
}, (response) => {
currentPane.empty().append(response);
return currentPane.renderGFM();
});
})
.then(({ data }) => {
currentPane.empty().append(data);
currentPane.renderGFM();
})
.catch(() => createFlash(__('An error occurred previewing the blob')));
}
this.$toggleButton.show();
......
......@@ -87,6 +87,9 @@ export default {
isEditForm() {
return this.currentPage === 'edit';
},
isVisible() {
return this.currentPage !== '';
},
buttonText() {
if (this.isNewForm) {
return 'Create board';
......@@ -181,14 +184,14 @@ export default {
<template>
<modal
v-show="currentPage"
v-show="isVisible"
modal-dialog-class="board-config-modal"
:hide-footer="readonly"
:title="title"
:primary-button-label="buttonText"
:kind="buttonKind"
:submit-disabled="submitDisabled"
@toggle="cancel"
@cancel="cancel"
@submit="submit"
>
<template slot="body">
......
......@@ -14,6 +14,7 @@ import {
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/**
* Cluster page has 2 separate parts:
......@@ -48,12 +49,9 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath,
});
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this);
this.showToken = this.showToken.bind(this);
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
......@@ -63,6 +61,7 @@ export default class Clusters {
this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
if (this.store.state.status !== 'created') {
......@@ -101,13 +100,11 @@ export default class Clusters {
}
addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
}
removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
}
......@@ -151,11 +148,6 @@ export default class Clusters {
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
}
toggle() {
this.toggleButton.classList.toggle('is-checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
}
showToken() {
const type = this.tokenField.getAttribute('type');
......
import Flash from '../flash';
import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import ClustersService from './services/clusters_service';
/**
* Toggles loading and disabled classes.
* @param {HTMLElement} button
*/
const toggleLoadingButton = (button) => {
if (button.getAttribute('disabled')) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', true);
}
button.classList.toggle('is-loading');
};
/**
* Toggles checked class for the given button
* @param {HTMLElement} button
*/
const toggleValue = (button) => {
button.classList.toggle('is-checked');
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
// The empty state won't have a clusterList
if (clusterList) {
setupToggleButtons(
document.querySelector('.js-clusters-list'),
(value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
.catch((err) => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
throw err;
}),
);
}
};
/**
* Handles toggle buttons in the cluster's table.
*
* When the user clicks the toggle button for each cluster, it:
* - toggles the button
* - shows a loading and disables button
* - Makes a put request to the given endpoint
* Once we receive the response, either:
* 1) Show updated status in case of successfull response
* 2) Show initial status in case of failed response
*/
export default function setClusterTableToggles() {
document.querySelectorAll('.js-toggle-cluster-list')
.forEach(button => button.addEventListener('click', (e) => {
const toggleButton = e.currentTarget;
const endpoint = toggleButton.getAttribute('data-endpoint');
toggleValue(toggleButton);
toggleLoadingButton(toggleButton);
const value = toggleButton.classList.contains('is-checked');
ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
.then(() => {
toggleLoadingButton(toggleButton);
})
.catch(() => {
toggleLoadingButton(toggleButton);
toggleValue(toggleButton);
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
});
}));
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
import { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare {
constructor(opts) {
......@@ -41,17 +42,14 @@ export default class Compare {
}
getTargetProject() {
return $.ajax({
url: this.opts.targetProjectUrl,
data: {
target_project_id: $("input[name='merge_request[target_project_id]']").val()
},
beforeSend: function() {
return $('.mr_target_commit').empty();
$('.mr_target_commit').empty();
return axios.get(this.opts.targetProjectUrl, {
params: {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
},
success: function(html) {
return $('.js-target-branch-dropdown .dropdown-content').html(html);
}
}).then(({ data }) => {
$('.js-target-branch-dropdown .dropdown-content').html(data);
});
}
......@@ -68,22 +66,19 @@ export default class Compare {
});
}
static sendAjax(url, loading, target, data) {
var $target;
$target = $(target);
return $.ajax({
url: url,
data: data,
beforeSend: function() {
loading.show();
return $target.empty();
},
success: function(html) {
loading.hide();
$target.html(html);
var className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
}
static sendAjax(url, loading, target, params) {
const $target = $(target);
loading.show();
$target.empty();
return axios.get(url, {
params,
}).then(({ data }) => {
loading.hide();
$target.html(data);
const className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
});
}
}
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default function initCompareAutocomplete() {
$('.js-compare-dropdown').each(function() {
......@@ -10,15 +13,14 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
axios.get($dropdown.data('refsUrl'), {
params: {
ref: $dropdown.data('ref'),
search: term,
}
}).done(function(refs) {
return callback(refs);
});
},
}).then(({ data }) => {
callback(data);
}).catch(() => flash(__('Error fetching refs')));
},
selectable: true,
filterable: true,
......
/* eslint-disable no-new */
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
......@@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown {
}
checkAbilityToCreateBranch() {
return $.ajax({
type: 'GET',
dataType: 'json',
url: this.canCreatePath,
beforeSend: () => this.setUnavailableButtonState(),
})
.done((data) => {
this.setUnavailableButtonState(false);
if (data.can_create_branch) {
this.available();
this.enable();
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
this.setUnavailableButtonState();
axios.get(this.canCreatePath)
.then(({ data }) => {
this.setUnavailableButtonState(false);
if (data.can_create_branch) {
this.available();
this.enable();
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
this.hide();
}
} else if (data.has_related_branch) {
this.hide();
}
}).fail(() => {
this.unavailable();
this.disable();
new Flash('Failed to check if a new branch can be created.');
});
})
.catch(() => {
this.unavailable();
this.disable();
Flash('Failed to check if a new branch can be created.');
});
}
createBranch() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createBranchPath,
beforeSend: () => (this.isCreatingBranch = true),
})
.done((data) => {
this.branchCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
this.isCreatingBranch = true;
return axios.post(this.createBranchPath)
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
})
.catch(() => Flash('Failed to create a branch for this issue. Please try again.'));
}
createMergeRequest() {
return $.ajax({
method: 'POST',
dataType: 'json',
url: this.createMrPath,
beforeSend: () => (this.isCreatingMergeRequest = true),
})
.done((data) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
this.isCreatingMergeRequest = true;
return axios.post(this.createMrPath)
.then(({ data }) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
})
.catch(() => Flash('Failed to create Merge Request. Please try again.'));
}
disable() {
......@@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') {
if (!ref) return false;
return $.ajax({
method: 'GET',
dataType: 'json',
url: this.refsPath + ref,
beforeSend: () => {
this.isGettingRef = true;
},
})
.always(() => {
this.isGettingRef = false;
})
.done((data) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
let result;
return axios.get(this.refsPath + ref)
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
let result;
if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
if (target === 'branch') {
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
this.isGettingRef = false;
return this.updateInputState(target, ref, result);
})
.fail(() => {
this.unavailable();
this.disable();
new Flash('Failed to get ref.');
return this.updateInputState(target, ref, result);
})
.catch(() => {
this.unavailable();
this.disable();
new Flash('Failed to get ref.');
return false;
});
this.isGettingRef = false;
return false;
});
}
getTargetData(target) {
......@@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown {
xhr = this.createBranch();
}
xhr.fail(() => {
xhr.catch(() => {
this.isCreatingMergeRequest = false;
this.isCreatingBranch = false;
});
xhr.always(() => this.enable());
this.enable();
});
this.disable();
}
......
......@@ -2,6 +2,7 @@ import Dropzone from 'dropzone';
import _ from 'underscore';
import './preview_markdown';
import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
Dropzone.autoDiscover = false;
......@@ -235,25 +236,21 @@ export default function dropzoneInput(form) {
uploadFile = (item, filename) => {
const formData = new FormData();
formData.append('file', item, filename);
return $.ajax({
url: uploadsPath,
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
headers: csrf.headers,
beforeSend: () => {
showSpinner();
return closeAlertMessage();
},
success: (e, text, response) => {
const md = response.responseJSON.link.markdown;
showSpinner();
closeAlertMessage();
axios.post(uploadsPath, formData)
.then(({ data }) => {
const md = data.link.markdown;
insertToTextArea(filename, md);
},
error: response => showError(response.responseJSON.message),
complete: () => closeSpinner(),
});
closeSpinner();
})
.catch((e) => {
showError(e.response.data.message);
closeSpinner();
});
};
updateAttachingMessage = (files, messageContainer) => {
......
/* global dateFormat */
import Pikaday from 'pikaday';
import axios from './lib/utils/axios_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
......@@ -125,37 +126,30 @@ class DueDateSelect {
}
submitSelectedDate(isDropdown) {
return $.ajax({
type: 'PUT',
url: this.issueUpdateURL,
data: this.datePayload,
dataType: 'json',
beforeSend: () => {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
}
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
}
this.$value.css('display', '');
this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
this.$sidebarValue.html(this.displayedDate);
this.$value.css('display', '');
this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
this.$sidebarValue.html(this.displayedDate);
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
},
}).done(() => {
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
return this.$loading.fadeOut();
});
$('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
return this.$loading.fadeOut();
});
}
}
......
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
/**
* Makes search request for content when user types a value in the search input.
......@@ -54,32 +55,26 @@ export default class FilterableList {
this.listFilterElement.removeEventListener('input', this.debounceFilter);
}
filterResults(queryData) {
filterResults(params) {
if (this.isBusy) {
return false;
}
$(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({
url: this.getFilterEndpoint(),
data: queryData,
type: 'GET',
dataType: 'json',
context: this,
complete: this.onFilterComplete,
beforeSend: () => {
this.isBusy = true;
},
success: (response, textStatus, xhr) => {
this.onFilterSuccess(response, xhr, queryData);
},
});
this.isBusy = true;
return axios.get(this.getFilterEndpoint(), {
params,
}).then((res) => {
this.onFilterSuccess(res, params);
this.onFilterComplete();
}).catch(() => this.onFilterComplete());
}
onFilterSuccess(response, xhr, queryData) {
if (response.html) {
this.listHolderElement.innerHTML = response.html;
onFilterSuccess(response, queryData) {
if (response.data.html) {
this.listHolderElement.innerHTML = response.data.html;
}
// Change url so if user reload a page - search results are saved
......
import FilterableList from '~/filterable_list';
import eventHub from './event_hub';
import { getParameterByName } from '../lib/utils/common_utils';
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
......@@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList {
this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
}
onFilterSuccess(data, xhr, queryData) {
onFilterSuccess(res, queryData) {
const currentPath = this.getPagePath(queryData);
const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
'X-Page': xhr.getResponseHeader('X-Page'),
'X-Total': xhr.getResponseHeader('X-Total'),
'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'),
'X-Next-Page': xhr.getResponseHeader('X-Next-Page'),
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
};
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
eventHub.$emit('updatePagination', paginationData);
eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
}
}
......@@ -26,6 +26,9 @@
'currentBranchId',
'rightPanelCollapsed',
]),
isCommitInfoShown() {
return this.rightPanelCollapsed || this.fileList.length;
},
},
methods: {
toggleCollapsed() {
......@@ -36,7 +39,11 @@
</script>
<template>
<div class="multi-file-commit-list">
<div
:class="{
'multi-file-commit-list': isCommitInfoShown
}"
>
<list-collapsed
v-if="rightPanelCollapsed"
/>
......@@ -54,12 +61,6 @@
/>
</li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div>
</template>
......@@ -23,6 +23,14 @@
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
......@@ -94,6 +102,9 @@
</div>
</template>
</div>
<ide-contextbar/>
<ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</template>
......@@ -10,6 +10,16 @@
icon,
panelResizer,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
data() {
return {
width: 290,
......@@ -46,6 +56,11 @@
collapsed: !this.rightPanelCollapsed,
});
},
toggleFullbarCollapsed() {
if (this.rightPanelCollapsed) {
this.toggleCollapsed();
}
},
resizingStarted() {
this.setResizingStatus(true);
},
......@@ -63,8 +78,11 @@
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
>
<div class="multi-file-commit-panel-section">
<div
class="multi-file-commit-panel-section"
>
<header
class="multi-file-commit-panel-header"
:class="{
......@@ -75,16 +93,20 @@
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
<div
v-if="changedFiles.length"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
@click.stop="toggleCollapsed"
>
<icon
:name="currentIcon"
......@@ -92,7 +114,10 @@
/>
</button>
</header>
<repo-commit-section />
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
<panel-resizer
:size.sync="width"
......
......@@ -14,6 +14,16 @@ export default {
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
data() {
return {
showNewBranchModal: false,
......@@ -27,6 +37,7 @@ export default {
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
'lastCommitMsg',
]),
...mapGetters([
'changedFiles',
......@@ -37,6 +48,9 @@ export default {
commitMessageCount() {
return this.commitMessage.length;
},
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
...mapActions([
......@@ -101,7 +115,12 @@ export default {
</script>
<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
v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
......@@ -118,54 +137,92 @@ you started editing. Would you like to create a new branch?`)"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
v-if="!rightPanelCollapsed"
<template
v-if="changedFiles.length"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message"
name="commit-message"
v-model="commitMessage"
placeholder="Commit message"
>
</textarea>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<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 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 }"
<div class="col-xs-10 col-xs-offset-1">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<i
v-if="submitCommitsLoading"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading"
>
</i>
Commit
</button>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</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>
</form>
</div>
</div>
</template>
......@@ -18,6 +18,8 @@ function initIde(el) {
return createElement('ide', {
props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
},
});
},
......
......@@ -110,15 +110,7 @@ export const commitChanges = (
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
flash(
commitMsg,
'notice',
document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
commit(types.SET_LAST_COMMIT_MSG, commitMsg);
if (newMr) {
dispatch('discardAllChanges');
......
......@@ -3,6 +3,7 @@ export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
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_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
......
......@@ -63,6 +63,11 @@ export default {
updatedAt: lastCommit.commit.authored_date,
});
},
[types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) {
Object.assign(state, {
lastCommitMsg,
});
},
...projectMutations,
...fileMutations,
...treeMutations,
......
......@@ -8,6 +8,7 @@ export default () => ({
endpoints: {},
isRoot: false,
isInitialRoot: false,
lastCommitMsg: '',
lastCommitPath: '',
loading: false,
onTopOfBranch: false,
......@@ -18,6 +19,6 @@ export default () => ({
trees: {},
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
rightPanelCollapsed: false,
panelResizing: false,
});
......@@ -115,9 +115,12 @@ export default {
reordered(event) {
this.removeDraggingCursor();
const { beforeId, afterId } = this.getBeforeAfterId(event.item);
const { oldIndex, newIndex } = event;
this.$emit('saveReorder', {
issueId: parseInt(event.item.dataset.key, 10),
oldIndex,
newIndex,
afterId,
beforeId,
});
......
......@@ -176,7 +176,7 @@ export default {
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);
if (issueToReorder) {
......@@ -184,7 +184,14 @@ export default {
endpoint: issueToReorder.relation_path,
move_before_id: beforeId,
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.');
});
}
......
......@@ -16,6 +16,13 @@ class RelatedIssuesStore {
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) {
this.state.pendingReferences = issues;
}
......
......@@ -22,3 +22,11 @@ axios.interceptors.response.use((config) => {
});
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 {
return !this.showLess || (index < this.defaultRenderCount && this.showLess);
},
avatarUrl(user) {
return user.avatar || user.avatar_url;
return user.avatar || user.avatar_url || gon.default_avatar_url;
},
assigneeUrl(user) {
return `${this.rootPath}${user.username}`;
......
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 = {}) {
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, username;
username = user.username ? "@" + user.username : "";
avatar = user.avatar_url ? user.avatar_url : false;
avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
let selected = false;
......@@ -513,9 +513,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (user.beforeDivider != null) {
`<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`;
} else {
if (avatar) {
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
}
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
}
return `
......
......@@ -76,6 +76,7 @@ export default {
<a
href="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm inline">
Check out branch
</a>
......
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';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue';
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
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 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 AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';
export { default as CheckingState } from './components/states/mr_widget_checking.vue';
......
......@@ -20,7 +20,7 @@
width: 100%;
}
$image-widths: 250 306 394 430;
$image-widths: 80 250 306 394 430;
@each $width in $image-widths {
&.svg-#{$width} {
img,
......
......@@ -355,6 +355,11 @@ table.table tr td.multi-file-table-name {
flex: 1;
}
.multi-file-commit-empty-state-container {
align-items: center;
justify-content: center;
}
.multi-file-commit-panel-header {
display: flex;
align-items: center;
......@@ -381,7 +386,7 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
padding: $gl-btn-padding;
padding: 0 $gl-btn-padding;
svg {
margin-right: $gl-btn-padding;
......
......@@ -89,7 +89,7 @@ module ApplicationHelper
end
def default_avatar
'no_avatar.png'
asset_path('no_avatar.png')
end
def last_commit(project)
......
......@@ -476,7 +476,7 @@ module Ci
if cache && project.jobs_cache_index
cache = cache.merge(
key: "#{cache[:key]}:#{project.jobs_cache_index}")
key: "#{cache[:key]}_#{project.jobs_cache_index}")
end
[cache]
......
......@@ -1016,13 +1016,13 @@ class MergeRequest < ActiveRecord::Base
merged_at = metrics&.merged_at
notes_association = notes_with_associations
# It is not guaranteed that Note#created_at will be strictly later than
# MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
# comparison, as will a HA environment if clocks are not *precisely*
# synchronized. Add a minute's leeway to compensate for both possibilities
cutoff = merged_at - 1.minute
if merged_at
# It is not guaranteed that Note#created_at will be strictly later than
# MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
# comparison, as will a HA environment if clocks are not *precisely*
# synchronized. Add a minute's leeway to compensate for both possibilities
cutoff = merged_at - 1.minute
notes_association = notes_association.where('created_at >= ?', cutoff)
end
......
......@@ -576,6 +576,9 @@ class Project < ActiveRecord::Base
RepositoryForkWorker.perform_async(id,
forked_from_project.repository_storage_path,
forked_from_project.disk_path)
elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id)
else
RepositoryImportWorker.perform_async(self.id)
end
......
......@@ -2,7 +2,7 @@ class EmailsOnPushService < Service
boolean_accessor :send_from_committer_email
boolean_accessor :disable_diffs
prop_accessor :recipients
validates :recipients, presence: true, if: :activated?
validates :recipients, presence: true, if: :valid_recipients?
def title
'Emails on push'
......
......@@ -4,7 +4,7 @@ class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated?
validates :recipients, presence: true, if: :valid_recipients?
before_validation :get_channels
......
......@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService
username: self.username,
password: self.password,
site: URI.join(url, '/').to_s,
context_path: url.path,
context_path: url.path.chomp('/'),
auth_type: :basic,
read_timeout: 120,
use_cookies: true,
......
class PipelinesEmailService < Service
prop_accessor :recipients
boolean_accessor :notify_only_broken_pipelines
validates :recipients, presence: true, if: :activated?
validates :recipients, presence: true, if: :valid_recipients?
def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true }
......
......@@ -2,6 +2,8 @@
# and implement a set of methods
class Service < ActiveRecord::Base
include Sortable
include Importable
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, false
......@@ -301,4 +303,8 @@ class Service < ActiveRecord::Base
project.cache_has_external_wiki
end
end
def valid_recipients?
activated? && !importing?
end
end
......@@ -9,7 +9,8 @@ module MergeRequests
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
close_merge_requests
close_upon_missing_source_branch_ref
post_merge_manually_merged
reload_merge_requests
reset_merge_when_pipeline_succeeds
mark_pending_todos_done
......@@ -30,11 +31,22 @@ module MergeRequests
private
def close_upon_missing_source_branch_ref
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
# create an `empty` diff for `closed` MRs without a source branch, keeping
# the latest diff state as the last _valid_ one.
merge_requests_for_source_branch.reject(&:source_branch_exists?).each do |mr|
MergeRequests::CloseService
.new(mr.target_project, @current_user)
.execute(mr)
end
end
# Collect open merge requests that target same branch we push into
# and close if push to master include last commit from merge request
# We need this to close(as merged) merge requests that were merged into
# target branch manually
def close_merge_requests
def post_merge_manually_merged
commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit)
......
......@@ -5,16 +5,16 @@
= markdown(current_application_settings.help_page_text)
%hr
- unless current_application_settings.help_page_hide_commercial_content?
%h1
GitLab
Enterprise Edition
- if user_signed_in?
%span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ee', Gitlab::REVISION)
- if current_application_settings.version_check_enabled
= version_status_badge
%h1
GitLab
Enterprise Edition
- if user_signed_in?
%span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ee', Gitlab::REVISION)
= version_status_badge
%hr
- unless current_application_settings.help_page_hide_commercial_content?
%p.slead
GitLab is open source software to collaborate on code.
%br
......
......@@ -5,7 +5,9 @@
= webpack_bundle_tag 'common_vue'
= 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
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
......@@ -14,5 +14,5 @@
#{time_ago_with_tooltip(event.created_at)}
.pull-right
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm" do
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
#{ _('Create merge request') }
......@@ -12,11 +12,12 @@
.table-section.section-10
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%button{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
= icon("spinner spin", class: "loading-icon")
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
......
......@@ -10,13 +10,12 @@
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
%label.append-bottom-10
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%label.append-bottom-10.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
......
......@@ -15,7 +15,7 @@
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea',
classes: 'note-textarea qa-issuable-form-description',
placeholder: "Write a comment or drag your files here...",
supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
......
......@@ -67,7 +67,7 @@
%span.append-right-10
- if issuable.new_record?
= form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
= form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create qa-issuable-create-button'
- else
= form.submit 'Save changes', class: 'btn btn-save'
......
......@@ -6,7 +6,7 @@
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
autocomplete: 'off', class: 'form-control pad'
autocomplete: 'off', class: 'form-control pad qa-issuable-form-title'
- if issuable.respond_to?(:work_in_progress?)
%p.help-block
......
......@@ -20,7 +20,11 @@ class RepositoryImportWorker
# to those importers to mark the import process as complete.
return if service.async?
raise result[:message] if result[:status] == :error
if result[:status] == :error
fail_import(project, result[:message]) if project.gitlab_project_import?
raise result[:message]
end
project.after_import
......@@ -37,4 +41,8 @@ class RepositoryImportWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
def fail_import(project, message)
project.mark_import_as_failed(message)
end
end
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'
module Gitlab
class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
......
......@@ -190,3 +190,26 @@ synchronization operations.
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
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:
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
data.
......@@ -267,12 +267,6 @@ because we have not yet configured the secondary server. This is the next step.
### 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:
```
......@@ -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
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
Install the `server.crt` file:
......@@ -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
`gitlab_replicator` user. If all worked correctly, you should see the
database prompt.
`gitlab_replicator` user. If all worked correctly, you should see
the list of primary's databases.
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
......
......@@ -9,18 +9,17 @@ fail-over with minimal effort, in a disaster situation.
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
> 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
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.
This process promotes a secondary Geo replica to a primary. To regain
geographical redundancy as quickly as possible, you should add a new secondary
immediately after following these instructions.
### Step 1. Promoting a secondary Geo replica
1. SSH into your **primary** to stop and disable GitLab.
```bash
......@@ -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
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
GitLab Geo on the new primary.
To bring a new secondary online, follow the [GitLab Geo 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:
1. [Reconfigure GitLab] for the changes to take effect
#### Digital Ocean Spaces and other S3-compatible providers
#### Digital Ocean Spaces
Not all S3 providers are fully-compatible with the Fog library. For example,
if you see `411 Length Required` errors after attempting to upload, you may
need to downgrade the `aws_signature_version` value from the default value to
2 [due to this issue](https://github.com/fog/fog-aws/issues/428).
This example can be used for a bucket in Amsterdam (AMS3).
1. For example, with [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/),
this example configuration can be used for a bucket in Amsterdam (AMS3):
1. Add the following to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['backup_upload_connection'] = {
......@@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3):
'region' => 'ams3',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123',
'aws_signature_version' => 2,
'endpoint' => 'https://ams3.digitaloceanspaces.com'
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
......@@ -193,6 +188,13 @@ this example configuration can be used for a bucket in Amsterdam (AMS3):
1. [Reconfigure GitLab] for the changes to take effect
#### Other S3 Providers
Not all S3 providers are fully-compatible with the Fog library. For example,
if you see `411 Length Required` errors after attempting to upload, you may
need to downgrade the `aws_signature_version` value from the default value to
2 [due to this issue](https://github.com/fog/fog-aws/issues/428).
---
For installations from source:
......@@ -494,7 +496,7 @@ more of the following options:
- `BACKUP=timestamp_of_backup` - Required if more than one backup exists.
Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Do not ask if the authorized_keys file should get regenerated.
- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed.
### Restore for installation from source
......
......@@ -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
the information inside the merge request.
`codeclimate.json` needs to be the only artifact file for the job. If you try
to also include other files, like Code Climate's HTML report, it will break the
Code Climate display in the merge request.
>**Note:**
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
`codequality` job in your `.gitlab-ci.yml` for the very first time.
......
......@@ -90,10 +90,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dockerReport.vulnerabilities = parsedVulnerabilities || [];
// There is a typo in the original repo:
// https://github.com/arminc/clair-scanner/pull/39/files
// Fix this when the above PR is accepted
const unapproved = data.unapproved || data.unaproved || [];
const unapproved = data.unapproved || [];
// Approved can be calculated by subtracting unapproved from vulnerabilities.
this.dockerReport.approved = parsedVulnerabilities
......
......@@ -63,7 +63,7 @@ module EE
private
def has_artifact?(name)
options.dig(:artifacts, :paths) == [name] &&
options.dig(:artifacts, :paths)&.include?(name) &&
artifacts_metadata?
end
end
......
......@@ -334,12 +334,6 @@ module EE
repository.async_remove_remote(::Repository::MIRROR_REMOTE)
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
bare_url = read_attribute(:import_url)
return bare_url unless ::Gitlab::UrlSanitizer.valid?(bare_url)
......
......@@ -17,8 +17,6 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
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?
before_save :set_new_remote_name, if: :mirror_url_changed?
......@@ -174,14 +172,6 @@ class RemoteMirror < ActiveRecord::Base
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
update_columns(
last_error: nil,
......
......@@ -3,6 +3,11 @@ module Geo
include ApplicationWorker
include CronjobQueue
HEALTHY_SHARD_CHECKS = [
Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
def perform
return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary?
......@@ -17,10 +22,13 @@ module Geo
end
def healthy_shards
Gitlab::HealthChecks::FsShardsCheck
.readiness
.select(&:success)
.map { |check| check.labels[:shard] }
# For now, we need to perform both Gitaly and direct filesystem checks to ensure
# the shard is healthy. We take the intersection of the successful checks
# as the healthy shards.
HEALTHY_SHARD_CHECKS.map(&:readiness)
.map { |check_result| check_result.select(&:success) }
.inject(:&)
.map { |check_result| check_result.labels[:shard] }
.compact
.uniq
end
......
......@@ -575,7 +575,15 @@ module API
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
expose :merge_when_pipeline_succeeds
expose :merge_status
# Ideally we should deprecate `MergeRequest#merge_status` exposure and
# use `MergeRequest#mergeable?` instead (boolean).
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
# information.
expose :merge_status do |merge_request|
merge_request.check_if_can_be_merged
merge_request.merge_status
end
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :user_notes_count
......
......@@ -42,9 +42,7 @@ module Gitlab
end
def load_blame_by_shelling_out
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
# Read in binary mode to ensure ASCII-8BIT
IO.popen(cmd, 'rb') {|io| io.read }
@repo.shell_blame(@sha, @path)
end
def process_raw_blame(output)
......
......@@ -19,6 +19,8 @@ module Gitlab
cmd_output = ""
cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
stdout.set_encoding(Encoding::ASCII_8BIT)
yield(stdin) if block_given?
stdin.close
......
......@@ -468,9 +468,13 @@ module Gitlab
}
options = default_options.merge(options)
options[:limit] ||= 0
options[:offset] ||= 0
limit = options[:limit]
if limit == 0 || !limit.is_a?(Integer)
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end
gitaly_migrate(:find_commits) do |is_enabled|
if is_enabled
gitaly_commit_client.find_commits(options)
......@@ -614,11 +618,11 @@ module Gitlab
if is_enabled
gitaly_ref_client.find_ref_name(sha, ref_path)
else
args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
args = %W(for-each-ref --count=1 #{ref_path} --contains #{sha})
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
popen(args, @path).first.split.last
run_git(args).first.split.last
end
end
end
......@@ -887,8 +891,7 @@ module Gitlab
"delete #{ref}\x00\x00"
end
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
message, status = popen(command, path) do |stdin|
message, status = run_git(%w[update-ref --stdin -z]) do |stdin|
stdin.write(instructions.join)
end
......@@ -1409,6 +1412,11 @@ module Gitlab
end
end
def shell_blame(sha, path)
output, _status = run_git(%W(blame -p #{sha} -- #{path}))
output
end
private
def shell_write_ref(ref_path, ref, old_ref)
......@@ -1433,6 +1441,12 @@ module Gitlab
def run_git(args, chdir: path, env: {}, nice: false, &block)
cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice
object_directories = alternate_object_directories
if object_directories.any?
env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = object_directories.join(File::PATH_SEPARATOR)
end
circuit_breaker.perform do
popen(cmd, chdir, env, &block)
end
......@@ -1624,7 +1638,7 @@ module Gitlab
offset_in_ruby = use_follow_flag && options[:offset].present?
limit += offset if offset_in_ruby
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
cmd = %w[log]
cmd << "--max-count=#{limit}"
cmd << '--format=%H'
cmd << "--skip=#{offset}" unless offset_in_ruby
......@@ -1640,7 +1654,7 @@ module Gitlab
cmd += Array(options[:path])
end
raw_output = IO.popen(cmd) { |io| io.read }
raw_output, _status = run_git(cmd)
lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
......@@ -1678,18 +1692,23 @@ module Gitlab
end
def alternate_object_directories
relative_paths = Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
relative_paths = relative_object_directories
if relative_paths.any?
relative_paths.map { |d| File.join(path, d) }
else
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES)
.flatten
.compact
.flat_map { |d| d.split(File::PATH_SEPARATOR) }
absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) }
end
end
def relative_object_directories
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
end
def absolute_object_directories
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).flatten.compact
end
# Get the content of a blob for a given commit. If the blob is a commit
# (for submodules) then return the blob's OID.
def blob_content(commit, blob_name)
......@@ -1833,13 +1852,13 @@ module Gitlab
def count_commits_by_shelling_out(options)
cmd = count_commits_shelling_command(options)
raw_output = IO.popen(cmd) { |io| io.read }
raw_output, _status = run_git(cmd)
process_count_commits_raw_output(raw_output, options)
end
def count_commits_shelling_command(options)
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
cmd = %w[rev-list]
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
......@@ -1884,20 +1903,17 @@ module Gitlab
return []
end
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
cmd += %w(-r)
cmd += %w(--full-tree)
cmd += %w(--full-name)
cmd += %W(-- #{actual_ref})
cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref})
raw_output, _status = run_git(cmd)
raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
lines = raw_output.split("\n").map do |f|
stuff, path = f.split("\t")
_mode, type, _sha = stuff.split(" ")
path if type == "blob"
# Contain only blob type
end
raw_output.compact
lines.compact
end
# Returns true if the given ref name exists
......
......@@ -19,8 +19,13 @@ module Gitlab
def error(error)
error_out(error.message, caller[0].dup)
@errors << error.message
# Debug:
Rails.logger.error(error.backtrace.join("\n"))
if error.backtrace
Rails.logger.error("Import/Export backtrace: #{error.backtrace.join("\n")}")
else
Rails.logger.error("No backtrace found")
end
end
private
......
......@@ -5,7 +5,17 @@ module Gitlab
module Popen
extend self
def popen(cmd, path = nil, vars = {})
Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
# Returns [stdout + stderr, status]
def popen(cmd, path = nil, vars = {}, &block)
result = popen_with_detail(cmd, path, vars, &block)
[result.stdout << result.stderr, result.status&.exitstatus]
end
# Returns Result
def popen_with_detail(cmd, path = nil, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
......@@ -18,18 +28,21 @@ module Gitlab
FileUtils.mkdir_p(path)
end
cmd_output = ""
cmd_status = 0
cmd_stdout = ''
cmd_stderr = ''
cmd_status = nil
start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given?
stdin.close
cmd_output << stdout.read
cmd_output << stderr.read
cmd_status = wait_thr.value.exitstatus
cmd_stdout = stdout.read
cmd_stderr = stderr.read
cmd_status = wait_thr.value
end
[cmd_output, cmd_status]
Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
end
end
end
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:
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present?
module Gitlab
module Redis
......
......@@ -5,9 +5,15 @@ module DeliverNever
end
end
module MuteNotifications
def new_note(note)
end
end
module Gitlab
class Seeder
def self.quiet
mute_notifications
mute_mailer
SeedFu.quiet = true
......@@ -18,6 +24,10 @@ module Gitlab
puts "\nOK".color(:green)
end
def self.mute_notifications
NotificationService.prepend(MuteNotifications)
end
def self.mute_mailer
ActionMailer::MessageDelivery.prepend(DeliverNever)
end
......
require 'rainbow/ext/string'
require 'gitlab/utils/strong_memoize'
# rubocop:disable Rails/Output
module Gitlab
TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError)
......@@ -96,11 +97,9 @@ module Gitlab
end
def gid_for(group_name)
begin
Etc.getgrnam(group_name).gid
rescue ArgumentError # no group
"group #{group_name} doesn't exist"
end
Etc.getgrnam(group_name).gid
rescue ArgumentError # no group
"group #{group_name} doesn't exist"
end
def gitlab_user
......
require_relative "popen"
require_relative "version_info"
module Gitlab
class Upgrader
def execute
......
......@@ -17,6 +17,8 @@
## See installation.md#using-https for additional HTTPS configuration details.
upstream gitlab-workhorse {
# Gitlab socket file,
# for Omnibus this would be: unix:/var/opt/gitlab/gitlab-workhorse/socket
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
......@@ -110,6 +112,8 @@ server {
error_page 502 /502.html;
error_page 503 /503.html;
location ~ ^/(404|422|500|502|503)\.html$ {
# Location to the Gitlab's public directory,
# for Omnibus this would be: /opt/gitlab/embedded/service/gitlab-rails/public.
root /home/git/gitlab/public;
internal;
}
......
......@@ -21,6 +21,8 @@
## See installation.md#using-https for additional HTTPS configuration details.
upstream gitlab-workhorse {
# Gitlab socket file,
# for Omnibus this would be: unix:/var/opt/gitlab/gitlab-workhorse/socket
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
......@@ -160,6 +162,8 @@ server {
error_page 502 /502.html;
error_page 503 /503.html;
location ~ ^/(404|422|500|502|503)\.html$ {
# Location to the Gitlab's public directory,
# for Omnibus this would be: /opt/gitlab/embedded/service/gitlab-rails/public
root /home/git/gitlab/public;
internal;
}
......
require 'tasks/gitlab/task_helpers'
module SystemCheck
module Helpers
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