Commit b058af1b authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into...

Merge branch 'master' into 39957-redirect-to-gpc-page-if-users-try-to-create-a-cluster-but-the-account-is-not-enabled
parents 12984a73 3d162d19
...@@ -718,7 +718,7 @@ GEM ...@@ -718,7 +718,7 @@ GEM
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (2.0.3) redis-rack (2.0.4)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-rails (5.0.2) redis-rails (5.0.2)
......
...@@ -74,6 +74,18 @@ const gfmRules = { ...@@ -74,6 +74,18 @@ const gfmRules = {
return `![${el.dataset.title}](${el.getAttribute('src')})`; 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: { MathFilter: {
'pre.code.math[data-math-style=display]'(el, text) { 'pre.code.math[data-math-style=display]'(el, text) {
return `\`\`\`math\n${text.trim()}\n\`\`\``; return `\`\`\`math\n${text.trim()}\n\`\`\``;
......
...@@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown { ...@@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown {
let target; let target;
let value; let value;
if (event.srcElement === this.branchInput) { if (event.target === this.branchInput) {
target = 'branch'; target = 'branch';
value = this.branchInput.value; value = this.branchInput.value;
} else if (event.srcElement === this.refInput) { } else if (event.target === this.refInput) {
target = 'ref'; target = 'ref';
value = event.srcElement.value.slice(0, event.srcElement.selectionStart) + value = event.target.value.slice(0, event.target.selectionStart) +
event.srcElement.value.slice(event.srcElement.selectionEnd); event.target.value.slice(event.target.selectionEnd);
} else { } else {
return false; return false;
} }
......
...@@ -45,11 +45,9 @@ export default { ...@@ -45,11 +45,9 @@ export default {
onLeaveGroup() { onLeaveGroup() {
this.modalStatus = true; this.modalStatus = true;
}, },
leaveGroup(leaveConfirmed) { leaveGroup() {
this.modalStatus = false; 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 { ...@@ -42,28 +42,28 @@ export default {
v-if="isGroup" v-if="isGroup"
css-class="number-subgroups" css-class="number-subgroups"
icon-name="folder" icon-name="folder"
:title="s__('Subgroups')" :title="__('Subgroups')"
:value=item.subgroupCount :value="item.subgroupCount"
/> />
<item-stats-value <item-stats-value
v-if="isGroup" v-if="isGroup"
css-class="number-projects" css-class="number-projects"
icon-name="bookmark" icon-name="bookmark"
:title="s__('Projects')" :title="__('Projects')"
:value=item.projectCount :value="item.projectCount"
/> />
<item-stats-value <item-stats-value
v-if="isGroup" v-if="isGroup"
css-class="number-users" css-class="number-users"
icon-name="users" icon-name="users"
:title="s__('Members')" :title="__('Members')"
:value=item.memberCount :value="item.memberCount"
/> />
<item-stats-value <item-stats-value
v-if="isProject" v-if="isProject"
css-class="project-stars" css-class="project-stars"
icon-name="star" icon-name="star"
:value=item.starCount :value="item.starCount"
/> />
<item-stats-value <item-stats-value
css-class="item-visibility" css-class="item-visibility"
......
...@@ -32,10 +32,10 @@ ...@@ -32,10 +32,10 @@
methods: { methods: {
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
this.toggleModalOpen(); this.openModal = true;
}, },
toggleModalOpen() { hideModal() {
this.openModal = !this.openModal; this.openModal = false;
}, },
}, },
}; };
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
:branch-id="branch" :branch-id="branch"
:path="path" :path="path"
:parent="parent" :parent="parent"
@toggle="toggleModalOpen" @hide="hideModal"
/> />
</div> </div>
</template> </template>
...@@ -43,10 +43,10 @@ ...@@ -43,10 +43,10 @@
type: this.type, type: this.type,
}); });
this.toggleModalOpen(); this.hideModal();
}, },
toggleModalOpen() { hideModal() {
this.$emit('toggle'); this.$emit('hide');
}, },
}, },
computed: { computed: {
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
:title="modalTitle" :title="modalTitle"
:primary-button-label="buttonLabel" :primary-button-label="buttonLabel"
kind="success" kind="success"
@toggle="toggleModalOpen" @cancel="hideModal"
@submit="createEntryInStore" @submit="createEntryInStore"
> >
<form <form
......
...@@ -110,7 +110,7 @@ export default { ...@@ -110,7 +110,7 @@ export default {
kind="primary" kind="primary"
:title="__('Branch has changed')" :title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')" :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)" @submit="makeCommit(true)"
/> />
<commit-files-list <commit-files-list
......
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
kind="warning" kind="warning"
:title="__('Are you sure?')" :title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')" :text="__('Are you sure you want to discard your changes?')"
@toggle="closeDiscardPopup" @cancel="closeDiscardPopup"
@submit="toggleEditMode(true)" @submit="toggleEditMode(true)"
/> />
</div> </div>
......
...@@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - ...@@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength -
export function capitalizeFirstCharacter(text) { export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`; 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);
<script> <script>
import modal from '../../../vue_shared/components/modal.vue'; import modal from '~/vue_shared/components/modal.vue';
import { __, s__, sprintf } from '../../../locale'; import { __, s__, sprintf } from '~/locale';
import csrf from '../../../lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
export default { export default {
props: { props: {
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
return { return {
enteredPassword: '', enteredPassword: '',
enteredUsername: '', enteredUsername: '',
isOpen: false,
}; };
}, },
components: { components: {
...@@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
return this.enteredUsername === this.username; return this.enteredUsername === this.username;
}, },
onSubmit(status) { onSubmit() {
if (status) { this.$refs.form.submit();
if (!this.canSubmit()) {
return;
}
this.$refs.form.submit();
}
this.toggleOpen(false);
},
toggleOpen(isOpen) {
this.isOpen = isOpen;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <modal
<modal id="delete-account-modal"
v-if="isOpen" :title="s__('Profiles|Delete your account?')"
:title="s__('Profiles|Delete your account?')" :text="text"
:text="text" kind="danger"
:kind="`danger ${!canSubmit() && 'disabled'}`" :primary-button-label="s__('Profiles|Delete account')"
:primary-button-label="s__('Profiles|Delete account')" @submit="onSubmit"
@toggle="toggleOpen" :submit-disabled="!canSubmit()">
@submit="onSubmit">
<template slot="body" slot-scope="props">
<p v-html="props.text"></p>
<form <template slot="body" slot-scope="props">
ref="form" <p v-html="props.text"></p>
:action="actionUrl"
method="post">
<input <form
type="hidden" ref="form"
name="_method" :action="actionUrl"
value="delete" /> method="post">
<input
type="hidden"
name="authenticity_token"
:value="csrfToken" />
<p id="input-label" v-html="inputLabel"></p> <input
type="hidden"
name="_method"
value="delete" />
<input
type="hidden"
name="authenticity_token"
:value="csrfToken" />
<input <p id="input-label" v-html="inputLabel"></p>
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> <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>
<button </modal>
type="button"
class="btn btn-danger"
@click="toggleOpen(true)">
{{ s__('Profiles|Delete account') }}
</button>
</div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import deleteAccountModal from './components/delete_account_modal.vue'; 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'); const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
...@@ -9,6 +14,9 @@ new Vue({ ...@@ -9,6 +14,9 @@ new Vue({
components: { components: {
deleteAccountModal, deleteAccountModal,
}, },
mounted() {
deleteAccountButton.classList.remove('disabled');
},
render(createElement) { render(createElement) {
return createElement('delete-account-modal', { return createElement('delete-account-modal', {
props: { props: {
......
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { const deriveProjectPathFromUrl = ($projectImportUrl) => {
const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path');
if (hasUserDefinedProjectPath) { if (hasUserDefinedProjectPath) {
return; return;
} }
...@@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { ...@@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
// extract everything after the last slash // extract everything after the last slash
const pathMatch = /\/([^/]+)$/.exec(importUrl); const pathMatch = /\/([^/]+)$/.exec(importUrl);
if (pathMatch) { if (pathMatch) {
$projectPath.val(pathMatch[1]); $currentProjectPath.val(pathMatch[1]);
} }
}; };
...@@ -96,7 +97,7 @@ const bindEvents = () => { ...@@ -96,7 +97,7 @@ const bindEvents = () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
}); });
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
}; };
document.addEventListener('DOMContentLoaded', bindEvents); document.addEventListener('DOMContentLoaded', bindEvents);
......
...@@ -24,7 +24,25 @@ export default function renderMermaid($els) { ...@@ -24,7 +24,25 @@ export default function renderMermaid($els) {
}); });
$els.each((i, el) => { $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) => { }).catch((err) => {
Flash(`Can't load mermaid module: ${err}`); Flash(`Can't load mermaid module: ${err}`);
......
<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 ...@@ -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 PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds';
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 AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed';
export { default as CheckingState } from './components/states/mr_widget_checking'; export { default as CheckingState } from './components/states/mr_widget_checking';
export { default as MRWidgetStore } from './stores/mr_widget_store'; export { default as MRWidgetStore } from './stores/mr_widget_store';
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
MergedState, MergedState,
ClosedState, ClosedState,
MergingState, MergingState,
RebaseState,
WipState, WipState,
ArchivedState, ArchivedState,
ConflictsState, ConflictsState,
...@@ -79,6 +80,7 @@ export default { ...@@ -79,6 +80,7 @@ export default {
ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath, ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
statusPath: store.statusPath, statusPath: store.statusPath,
mergeActionsContentPath: store.mergeActionsContentPath, mergeActionsContentPath: store.mergeActionsContentPath,
rebasePath: store.rebasePath,
}; };
return new MRWidgetService(endpoints); return new MRWidgetService(endpoints);
}, },
...@@ -232,6 +234,7 @@ export default { ...@@ -232,6 +234,7 @@ export default {
'mr-widget-pipeline-failed': PipelineFailedState, 'mr-widget-pipeline-failed': PipelineFailedState,
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
}, },
template: ` template: `
<div class="mr-state-widget prepend-top-default"> <div class="mr-state-widget prepend-top-default">
......
...@@ -37,6 +37,10 @@ export default class MRWidgetService { ...@@ -37,6 +37,10 @@ export default class MRWidgetService {
return axios.get(this.endpoints.mergeActionsContentPath); return axios.get(this.endpoints.mergeActionsContentPath);
} }
rebase() {
return axios.post(this.endpoints.rebasePath);
}
static stopEnvironment(url) { static stopEnvironment(url) {
return axios.post(url); return axios.post(url);
} }
......
...@@ -25,6 +25,8 @@ export default function deviseState(data) { ...@@ -25,6 +25,8 @@ export default function deviseState(data) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds; return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) { } else if (!this.canMerge) {
return stateKey.notAllowedToMerge; return stateKey.notAllowedToMerge;
} else if (this.shouldBeRebased) {
return stateKey.rebase;
} else if (this.canBeMerged) { } else if (this.canBeMerged) {
return stateKey.readyToMerge; return stateKey.readyToMerge;
} }
......
...@@ -26,6 +26,7 @@ export default class MergeRequestStore { ...@@ -26,6 +26,7 @@ export default class MergeRequestStore {
this.divergedCommitsCount = data.diverged_commits_count; this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {}; this.pipeline = data.pipeline || {};
this.deployments = this.deployments || data.deployments || []; this.deployments = this.deployments || data.deployments || [];
this.initRebase(data);
if (data.issues_links) { if (data.issues_links) {
const links = data.issues_links; const links = data.issues_links;
...@@ -124,6 +125,13 @@ export default class MergeRequestStore { ...@@ -124,6 +125,13 @@ export default class MergeRequestStore {
return this.state === stateKey.nothingToMerge; 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) { static buildMetrics(metrics) {
if (!metrics) { if (!metrics) {
return {}; return {};
......
...@@ -17,6 +17,7 @@ const stateToComponentMap = { ...@@ -17,6 +17,7 @@ const stateToComponentMap = {
failedToMerge: 'mr-widget-failed-to-merge', failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed', autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'mr-widget-sha-mismatch', shaMismatch: 'mr-widget-sha-mismatch',
rebase: 'mr-widget-rebase',
}; };
const statesToShowHelpWidget = [ const statesToShowHelpWidget = [
...@@ -29,6 +30,7 @@ const statesToShowHelpWidget = [ ...@@ -29,6 +30,7 @@ const statesToShowHelpWidget = [
'pipelineFailed', 'pipelineFailed',
'pipelineBlocked', 'pipelineBlocked',
'autoMergeFailed', 'autoMergeFailed',
'rebase',
]; ];
export const stateKey = { export const stateKey = {
...@@ -46,6 +48,7 @@ export const stateKey = { ...@@ -46,6 +48,7 @@ export const stateKey = {
mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds', mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
notAllowedToMerge: 'notAllowedToMerge', notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge', readyToMerge: 'readyToMerge',
rebase: 'rebase',
}; };
export default { export default {
......
<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 { ...@@ -3,6 +3,10 @@ export default {
name: 'modal', name: 'modal',
props: { props: {
id: {
type: String,
required: false,
},
title: { title: {
type: String, type: String,
required: false, required: false,
...@@ -62,11 +66,11 @@ export default { ...@@ -62,11 +66,11 @@ export default {
}, },
methods: { methods: {
close() { emitCancel(event) {
this.$emit('toggle', false); this.$emit('cancel', event);
}, },
emitSubmit(status) { emitSubmit(event) {
this.$emit('submit', status); this.$emit('submit', event);
}, },
}, },
}; };
...@@ -75,7 +79,9 @@ export default { ...@@ -75,7 +79,9 @@ export default {
<template> <template>
<div class="modal-open"> <div class="modal-open">
<div <div
class="modal show" :id="id"
class="modal"
:class="id ? '' : 'show'"
role="dialog" role="dialog"
tabindex="-1" tabindex="-1"
> >
...@@ -93,7 +99,8 @@ export default { ...@@ -93,7 +99,8 @@ export default {
<button <button
type="button" type="button"
class="close pull-right" class="close pull-right"
@click="close" @click="emitCancel($event)"
data-dismiss="modal"
aria-label="Close" aria-label="Close"
> >
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
...@@ -110,7 +117,8 @@ export default { ...@@ -110,7 +117,8 @@ export default {
type="button" type="button"
class="btn pull-left" class="btn pull-left"
:class="btnCancelKindClass" :class="btnCancelKindClass"
@click="close"> @click="emitCancel($event)"
data-dismiss="modal">
{{ closeButtonLabel }} {{ closeButtonLabel }}
</button> </button>
<button <button
...@@ -119,13 +127,17 @@ export default { ...@@ -119,13 +127,17 @@ export default {
class="btn pull-right js-primary-button" class="btn pull-right js-primary-button"
:disabled="submitDisabled" :disabled="submitDisabled"
:class="btnKindClass" :class="btnKindClass"
@click="emitSubmit(true)"> @click="emitSubmit($event)"
data-dismiss="modal">
{{ primaryButtonLabel }} {{ primaryButtonLabel }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-backdrop fade in" /> <div
v-if="!id"
class="modal-backdrop fade in">
</div>
</div> </div>
</template> </template>
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
class="recaptcha-modal js-recaptcha-modal" class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true" :hide-footer="true"
:title="__('Please solve the reCAPTCHA')" :title="__('Please solve the reCAPTCHA')"
@toggle="close" @cancel="close"
> >
<div slot="body"> <div slot="body">
<p> <p>
......
...@@ -10,6 +10,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -10,6 +10,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
def index def index
@merge_requests = @issuables @merge_requests = @issuables
...@@ -223,6 +224,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -223,6 +224,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: environments render json: environments
end end
def rebase
RebaseWorker.perform_async(@merge_request.id, current_user.id)
render nothing: true, status: 200
end
protected protected
alias_method :subscribable_resource, :merge_request alias_method :subscribable_resource, :merge_request
...@@ -322,4 +329,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -322,4 +329,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@finder_type = MergeRequestsFinder @finder_type = MergeRequestsFinder
super super
end 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 end
...@@ -353,7 +353,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -353,7 +353,7 @@ class ProjectsController < Projects::ApplicationController
end end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? && project.repo project.repository_exists? && !project.empty_repo?
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
project.repository.expire_exists_cache project.repository.expire_exists_cache
......
class LabelsFinder < UnionFinder class LabelsFinder < UnionFinder
include Gitlab::Utils::StrongMemoize
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
...@@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder ...@@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder
label_ids << project.labels label_ids << project.labels
end end
end end
elsif only_group_labels?
label_ids << Label.where(group_id: group.id)
else else
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) label_ids << Label.where(project_id: projects.select(:id))
...@@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder ...@@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def group
strong_memoize(:group) do
group = Group.find(params[:group_id])
authorized_to_read_labels?(group) && group
end
end
def group? def group?
params[:group_id].present? params[:group_id].present?
end end
...@@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder ...@@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder
params[:project_ids].present? params[:project_ids].present?
end end
def only_group_labels?
params[:only_group_labels]
end
def title def title
params[:title] || params[:name] params[:title] || params[:name]
end end
...@@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder ...@@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder
@projects @projects
end end
def authorized_to_read_labels?(project) def authorized_to_read_labels?(label_parent)
return true if skip_authorization return true if skip_authorization
Ability.allowed?(current_user, :read_label, project) Ability.allowed?(current_user, :read_label, label_parent)
end end
end end
...@@ -23,4 +23,12 @@ module BranchesHelper ...@@ -23,4 +23,12 @@ module BranchesHelper
def protected_branch?(project, branch) def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name) ProtectedBranch.protected?(project, branch.name)
end end
def diverging_count_label(count)
if count >= Repository::MAX_DIVERGING_COUNT
"#{Repository::MAX_DIVERGING_COUNT - 1}+"
else
count.to_s
end
end
end end
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 ...@@ -10,12 +10,12 @@ module RelativePositioning
after_save :save_positionable_neighbours after_save :save_positionable_neighbours
end end
def project_ids def min_relative_position
[project.id] self.class.in_parents(parent_ids).minimum(:relative_position)
end end
def max_relative_position def max_relative_position
self.class.in_projects(project_ids).maximum(:relative_position) self.class.in_parents(parent_ids).maximum(:relative_position)
end end
def prev_relative_position def prev_relative_position
...@@ -23,7 +23,7 @@ module RelativePositioning ...@@ -23,7 +23,7 @@ module RelativePositioning
if self.relative_position if self.relative_position
prev_pos = self.class prev_pos = self.class
.in_projects(project_ids) .in_parents(parent_ids)
.where('relative_position < ?', self.relative_position) .where('relative_position < ?', self.relative_position)
.maximum(:relative_position) .maximum(:relative_position)
end end
...@@ -36,7 +36,7 @@ module RelativePositioning ...@@ -36,7 +36,7 @@ module RelativePositioning
if self.relative_position if self.relative_position
next_pos = self.class next_pos = self.class
.in_projects(project_ids) .in_parents(parent_ids)
.where('relative_position > ?', self.relative_position) .where('relative_position > ?', self.relative_position)
.minimum(:relative_position) .minimum(:relative_position)
end end
...@@ -63,7 +63,7 @@ module RelativePositioning ...@@ -63,7 +63,7 @@ module RelativePositioning
pos_after = before.next_relative_position pos_after = before.next_relative_position
if before.shift_after? 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 issue_to_move.move_after
@positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
...@@ -78,7 +78,7 @@ module RelativePositioning ...@@ -78,7 +78,7 @@ module RelativePositioning
pos_before = after.prev_relative_position pos_before = after.prev_relative_position
if after.shift_before? 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 issue_to_move.move_before
@positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
...@@ -92,6 +92,10 @@ module RelativePositioning ...@@ -92,6 +92,10 @@ module RelativePositioning
self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
end 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 # Indicates if there is an issue that should be shifted to free the place
def shift_after? def shift_after?
next_pos = next_relative_position next_pos = next_relative_position
......
...@@ -35,6 +35,8 @@ class Issue < ActiveRecord::Base ...@@ -35,6 +35,8 @@ class Issue < ActiveRecord::Base
validates :project, presence: true validates :project, presence: true
alias_attribute :parent_ids, :project_id
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
...@@ -78,6 +80,10 @@ class Issue < ActiveRecord::Base ...@@ -78,6 +80,10 @@ class Issue < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid
class << self
alias_method :in_parents, :in_projects
end
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
......
...@@ -156,6 +156,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -156,6 +156,13 @@ class MergeRequest < ActiveRecord::Base
'!' '!'
end 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 # 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. # 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 # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
...@@ -607,7 +614,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -607,7 +614,7 @@ class MergeRequest < ActiveRecord::Base
check_if_can_be_merged check_if_can_be_merged
can_be_merged? can_be_merged? && !should_be_rebased?
end end
def mergeable_state?(skip_ci_check: false) def mergeable_state?(skip_ci_check: false)
......
...@@ -19,6 +19,7 @@ class Project < ActiveRecord::Base ...@@ -19,6 +19,7 @@ class Project < ActiveRecord::Base
include Routable include Routable
include GroupDescendant include GroupDescendant
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include DeploymentPlatform
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings extend Gitlab::CurrentSettings
...@@ -904,12 +905,6 @@ class Project < ActiveRecord::Base ...@@ -904,12 +905,6 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true) @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end 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 def monitoring_services
services.where(category: :monitoring) services.where(category: :monitoring)
end end
...@@ -992,10 +987,6 @@ class Project < ActiveRecord::Base ...@@ -992,10 +987,6 @@ class Project < ActiveRecord::Base
false false
end end
def repo
repository.rugged
end
def url_to_repo def url_to_repo
gitlab_shell.url_to_repo(full_path) gitlab_shell.url_to_repo(full_path)
end end
...@@ -1438,7 +1429,7 @@ class Project < ActiveRecord::Base ...@@ -1438,7 +1429,7 @@ class Project < ActiveRecord::Base
# We'd need to keep track of project full path otherwise directory tree # We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using # created with hashed storage enabled cannot be usefully imported using
# the import rake task. # 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 rescue Gitlab::Git::Repository::NoRepository => e
Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil nil
......
...@@ -4,6 +4,7 @@ class Repository ...@@ -4,6 +4,7 @@ class Repository
REF_MERGE_REQUEST = 'merge-requests'.freeze REF_MERGE_REQUEST = 'merge-requests'.freeze
REF_KEEP_AROUND = 'keep-around'.freeze REF_KEEP_AROUND = 'keep-around'.freeze
REF_ENVIRONMENTS = 'environments'.freeze REF_ENVIRONMENTS = 'environments'.freeze
MAX_DIVERGING_COUNT = 1000
RESERVED_REFS_NAMES = %W[ RESERVED_REFS_NAMES = %W[
heads heads
...@@ -278,11 +279,12 @@ class Repository ...@@ -278,11 +279,12 @@ class Repository
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather # Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes # than SHA-1 hashes
number_commits_behind = raw_repository number_commits_behind, number_commits_ahead =
.count_commits_between(branch.dereferenced_target.sha, root_ref_hash) raw_repository.count_commits_between(
root_ref_hash,
number_commits_ahead = raw_repository branch.dereferenced_target.sha,
.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 } { behind: number_commits_behind, ahead: number_commits_ahead }
end end
...@@ -1099,6 +1101,13 @@ class Repository ...@@ -1099,6 +1101,13 @@ class Repository
@project.repository_storage_path @project.repository_storage_path
end 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 private
# TODO Generice finder, later split this on finders by Ref or Oid # TODO Generice finder, later split this on finders by Ref or Oid
......
...@@ -44,6 +44,7 @@ class Service < ActiveRecord::Base ...@@ -44,6 +44,7 @@ class Service < ActiveRecord::Base
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_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 :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
default_value_for :category, 'common' default_value_for :category, 'common'
...@@ -271,6 +272,10 @@ class Service < ActiveRecord::Base ...@@ -271,6 +272,10 @@ class Service < ActiveRecord::Base
nil nil
end end
def self.find_by_template
find_by(template: true)
end
private private
def cache_project_has_external_issue_tracker def cache_project_has_external_issue_tracker
......
...@@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy ...@@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled } 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 { logged_in_viewable }.enable :read_group
rule { guest }.policy do rule { guest }.policy do
enable :read_group enable :read_group
enable :upload_file enable :upload_file
enable :read_label
end end
rule { admin } .enable :read_group rule { admin } .enable :read_group
......
...@@ -76,6 +76,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -76,6 +76,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
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 def target_branch_tree_path
if target_branch_exists? if target_branch_exists?
project_tree_path(project, target_branch) project_tree_path(project, target_branch)
...@@ -152,6 +158,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -152,6 +158,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
user_can_collaborate_with_project? && can_be_cherry_picked? user_can_collaborate_with_project? && can_be_cherry_picked?
end end
def can_push_to_source_branch?
source_branch_exists? && user_can_push_to_source_branch?
end
private private
def conflicts def conflicts
...@@ -174,6 +184,14 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -174,6 +184,14 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end.sort.to_sentence end.sort.to_sentence
end 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? def user_can_collaborate_with_project?
can?(current_user, :push_code, project) || can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project)) (current_user && current_user.already_forked?(project))
......
...@@ -4,4 +4,5 @@ class MergeRequestBasicEntity < IssuableSidebarEntity ...@@ -4,4 +4,5 @@ class MergeRequestBasicEntity < IssuableSidebarEntity
expose :merge_error expose :merge_error
expose :state expose :state
expose :source_branch_exists?, as: :source_branch_exists expose :source_branch_exists?, as: :source_branch_exists
expose :rebase_in_progress?, as: :rebase_in_progress
end end
...@@ -23,6 +23,16 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -23,6 +23,16 @@ class MergeRequestWidgetEntity < IssuableEntity
MergeRequestMetricsEntity.new(metrics).as_json MergeRequestMetricsEntity.new(metrics).as_json
end 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 # User entities
expose :merge_user, using: UserEntity expose :merge_user, using: UserEntity
......
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
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 @@ ...@@ -84,11 +84,13 @@
= s_('Profiles|Deleting an account has the following effects:') = s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user = 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, #delete-account-modal{ data: { action_url: user_registration_path,
confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
username: current_user.username } } username: current_user.username } }
%button.btn.btn-danger.disabled
= s_('Profiles|Delete account')
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
%p %p
......
...@@ -10,4 +10,4 @@ ...@@ -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. 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 %br
%span.descr %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 @@ ...@@ -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. This way you could make sure that if this merge request would build, after merging to target branch it would also build.
%br %br
%span.descr %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 @@ ...@@ -66,16 +66,16 @@
= icon("trash-o") = icon("trash-o")
- if branch.name != @repository.root_ref - 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, default_branch: @repository.root_ref,
number_commits_ahead: number_commits_ahead } } number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side .graph-side
.bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } .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-separator
.graph-side .graph-side
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } .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 - if commit
......
...@@ -11,5 +11,5 @@ ...@@ -11,5 +11,5 @@
%label.text-danger %label.text-danger
= s_('ClusterIntegration|Remove cluster integration') = s_('ClusterIntegration|Remove cluster integration')
%p %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.') = 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: "Are you sure you want to remove cluster integration from this project? This 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: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
%h4= s_('ClusterIntegration|Enable cluster integration') %h4= s_('ClusterIntegration|Cluster integration')
.settings-content
.settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } .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') = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
%p.js-error-reason %p.js-error-reason
...@@ -11,11 +11,4 @@ ...@@ -11,11 +11,4 @@
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } .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') = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
%p %p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
- 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.')
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.table-mobile-content .table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30 .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-mobile-content= cluster.environment_scope
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
......
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group.append-bottom-20 .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 %label.append-bottom-10
= field.hidden_field :enabled, { class: 'js-toggle-input'} = field.hidden_field :enabled, { class: 'js-toggle-input'}
...@@ -12,6 +21,13 @@ ...@@ -12,6 +21,13 @@
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
- if can?(current_user, :update_cluster, @cluster) .form-group
.form-group %h5= s_('ClusterIntegration|Environment scope')
= field.submit _('Save'), class: 'btn btn-success' %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 @@ ...@@ -9,10 +9,6 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster) = 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| = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group .form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Cluster") = s_("ClusterIntegration|Cluster")
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment pattern") = s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace") = s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" } .table-section.section-10{ role: "rowheader" }
......
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
.js-cluster-application-notice .js-cluster-application-notice
.flash-container .flash-container
%section.settings.no-animate.expanded %section.settings.no-animate.expanded#cluster-integration
= render 'banner' = render 'banner'
= render 'enabled' = render 'integration_form'
.cluster-applications-table#js-cluster-applications .cluster-applications-table#js-cluster-applications
...@@ -41,6 +41,6 @@ ...@@ -41,6 +41,6 @@
%h4= _('Advanced settings') %h4= _('Advanced settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = 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 .settings-content
= render 'advanced_settings' = render 'advanced_settings'
...@@ -4,10 +4,6 @@ ...@@ -4,10 +4,6 @@
= field.label :name, s_('ClusterIntegration|Cluster name') = field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: 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| = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group .form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
......
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
- project_service - project_service
- propagate_service_template - propagate_service_template
- reactive_caching - reactive_caching
- rebase
- repository_fork - repository_fork
- repository_import - repository_import
- storage_migrator - storage_migrator
......
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
---
title: Fix gitlab-rake gitlab:import:repos import schedule
merge_request: 15931
author:
type: fixed
---
title: Allow user to rebase merge requests.
merge_request:
author:
type: added
---
title: Improve the performance for counting diverging commits. Show 999+
if it is more than 1000 commits
merge_request: 15963
author:
type: performance
---
title: Allow automatic creation of Kubernetes Integration from template
merge_request: 16104
author:
type: added
---
title: Force Auto DevOps kubectl version to 1.8.6
merge_request: 16218
author:
type: fixed
---
title: Expose project_id on /api/v4/pages/domains
merge_request: 16200
author: Luc Didry
type: changed
---
title: Add online and status attribute to runner api entity
merge_request: 11750
author:
type: added
---
title: 'API: get participants from merge_requests & issues'
merge_request: 16187
author: Brent Greeff
type: added
---
title: Fix import project url not updating project name
merge_request: 16120
author:
type: fixed
---
title: Modify `LDAP::Person` to return username value based on attributes
merge_request:
author:
type: fixed
---
title: Update redis-rack to 2.0.4
merge_request:
author:
type: other
---
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 ...@@ -96,6 +96,7 @@ constraints(ProjectUrlConstrainer.new) do
post :toggle_subscription post :toggle_subscription
post :remove_wip post :remove_wip
post :assign_related_issues post :assign_related_issues
post :rebase
scope constraints: { format: nil }, action: :show do scope constraints: { format: nil }, action: :show do
get :commits, defaults: { tab: 'commits' } get :commits, defaults: { tab: 'commits' }
......
class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :merge_requests, :rebase_commit_sha, :string
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/Datetime
class ScheduleIssuesClosedAtTypeChange < ActiveRecord::Migration class ScheduleIssuesClosedAtTypeChange < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1099,6 +1099,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do ...@@ -1099,6 +1099,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do
t.string "merge_jid" t.string "merge_jid"
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.integer "latest_merge_request_diff_id" t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree 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. ...@@ -28,19 +28,25 @@ exactly which repositories are causing the trouble.
### Check all GitLab repositories ### 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 This task loops through all repositories on the GitLab server and runs the
3 integrity checks described previously. 3 integrity checks described previously.
**Omnibus Installation** **Omnibus Installation**
``` ```
sudo gitlab-rake gitlab:repo:check sudo gitlab-rake gitlab:git:fsck
``` ```
**Source Installation** **Source Installation**
```bash ```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 ### Check repositories for a specific user
...@@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials ...@@ -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 (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. executed as part of the `gitlab:check` task, but can run independently.
See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details. 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 ...@@ -15,10 +15,10 @@ GET /projects/:id/boards
| Attribute | Type | Required | Description | | 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 ```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: Example response:
...@@ -27,6 +27,19 @@ Example response: ...@@ -27,6 +27,19 @@ Example response:
[ [
{ {
"id" : 1, "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" : [ "lists" : [
{ {
"id" : 1, "id" : 1,
...@@ -60,6 +73,74 @@ Example response: ...@@ -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 ## List board lists
Get a list of the board's lists. Get a list of the board's lists.
...@@ -71,8 +152,8 @@ GET /projects/:id/boards/:board_id/lists ...@@ -71,8 +152,8 @@ GET /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description | | 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 |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists 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 ...@@ -122,9 +203,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description | | 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 |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
| `list_id`| integer | yes | The ID of a board's list | | `list_id`| integer | yes | The ID of a board's list |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 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 ...@@ -154,9 +235,9 @@ POST /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description | | 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 |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
| `label_id` | integer | yes | The ID of a label | | `label_id` | integer | yes | The ID of a label |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5 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 ...@@ -186,10 +267,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description | | 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 |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list | | `list_id` | integer | yes | The ID of a board's list |
| `position` | integer | yes | The position of the list | | `position` | integer | yes | The position of the list |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2 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 ...@@ -219,9 +300,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description | | 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 |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list | | `list_id` | integer | yes | The ID of a board's list |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 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: ...@@ -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 on issues
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
......
...@@ -308,6 +308,41 @@ Parameters: ...@@ -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 single MR commits
Get a list of merge request commits. Get a list of merge request commits.
......
...@@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a ...@@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
{ {
"domain": "ssl.domain.example", "domain": "ssl.domain.example",
"url": "https://ssl.domain.example", "url": "https://ssl.domain.example",
"project_id": 1337,
"certificate": { "certificate": {
"expired": false, "expired": false,
"expiration": "2020-04-12T14:32:00.000Z" "expiration": "2020-04-12T14:32:00.000Z"
......
...@@ -30,14 +30,18 @@ Example response: ...@@ -30,14 +30,18 @@ Example response:
"description": "test-1-20150125", "description": "test-1-20150125",
"id": 6, "id": 6,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": true,
"status": "online"
}, },
{ {
"active": true, "active": true,
"description": "test-2-20150125", "description": "test-2-20150125",
"id": 8, "id": 8,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": false,
"status": "offline"
} }
] ]
``` ```
...@@ -69,28 +73,36 @@ Example response: ...@@ -69,28 +73,36 @@ Example response:
"description": "shared-runner-1", "description": "shared-runner-1",
"id": 1, "id": 1,
"is_shared": true, "is_shared": true,
"name": null "name": null,
"online": true,
"status": "online"
}, },
{ {
"active": true, "active": true,
"description": "shared-runner-2", "description": "shared-runner-2",
"id": 3, "id": 3,
"is_shared": true, "is_shared": true,
"name": null "name": null,
"online": false
"status": "offline"
}, },
{ {
"active": true, "active": true,
"description": "test-1-20150125", "description": "test-1-20150125",
"id": 6, "id": 6,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": true
"status": "paused"
}, },
{ {
"active": true, "active": true,
"description": "test-2-20150125", "description": "test-2-20150125",
"id": 8, "id": 8,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": false,
"status": "offline"
} }
] ]
``` ```
...@@ -122,6 +134,8 @@ Example response: ...@@ -122,6 +134,8 @@ Example response:
"is_shared": false, "is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z", "contacted_at": "2016-01-25T16:39:48.066Z",
"name": null, "name": null,
"online": true,
"status": "online",
"platform": null, "platform": null,
"projects": [ "projects": [
{ {
...@@ -176,6 +190,8 @@ Example response: ...@@ -176,6 +190,8 @@ Example response:
"is_shared": false, "is_shared": false,
"contacted_at": "2016-01-25T16:39:48.066Z", "contacted_at": "2016-01-25T16:39:48.066Z",
"name": null, "name": null,
"online": true,
"status": "online",
"platform": null, "platform": null,
"projects": [ "projects": [
{ {
...@@ -327,14 +343,18 @@ Example response: ...@@ -327,14 +343,18 @@ Example response:
"description": "test-2-20150125", "description": "test-2-20150125",
"id": 8, "id": 8,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": false,
"status": "offline"
}, },
{ {
"active": true, "active": true,
"description": "development_runner", "description": "development_runner",
"id": 5, "id": 5,
"is_shared": true, "is_shared": true,
"name": null "name": null,
"online": true
"status": "paused"
} }
] ]
``` ```
...@@ -364,7 +384,9 @@ Example response: ...@@ -364,7 +384,9 @@ Example response:
"description": "test-2016-02-01", "description": "test-2016-02-01",
"id": 9, "id": 9,
"is_shared": false, "is_shared": false,
"name": null "name": null,
"online": true,
"status": "online"
} }
``` ```
......
...@@ -69,7 +69,7 @@ PUT /application/settings ...@@ -69,7 +69,7 @@ PUT /application/settings
| `after_sign_up_text` | string | no | Text shown to the user after signing up | | `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_api_key` | string | no | API key for akismet spam protection |
| `akismet_enabled` | boolean | no | Enable or disable 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_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_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. | | `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 ...@@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do
end 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) [Return to Development documentation](README.md)
# 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. ...@@ -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 ## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) 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. ...@@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead.
[Capybara]: https://github.com/teamcapybara/capybara [Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/ [Karma]: http://karma-runner.github.io/
[Jasmine]: https://jasmine.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. ...@@ -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 The actual test scenarios and steps are [part of GitLab Rails] so that they're
always in-sync with the codebase. 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 [multiple pieces]: ../architecture.md#components
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell [GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse [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 ...@@ -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 commits will be created and all merges are fast-forwarded, which means that
merging is only allowed if the branch could be fast-forwarded. 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 ## Use cases
...@@ -25,7 +25,7 @@ merge commits. In such cases, the fast-forward merge is the perfect candidate. ...@@ -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 Now, when you visit the merge request page, you will be able to accept it
**only if a fast-forward merge is possible**. **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 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. source branch locally before you will be able to do a fast-forward merge.
......
...@@ -22,3 +22,20 @@ Feature: Project Ff Merge Requests ...@@ -22,3 +22,20 @@ Feature: Project Ff Merge Requests
Then I should see ff-only merge button Then I should see ff-only merge button
When I accept this merge request When I accept this merge request
Then I should see merged 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 ...@@ -17,6 +17,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
author: project.users.first) author: project.users.first)
end end
step 'merge request is mergeable' do
expect(page).to have_button 'Merge'
end
step 'I should see ff-only merge button' do 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_content "Fast-forward merge without a merge commit"
expect(page).to have_button 'Merge' expect(page).to have_button 'Merge'
...@@ -45,6 +49,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps ...@@ -45,6 +49,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
project.save! project.save!
end end
step 'I should see rebase button' do
expect(page).to have_button "Rebase"
end
step 'merge request "Bug NS-05" is rebased' do step 'merge request "Bug NS-05" is rebased' do
merge_request.source_branch = 'flatten-dir' merge_request.source_branch = 'flatten-dir'
merge_request.target_branch = 'improve/awesome' merge_request.target_branch = 'improve/awesome'
...@@ -59,6 +67,20 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps ...@@ -59,6 +67,20 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
merge_request.save! merge_request.save!
end 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 def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end end
......
...@@ -119,6 +119,7 @@ module API ...@@ -119,6 +119,7 @@ module API
mount ::API::Features mount ::API::Features
mount ::API::Files mount ::API::Files
mount ::API::Groups mount ::API::Groups
mount ::API::GroupMilestones
mount ::API::Internal mount ::API::Internal
mount ::API::Issues mount ::API::Issues
mount ::API::Jobs mount ::API::Jobs
...@@ -129,8 +130,6 @@ module API ...@@ -129,8 +130,6 @@ module API
mount ::API::Members mount ::API::Members
mount ::API::MergeRequestDiffs mount ::API::MergeRequestDiffs
mount ::API::MergeRequests mount ::API::MergeRequests
mount ::API::ProjectMilestones
mount ::API::GroupMilestones
mount ::API::Namespaces mount ::API::Namespaces
mount ::API::Notes mount ::API::Notes
mount ::API::NotificationSettings mount ::API::NotificationSettings
...@@ -139,6 +138,7 @@ module API ...@@ -139,6 +138,7 @@ module API
mount ::API::PipelineSchedules mount ::API::PipelineSchedules
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectMilestones
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::ProtectedBranches mount ::API::ProtectedBranches
mount ::API::Repositories mount ::API::Repositories
......
module API module API
class Boards < Grape::API class Boards < Grape::API
include BoardsResponses
include PaginationParams include PaginationParams
before { authenticate! } before { authenticate! }
helpers do
def board_parent
user_project
end
end
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all project boards' do segment ':id/boards' do
detail 'This feature was introduced in 8.13' desc 'Get all project boards' do
success Entities::Board detail 'This feature was introduced in 8.13'
end success Entities::Board
params do end
use :pagination params do
end use :pagination
get ':id/boards' do end
authorize!(:read_board, user_project) get '/' do
present paginate(user_project.boards), with: Entities::Board 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 end
params do params do
requires :board_id, type: Integer, desc: 'The ID of a board' requires :board_id, type: Integer, desc: 'The ID of a board'
end end
segment ':id/boards/:board_id' do 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 desc 'Get the lists of a project board' do
detail 'Does not include `done` list. This feature was introduced in 8.13' detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List success Entities::List
...@@ -72,22 +73,13 @@ module API ...@@ -72,22 +73,13 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label' requires :label_id, type: Integer, desc: 'The ID of an existing label'
end end
post '/lists' do 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) render_api_error!({ error: 'Label not found!' }, 400)
end end
authorize!(:admin_list, user_project) authorize!(:admin_list, user_project)
service = ::Boards::Lists::CreateService.new(user_project, current_user, create_list
{ label_id: params[:label_id] })
list = service.execute(project_board)
if list.valid?
present list, with: Entities::List
else
render_validation_error!(list)
end
end end
desc 'Moves a board list to a new position' do desc 'Moves a board list to a new position' do
...@@ -99,18 +91,11 @@ module API ...@@ -99,18 +91,11 @@ module API
requires :position, type: Integer, desc: 'The position of the list' requires :position, type: Integer, desc: 'The position of the list'
end end
put '/lists/:list_id' do 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) authorize!(:admin_list, user_project)
service = ::Boards::Lists::MoveService.new(user_project, current_user, move_list(list)
{ position: params[:position] })
if service.execute(list)
present list, with: Entities::List
else
render_api_error!({ error: "List could not be moved!" }, 400)
end
end end
desc 'Delete a board list' do desc 'Delete a board list' do
...@@ -124,12 +109,7 @@ module API ...@@ -124,12 +109,7 @@ module API
authorize!(:admin_list, user_project) authorize!(:admin_list, user_project)
list = board_lists.find(params[:list_id]) list = board_lists.find(params[:list_id])
destroy_conditionally!(list) do |list| destroy_list(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
end end
end end
end end
......
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 ...@@ -791,6 +791,8 @@ module API
class Board < Grape::Entity class Board < Grape::Entity
expose :id expose :id
expose :project, using: Entities::BasicProjectDetails
expose :lists, using: Entities::List do |board| expose :lists, using: Entities::List do |board|
board.lists.destroyable board.lists.destroyable
end end
...@@ -862,6 +864,8 @@ module API ...@@ -862,6 +864,8 @@ module API
expose :active expose :active
expose :is_shared expose :is_shared
expose :name expose :name
expose :online?, as: :online
expose :status
end end
class RunnerDetails < Runner class RunnerDetails < Runner
...@@ -1133,6 +1137,7 @@ module API ...@@ -1133,6 +1137,7 @@ module API
class PagesDomainBasic < Grape::Entity class PagesDomainBasic < Grape::Entity
expose :domain expose :domain
expose :url expose :url
expose :project_id
expose :certificate, expose :certificate,
as: :certificate_expiration, as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? }, if: ->(pages_domain, _) { pages_domain.certificate? },
......
...@@ -74,8 +74,15 @@ module API ...@@ -74,8 +74,15 @@ module API
page || not_found!('Wiki Page') page || not_found!('Wiki Page')
end end
def available_labels def available_labels_for(label_parent)
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute 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 end
def find_user(id) def find_user(id)
...@@ -141,7 +148,9 @@ module API ...@@ -141,7 +148,9 @@ module API
end end
def find_project_label(id) 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') label || not_found!('Label')
end end
......
...@@ -277,6 +277,19 @@ module API ...@@ -277,6 +277,19 @@ module API
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
end 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 desc 'Get the user agent details for an issue' do
success Entities::UserAgentDetail success Entities::UserAgentDetail
end end
......
...@@ -15,7 +15,7 @@ module API ...@@ -15,7 +15,7 @@ module API
use :pagination use :pagination
end end
get ':id/labels' do 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 end
desc 'Create a new label' do desc 'Create a new label' do
...@@ -30,7 +30,7 @@ module API ...@@ -30,7 +30,7 @@ module API
post ':id/labels' do post ':id/labels' do
authorize! :admin_label, user_project 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 conflict!('Label already exists') if label
priority = params.delete(:priority) priority = params.delete(:priority)
......
...@@ -185,6 +185,16 @@ module API ...@@ -185,6 +185,16 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end 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 desc 'Get the commits of a merge request' do
success Entities::Commit success Entities::Commit
end end
......
...@@ -11,7 +11,7 @@ module API ...@@ -11,7 +11,7 @@ module API
success ::API::Entities::Label success ::API::Entities::Label
end end
get ':id/labels' do 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 end
desc 'Delete an existing label' do desc 'Delete an existing label' do
......
...@@ -2,16 +2,7 @@ module Banzai ...@@ -2,16 +2,7 @@ module Banzai
module Filter module Filter
class MermaidFilter < HTML::Pipeline::Filter class MermaidFilter < HTML::Pipeline::Filter
def call def call
doc.css('pre[lang="mermaid"]').add_class('mermaid') doc.css('pre[lang="mermaid"] > code').add_class('js-render-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 doc
end end
......
...@@ -14,14 +14,7 @@ module Gitlab ...@@ -14,14 +14,7 @@ module Gitlab
ENCODING_CONFIDENCE_THRESHOLD = 50 ENCODING_CONFIDENCE_THRESHOLD = 50
def encode!(message) def encode!(message)
return nil unless message.respond_to?(:force_encoding) message = force_encode_utf8(message)
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")
return message if message.valid_encoding? return message if message.valid_encoding?
# return message if message type is binary # return message if message type is binary
...@@ -35,6 +28,8 @@ module Gitlab ...@@ -35,6 +28,8 @@ module Gitlab
# encode and clean the bad chars # encode and clean the bad chars
message.replace clean(message) message.replace clean(message)
rescue ArgumentError
return nil
rescue rescue
encoding = detect ? detect[:encoding] : "unknown" encoding = detect ? detect[:encoding] : "unknown"
"--broken encoding: #{encoding}" "--broken encoding: #{encoding}"
...@@ -54,8 +49,8 @@ module Gitlab ...@@ -54,8 +49,8 @@ module Gitlab
end end
def encode_utf8(message) def encode_utf8(message)
return nil unless message.is_a?(String) message = force_encode_utf8(message)
return message if message.encoding == Encoding::UTF_8 && message.valid_encoding? return message if message.valid_encoding?
detect = CharlockHolmes::EncodingDetector.detect(message) detect = CharlockHolmes::EncodingDetector.detect(message)
if detect && detect[:encoding] if detect && detect[:encoding]
...@@ -69,6 +64,8 @@ module Gitlab ...@@ -69,6 +64,8 @@ module Gitlab
else else
clean(message) clean(message)
end end
rescue ArgumentError
return nil
end end
def encode_binary(s) def encode_binary(s)
...@@ -83,6 +80,15 @@ module Gitlab ...@@ -83,6 +80,15 @@ module Gitlab
private 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) def clean(message)
message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "") message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
.encode("UTF-8") .encode("UTF-8")
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
def ref_name(ref) def ref_name(ref)
encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '')
end end
def branch_name(ref) def branch_name(ref)
......
...@@ -50,10 +50,19 @@ module Gitlab ...@@ -50,10 +50,19 @@ module Gitlab
# to the caller to limit the number of blobs and blob_size_limit. # to the caller to limit the number of blobs and blob_size_limit.
# #
# Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798 # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798
def batch(repository, blob_references, blob_size_limit: nil) def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
blob_size_limit ||= MAX_DATA_DISPLAY_SIZE Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled|
blob_references.map do |sha, path| if is_enabled
find_by_rugged(repository, sha, path, limit: blob_size_limit) Gitlab::GitalyClient.allow_n_plus_1_calls do
blob_references.map do |sha, path|
find_by_gitaly(repository, sha, path, limit: blob_size_limit)
end
end
else
blob_references.map do |sha, path|
find_by_rugged(repository, sha, path, limit: blob_size_limit)
end
end
end end
end end
...@@ -122,13 +131,23 @@ module Gitlab ...@@ -122,13 +131,23 @@ module Gitlab
) )
end end
def find_by_gitaly(repository, sha, path) def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
path = path.sub(/\A\/*/, '') path = path.sub(/\A\/*/, '')
path = '/' if path.empty? path = '/' if path.empty?
name = File.basename(path) name = File.basename(path)
entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE)
# Gitaly will think that setting the limit to 0 means unlimited, while
# the client might only need the metadata and thus set the limit to 0.
# In this method we'll then set the limit to 1, but clear the byte of data
# that we got back so for the outside world it looks like the limit was
# actually 0.
req_limit = limit == 0 ? 1 : limit
entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit)
return unless entry return unless entry
entry.data = "" if limit == 0
case entry.type case entry.type
when :COMMIT when :COMMIT
new( new(
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment