Merge branch 'master' into 39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled
This commit is contained in:
commit
b058af1be6
175 changed files with 2889 additions and 755 deletions
|
@ -718,7 +718,7 @@ GEM
|
|||
redis-store (>= 1.3, < 2)
|
||||
redis-namespace (1.5.2)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
redis-rack (2.0.3)
|
||||
redis-rack (2.0.4)
|
||||
rack (>= 1.5, < 3)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-rails (5.0.2)
|
||||
|
|
|
@ -74,6 +74,18 @@ const gfmRules = {
|
|||
return `![${el.dataset.title}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
MermaidFilter: {
|
||||
'svg.mermaid'(el, text) {
|
||||
const sourceEl = el.querySelector('text.source');
|
||||
if (!sourceEl) return false;
|
||||
|
||||
return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
|
||||
},
|
||||
'svg.mermaid style, svg.mermaid g'(el, text) {
|
||||
// We don't want to include the content of these elements in the copied text.
|
||||
return '';
|
||||
},
|
||||
},
|
||||
MathFilter: {
|
||||
'pre.code.math[data-math-style=display]'(el, text) {
|
||||
return `\`\`\`math\n${text.trim()}\n\`\`\``;
|
||||
|
|
|
@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown {
|
|||
let target;
|
||||
let value;
|
||||
|
||||
if (event.srcElement === this.branchInput) {
|
||||
if (event.target === this.branchInput) {
|
||||
target = 'branch';
|
||||
value = this.branchInput.value;
|
||||
} else if (event.srcElement === this.refInput) {
|
||||
} else if (event.target === this.refInput) {
|
||||
target = 'ref';
|
||||
value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
|
||||
event.srcElement.value.slice(event.srcElement.selectionEnd);
|
||||
value = event.target.value.slice(0, event.target.selectionStart) +
|
||||
event.target.value.slice(event.target.selectionEnd);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -45,11 +45,9 @@ export default {
|
|||
onLeaveGroup() {
|
||||
this.modalStatus = true;
|
||||
},
|
||||
leaveGroup(leaveConfirmed) {
|
||||
leaveGroup() {
|
||||
this.modalStatus = false;
|
||||
if (leaveConfirmed) {
|
||||
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
|
||||
}
|
||||
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -42,28 +42,28 @@ export default {
|
|||
v-if="isGroup"
|
||||
css-class="number-subgroups"
|
||||
icon-name="folder"
|
||||
:title="s__('Subgroups')"
|
||||
:value=item.subgroupCount
|
||||
:title="__('Subgroups')"
|
||||
:value="item.subgroupCount"
|
||||
/>
|
||||
<item-stats-value
|
||||
v-if="isGroup"
|
||||
css-class="number-projects"
|
||||
icon-name="bookmark"
|
||||
:title="s__('Projects')"
|
||||
:value=item.projectCount
|
||||
:title="__('Projects')"
|
||||
:value="item.projectCount"
|
||||
/>
|
||||
<item-stats-value
|
||||
v-if="isGroup"
|
||||
css-class="number-users"
|
||||
icon-name="users"
|
||||
:title="s__('Members')"
|
||||
:value=item.memberCount
|
||||
:title="__('Members')"
|
||||
:value="item.memberCount"
|
||||
/>
|
||||
<item-stats-value
|
||||
v-if="isProject"
|
||||
css-class="project-stars"
|
||||
icon-name="star"
|
||||
:value=item.starCount
|
||||
:value="item.starCount"
|
||||
/>
|
||||
<item-stats-value
|
||||
css-class="item-visibility"
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
methods: {
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.toggleModalOpen();
|
||||
this.openModal = true;
|
||||
},
|
||||
toggleModalOpen() {
|
||||
this.openModal = !this.openModal;
|
||||
hideModal() {
|
||||
this.openModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -95,7 +95,7 @@
|
|||
:branch-id="branch"
|
||||
:path="path"
|
||||
:parent="parent"
|
||||
@toggle="toggleModalOpen"
|
||||
@hide="hideModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -43,10 +43,10 @@
|
|||
type: this.type,
|
||||
});
|
||||
|
||||
this.toggleModalOpen();
|
||||
this.hideModal();
|
||||
},
|
||||
toggleModalOpen() {
|
||||
this.$emit('toggle');
|
||||
hideModal() {
|
||||
this.$emit('hide');
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -86,7 +86,7 @@
|
|||
:title="modalTitle"
|
||||
:primary-button-label="buttonLabel"
|
||||
kind="success"
|
||||
@toggle="toggleModalOpen"
|
||||
@cancel="hideModal"
|
||||
@submit="createEntryInStore"
|
||||
>
|
||||
<form
|
||||
|
|
|
@ -110,7 +110,7 @@ export default {
|
|||
kind="primary"
|
||||
:title="__('Branch has changed')"
|
||||
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
|
||||
@toggle="showNewBranchModal = false"
|
||||
@cancel="showNewBranchModal = false"
|
||||
@submit="makeCommit(true)"
|
||||
/>
|
||||
<commit-files-list
|
||||
|
|
|
@ -50,7 +50,7 @@ export default {
|
|||
kind="warning"
|
||||
:title="__('Are you sure?')"
|
||||
:text="__('Are you sure you want to discard your changes?')"
|
||||
@toggle="closeDiscardPopup"
|
||||
@cancel="closeDiscardPopup"
|
||||
@submit="toggleEditMode(true)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength -
|
|||
export function capitalizeFirstCharacter(text) {
|
||||
return `${text[0].toUpperCase()}${text.slice(1)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all html tags from a string with the given replacement.
|
||||
*
|
||||
* @param {String} string
|
||||
* @param {*} replace
|
||||
* @returns {String}
|
||||
*/
|
||||
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import modal from '../../../vue_shared/components/modal.vue';
|
||||
import { __, s__, sprintf } from '../../../locale';
|
||||
import csrf from '../../../lib/utils/csrf';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -22,7 +22,6 @@
|
|||
return {
|
||||
enteredPassword: '',
|
||||
enteredUsername: '',
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
|
|||
|
||||
return this.enteredUsername === this.username;
|
||||
},
|
||||
onSubmit(status) {
|
||||
if (status) {
|
||||
if (!this.canSubmit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs.form.submit();
|
||||
}
|
||||
|
||||
this.toggleOpen(false);
|
||||
},
|
||||
toggleOpen(isOpen) {
|
||||
this.isOpen = isOpen;
|
||||
onSubmit() {
|
||||
this.$refs.form.submit();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<modal
|
||||
v-if="isOpen"
|
||||
:title="s__('Profiles|Delete your account?')"
|
||||
:text="text"
|
||||
:kind="`danger ${!canSubmit() && 'disabled'}`"
|
||||
:primary-button-label="s__('Profiles|Delete account')"
|
||||
@toggle="toggleOpen"
|
||||
@submit="onSubmit">
|
||||
<modal
|
||||
id="delete-account-modal"
|
||||
:title="s__('Profiles|Delete your account?')"
|
||||
:text="text"
|
||||
kind="danger"
|
||||
:primary-button-label="s__('Profiles|Delete account')"
|
||||
@submit="onSubmit"
|
||||
:submit-disabled="!canSubmit()">
|
||||
|
||||
<template slot="body" slot-scope="props">
|
||||
<p v-html="props.text"></p>
|
||||
<template slot="body" slot-scope="props">
|
||||
<p v-html="props.text"></p>
|
||||
|
||||
<form
|
||||
ref="form"
|
||||
:action="actionUrl"
|
||||
method="post">
|
||||
<form
|
||||
ref="form"
|
||||
:action="actionUrl"
|
||||
method="post">
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="_method"
|
||||
value="delete" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="csrfToken" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="_method"
|
||||
value="delete" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="csrfToken" />
|
||||
|
||||
<p id="input-label" v-html="inputLabel"></p>
|
||||
<p id="input-label" v-html="inputLabel"></p>
|
||||
|
||||
<input
|
||||
v-if="confirmWithPassword"
|
||||
name="password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
v-model="enteredPassword"
|
||||
aria-labelledby="input-label" />
|
||||
<input
|
||||
v-else
|
||||
name="username"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-model="enteredUsername"
|
||||
aria-labelledby="input-label" />
|
||||
</form>
|
||||
</template>
|
||||
<input
|
||||
v-if="confirmWithPassword"
|
||||
name="password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
v-model="enteredPassword"
|
||||
aria-labelledby="input-label" />
|
||||
<input
|
||||
v-else
|
||||
name="username"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-model="enteredUsername"
|
||||
aria-labelledby="input-label" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
@click="toggleOpen(true)">
|
||||
{{ s__('Profiles|Delete account') }}
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import Translate from '~/vue_shared/translate';
|
||||
|
||||
import deleteAccountModal from './components/delete_account_modal.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
const deleteAccountButton = document.getElementById('delete-account-button');
|
||||
const deleteAccountModalEl = document.getElementById('delete-account-modal');
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
|
@ -9,6 +14,9 @@ new Vue({
|
|||
components: {
|
||||
deleteAccountModal,
|
||||
},
|
||||
mounted() {
|
||||
deleteAccountButton.classList.remove('disabled');
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('delete-account-modal', {
|
||||
props: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
let hasUserDefinedProjectPath = false;
|
||||
|
||||
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
|
||||
const deriveProjectPathFromUrl = ($projectImportUrl) => {
|
||||
const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path');
|
||||
if (hasUserDefinedProjectPath) {
|
||||
return;
|
||||
}
|
||||
|
@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
|
|||
// extract everything after the last slash
|
||||
const pathMatch = /\/([^/]+)$/.exec(importUrl);
|
||||
if (pathMatch) {
|
||||
$projectPath.val(pathMatch[1]);
|
||||
$currentProjectPath.val(pathMatch[1]);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -96,7 +97,7 @@ const bindEvents = () => {
|
|||
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
|
||||
});
|
||||
|
||||
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
|
||||
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bindEvents);
|
||||
|
|
|
@ -24,7 +24,25 @@ export default function renderMermaid($els) {
|
|||
});
|
||||
|
||||
$els.each((i, el) => {
|
||||
mermaid.init(undefined, el);
|
||||
const source = el.textContent;
|
||||
|
||||
mermaid.init(undefined, el, (id) => {
|
||||
const svg = document.getElementById(id);
|
||||
|
||||
svg.classList.add('mermaid');
|
||||
|
||||
// pre > code > svg
|
||||
svg.closest('pre').replaceWith(svg);
|
||||
|
||||
// We need to add the original source into the DOM to allow Copy-as-GFM
|
||||
// to access it.
|
||||
const sourceEl = document.createElement('text');
|
||||
sourceEl.classList.add('source');
|
||||
sourceEl.setAttribute('display', 'none');
|
||||
sourceEl.textContent = source;
|
||||
|
||||
svg.appendChild(sourceEl);
|
||||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
Flash(`Can't load mermaid module: ${err}`);
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<script>
|
||||
import simplePoll from '../../../lib/utils/simple_poll';
|
||||
import eventHub from '../../event_hub';
|
||||
import statusIcon from '../mr_widget_status_icon';
|
||||
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
|
||||
import Flash from '../../../flash';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetRebase',
|
||||
props: {
|
||||
mr: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
statusIcon,
|
||||
loadingIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMakingRequest: false,
|
||||
rebasingError: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
status() {
|
||||
if (this.mr.rebaseInProgress || this.isMakingRequest) {
|
||||
return 'loading';
|
||||
}
|
||||
if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) {
|
||||
return 'warning';
|
||||
}
|
||||
return 'success';
|
||||
},
|
||||
showDisabledButton() {
|
||||
return ['failed', 'loading'].includes(this.status);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
rebase() {
|
||||
this.isMakingRequest = true;
|
||||
this.rebasingError = null;
|
||||
|
||||
this.service.rebase()
|
||||
.then(() => {
|
||||
simplePoll(this.checkRebaseStatus);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.rebasingError = error.merge_error;
|
||||
this.isMakingRequest = false;
|
||||
Flash('Something went wrong. Please try again.');
|
||||
});
|
||||
},
|
||||
checkRebaseStatus(continuePolling, stopPolling) {
|
||||
this.service.poll()
|
||||
.then(res => res.data)
|
||||
.then((res) => {
|
||||
if (res.rebase_in_progress) {
|
||||
continuePolling();
|
||||
} else {
|
||||
this.isMakingRequest = false;
|
||||
|
||||
if (res.merge_error && res.merge_error.length) {
|
||||
this.rebasingError = res.merge_error;
|
||||
Flash('Something went wrong. Please try again.');
|
||||
}
|
||||
|
||||
eventHub.$emit('MRWidgetUpdateRequested');
|
||||
stopPolling();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.isMakingRequest = false;
|
||||
Flash('Something went wrong. Please try again.');
|
||||
stopPolling();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="mr-widget-body media">
|
||||
<status-icon
|
||||
:status="status"
|
||||
:show-disabled-button="showDisabledButton"
|
||||
/>
|
||||
|
||||
<div class="rebase-state-find-class-convention media media-body space-children">
|
||||
<template v-if="mr.rebaseInProgress || isMakingRequest">
|
||||
<span class="bold">
|
||||
Rebase in progress
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch">
|
||||
<span class="bold">
|
||||
Fast-forward merge is not possible.
|
||||
Rebase the source branch onto
|
||||
<span class="label-branch">{{mr.targetBranch}}</span>
|
||||
to allow this merge request to be merged.
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest">
|
||||
<div class="accept-merge-holder clearfix js-toggle-container accept-action media space-children">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-reopen btn-success"
|
||||
:disabled="isMakingRequest"
|
||||
@click="rebase">
|
||||
<loading-icon v-if="isMakingRequest" />
|
||||
Rebase
|
||||
</button>
|
||||
<span
|
||||
v-if="!rebasingError"
|
||||
class="bold">
|
||||
Fast-forward merge is not possible.
|
||||
Rebase the source branch onto the target branch or merge target
|
||||
branch into source branch to allow this merge request to be merged.
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="bold danger">
|
||||
{{rebasingError}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -32,6 +32,7 @@ export { default as UnresolvedDiscussionsState } from './components/states/mr_wi
|
|||
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 RebaseState } from './components/states/mr_widget_rebase.vue';
|
||||
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed';
|
||||
export { default as CheckingState } from './components/states/mr_widget_checking';
|
||||
export { default as MRWidgetStore } from './stores/mr_widget_store';
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
MergedState,
|
||||
ClosedState,
|
||||
MergingState,
|
||||
RebaseState,
|
||||
WipState,
|
||||
ArchivedState,
|
||||
ConflictsState,
|
||||
|
@ -79,6 +80,7 @@ export default {
|
|||
ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
|
||||
statusPath: store.statusPath,
|
||||
mergeActionsContentPath: store.mergeActionsContentPath,
|
||||
rebasePath: store.rebasePath,
|
||||
};
|
||||
return new MRWidgetService(endpoints);
|
||||
},
|
||||
|
@ -232,6 +234,7 @@ export default {
|
|||
'mr-widget-pipeline-failed': PipelineFailedState,
|
||||
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
|
||||
'mr-widget-auto-merge-failed': AutoMergeFailed,
|
||||
'mr-widget-rebase': RebaseState,
|
||||
},
|
||||
template: `
|
||||
<div class="mr-state-widget prepend-top-default">
|
||||
|
|
|
@ -37,6 +37,10 @@ export default class MRWidgetService {
|
|||
return axios.get(this.endpoints.mergeActionsContentPath);
|
||||
}
|
||||
|
||||
rebase() {
|
||||
return axios.post(this.endpoints.rebasePath);
|
||||
}
|
||||
|
||||
static stopEnvironment(url) {
|
||||
return axios.post(url);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ export default function deviseState(data) {
|
|||
return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
|
||||
} else if (!this.canMerge) {
|
||||
return stateKey.notAllowedToMerge;
|
||||
} else if (this.shouldBeRebased) {
|
||||
return stateKey.rebase;
|
||||
} else if (this.canBeMerged) {
|
||||
return stateKey.readyToMerge;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export default class MergeRequestStore {
|
|||
this.divergedCommitsCount = data.diverged_commits_count;
|
||||
this.pipeline = data.pipeline || {};
|
||||
this.deployments = this.deployments || data.deployments || [];
|
||||
this.initRebase(data);
|
||||
|
||||
if (data.issues_links) {
|
||||
const links = data.issues_links;
|
||||
|
@ -124,6 +125,13 @@ export default class MergeRequestStore {
|
|||
return this.state === stateKey.nothingToMerge;
|
||||
}
|
||||
|
||||
initRebase(data) {
|
||||
this.canPushToSourceBranch = data.can_push_to_source_branch;
|
||||
this.rebaseInProgress = data.rebase_in_progress;
|
||||
this.approvalsLeft = !data.approved;
|
||||
this.rebasePath = data.rebase_path;
|
||||
}
|
||||
|
||||
static buildMetrics(metrics) {
|
||||
if (!metrics) {
|
||||
return {};
|
||||
|
|
|
@ -17,6 +17,7 @@ const stateToComponentMap = {
|
|||
failedToMerge: 'mr-widget-failed-to-merge',
|
||||
autoMergeFailed: 'mr-widget-auto-merge-failed',
|
||||
shaMismatch: 'mr-widget-sha-mismatch',
|
||||
rebase: 'mr-widget-rebase',
|
||||
};
|
||||
|
||||
const statesToShowHelpWidget = [
|
||||
|
@ -29,6 +30,7 @@ const statesToShowHelpWidget = [
|
|||
'pipelineFailed',
|
||||
'pipelineBlocked',
|
||||
'autoMergeFailed',
|
||||
'rebase',
|
||||
];
|
||||
|
||||
export const stateKey = {
|
||||
|
@ -46,6 +48,7 @@ export const stateKey = {
|
|||
mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
|
||||
notAllowedToMerge: 'notAllowedToMerge',
|
||||
readyToMerge: 'readyToMerge',
|
||||
rebase: 'rebase',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
/**
|
||||
* Port of detail_behavior expand button.
|
||||
*
|
||||
* @example
|
||||
* <expand-button>
|
||||
* <template slot="expanded">
|
||||
* Text goes here.
|
||||
* </template>
|
||||
* </expand-button>
|
||||
*/
|
||||
export default {
|
||||
name: 'expandButton',
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return __('Click to expand text');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
v-show="isCollapsed"
|
||||
class="text-expander btn-blank"
|
||||
:aria-label="ariaLabel"
|
||||
@click="onClick">
|
||||
...
|
||||
</button>
|
||||
<span v-show="!isCollapsed">
|
||||
<slot name="expanded"></slot>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
|
@ -3,6 +3,10 @@ export default {
|
|||
name: 'modal',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -62,11 +66,11 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('toggle', false);
|
||||
emitCancel(event) {
|
||||
this.$emit('cancel', event);
|
||||
},
|
||||
emitSubmit(status) {
|
||||
this.$emit('submit', status);
|
||||
emitSubmit(event) {
|
||||
this.$emit('submit', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -75,7 +79,9 @@ export default {
|
|||
<template>
|
||||
<div class="modal-open">
|
||||
<div
|
||||
class="modal show"
|
||||
:id="id"
|
||||
class="modal"
|
||||
:class="id ? '' : 'show'"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
@ -93,7 +99,8 @@ export default {
|
|||
<button
|
||||
type="button"
|
||||
class="close pull-right"
|
||||
@click="close"
|
||||
@click="emitCancel($event)"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
|
@ -110,7 +117,8 @@ export default {
|
|||
type="button"
|
||||
class="btn pull-left"
|
||||
:class="btnCancelKindClass"
|
||||
@click="close">
|
||||
@click="emitCancel($event)"
|
||||
data-dismiss="modal">
|
||||
{{ closeButtonLabel }}
|
||||
</button>
|
||||
<button
|
||||
|
@ -119,13 +127,17 @@ export default {
|
|||
class="btn pull-right js-primary-button"
|
||||
:disabled="submitDisabled"
|
||||
:class="btnKindClass"
|
||||
@click="emitSubmit(true)">
|
||||
@click="emitSubmit($event)"
|
||||
data-dismiss="modal">
|
||||
{{ primaryButtonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop fade in" />
|
||||
<div
|
||||
v-if="!id"
|
||||
class="modal-backdrop fade in">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
class="recaptcha-modal js-recaptcha-modal"
|
||||
:hide-footer="true"
|
||||
:title="__('Please solve the reCAPTCHA')"
|
||||
@toggle="close"
|
||||
@cancel="close"
|
||||
>
|
||||
<div slot="body">
|
||||
<p>
|
||||
|
|
|
@ -10,6 +10,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
|
||||
before_action :set_issuables_index, only: [:index]
|
||||
before_action :authenticate_user!, only: [:assign_related_issues]
|
||||
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
|
||||
|
||||
def index
|
||||
@merge_requests = @issuables
|
||||
|
@ -223,6 +224,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
render json: environments
|
||||
end
|
||||
|
||||
def rebase
|
||||
RebaseWorker.perform_async(@merge_request.id, current_user.id)
|
||||
|
||||
render nothing: true, status: 200
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
alias_method :subscribable_resource, :merge_request
|
||||
|
@ -322,4 +329,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
@finder_type = MergeRequestsFinder
|
||||
super
|
||||
end
|
||||
|
||||
def check_user_can_push_to_source_branch!
|
||||
return access_denied! unless @merge_request.source_branch_exists?
|
||||
|
||||
access_check = ::Gitlab::UserAccess
|
||||
.new(current_user, project: @merge_request.source_project)
|
||||
.can_push_to_branch?(@merge_request.source_branch)
|
||||
|
||||
access_denied! unless access_check
|
||||
end
|
||||
end
|
||||
|
|
|
@ -353,7 +353,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def repo_exists?
|
||||
project.repository_exists? && !project.empty_repo? && project.repo
|
||||
project.repository_exists? && !project.empty_repo?
|
||||
|
||||
rescue Gitlab::Git::Repository::NoRepository
|
||||
project.repository.expire_exists_cache
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class LabelsFinder < UnionFinder
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(current_user, params = {})
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
|
@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder
|
|||
label_ids << project.labels
|
||||
end
|
||||
end
|
||||
elsif only_group_labels?
|
||||
label_ids << Label.where(group_id: group.id)
|
||||
else
|
||||
label_ids << Label.where(group_id: projects.group_ids)
|
||||
label_ids << Label.where(project_id: projects.select(:id))
|
||||
|
@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder
|
|||
items.where(title: title)
|
||||
end
|
||||
|
||||
def group
|
||||
strong_memoize(:group) do
|
||||
group = Group.find(params[:group_id])
|
||||
authorized_to_read_labels?(group) && group
|
||||
end
|
||||
end
|
||||
|
||||
def group?
|
||||
params[:group_id].present?
|
||||
end
|
||||
|
@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder
|
|||
params[:project_ids].present?
|
||||
end
|
||||
|
||||
def only_group_labels?
|
||||
params[:only_group_labels]
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title] || params[:name]
|
||||
end
|
||||
|
@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder
|
|||
@projects
|
||||
end
|
||||
|
||||
def authorized_to_read_labels?(project)
|
||||
def authorized_to_read_labels?(label_parent)
|
||||
return true if skip_authorization
|
||||
|
||||
Ability.allowed?(current_user, :read_label, project)
|
||||
Ability.allowed?(current_user, :read_label, label_parent)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,4 +23,12 @@ module BranchesHelper
|
|||
def protected_branch?(project, branch)
|
||||
ProtectedBranch.protected?(project, branch.name)
|
||||
end
|
||||
|
||||
def diverging_count_label(count)
|
||||
if count >= Repository::MAX_DIVERGING_COUNT
|
||||
"#{Repository::MAX_DIVERGING_COUNT - 1}+"
|
||||
else
|
||||
count.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
48
app/models/concerns/deployment_platform.rb
Normal file
48
app/models/concerns/deployment_platform.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
module DeploymentPlatform
|
||||
def deployment_platform
|
||||
@deployment_platform ||=
|
||||
find_cluster_platform_kubernetes ||
|
||||
find_kubernetes_service_integration ||
|
||||
build_cluster_and_deployment_platform
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_cluster_platform_kubernetes
|
||||
clusters.find_by(enabled: true)&.platform_kubernetes
|
||||
end
|
||||
|
||||
def find_kubernetes_service_integration
|
||||
services.deployment.reorder(nil).find_by(active: true)
|
||||
end
|
||||
|
||||
def build_cluster_and_deployment_platform
|
||||
return unless kubernetes_service_template
|
||||
|
||||
cluster = ::Clusters::Cluster.create(cluster_attributes_from_service_template)
|
||||
cluster.platform_kubernetes if cluster.persisted?
|
||||
end
|
||||
|
||||
def kubernetes_service_template
|
||||
@kubernetes_service_template ||= KubernetesService.active.find_by_template
|
||||
end
|
||||
|
||||
def cluster_attributes_from_service_template
|
||||
{
|
||||
name: 'kubernetes-template',
|
||||
projects: [self],
|
||||
provider_type: :user,
|
||||
platform_type: :kubernetes,
|
||||
platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
|
||||
}
|
||||
end
|
||||
|
||||
def platform_kubernetes_attributes_from_service_template
|
||||
{
|
||||
api_url: kubernetes_service_template.api_url,
|
||||
ca_pem: kubernetes_service_template.ca_pem,
|
||||
token: kubernetes_service_template.token,
|
||||
namespace: kubernetes_service_template.namespace
|
||||
}
|
||||
end
|
||||
end
|
|
@ -10,12 +10,12 @@ module RelativePositioning
|
|||
after_save :save_positionable_neighbours
|
||||
end
|
||||
|
||||
def project_ids
|
||||
[project.id]
|
||||
def min_relative_position
|
||||
self.class.in_parents(parent_ids).minimum(:relative_position)
|
||||
end
|
||||
|
||||
def max_relative_position
|
||||
self.class.in_projects(project_ids).maximum(:relative_position)
|
||||
self.class.in_parents(parent_ids).maximum(:relative_position)
|
||||
end
|
||||
|
||||
def prev_relative_position
|
||||
|
@ -23,7 +23,7 @@ module RelativePositioning
|
|||
|
||||
if self.relative_position
|
||||
prev_pos = self.class
|
||||
.in_projects(project_ids)
|
||||
.in_parents(parent_ids)
|
||||
.where('relative_position < ?', self.relative_position)
|
||||
.maximum(:relative_position)
|
||||
end
|
||||
|
@ -36,7 +36,7 @@ module RelativePositioning
|
|||
|
||||
if self.relative_position
|
||||
next_pos = self.class
|
||||
.in_projects(project_ids)
|
||||
.in_parents(parent_ids)
|
||||
.where('relative_position > ?', self.relative_position)
|
||||
.minimum(:relative_position)
|
||||
end
|
||||
|
@ -63,7 +63,7 @@ module RelativePositioning
|
|||
pos_after = before.next_relative_position
|
||||
|
||||
if before.shift_after?
|
||||
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after)
|
||||
issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_after)
|
||||
issue_to_move.move_after
|
||||
@positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
|
@ -78,7 +78,7 @@ module RelativePositioning
|
|||
pos_before = after.prev_relative_position
|
||||
|
||||
if after.shift_before?
|
||||
issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before)
|
||||
issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_before)
|
||||
issue_to_move.move_before
|
||||
@positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
|
@ -92,6 +92,10 @@ module RelativePositioning
|
|||
self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
|
||||
end
|
||||
|
||||
def move_to_start
|
||||
self.relative_position = position_between(min_relative_position || START_POSITION, MIN_POSITION)
|
||||
end
|
||||
|
||||
# Indicates if there is an issue that should be shifted to free the place
|
||||
def shift_after?
|
||||
next_pos = next_relative_position
|
||||
|
|
|
@ -35,6 +35,8 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
validates :project, presence: true
|
||||
|
||||
alias_attribute :parent_ids, :project_id
|
||||
|
||||
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
|
||||
|
||||
scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
|
||||
|
@ -78,6 +80,10 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
acts_as_paranoid
|
||||
|
||||
class << self
|
||||
alias_method :in_parents, :in_projects
|
||||
end
|
||||
|
||||
def self.reference_prefix
|
||||
'#'
|
||||
end
|
||||
|
|
|
@ -156,6 +156,13 @@ class MergeRequest < ActiveRecord::Base
|
|||
'!'
|
||||
end
|
||||
|
||||
def rebase_in_progress?
|
||||
# The source project can be deleted
|
||||
return false unless source_project
|
||||
|
||||
source_project.repository.rebase_in_progress?(id)
|
||||
end
|
||||
|
||||
# Use this method whenever you need to make sure the head_pipeline is synced with the
|
||||
# branch head commit, for example checking if a merge request can be merged.
|
||||
# For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
|
||||
|
@ -607,7 +614,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
check_if_can_be_merged
|
||||
|
||||
can_be_merged?
|
||||
can_be_merged? && !should_be_rebased?
|
||||
end
|
||||
|
||||
def mergeable_state?(skip_ci_check: false)
|
||||
|
|
|
@ -19,6 +19,7 @@ class Project < ActiveRecord::Base
|
|||
include Routable
|
||||
include GroupDescendant
|
||||
include Gitlab::SQL::Pattern
|
||||
include DeploymentPlatform
|
||||
|
||||
extend Gitlab::ConfigHelper
|
||||
extend Gitlab::CurrentSettings
|
||||
|
@ -904,12 +905,6 @@ class Project < ActiveRecord::Base
|
|||
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
|
||||
end
|
||||
|
||||
# TODO: This will be extended for multiple enviroment clusters
|
||||
def deployment_platform
|
||||
@deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
|
||||
@deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
|
||||
end
|
||||
|
||||
def monitoring_services
|
||||
services.where(category: :monitoring)
|
||||
end
|
||||
|
@ -992,10 +987,6 @@ class Project < ActiveRecord::Base
|
|||
false
|
||||
end
|
||||
|
||||
def repo
|
||||
repository.rugged
|
||||
end
|
||||
|
||||
def url_to_repo
|
||||
gitlab_shell.url_to_repo(full_path)
|
||||
end
|
||||
|
@ -1438,7 +1429,7 @@ class Project < ActiveRecord::Base
|
|||
# We'd need to keep track of project full path otherwise directory tree
|
||||
# created with hashed storage enabled cannot be usefully imported using
|
||||
# the import rake task.
|
||||
repo.config['gitlab.fullpath'] = gl_full_path
|
||||
repository.rugged.config['gitlab.fullpath'] = gl_full_path
|
||||
rescue Gitlab::Git::Repository::NoRepository => e
|
||||
Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
|
||||
nil
|
||||
|
|
|
@ -4,6 +4,7 @@ class Repository
|
|||
REF_MERGE_REQUEST = 'merge-requests'.freeze
|
||||
REF_KEEP_AROUND = 'keep-around'.freeze
|
||||
REF_ENVIRONMENTS = 'environments'.freeze
|
||||
MAX_DIVERGING_COUNT = 1000
|
||||
|
||||
RESERVED_REFS_NAMES = %W[
|
||||
heads
|
||||
|
@ -278,11 +279,12 @@ class Repository
|
|||
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
|
||||
# Rugged seems to throw a `ReferenceError` when given branch_names rather
|
||||
# than SHA-1 hashes
|
||||
number_commits_behind = raw_repository
|
||||
.count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
|
||||
|
||||
number_commits_ahead = raw_repository
|
||||
.count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
|
||||
number_commits_behind, number_commits_ahead =
|
||||
raw_repository.count_commits_between(
|
||||
root_ref_hash,
|
||||
branch.dereferenced_target.sha,
|
||||
left_right: true,
|
||||
max_count: MAX_DIVERGING_COUNT)
|
||||
|
||||
{ behind: number_commits_behind, ahead: number_commits_ahead }
|
||||
end
|
||||
|
@ -1099,6 +1101,13 @@ class Repository
|
|||
@project.repository_storage_path
|
||||
end
|
||||
|
||||
def rebase(user, merge_request)
|
||||
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
|
||||
branch_sha: merge_request.source_branch_sha,
|
||||
remote_repository: merge_request.target_project.repository.raw,
|
||||
remote_branch: merge_request.target_branch)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO Generice finder, later split this on finders by Ref or Oid
|
||||
|
|
|
@ -44,6 +44,7 @@ class Service < ActiveRecord::Base
|
|||
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
|
||||
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
|
||||
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
|
||||
scope :deployment, -> { where(category: 'deployment') }
|
||||
|
||||
default_value_for :category, 'common'
|
||||
|
||||
|
@ -271,6 +272,10 @@ class Service < ActiveRecord::Base
|
|||
nil
|
||||
end
|
||||
|
||||
def self.find_by_template
|
||||
find_by(template: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cache_project_has_external_issue_tracker
|
||||
|
|
|
@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy
|
|||
with_options scope: :subject, score: 0
|
||||
condition(:request_access_enabled) { @subject.request_access_enabled }
|
||||
|
||||
rule { public_group } .enable :read_group
|
||||
rule { public_group }.policy do
|
||||
enable :read_group
|
||||
enable :read_list
|
||||
enable :read_label
|
||||
end
|
||||
|
||||
rule { logged_in_viewable }.enable :read_group
|
||||
|
||||
rule { guest }.policy do
|
||||
enable :read_group
|
||||
enable :upload_file
|
||||
enable :read_label
|
||||
end
|
||||
|
||||
rule { admin } .enable :read_group
|
||||
|
|
|
@ -76,6 +76,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
end
|
||||
|
||||
def rebase_path
|
||||
if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch?
|
||||
rebase_project_merge_request_path(project, merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
def target_branch_tree_path
|
||||
if target_branch_exists?
|
||||
project_tree_path(project, target_branch)
|
||||
|
@ -152,6 +158,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
|
|||
user_can_collaborate_with_project? && can_be_cherry_picked?
|
||||
end
|
||||
|
||||
def can_push_to_source_branch?
|
||||
source_branch_exists? && user_can_push_to_source_branch?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conflicts
|
||||
|
@ -174,6 +184,14 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
|
|||
end.sort.to_sentence
|
||||
end
|
||||
|
||||
def user_can_push_to_source_branch?
|
||||
return false unless source_branch_exists?
|
||||
|
||||
::Gitlab::UserAccess
|
||||
.new(current_user, project: source_project)
|
||||
.can_push_to_branch?(source_branch)
|
||||
end
|
||||
|
||||
def user_can_collaborate_with_project?
|
||||
can?(current_user, :push_code, project) ||
|
||||
(current_user && current_user.already_forked?(project))
|
||||
|
|
|
@ -4,4 +4,5 @@ class MergeRequestBasicEntity < IssuableSidebarEntity
|
|||
expose :merge_error
|
||||
expose :state
|
||||
expose :source_branch_exists?, as: :source_branch_exists
|
||||
expose :rebase_in_progress?, as: :rebase_in_progress
|
||||
end
|
||||
|
|
|
@ -23,6 +23,16 @@ class MergeRequestWidgetEntity < IssuableEntity
|
|||
MergeRequestMetricsEntity.new(metrics).as_json
|
||||
end
|
||||
|
||||
expose :rebase_commit_sha
|
||||
expose :rebase_in_progress?, as: :rebase_in_progress
|
||||
|
||||
expose :can_push_to_source_branch do |merge_request|
|
||||
presenter(merge_request).can_push_to_source_branch?
|
||||
end
|
||||
expose :rebase_path do |merge_request|
|
||||
presenter(merge_request).rebase_path
|
||||
end
|
||||
|
||||
# User entities
|
||||
expose :merge_user, using: UserEntity
|
||||
|
||||
|
|
30
app/services/merge_requests/rebase_service.rb
Normal file
30
app/services/merge_requests/rebase_service.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
module MergeRequests
|
||||
class RebaseService < MergeRequests::WorkingCopyBaseService
|
||||
def execute(merge_request)
|
||||
@merge_request = merge_request
|
||||
|
||||
if rebase
|
||||
success
|
||||
else
|
||||
error('Failed to rebase. Should be done manually')
|
||||
end
|
||||
end
|
||||
|
||||
def rebase
|
||||
if merge_request.rebase_in_progress?
|
||||
log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
|
||||
return false
|
||||
end
|
||||
|
||||
rebase_sha = repository.rebase(current_user, merge_request)
|
||||
|
||||
merge_request.update_attributes(rebase_commit_sha: rebase_sha)
|
||||
|
||||
true
|
||||
rescue => e
|
||||
log_error('Failed to rebase branch:')
|
||||
log_error(e.message, save_message_on_model: true)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
24
app/services/merge_requests/working_copy_base_service.rb
Normal file
24
app/services/merge_requests/working_copy_base_service.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
module MergeRequests
|
||||
class WorkingCopyBaseService < MergeRequests::BaseService
|
||||
attr_reader :merge_request
|
||||
|
||||
def source_project
|
||||
@source_project ||= merge_request.source_project
|
||||
end
|
||||
|
||||
def target_project
|
||||
@target_project ||= merge_request.target_project
|
||||
end
|
||||
|
||||
def log_error(message, save_message_on_model: false)
|
||||
Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}")
|
||||
|
||||
merge_request.update(merge_error: message) if save_message_on_model
|
||||
end
|
||||
|
||||
# Don't try to print expensive instance variables.
|
||||
def inspect
|
||||
"#<#{self.class} #{merge_request.to_reference(full: true)}>"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -84,11 +84,13 @@
|
|||
= s_('Profiles|Deleting an account has the following effects:')
|
||||
= render 'users/deletion_guidance', user: current_user
|
||||
|
||||
%button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
|
||||
target: '#delete-account-modal' } }
|
||||
= s_('Profiles|Delete account')
|
||||
|
||||
#delete-account-modal{ data: { action_url: user_registration_path,
|
||||
confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
|
||||
username: current_user.username } }
|
||||
%button.btn.btn-danger.disabled
|
||||
= s_('Profiles|Delete account')
|
||||
- else
|
||||
- if @user.solo_owned_groups.present?
|
||||
%p
|
||||
|
|
|
@ -10,4 +10,4 @@
|
|||
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
|
||||
%br
|
||||
%span.descr
|
||||
When fast-forward merge is not possible, the user must first rebase locally.
|
||||
When fast-forward merge is not possible, the user is given the option to rebase.
|
||||
|
|
|
@ -10,4 +10,4 @@
|
|||
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
|
||||
%br
|
||||
%span.descr
|
||||
When fast-forward merge is not possible, the user must first rebase locally.
|
||||
When fast-forward merge is not possible, the user is given the option to rebase.
|
||||
|
|
|
@ -66,16 +66,16 @@
|
|||
= icon("trash-o")
|
||||
|
||||
- if branch.name != @repository.root_ref
|
||||
.divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind,
|
||||
.divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
|
||||
default_branch: @repository.root_ref,
|
||||
number_commits_ahead: number_commits_ahead } }
|
||||
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
|
||||
.graph-side
|
||||
.bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
|
||||
%span.count.count-behind= number_commits_behind
|
||||
%span.count.count-behind= diverging_count_label(number_commits_behind)
|
||||
.graph-separator
|
||||
.graph-side
|
||||
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
|
||||
%span.count.count-ahead= number_commits_ahead
|
||||
%span.count.count-ahead= diverging_count_label(number_commits_ahead)
|
||||
|
||||
|
||||
- if commit
|
||||
|
|
|
@ -11,5 +11,5 @@
|
|||
%label.text-danger
|
||||
= s_('ClusterIntegration|Remove cluster integration')
|
||||
%p
|
||||
= s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.')
|
||||
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"})
|
||||
= s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.")
|
||||
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%h4= s_('ClusterIntegration|Enable cluster integration')
|
||||
.settings-content
|
||||
%h4= s_('ClusterIntegration|Cluster integration')
|
||||
|
||||
.settings-content
|
||||
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
|
||||
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
|
||||
%p.js-error-reason
|
||||
|
@ -11,11 +11,4 @@
|
|||
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
|
||||
= s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
|
||||
|
||||
%p
|
||||
- if @cluster.enabled?
|
||||
- if can?(current_user, :update_cluster, @cluster)
|
||||
= s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
|
||||
- else
|
||||
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
|
||||
- else
|
||||
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
|
||||
%p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.table-mobile-content
|
||||
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
|
||||
.table-section.section-30
|
||||
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
|
||||
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
|
||||
.table-mobile-content= cluster.environment_scope
|
||||
.table-section.section-30
|
||||
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
|
||||
= form_errors(@cluster)
|
||||
.form-group.append-bottom-20
|
||||
%label.append-bottom-10
|
||||
= field.hidden_field :enabled, { class: 'js-toggle-input'}
|
||||
|
||||
%button{ type: 'button',
|
||||
class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
|
||||
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
|
||||
disabled: !can?(current_user, :update_cluster, @cluster) }
|
||||
%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')
|
||||
|
||||
- if can?(current_user, :update_cluster, @cluster)
|
||||
.form-group
|
||||
= field.submit _('Save'), class: 'btn btn-success'
|
33
app/views/projects/clusters/_integration_form.html.haml
Normal file
33
app/views/projects/clusters/_integration_form.html.haml
Normal file
|
@ -0,0 +1,33 @@
|
|||
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
|
||||
= form_errors(@cluster)
|
||||
.form-group.append-bottom-20
|
||||
%h5= s_('ClusterIntegration|Integration status')
|
||||
%p
|
||||
- if @cluster.enabled?
|
||||
- if can?(current_user, :update_cluster, @cluster)
|
||||
= s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
|
||||
- else
|
||||
= 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'}
|
||||
|
||||
%button{ type: 'button',
|
||||
class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
|
||||
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
|
||||
disabled: !can?(current_user, :update_cluster, @cluster) }
|
||||
%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')
|
||||
|
||||
.form-group
|
||||
%h5= s_('ClusterIntegration|Environment scope')
|
||||
%p
|
||||
= s_("ClusterIntegration|Choose which of your project's environments will use this cluster.")
|
||||
= link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
|
||||
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
|
||||
|
||||
- if can?(current_user, :update_cluster, @cluster)
|
||||
.form-group
|
||||
= field.submit _('Save changes'), class: 'btn btn-success'
|
|
@ -9,10 +9,6 @@
|
|||
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
|
||||
= form_errors(@cluster)
|
||||
|
||||
.form-group
|
||||
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
|
||||
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
|
||||
|
||||
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
|
||||
.form-group
|
||||
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
.table-section.section-30{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Cluster")
|
||||
.table-section.section-30{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Environment pattern")
|
||||
= s_("ClusterIntegration|Environment scope")
|
||||
.table-section.section-30{ role: "rowheader" }
|
||||
= s_("ClusterIntegration|Project namespace")
|
||||
.table-section.section-10{ role: "rowheader" }
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
.js-cluster-application-notice
|
||||
.flash-container
|
||||
|
||||
%section.settings.no-animate.expanded
|
||||
%section.settings.no-animate.expanded#cluster-integration
|
||||
= render 'banner'
|
||||
= render 'enabled'
|
||||
= render 'integration_form'
|
||||
|
||||
.cluster-applications-table#js-cluster-applications
|
||||
|
||||
|
@ -41,6 +41,6 @@
|
|||
%h4= _('Advanced settings')
|
||||
%button.btn.js-settings-toggle
|
||||
= expanded ? 'Collapse' : 'Expand'
|
||||
%p= s_('ClusterIntegration|Manage cluster integration on your GitLab project')
|
||||
%p= s_("ClusterIntegration|Advanced options on this cluster's integration")
|
||||
.settings-content
|
||||
= render 'advanced_settings'
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
= field.label :name, s_('ClusterIntegration|Cluster name')
|
||||
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
|
||||
|
||||
.form-group
|
||||
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
|
||||
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
|
||||
|
||||
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
|
||||
.form-group
|
||||
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
- project_service
|
||||
- propagate_service_template
|
||||
- reactive_caching
|
||||
- rebase
|
||||
- repository_fork
|
||||
- repository_import
|
||||
- storage_migrator
|
||||
|
|
12
app/workers/rebase_worker.rb
Normal file
12
app/workers/rebase_worker.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class RebaseWorker
|
||||
include ApplicationWorker
|
||||
|
||||
def perform(merge_request_id, current_user_id)
|
||||
current_user = User.find(current_user_id)
|
||||
merge_request = MergeRequest.find(merge_request_id)
|
||||
|
||||
MergeRequests::RebaseService
|
||||
.new(merge_request.source_project, current_user)
|
||||
.execute(merge_request)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix gitlab-rake gitlab:import:repos import schedule
|
||||
merge_request: 15931
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/40301-rebase.yml
Normal file
5
changelogs/unreleased/40301-rebase.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow user to rebase merge requests.
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Improve the performance for counting diverging commits. Show 999+
|
||||
if it is more than 1000 commits
|
||||
merge_request: 15963
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow automatic creation of Kubernetes Integration from template
|
||||
merge_request: 16104
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Force Auto DevOps kubectl version to 1.8.6
|
||||
merge_request: 16218
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/api-domains-expose-project_id.yml
Normal file
5
changelogs/unreleased/api-domains-expose-project_id.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose project_id on /api/v4/pages/domains
|
||||
merge_request: 16200
|
||||
author: Luc Didry
|
||||
type: changed
|
5
changelogs/unreleased/feature-api_runners_online.yml
Normal file
5
changelogs/unreleased/feature-api_runners_online.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add online and status attribute to runner api entity
|
||||
merge_request: 11750
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'API: get participants from merge_requests & issues'
|
||||
merge_request: 16187
|
||||
author: Brent Greeff
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix import project url not updating project name
|
||||
merge_request: 16120
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/ldap_username_attributes.yml
Normal file
5
changelogs/unreleased/ldap_username_attributes.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Modify `LDAP::Person` to return username value based on attributes
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/update-redis-rack.yml
Normal file
5
changelogs/unreleased/update-redis-rack.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update redis-rack to 2.0.4
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/winh-modal-target-id.yml
Normal file
5
changelogs/unreleased/winh-modal-target-id.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add id to modal.vue to support data-toggle="modal"
|
||||
merge_request: 16189
|
||||
author:
|
||||
type: other
|
|
@ -96,6 +96,7 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
post :toggle_subscription
|
||||
post :remove_wip
|
||||
post :assign_related_issues
|
||||
post :rebase
|
||||
|
||||
scope constraints: { format: nil }, action: :show do
|
||||
get :commits, defaults: { tab: 'commits' }
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :merge_requests, :rebase_commit_sha, :string
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
# rubocop:disable Migration/Datetime
|
||||
class ScheduleIssuesClosedAtTypeChange < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20171229225929) do
|
||||
ActiveRecord::Schema.define(version: 20171230123729) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -1099,6 +1099,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do
|
|||
t.string "merge_jid"
|
||||
t.boolean "discussion_locked"
|
||||
t.integer "latest_merge_request_diff_id"
|
||||
t.string "rebase_commit_sha"
|
||||
end
|
||||
|
||||
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
|
||||
|
|
|
@ -28,19 +28,25 @@ exactly which repositories are causing the trouble.
|
|||
|
||||
### Check all GitLab repositories
|
||||
|
||||
>**Note:**
|
||||
>
|
||||
> - `gitlab:repo:check` has been deprecated in favor of `gitlab:git:fsck`
|
||||
> - [Deprecated][ce-15931] in GitLab 10.4.
|
||||
> - `gitlab:repo:check` will be removed in the future. [Removal issue][ce-41699]
|
||||
|
||||
This task loops through all repositories on the GitLab server and runs the
|
||||
3 integrity checks described previously.
|
||||
|
||||
**Omnibus Installation**
|
||||
|
||||
```
|
||||
sudo gitlab-rake gitlab:repo:check
|
||||
sudo gitlab-rake gitlab:git:fsck
|
||||
```
|
||||
|
||||
**Source Installation**
|
||||
|
||||
```bash
|
||||
sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production
|
||||
sudo -u git -H bundle exec rake gitlab:git:fsck RAILS_ENV=production
|
||||
```
|
||||
|
||||
### Check repositories for a specific user
|
||||
|
@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials
|
|||
(if configured) and will list a sample of LDAP users. This task is also
|
||||
executed as part of the `gitlab:check` task, but can run independently.
|
||||
See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details.
|
||||
|
||||
[ce-15931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15931
|
||||
[ce-41699]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41699
|
||||
|
|
|
@ -15,10 +15,10 @@ GET /projects/:id/boards
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -27,6 +27,19 @@ Example response:
|
|||
[
|
||||
{
|
||||
"id" : 1,
|
||||
"project": {
|
||||
"id": 5,
|
||||
"name": "Diaspora Project Site",
|
||||
"name_with_namespace": "Diaspora / Diaspora Project Site",
|
||||
"path": "diaspora-project-site",
|
||||
"path_with_namespace": "diaspora/diaspora-project-site",
|
||||
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
|
||||
"web_url": "http://example.com/diaspora/diaspora-project-site"
|
||||
},
|
||||
"milestone": {
|
||||
"id": 12
|
||||
"title": "10.0"
|
||||
},
|
||||
"lists" : [
|
||||
{
|
||||
"id" : 1,
|
||||
|
@ -60,6 +73,74 @@ Example response:
|
|||
]
|
||||
```
|
||||
|
||||
## Single board
|
||||
|
||||
Get a single board.
|
||||
|
||||
```
|
||||
GET /projects/:id/boards/:board_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name:": "project issue board",
|
||||
"project": {
|
||||
"id": 5,
|
||||
"name": "Diaspora Project Site",
|
||||
"name_with_namespace": "Diaspora / Diaspora Project Site",
|
||||
"path": "diaspora-project-site",
|
||||
"path_with_namespace": "diaspora/diaspora-project-site",
|
||||
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
|
||||
"web_url": "http://example.com/diaspora/diaspora-project-site"
|
||||
},
|
||||
"milestone": {
|
||||
"id": 12
|
||||
"title": "10.0"
|
||||
},
|
||||
"lists" : [
|
||||
{
|
||||
"id" : 1,
|
||||
"label" : {
|
||||
"name" : "Testing",
|
||||
"color" : "#F0AD4E",
|
||||
"description" : null
|
||||
},
|
||||
"position" : 1
|
||||
},
|
||||
{
|
||||
"id" : 2,
|
||||
"label" : {
|
||||
"name" : "Ready",
|
||||
"color" : "#FF0000",
|
||||
"description" : null
|
||||
},
|
||||
"position" : 2
|
||||
},
|
||||
{
|
||||
"id" : 3,
|
||||
"label" : {
|
||||
"name" : "Production",
|
||||
"color" : "#FF5F00",
|
||||
"description" : null
|
||||
},
|
||||
"position" : 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## List board lists
|
||||
|
||||
Get a list of the board's lists.
|
||||
|
@ -71,8 +152,8 @@ GET /projects/:id/boards/:board_id/lists
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists
|
||||
|
@ -122,9 +203,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `list_id`| integer | yes | The ID of a board's list |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `list_id`| integer | yes | The ID of a board's list |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
|
||||
|
@ -154,9 +235,9 @@ POST /projects/:id/boards/:board_id/lists
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `label_id` | integer | yes | The ID of a label |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `label_id` | integer | yes | The ID of a label |
|
||||
|
||||
```bash
|
||||
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
|
||||
|
@ -186,10 +267,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `list_id` | integer | yes | The ID of a board's list |
|
||||
| `position` | integer | yes | The position of the list |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `list_id` | integer | yes | The ID of a board's list |
|
||||
| `position` | integer | yes | The position of the list |
|
||||
|
||||
```bash
|
||||
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2
|
||||
|
@ -219,9 +300,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `list_id` | integer | yes | The ID of a board's list |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `list_id` | integer | yes | The ID of a board's list |
|
||||
|
||||
```bash
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
|
||||
|
|
|
@ -1124,6 +1124,45 @@ Example response:
|
|||
```
|
||||
|
||||
|
||||
## Participants on issues
|
||||
|
||||
```
|
||||
GET /projects/:id/issues/:issue_iid/participants
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|--------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `issue_iid` | integer | yes | The internal ID of a project's issue |
|
||||
|
||||
```bash
|
||||
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/participants
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "John Doe1",
|
||||
"username": "user1",
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
|
||||
"web_url": "http://localhost/user1"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "John Doe5",
|
||||
"username": "user5",
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80&d=identicon",
|
||||
"web_url": "http://localhost/user5"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Comments on issues
|
||||
|
||||
Comments are done via the [notes](notes.md) resource.
|
||||
|
|
|
@ -308,6 +308,41 @@ Parameters:
|
|||
}
|
||||
```
|
||||
|
||||
## Get single MR participants
|
||||
|
||||
Get a list of merge request participants.
|
||||
|
||||
```
|
||||
GET /projects/:id/merge_requests/:merge_request_iid/participants
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
|
||||
- `merge_request_iid` (required) - The internal ID of the merge request
|
||||
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "John Doe1",
|
||||
"username": "user1",
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
|
||||
"web_url": "http://localhost/user1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "John Doe2",
|
||||
"username": "user2",
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon",
|
||||
"web_url": "http://localhost/user2"
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## Get single MR commits
|
||||
|
||||
Get a list of merge request commits.
|
||||
|
|
|
@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
|
|||
{
|
||||
"domain": "ssl.domain.example",
|
||||
"url": "https://ssl.domain.example",
|
||||
"project_id": 1337,
|
||||
"certificate": {
|
||||
"expired": false,
|
||||
"expiration": "2020-04-12T14:32:00.000Z"
|
||||
|
|
|
@ -30,14 +30,18 @@ Example response:
|
|||
"description": "test-1-20150125",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": true,
|
||||
"status": "online"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2-20150125",
|
||||
"id": 8,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": false,
|
||||
"status": "offline"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -69,28 +73,36 @@ Example response:
|
|||
"description": "shared-runner-1",
|
||||
"id": 1,
|
||||
"is_shared": true,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": true,
|
||||
"status": "online"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "shared-runner-2",
|
||||
"id": 3,
|
||||
"is_shared": true,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": false
|
||||
"status": "offline"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-1-20150125",
|
||||
"id": 6,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": true
|
||||
"status": "paused"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "test-2-20150125",
|
||||
"id": 8,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": false,
|
||||
"status": "offline"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -122,6 +134,8 @@ Example response:
|
|||
"is_shared": false,
|
||||
"contacted_at": "2016-01-25T16:39:48.066Z",
|
||||
"name": null,
|
||||
"online": true,
|
||||
"status": "online",
|
||||
"platform": null,
|
||||
"projects": [
|
||||
{
|
||||
|
@ -176,6 +190,8 @@ Example response:
|
|||
"is_shared": false,
|
||||
"contacted_at": "2016-01-25T16:39:48.066Z",
|
||||
"name": null,
|
||||
"online": true,
|
||||
"status": "online",
|
||||
"platform": null,
|
||||
"projects": [
|
||||
{
|
||||
|
@ -327,14 +343,18 @@ Example response:
|
|||
"description": "test-2-20150125",
|
||||
"id": 8,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": false,
|
||||
"status": "offline"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"description": "development_runner",
|
||||
"id": 5,
|
||||
"is_shared": true,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": true
|
||||
"status": "paused"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -364,7 +384,9 @@ Example response:
|
|||
"description": "test-2016-02-01",
|
||||
"id": 9,
|
||||
"is_shared": false,
|
||||
"name": null
|
||||
"name": null,
|
||||
"online": true,
|
||||
"status": "online"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ PUT /application/settings
|
|||
| `after_sign_up_text` | string | no | Text shown to the user after signing up |
|
||||
| `akismet_api_key` | string | no | API key for akismet spam protection |
|
||||
| `akismet_enabled` | boolean | no | Enable or disable akismet spam protection |
|
||||
| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. |
|
||||
| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. |
|
||||
| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. |
|
||||
| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. |
|
||||
| `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. |
|
||||
|
|
|
@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do
|
|||
end
|
||||
```
|
||||
|
||||
## Running tests with a locally modified version of Gitaly
|
||||
|
||||
Normally, gitlab-ce/ee tests use a local clone of Gitaly in `tmp/tests/gitaly`
|
||||
pinned at the version specified in GITALY_SERVER_VERSION. If you want
|
||||
to run tests locally against a modified version of Gitaly you can
|
||||
replace `tmp/tests/gitaly` with a symlink.
|
||||
|
||||
```shell
|
||||
rm -rf tmp/tests/gitaly
|
||||
ln -s /path/to/gitaly tmp/tests/gitaly
|
||||
```
|
||||
|
||||
Make sure you run `make` in your local Gitaly directory before running
|
||||
tests. Otherwise, Gitaly will fail to boot.
|
||||
|
||||
If you make changes to your local Gitaly in between test runs you need
|
||||
to manually run `make` again.
|
||||
|
||||
Note that CI tests will not use your locally modified version of
|
||||
Gitaly. To use a custom Gitaly version in CI you need to update
|
||||
GITALY_SERVER_VERSION. You can use the format `=revision` to use a
|
||||
non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI.
|
||||
|
||||
---
|
||||
|
||||
[Return to Development documentation](README.md)
|
||||
|
|
80
doc/development/testing_guide/end_to_end_tests.md
Normal file
80
doc/development/testing_guide/end_to_end_tests.md
Normal file
|
@ -0,0 +1,80 @@
|
|||
# End-to-End Testing
|
||||
|
||||
## What is End-to-End testing?
|
||||
|
||||
End-to-End testing is a strategy used to check whether your application works
|
||||
as expected across entire software stack and architecture, including
|
||||
integration of all microservices and components that are supposed to work
|
||||
together.
|
||||
|
||||
## How do we test GitLab?
|
||||
|
||||
We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we
|
||||
test these packages using [GitLab QA][gitlab-qa] project, which is entirely
|
||||
black-box, click-driven testing framework.
|
||||
|
||||
### Testing nightly builds
|
||||
|
||||
We run scheduled pipeline each night to test nightly builds created by Omnibus.
|
||||
You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines].
|
||||
|
||||
### Testing code in merge requests
|
||||
|
||||
It is possible to run end-to-end tests (eventually being run within a
|
||||
[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering
|
||||
the `package-qa` manual action, that should be present in a merge request
|
||||
widget.
|
||||
|
||||
Mmanual action that starts end-to-end tests is also available in merge requests
|
||||
in Omnibus GitLab project.
|
||||
|
||||
Below you can read more about how to use it and how does it work.
|
||||
|
||||
#### How does it work?
|
||||
|
||||
Currently, we are using _multi-project pipeline_-like approach to run QA
|
||||
pipelines.
|
||||
|
||||
1. Developer triggers a manual action, that can be found in CE and EE merge
|
||||
requests. This starts a chain of pipelines in multiple projects.
|
||||
|
||||
1. The script being executed triggers a pipeline in GitLab Omnibus and waits
|
||||
for the resulting status. We call this a _status attribution_.
|
||||
|
||||
1. GitLab packages are being built in Omnibus pipeline. Packages are going to be
|
||||
pushed to Container Registry.
|
||||
|
||||
1. When packages are ready, and available in the registry, a final step in the
|
||||
pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab
|
||||
QA project. It also waits for a resulting status.
|
||||
|
||||
1. GitLab QA pulls images from the registry, spins-up containers and runs tests
|
||||
against a test environment that has been just orchestrated by the `gitlab-qa`
|
||||
tool.
|
||||
|
||||
1. The result of the GitLab QA pipeline is being propagated upstream, through
|
||||
Omnibus, back to CE / EE merge request.
|
||||
|
||||
#### How do I write tests?
|
||||
|
||||
In order to write new tests, you first need to learn more about GitLab QA
|
||||
architecture. See the [documentation about it][gitlab-qa-architecture] in
|
||||
GitLab QA project.
|
||||
|
||||
Once you decided where to put test environment orchestration scenarios and
|
||||
instance specs, take a look at the [relevant documentation][instance-qa-readme]
|
||||
and examples in [the `qa/` directory][instance-qa-examples].
|
||||
|
||||
## Where can I ask for help?
|
||||
|
||||
You can ask question in the `#qa` channel on Slack (GitLab internal) or you can
|
||||
find an issue you would like to work on in [the issue tracker][gitlab-qa-issues]
|
||||
and start a new discussion there.
|
||||
|
||||
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
|
||||
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
|
||||
[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
|
||||
[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md
|
||||
[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues
|
||||
[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md
|
||||
[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa
|
|
@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks.
|
|||
|
||||
---
|
||||
|
||||
## [End-to-end tests](end_to_end_tests.md)
|
||||
|
||||
Everything you should know about how to run end-to-end tests using
|
||||
[GitLab QA][gitlab-qa] testing framework.
|
||||
|
||||
---
|
||||
|
||||
## Spinach (feature) tests
|
||||
|
||||
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
|
||||
|
@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead.
|
|||
[Capybara]: https://github.com/teamcapybara/capybara
|
||||
[Karma]: http://karma-runner.github.io/
|
||||
[Jasmine]: https://jasmine.github.io/
|
||||
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
|
||||
|
|
|
@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it.
|
|||
The actual test scenarios and steps are [part of GitLab Rails] so that they're
|
||||
always in-sync with the codebase.
|
||||
|
||||
Read a separate document about [end-to-end tests](end_to_end_tests.md) to
|
||||
learn more.
|
||||
|
||||
[multiple pieces]: ../architecture.md#components
|
||||
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
|
||||
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
|
||||
|
|
|
@ -9,7 +9,7 @@ When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge
|
|||
commits will be created and all merges are fast-forwarded, which means that
|
||||
merging is only allowed if the branch could be fast-forwarded.
|
||||
|
||||
When a fast-forward merge is not possible, the user must rebase the branch manually.
|
||||
When a fast-forward merge is not possible, the user is given the option to rebase.
|
||||
|
||||
## Use cases
|
||||
|
||||
|
@ -25,7 +25,7 @@ merge commits. In such cases, the fast-forward merge is the perfect candidate.
|
|||
Now, when you visit the merge request page, you will be able to accept it
|
||||
**only if a fast-forward merge is possible**.
|
||||
|
||||
![Fast forward merge request](img/ff_merge_mr.png)
|
||||
![Fast forward merge request](img/ff_merge_rebase.png)
|
||||
|
||||
If the target branch is ahead of the source branch, you need to rebase the
|
||||
source branch locally before you will be able to do a fast-forward merge.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
BIN
doc/user/project/merge_requests/img/ff_merge_rebase.png
Normal file
BIN
doc/user/project/merge_requests/img/ff_merge_rebase.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -22,3 +22,20 @@ Feature: Project Ff Merge Requests
|
|||
Then I should see ff-only merge button
|
||||
When I accept this merge request
|
||||
Then I should see merged request
|
||||
|
||||
@javascript
|
||||
Scenario: I do rebase before ff-only merge
|
||||
Given ff merge enabled
|
||||
And rebase before merge enabled
|
||||
When I visit merge request page "Bug NS-05"
|
||||
Then I should see rebase button
|
||||
When I press rebase button
|
||||
Then I should see rebase in progress message
|
||||
|
||||
@javascript
|
||||
Scenario: I do rebase before regular merge
|
||||
Given rebase before merge enabled
|
||||
When I visit merge request page "Bug NS-05"
|
||||
Then I should see rebase button
|
||||
When I press rebase button
|
||||
Then I should see rebase in progress message
|
||||
|
|
|
@ -17,6 +17,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
|
|||
author: project.users.first)
|
||||
end
|
||||
|
||||
step 'merge request is mergeable' do
|
||||
expect(page).to have_button 'Merge'
|
||||
end
|
||||
|
||||
step 'I should see ff-only merge button' do
|
||||
expect(page).to have_content "Fast-forward merge without a merge commit"
|
||||
expect(page).to have_button 'Merge'
|
||||
|
@ -45,6 +49,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
|
|||
project.save!
|
||||
end
|
||||
|
||||
step 'I should see rebase button' do
|
||||
expect(page).to have_button "Rebase"
|
||||
end
|
||||
|
||||
step 'merge request "Bug NS-05" is rebased' do
|
||||
merge_request.source_branch = 'flatten-dir'
|
||||
merge_request.target_branch = 'improve/awesome'
|
||||
|
@ -59,6 +67,20 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
|
|||
merge_request.save!
|
||||
end
|
||||
|
||||
step 'rebase before merge enabled' do
|
||||
project = merge_request.target_project
|
||||
project.merge_requests_rebase_enabled = true
|
||||
project.save!
|
||||
end
|
||||
|
||||
step 'I press rebase button' do
|
||||
click_button "Rebase"
|
||||
end
|
||||
|
||||
step "I should see rebase in progress message" do
|
||||
expect(page).to have_content("Rebase in progress")
|
||||
end
|
||||
|
||||
def merge_request
|
||||
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
|
||||
end
|
||||
|
|
|
@ -119,6 +119,7 @@ module API
|
|||
mount ::API::Features
|
||||
mount ::API::Files
|
||||
mount ::API::Groups
|
||||
mount ::API::GroupMilestones
|
||||
mount ::API::Internal
|
||||
mount ::API::Issues
|
||||
mount ::API::Jobs
|
||||
|
@ -129,8 +130,6 @@ module API
|
|||
mount ::API::Members
|
||||
mount ::API::MergeRequestDiffs
|
||||
mount ::API::MergeRequests
|
||||
mount ::API::ProjectMilestones
|
||||
mount ::API::GroupMilestones
|
||||
mount ::API::Namespaces
|
||||
mount ::API::Notes
|
||||
mount ::API::NotificationSettings
|
||||
|
@ -139,6 +138,7 @@ module API
|
|||
mount ::API::PipelineSchedules
|
||||
mount ::API::ProjectHooks
|
||||
mount ::API::Projects
|
||||
mount ::API::ProjectMilestones
|
||||
mount ::API::ProjectSnippets
|
||||
mount ::API::ProtectedBranches
|
||||
mount ::API::Repositories
|
||||
|
|
|
@ -1,45 +1,46 @@
|
|||
module API
|
||||
class Boards < Grape::API
|
||||
include BoardsResponses
|
||||
include PaginationParams
|
||||
|
||||
before { authenticate! }
|
||||
|
||||
helpers do
|
||||
def board_parent
|
||||
user_project
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
|
||||
desc 'Get all project boards' do
|
||||
detail 'This feature was introduced in 8.13'
|
||||
success Entities::Board
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
get ':id/boards' do
|
||||
authorize!(:read_board, user_project)
|
||||
present paginate(user_project.boards), with: Entities::Board
|
||||
segment ':id/boards' do
|
||||
desc 'Get all project boards' do
|
||||
detail 'This feature was introduced in 8.13'
|
||||
success Entities::Board
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
get '/' do
|
||||
authorize!(:read_board, user_project)
|
||||
present paginate(board_parent.boards), with: Entities::Board
|
||||
end
|
||||
|
||||
desc 'Find a project board' do
|
||||
detail 'This feature was introduced in 10.4'
|
||||
success Entities::Board
|
||||
end
|
||||
get '/:board_id' do
|
||||
present board, with: Entities::Board
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :board_id, type: Integer, desc: 'The ID of a board'
|
||||
end
|
||||
segment ':id/boards/:board_id' do
|
||||
helpers do
|
||||
def project_board
|
||||
board = user_project.boards.first
|
||||
|
||||
if params[:board_id] == board.id
|
||||
board
|
||||
else
|
||||
not_found!('Board')
|
||||
end
|
||||
end
|
||||
|
||||
def board_lists
|
||||
project_board.lists.destroyable
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Get the lists of a project board' do
|
||||
detail 'Does not include `done` list. This feature was introduced in 8.13'
|
||||
success Entities::List
|
||||
|
@ -72,22 +73,13 @@ module API
|
|||
requires :label_id, type: Integer, desc: 'The ID of an existing label'
|
||||
end
|
||||
post '/lists' do
|
||||
unless available_labels.exists?(params[:label_id])
|
||||
unless available_labels_for(user_project).exists?(params[:label_id])
|
||||
render_api_error!({ error: 'Label not found!' }, 400)
|
||||
end
|
||||
|
||||
authorize!(:admin_list, user_project)
|
||||
|
||||
service = ::Boards::Lists::CreateService.new(user_project, current_user,
|
||||
{ label_id: params[:label_id] })
|
||||
|
||||
list = service.execute(project_board)
|
||||
|
||||
if list.valid?
|
||||
present list, with: Entities::List
|
||||
else
|
||||
render_validation_error!(list)
|
||||
end
|
||||
create_list
|
||||
end
|
||||
|
||||
desc 'Moves a board list to a new position' do
|
||||
|
@ -99,18 +91,11 @@ module API
|
|||
requires :position, type: Integer, desc: 'The position of the list'
|
||||
end
|
||||
put '/lists/:list_id' do
|
||||
list = project_board.lists.movable.find(params[:list_id])
|
||||
list = board_lists.find(params[:list_id])
|
||||
|
||||
authorize!(:admin_list, user_project)
|
||||
|
||||
service = ::Boards::Lists::MoveService.new(user_project, current_user,
|
||||
{ position: params[:position] })
|
||||
|
||||
if service.execute(list)
|
||||
present list, with: Entities::List
|
||||
else
|
||||
render_api_error!({ error: "List could not be moved!" }, 400)
|
||||
end
|
||||
move_list(list)
|
||||
end
|
||||
|
||||
desc 'Delete a board list' do
|
||||
|
@ -124,12 +109,7 @@ module API
|
|||
authorize!(:admin_list, user_project)
|
||||
list = board_lists.find(params[:list_id])
|
||||
|
||||
destroy_conditionally!(list) do |list|
|
||||
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
|
||||
unless service.execute(list)
|
||||
render_api_error!({ error: 'List could not be deleted!' }, 400)
|
||||
end
|
||||
end
|
||||
destroy_list(list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
50
lib/api/boards_responses.rb
Normal file
50
lib/api/boards_responses.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
module API
|
||||
module BoardsResponses
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
helpers do
|
||||
def board
|
||||
board_parent.boards.find(params[:board_id])
|
||||
end
|
||||
|
||||
def board_lists
|
||||
board.lists.destroyable
|
||||
end
|
||||
|
||||
def create_list
|
||||
create_list_service =
|
||||
::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] })
|
||||
|
||||
list = create_list_service.execute(board)
|
||||
|
||||
if list.valid?
|
||||
present list, with: Entities::List
|
||||
else
|
||||
render_validation_error!(list)
|
||||
end
|
||||
end
|
||||
|
||||
def move_list(list)
|
||||
move_list_service =
|
||||
::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i })
|
||||
|
||||
if move_list_service.execute(list)
|
||||
present list, with: Entities::List
|
||||
else
|
||||
render_api_error!({ error: "List could not be moved!" }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_list(list)
|
||||
destroy_conditionally!(list) do |list|
|
||||
service = ::Boards::Lists::DestroyService.new(board_parent, current_user)
|
||||
unless service.execute(list)
|
||||
render_api_error!({ error: 'List could not be deleted!' }, 400)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -791,6 +791,8 @@ module API
|
|||
|
||||
class Board < Grape::Entity
|
||||
expose :id
|
||||
expose :project, using: Entities::BasicProjectDetails
|
||||
|
||||
expose :lists, using: Entities::List do |board|
|
||||
board.lists.destroyable
|
||||
end
|
||||
|
@ -862,6 +864,8 @@ module API
|
|||
expose :active
|
||||
expose :is_shared
|
||||
expose :name
|
||||
expose :online?, as: :online
|
||||
expose :status
|
||||
end
|
||||
|
||||
class RunnerDetails < Runner
|
||||
|
@ -1133,6 +1137,7 @@ module API
|
|||
class PagesDomainBasic < Grape::Entity
|
||||
expose :domain
|
||||
expose :url
|
||||
expose :project_id
|
||||
expose :certificate,
|
||||
as: :certificate_expiration,
|
||||
if: ->(pages_domain, _) { pages_domain.certificate? },
|
||||
|
|
|
@ -74,8 +74,15 @@ module API
|
|||
page || not_found!('Wiki Page')
|
||||
end
|
||||
|
||||
def available_labels
|
||||
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
|
||||
def available_labels_for(label_parent)
|
||||
search_params =
|
||||
if label_parent.is_a?(Project)
|
||||
{ project_id: label_parent.id }
|
||||
else
|
||||
{ group_id: label_parent.id, only_group_labels: true }
|
||||
end
|
||||
|
||||
LabelsFinder.new(current_user, search_params).execute
|
||||
end
|
||||
|
||||
def find_user(id)
|
||||
|
@ -141,7 +148,9 @@ module API
|
|||
end
|
||||
|
||||
def find_project_label(id)
|
||||
label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
|
||||
labels = available_labels_for(user_project)
|
||||
label = labels.find_by_id(id) || labels.find_by_title(id)
|
||||
|
||||
label || not_found!('Label')
|
||||
end
|
||||
|
||||
|
|
|
@ -277,6 +277,19 @@ module API
|
|||
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
|
||||
end
|
||||
|
||||
desc 'List participants for an issue' do
|
||||
success Entities::UserBasic
|
||||
end
|
||||
params do
|
||||
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
|
||||
end
|
||||
get ':id/issues/:issue_iid/participants' do
|
||||
issue = find_project_issue(params[:issue_iid])
|
||||
participants = ::Kaminari.paginate_array(issue.participants)
|
||||
|
||||
present paginate(participants), with: Entities::UserBasic, current_user: current_user, project: user_project
|
||||
end
|
||||
|
||||
desc 'Get the user agent details for an issue' do
|
||||
success Entities::UserAgentDetail
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
get ':id/labels' do
|
||||
present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
|
||||
present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project
|
||||
end
|
||||
|
||||
desc 'Create a new label' do
|
||||
|
@ -30,7 +30,7 @@ module API
|
|||
post ':id/labels' do
|
||||
authorize! :admin_label, user_project
|
||||
|
||||
label = available_labels.find_by(title: params[:name])
|
||||
label = available_labels_for(user_project).find_by(title: params[:name])
|
||||
conflict!('Label already exists') if label
|
||||
|
||||
priority = params.delete(:priority)
|
||||
|
|
|
@ -185,6 +185,16 @@ module API
|
|||
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
|
||||
end
|
||||
|
||||
desc 'Get the participants of a merge request' do
|
||||
success Entities::UserBasic
|
||||
end
|
||||
get ':id/merge_requests/:merge_request_iid/participants' do
|
||||
merge_request = find_merge_request_with_access(params[:merge_request_iid])
|
||||
participants = ::Kaminari.paginate_array(merge_request.participants)
|
||||
|
||||
present paginate(participants), with: Entities::UserBasic
|
||||
end
|
||||
|
||||
desc 'Get the commits of a merge request' do
|
||||
success Entities::Commit
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module API
|
|||
success ::API::Entities::Label
|
||||
end
|
||||
get ':id/labels' do
|
||||
present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
|
||||
present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project
|
||||
end
|
||||
|
||||
desc 'Delete an existing label' do
|
||||
|
|
|
@ -2,16 +2,7 @@ module Banzai
|
|||
module Filter
|
||||
class MermaidFilter < HTML::Pipeline::Filter
|
||||
def call
|
||||
doc.css('pre[lang="mermaid"]').add_class('mermaid')
|
||||
doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid')
|
||||
|
||||
# The `<code></code>` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb
|
||||
# We want to keep context and consistency, so we the blocks are added for all filters.
|
||||
# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859
|
||||
doc.css('pre[lang="mermaid"]').each do |pre|
|
||||
document = pre.at('code')
|
||||
document.replace(document.content)
|
||||
end
|
||||
doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid')
|
||||
|
||||
doc
|
||||
end
|
||||
|
|
|
@ -14,14 +14,7 @@ module Gitlab
|
|||
ENCODING_CONFIDENCE_THRESHOLD = 50
|
||||
|
||||
def encode!(message)
|
||||
return nil unless message.respond_to?(:force_encoding)
|
||||
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
|
||||
|
||||
if message.respond_to?(:frozen?) && message.frozen?
|
||||
message = message.dup
|
||||
end
|
||||
|
||||
message.force_encoding("UTF-8")
|
||||
message = force_encode_utf8(message)
|
||||
return message if message.valid_encoding?
|
||||
|
||||
# return message if message type is binary
|
||||
|
@ -35,6 +28,8 @@ module Gitlab
|
|||
|
||||
# encode and clean the bad chars
|
||||
message.replace clean(message)
|
||||
rescue ArgumentError
|
||||
return nil
|
||||
rescue
|
||||
encoding = detect ? detect[:encoding] : "unknown"
|
||||
"--broken encoding: #{encoding}"
|
||||
|
@ -54,8 +49,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def encode_utf8(message)
|
||||
return nil unless message.is_a?(String)
|
||||
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
|
||||
message = force_encode_utf8(message)
|
||||
return message if message.valid_encoding?
|
||||
|
||||
detect = CharlockHolmes::EncodingDetector.detect(message)
|
||||
if detect && detect[:encoding]
|
||||
|
@ -69,6 +64,8 @@ module Gitlab
|
|||
else
|
||||
clean(message)
|
||||
end
|
||||
rescue ArgumentError
|
||||
return nil
|
||||
end
|
||||
|
||||
def encode_binary(s)
|
||||
|
@ -83,6 +80,15 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def force_encode_utf8(message)
|
||||
raise ArgumentError unless message.respond_to?(:force_encoding)
|
||||
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
|
||||
|
||||
message = message.dup if message.respond_to?(:frozen?) && message.frozen?
|
||||
|
||||
message.force_encoding("UTF-8")
|
||||
end
|
||||
|
||||
def clean(message)
|
||||
message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
|
||||
.encode("UTF-8")
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
include Gitlab::EncodingHelper
|
||||
|
||||
def ref_name(ref)
|
||||
encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
|
||||
encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
|
||||
end
|
||||
|
||||
def branch_name(ref)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue