Commit 34fd07f6 authored by Clement Ho's avatar Clement Ho

Merge branch 'new-mr-repo-editor' into 'master'

Add create merge checkbox.

Closes #38626

See merge request gitlab-org/gitlab-ce!14665
parents 3555252d c3195e83
...@@ -15,6 +15,7 @@ const Api = { ...@@ -15,6 +15,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath)
...@@ -123,6 +124,19 @@ const Api = { ...@@ -123,6 +124,19 @@ const Api = {
}); });
}, },
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', id)
.replace(':branch', branch);
return this.wrapAjaxCall({
url,
type: 'GET',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
});
},
// Return text for a specific license // Return text for a specific license
licenseText(key, data, callback) { licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath) const url = Api.buildUrl(Api.licensePath)
......
...@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) { ...@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1); return hashIndex === -1 ? null : url.substring(hashIndex + 1);
}; };
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) { export function visitUrl(url, external = false) {
...@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) { ...@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {
otherWindow.opener = null; otherWindow.opener = null;
otherWindow.location = url; otherWindow.location = url;
} else { } else {
document.location.href = url; window.location.href = url;
} }
} }
......
...@@ -3,11 +3,17 @@ import Flash from '../../flash'; ...@@ -3,11 +3,17 @@ import Flash from '../../flash';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
export default { export default {
mixins: [RepoMixin],
data: () => Store, data: () => Store,
mixins: [RepoMixin], components: {
PopupDialog,
},
computed: { computed: {
showCommitable() { showCommitable() {
...@@ -28,7 +34,16 @@ export default { ...@@ -28,7 +34,16 @@ export default {
}, },
methods: { methods: {
makeCommit() { commitToNewBranch(status) {
if (status) {
this.showNewBranchDialog = false;
this.tryCommit(null, true, true);
} else {
// reset the state
}
},
makeCommit(newBranch) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
...@@ -36,19 +51,63 @@ export default { ...@@ -36,19 +51,63 @@ export default {
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = { const payload = {
branch: Store.currentBranch, branch,
commit_message: commitMessage, commit_message: commitMessage,
actions, actions,
}; };
Store.submitCommitsLoading = true; if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(this.resetCommitState) .then(() => {
.catch(() => Flash('An error occurred while committing your changes')); this.resetCommitState();
if (this.startNewMR) {
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
},
tryCommit(e, skipBranchCheck = false, newBranch = false) {
if (skipBranchCheck) {
this.makeCommit(newBranch);
} else {
Store.setBranchHash()
.then(() => {
if (Store.branchChanged) {
Store.showNewBranchDialog = true;
return;
}
this.makeCommit(newBranch);
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
}
},
redirectToNewMr(branch) {
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
},
redirectToBranch(branch) {
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
}, },
resetCommitState() { resetCommitState() {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = []; this.changedFiles = [];
this.commitMessage = ''; this.commitMessage = '';
this.editMode = false; this.editMode = false;
...@@ -62,9 +121,17 @@ export default { ...@@ -62,9 +121,17 @@ export default {
<div <div
v-if="showCommitable" v-if="showCommitable"
id="commit-area"> id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@submit="commitToNewBranch"
/>
<form <form
class="form-horizontal" class="form-horizontal"
@submit.prevent="makeCommit"> @submit.prevent="tryCommit">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files"> <label class="col-md-4 control-label staged-files">
...@@ -117,7 +184,7 @@ export default { ...@@ -117,7 +184,7 @@ export default {
class="btn btn-success"> class="btn btn-success">
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin" class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true" aria-hidden="true"
aria-label="loading"> aria-label="loading">
</i> </i>
...@@ -126,6 +193,14 @@ export default { ...@@ -126,6 +193,14 @@ export default {
</span> </span>
</button> </button>
</div> </div>
<div class="col-md-offset-4 col-md-6">
<div class="checkbox">
<label>
<input type="checkbox" v-model="startNewMR">
<span>Start a <strong>new merge request</strong> with these changes</span>
</label>
</div>
</div>
</fieldset> </fieldset>
</form> </form>
</div> </div>
......
...@@ -31,8 +31,11 @@ function setInitialStore(data) { ...@@ -31,8 +31,11 @@ function setInitialStore(data) {
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit; Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch; Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable(); Store.checkIsCommitable();
Store.setBranchHash();
} }
function initRepo(el) { function initRepo(el) {
......
...@@ -64,6 +64,10 @@ const RepoService = { ...@@ -64,6 +64,10 @@ const RepoService = {
return urlArray.join('/'); return urlArray.join('/');
}, },
getBranch() {
return Api.branchSingle(Store.projectId, Store.currentBranch);
},
commitFiles(payload) { commitFiles(payload) {
return Api.commitMultiple(Store.projectId, payload) return Api.commitMultiple(Store.projectId, payload)
.then(this.commitFlash); .then(this.commitFlash);
......
...@@ -23,6 +23,7 @@ const RepoStore = { ...@@ -23,6 +23,7 @@ const RepoStore = {
title: '', title: '',
status: false, status: false,
}, },
showNewBranchDialog: false,
activeFile: Helper.getDefaultActiveFile(), activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0, activeFileIndex: 0,
activeLine: -1, activeLine: -1,
...@@ -31,6 +32,12 @@ const RepoStore = { ...@@ -31,6 +32,12 @@ const RepoStore = {
isCommitable: false, isCommitable: false,
binary: false, binary: false,
currentBranch: '', currentBranch: '',
startNewMR: false,
currentHash: '',
currentShortHash: '',
customBranchURL: '',
newMrTemplateUrl: '',
branchChanged: false,
commitMessage: '', commitMessage: '',
binaryTypes: { binaryTypes: {
png: false, png: false,
...@@ -49,6 +56,17 @@ const RepoStore = { ...@@ -49,6 +56,17 @@ const RepoStore = {
}); });
}, },
setBranchHash() {
return Service.getBranch()
.then((data) => {
if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
RepoStore.branchChanged = true;
}
RepoStore.currentHash = data.commit.id;
RepoStore.currentShortHash = data.commit.short_id;
});
},
// mutations // mutations
checkIsCommitable() { checkIsCommitable() {
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
......
...@@ -16,6 +16,11 @@ export default { ...@@ -16,6 +16,11 @@ export default {
required: false, required: false,
default: 'primary', default: 'primary',
}, },
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: { closeButtonLabel: {
type: String, type: String,
required: false, required: false,
...@@ -33,6 +38,11 @@ export default { ...@@ -33,6 +38,11 @@ export default {
[`btn-${this.kind}`]: true, [`btn-${this.kind}`]: true,
}; };
}, },
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
}, },
methods: { methods: {
...@@ -70,7 +80,8 @@ export default { ...@@ -70,7 +80,8 @@ export default {
<div class="modal-footer"> <div class="modal-footer">
<button <button
type="button" type="button"
class="btn btn-default" class="btn"
:class="btnCancelKindClass"
@click="emitSubmit(false)"> @click="emitSubmit(false)">
{{closeButtonLabel}} {{closeButtonLabel}}
</button> </button>
......
...@@ -3,5 +3,7 @@ ...@@ -3,5 +3,7 @@
refs_url: refs_project_path(project, format: :json), refs_url: refs_project_path(project, format: :json),
project_url: project_path(project), project_url: project_path(project),
project_id: project.id, project_id: project.id,
blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s, can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
---
title: 'Repo Editor: Add option to start a new MR directly from comit section'
merge_request: 14665
author:
type: added
export default (time = 0) => new Promise((resolve) => {
setTimeout(resolve, time);
});
...@@ -2,29 +2,13 @@ import Vue from 'vue'; ...@@ -2,29 +2,13 @@ import Vue from 'vue';
import repoCommitSection from '~/repo/components/repo_commit_section.vue'; import repoCommitSection from '~/repo/components/repo_commit_section.vue';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
import RepoService from '~/repo/services/repo_service'; import RepoService from '~/repo/services/repo_service';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('RepoCommitSection', () => { describe('RepoCommitSection', () => {
const branch = 'master'; const branch = 'master';
const projectUrl = 'projectUrl'; const projectUrl = 'projectUrl';
const changedFiles = [{ let changedFiles;
id: 0, let openedFiles;
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
path: 'dir/file0.ext',
newContent: 'a',
}, {
id: 1,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
path: 'dir/file1.ext',
newContent: 'b',
}];
const openedFiles = changedFiles.concat([{
id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext',
changed: false,
}]);
RepoStore.projectUrl = projectUrl; RepoStore.projectUrl = projectUrl;
...@@ -34,6 +18,29 @@ describe('RepoCommitSection', () => { ...@@ -34,6 +18,29 @@ describe('RepoCommitSection', () => {
return new RepoCommitSection().$mount(el); return new RepoCommitSection().$mount(el);
} }
beforeEach(() => {
// Create a copy for each test because these can get modified directly
changedFiles = [{
id: 0,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
path: 'dir/file0.ext',
newContent: 'a',
}, {
id: 1,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
path: 'dir/file1.ext',
newContent: 'b',
}];
openedFiles = changedFiles.concat([{
id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext',
changed: false,
}]);
});
it('renders a commit section', () => { it('renders a commit section', () => {
RepoStore.isCommitable = true; RepoStore.isCommitable = true;
RepoStore.currentBranch = branch; RepoStore.currentBranch = branch;
...@@ -85,55 +92,104 @@ describe('RepoCommitSection', () => { ...@@ -85,55 +92,104 @@ describe('RepoCommitSection', () => {
expect(vm.$el.innerHTML).toBeFalsy(); expect(vm.$el.innerHTML).toBeFalsy();
}); });
it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => { describe('when submitting', () => {
let el;
let vm;
const projectId = 'projectId'; const projectId = 'projectId';
const commitMessage = 'commitMessage'; const commitMessage = 'commitMessage';
RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles;
RepoStore.projectId = projectId;
// We need to append to body to get form `submit` events working beforeEach((done) => {
// Otherwise we run into, "Form submission canceled because the form is not connected" RepoStore.isCommitable = true;
// See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm RepoStore.currentBranch = branch;
const el = document.createElement('div'); RepoStore.targetBranch = branch;
document.body.appendChild(el); RepoStore.openedFiles = openedFiles;
RepoStore.projectId = projectId;
const vm = createComponent(el);
const commitMessageEl = vm.$el.querySelector('#commit-message'); // We need to append to body to get form `submit` events working
const submitCommit = vm.$refs.submitCommit; // Otherwise we run into, "Form submission canceled because the form is not connected"
// See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
el = document.createElement('div');
document.body.appendChild(el);
vm = createComponent(el);
vm.commitMessage = commitMessage;
spyOn(vm, 'tryCommit').and.callThrough();
spyOn(vm, 'redirectToNewMr').and.stub();
spyOn(vm, 'redirectToBranch').and.stub();
spyOn(RepoService, 'commitFiles').and.returnValue(Promise.resolve());
spyOn(RepoService, 'getBranch').and.returnValue(Promise.resolve({
commit: {
id: 1,
short_id: 1,
},
}));
// Wait for the vm data to be in place
Vue.nextTick(() => {
done();
});
});
vm.commitMessage = commitMessage; afterEach(() => {
vm.$destroy();
el.remove();
});
Vue.nextTick(() => { it('shows commit message', () => {
const commitMessageEl = vm.$el.querySelector('#commit-message');
expect(commitMessageEl.value).toBe(commitMessage); expect(commitMessageEl.value).toBe(commitMessage);
expect(submitCommit.disabled).toBeFalsy(); });
spyOn(vm, 'makeCommit').and.callThrough(); it('allows you to submit', () => {
spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve()); const submitCommit = vm.$refs.submitCommit;
expect(submitCommit.disabled).toBeFalsy();
});
it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
const submitCommit = vm.$refs.submitCommit;
submitCommit.click(); submitCommit.click();
Vue.nextTick(() => { // Wait for the branch check to finish
expect(vm.makeCommit).toHaveBeenCalled(); getSetTimeoutPromise()
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy(); .then(() => Vue.nextTick())
.then(() => {
const args = RepoService.commitFiles.calls.allArgs()[0]; expect(vm.tryCommit).toHaveBeenCalled();
const { commit_message, actions, branch: payloadBranch } = args[0]; expect(submitCommit.querySelector('.js-commit-loading-icon')).toBeTruthy();
expect(vm.redirectToBranch).toHaveBeenCalled();
expect(commit_message).toBe(commitMessage);
expect(actions.length).toEqual(2); const args = RepoService.commitFiles.calls.allArgs()[0];
expect(payloadBranch).toEqual(branch); const { commit_message, actions, branch: payloadBranch } = args[0];
expect(actions[0].action).toEqual('update');
expect(actions[1].action).toEqual('update'); expect(commit_message).toBe(commitMessage);
expect(actions[0].content).toEqual(openedFiles[0].newContent); expect(actions.length).toEqual(2);
expect(actions[1].content).toEqual(openedFiles[1].newContent); expect(payloadBranch).toEqual(branch);
expect(actions[0].file_path).toEqual(openedFiles[0].path); expect(actions[0].action).toEqual('update');
expect(actions[1].file_path).toEqual(openedFiles[1].path); expect(actions[1].action).toEqual('update');
expect(actions[0].content).toEqual(openedFiles[0].newContent);
expect(actions[1].content).toEqual(openedFiles[1].newContent);
expect(actions[0].file_path).toEqual(openedFiles[0].path);
expect(actions[1].file_path).toEqual(openedFiles[1].path);
})
.then(done)
.catch(done.fail);
});
done(); it('redirects to MR creation page if start new MR checkbox checked', (done) => {
}); vm.startNewMR = true;
Vue.nextTick()
.then(() => {
const submitCommit = vm.$refs.submitCommit;
submitCommit.click();
})
// Wait for the branch check to finish
.then(() => getSetTimeoutPromise())
.then(() => {
expect(vm.redirectToNewMr).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
}); });
}); });
...@@ -143,6 +199,7 @@ describe('RepoCommitSection', () => { ...@@ -143,6 +199,7 @@ describe('RepoCommitSection', () => {
const vm = { const vm = {
submitCommitsLoading: true, submitCommitsLoading: true,
changedFiles: new Array(10), changedFiles: new Array(10),
openedFiles: new Array(3),
commitMessage: 'commitMessage', commitMessage: 'commitMessage',
editMode: true, editMode: true,
}; };
......
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