Commit f62359c2 authored by Tim Zallmann's avatar Tim Zallmann

Basic Setup for MR Showing

parent 06afa5a3
...@@ -10,6 +10,9 @@ const Api = { ...@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
mergeRequestChangesPath:
'/api/:version/projects/:id/merge_requests/:mrid/changes',
groupLabelsPath: '/groups/:namespace_path/-/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
...@@ -22,25 +25,27 @@ const Api = { ...@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
.replace(':id', groupId); return axios.get(url).then(({ data }) => {
return axios.get(url) callback(data);
.then(({ data }) => {
callback(data);
return data; return data;
}); });
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
groups(query, options, callback = $.noop) { groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath); const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, { return axios
params: Object.assign({ .get(url, {
search: query, params: Object.assign(
per_page: 20, {
}, options), search: query,
}) per_page: 20,
},
options,
),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -51,12 +56,13 @@ const Api = { ...@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query // Return namespaces list. Filtered by query
namespaces(query, callback) { namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath); const url = Api.buildUrl(Api.namespacesPath);
return axios.get(url, { return axios
params: { .get(url, {
search: query, params: {
per_page: 20, search: query,
}, per_page: 20,
}) },
})
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
...@@ -73,9 +79,10 @@ const Api = { ...@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true; defaults.membership = true;
} }
return axios.get(url, { return axios
params: Object.assign(defaults, options), .get(url, {
}) params: Object.assign(defaults, options),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -85,8 +92,28 @@ const Api = { ...@@ -85,8 +92,28 @@ const Api = {
// Return single project // Return single project
project(projectPath) { project(projectPath) {
const url = Api.buildUrl(Api.projectPath) const url = Api.buildUrl(Api.projectPath).replace(
.replace(':id', encodeURIComponent(projectPath)); ':id',
encodeURIComponent(projectPath),
);
return axios.get(url);
},
// Return Merge Request for project
mergeRequest(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
// Return Merge Request Changes
mergeRequestChanges(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url); return axios.get(url);
}, },
...@@ -99,33 +126,39 @@ const Api = { ...@@ -99,33 +126,39 @@ const Api = {
.replace(':namespace_path', namespacePath) .replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath); .replace(':project_path', projectPath);
} else { } else {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); url = Api.buildUrl(Api.groupLabelsPath).replace(
':namespace_path',
namespacePath,
);
} }
return axios.post(url, { return axios
label: data, .post(url, {
}) label: data,
})
.then(res => callback(res.data)) .then(res => callback(res.data))
.catch(e => callback(e.response.data)); .catch(e => callback(e.response.data));
}, },
// Return group projects list. Filtered by query // Return group projects list. Filtered by query
groupProjects(groupId, query, callback) { groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath) const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
.replace(':id', groupId); return axios
return axios.get(url, { .get(url, {
params: { params: {
search: query, search: query,
per_page: 20, per_page: 20,
}, },
}) })
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
commitMultiple(id, data) { commitMultiple(id, data) {
// 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 url = Api.buildUrl(Api.commitPath) const url = Api.buildUrl(Api.commitPath).replace(
.replace(':id', encodeURIComponent(id)); ':id',
encodeURIComponent(id),
);
return axios.post(url, JSON.stringify(data), { return axios.post(url, JSON.stringify(data), {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
...@@ -136,39 +169,34 @@ const Api = { ...@@ -136,39 +169,34 @@ const Api = {
branchSingle(id, branch) { branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath) const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id)) .replace(':id', encodeURIComponent(id))
.replace(':branch', branch); .replace(':branch', encodeURIComponent(branch));
return axios.get(url); return axios.get(url);
}, },
// 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).replace(':key', key);
.replace(':key', key); return axios
return axios.get(url, { .get(url, {
params: data, params: data,
}) })
.then(res => callback(res.data)); .then(res => callback(res.data));
}, },
gitignoreText(key, callback) { gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath) const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
.replace(':key', key); return axios.get(url).then(({ data }) => callback(data));
return axios.get(url)
.then(({ data }) => callback(data));
}, },
gitlabCiYml(key, callback) { gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath) const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
.replace(':key', key); return axios.get(url).then(({ data }) => callback(data));
return axios.get(url)
.then(({ data }) => callback(data));
}, },
dockerfileYml(key, callback) { dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
return axios.get(url) return axios.get(url).then(({ data }) => callback(data));
.then(({ data }) => callback(data));
}, },
issueTemplate(namespacePath, projectPath, key, type, callback) { issueTemplate(namespacePath, projectPath, key, type, callback) {
...@@ -177,7 +205,8 @@ const Api = { ...@@ -177,7 +205,8 @@ const Api = {
.replace(':type', type) .replace(':type', type)
.replace(':project_path', projectPath) .replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath); .replace(':namespace_path', namespacePath);
return axios.get(url) return axios
.get(url)
.then(({ data }) => callback(null, data)) .then(({ data }) => callback(null, data))
.catch(callback); .catch(callback);
}, },
...@@ -185,10 +214,13 @@ const Api = { ...@@ -185,10 +214,13 @@ const Api = {
users(query, options) { users(query, options) {
const url = Api.buildUrl(this.usersPath); const url = Api.buildUrl(this.usersPath);
return axios.get(url, { return axios.get(url, {
params: Object.assign({ params: Object.assign(
search: query, {
per_page: 20, search: query,
}, options), per_page: 20,
},
options,
),
}); });
}, },
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
Icon, Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
}, },
props: { hasMergeRequest: {
hasChanges: { type: Boolean,
type: Boolean, required: false,
required: false, default: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
}, },
methods: { viewer: {
changeMode(mode) { type: String,
this.$emit('click', mode); required: true,
},
}, },
}; showShadow: {
type: Boolean,
required: true,
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script> </script>
<template> <template>
...@@ -43,7 +48,10 @@ ...@@ -43,7 +48,10 @@
}" }"
data-toggle="dropdown" data-toggle="dropdown"
> >
<template v-if="viewer === 'editor'"> <template v-if="viewer === 'mrdiff'">
{{ __('Reviewing (merge request)') }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }} {{ __('Editing') }}
</template> </template>
<template v-else> <template v-else>
...@@ -57,6 +65,21 @@ ...@@ -57,6 +65,21 @@
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul> <ul>
<li v-if="hasMergeRequest">
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing (merge request)') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes of the merge request') }}
</span>
</a>
</li>
<li v-if="hasMergeRequest" role="separator" class="divider"></li>
<li> <li>
<a <a
href="#" href="#"
......
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue'; import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
components: { components: {
ideSidebar, ideSidebar,
ideContextbar, ideContextbar,
repoTabs, repoTabs,
repoFileButtons, repoFileButtons,
ideStatusBar, ideStatusBar,
repoEditor, repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
}, },
props: { noChangesStateSvgPath: {
emptyStateSvgPath: { type: String,
type: String, required: true,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
}, },
computed: { committedStateSvgPath: {
...mapState(['changedFiles', 'openFiles', 'viewer']), type: String,
...mapGetters(['activeFile', 'hasChanges']), required: true,
}, },
mounted() { },
const returnValue = 'Are you sure you want to lose unsaved changes?'; computed: {
window.onbeforeunload = e => { ...mapState(['changedFiles', 'openFiles', 'viewer']),
if (!this.changedFiles.length) return undefined; ...mapGetters(['activeFile', 'hasChanges', 'hasMergeRequest']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, { Object.assign(e, {
returnValue, returnValue,
}); });
return returnValue; return returnValue;
}; };
}, },
}; };
</script> </script>
<template> <template>
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"
:has-changes="hasChanges" :has-changes="hasChanges"
:has-merge-request="hasMergeRequest"
/> />
<repo-editor <repo-editor
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
......
...@@ -70,7 +70,9 @@ export default { ...@@ -70,7 +70,9 @@ export default {
this.getRawFileData(this.file) this.getRawFileData(this.file)
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve(); const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor')
: Promise.resolve();
return viewerPromise; return viewerPromise;
}) })
...@@ -78,8 +80,15 @@ export default { ...@@ -78,8 +80,15 @@ export default {
this.updateDelayViewerUpdated(false); this.updateDelayViewerUpdated(false);
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch((err) => { .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); flash(
'Error setting up monaco. Please try again.',
'alert',
document,
null,
false,
true,
);
throw err; throw err;
}); });
}, },
...@@ -101,9 +110,13 @@ export default { ...@@ -101,9 +110,13 @@ export default {
this.model = this.editor.createModel(this.file); this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model); if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
}
this.model.onChange((model) => { this.model.onChange(model => {
const { file } = model; const { file } = model;
if (file.active) { if (file.active) {
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue'; import EditorMode from './editor_mode_dropdown.vue';
export default { export default {
components: { components: {
RepoTab, RepoTab,
EditorMode, EditorMode,
},
props: {
files: {
type: Array,
required: true,
}, },
props: { viewer: {
files: { type: String,
type: Array, required: true,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
}, },
data() { hasChanges: {
return { type: Boolean,
showShadow: false, required: true,
};
}, },
updated() { hasMergeRequest: {
if (!this.$refs.tabsScroller) return; type: Boolean,
required: true,
this.showShadow = default: false,
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
}, },
}; },
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
},
};
</script> </script>
<template> <template>
...@@ -55,6 +60,7 @@ ...@@ -55,6 +60,7 @@
:viewer="viewer" :viewer="viewer"
:show-shadow="showShadow" :show-shadow="showShadow"
:has-changes="hasChanges" :has-changes="hasChanges"
:has-merge-request="hasMergeRequest"
@click="updateViewer" @click="updateViewer"
/> />
</div> </div>
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import { getTreeEntry } from './stores/utils';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -44,7 +45,7 @@ const router = new VueRouter({ ...@@ -44,7 +45,7 @@ const router = new VueRouter({
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
{ {
path: 'mr/:mrid', path: 'merge_requests/:mrid',
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
], ],
...@@ -96,6 +97,84 @@ router.beforeEach((to, from, next) => { ...@@ -96,6 +97,84 @@ router.beforeEach((to, from, next) => {
); );
throw e; throw e;
}); });
} else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
store
.dispatch('getFiles', {
projectId: fullProjectId,
branchId: mr.source_branch,
})
.then(() => {
store
.dispatch('getMergeRequestChanges', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mrChanges => {
if (mrChanges.changes.length > 0) {
}
mrChanges.changes.forEach((change, ind) => {
console.log(`CHANGE : ${ind} : `, change);
const changeTreeEntry =
store.state.entries[change.new_path];
console.log(
'Tree Entry for the change ',
changeTreeEntry,
change.diff,
);
if (changeTreeEntry) {
store.dispatch('setFileMrDiff', {
file: changeTreeEntry,
mrDiff: change.diff,
});
store.dispatch('setFileTargetBranch', {
file: changeTreeEntry,
targetBranch: mrChanges.target_branch,
});
if (ind === 0) {
store.dispatch('getFileData', change.new_path);
} else {
// TODO : Implement Tab reloading
store.dispatch('preloadFileTab', changeTreeEntry);
}
} else {
console.warn(`No Tree Entry for ${change.new_path}`);
}
});
})
.catch(e => {
flash(
'Error while loading the merge request changes. Please try again.',
);
throw e;
});
})
.catch(e => {
flash(
'Error while loading the branch files. Please try again.',
);
throw e;
});
})
.catch(e => {
throw e;
});
} }
}) })
.catch(e => { .catch(e => {
......
...@@ -22,6 +22,16 @@ export default class Model { ...@@ -22,6 +22,16 @@ export default class Model {
)), )),
); );
if (this.file.targetBranch) {
this.disposable.add(
(this.targetModel = this.monaco.editor.createModel(
this.file.targetRaw,
undefined,
new this.monaco.Uri(null, null, `target/${this.file.path}`),
)),
);
}
this.events = new Map(); this.events = new Map();
this.updateContent = this.updateContent.bind(this); this.updateContent = this.updateContent.bind(this);
...@@ -58,6 +68,10 @@ export default class Model { ...@@ -58,6 +68,10 @@ export default class Model {
return this.originalModel; return this.originalModel;
} }
getTargetModel() {
return this.targetModel;
}
setValue(value) { setValue(value) {
this.getModel().setValue(value); this.getModel().setValue(value);
} }
......
export function revertPatch(source, uniDiff, options = {}) {
if (typeof uniDiff === 'string') {
uniDiff = parsePatch(uniDiff);
}
if (Array.isArray(uniDiff)) {
if (uniDiff.length > 1) {
throw new Error('applyPatch only works with a single input.');
}
uniDiff = uniDiff[0];
}
// Apply the diff to the input
let lines = source.split(/\r\n|[\n\v\f\r\x85]/),
delimiters = source.match(/\r\n|[\n\v\f\r\x85]/g) || [],
hunks = uniDiff.hunks,
compareLine =
options.compareLine ||
((lineNumber, line, operation, patchContent) => line === patchContent),
errorCount = 0,
fuzzFactor = options.fuzzFactor || 0,
minLine = 0,
offset = 0,
removeEOFNL,
addEOFNL;
/**
* Checks if the hunk exactly fits on the provided location
*/
function hunkFits(hunk, toPos) {
for (let j = 0; j < hunk.lines.length; j++) {
let line = hunk.lines[j],
operation = line[0],
content = line.substr(1);
if (operation === ' ' || operation === '-') {
// Context sanity check
if (!compareLine(toPos + 1, lines[toPos], operation, content)) {
errorCount++;
if (errorCount > fuzzFactor) {
return false;
}
}
toPos++;
}
}
return true;
}
// Search best fit offsets for each hunk based on the previous ones
for (let i = 0; i < hunks.length; i++) {
let hunk = hunks[i],
maxLine = lines.length - hunk.oldLines,
localOffset = 0,
toPos = offset + hunk.oldStart - 1;
const iterator = distanceIterator(toPos, minLine, maxLine);
for (; localOffset !== undefined; localOffset = iterator()) {
if (hunkFits(hunk, toPos + localOffset)) {
hunk.offset = offset += localOffset;
break;
}
}
if (localOffset === undefined) {
return false;
}
// Set lower text limit to end of the current hunk, so next ones don't try
// to fit over already patched text
minLine = hunk.offset + hunk.oldStart + hunk.oldLines;
}
// Apply patch hunks
let diffOffset = 0;
for (let i = 0; i < hunks.length; i++) {
let hunk = hunks[i],
toPos = hunk.oldStart + hunk.offset + diffOffset - 1;
diffOffset += hunk.newLines - hunk.oldLines;
if (toPos < 0) {
// Creating a new file
toPos = 0;
}
for (let j = 0; j < hunk.lines.length; j++) {
let line = hunk.lines[j],
operation = line[0],
content = line.substr(1),
delimiter = hunk.linedelimiters[j];
// Turned around the commands to revert the applying
if (operation === ' ') {
toPos++;
} else if (operation === '+') {
lines.splice(toPos, 1);
delimiters.splice(toPos, 1);
/* istanbul ignore else */
} else if (operation === '-') {
lines.splice(toPos, 0, content);
delimiters.splice(toPos, 0, delimiter);
toPos++;
} else if (operation === '\\') {
const previousOperation = hunk.lines[j - 1]
? hunk.lines[j - 1][0]
: null;
if (previousOperation === '+') {
removeEOFNL = true;
} else if (previousOperation === '-') {
addEOFNL = true;
}
}
}
}
// Handle EOFNL insertion/removal
if (removeEOFNL) {
while (!lines[lines.length - 1]) {
lines.pop();
delimiters.pop();
}
} else if (addEOFNL) {
lines.push('');
delimiters.push('\n');
}
for (let _k = 0; _k < lines.length - 1; _k++) {
lines[_k] = lines[_k] + delimiters[_k];
}
return lines.join('');
}
/**
* Utility Function
* @param {*} start
* @param {*} minLine
* @param {*} maxLine
*/
const distanceIterator = function(start, minLine, maxLine) {
let wantForward = true,
backwardExhausted = false,
forwardExhausted = false,
localOffset = 1;
return function iterator() {
if (wantForward && !forwardExhausted) {
if (backwardExhausted) {
localOffset++;
} else {
wantForward = false;
}
// Check if trying to fit beyond text length, and if not, check it fits
// after offset location (or desired location on first iteration)
if (start + localOffset <= maxLine) {
return localOffset;
}
forwardExhausted = true;
}
if (!backwardExhausted) {
if (!forwardExhausted) {
wantForward = true;
}
// Check if trying to fit before text beginning, and if not, check it fits
// before offset location
if (minLine <= start - localOffset) {
return -localOffset++;
}
backwardExhausted = true;
return iterator();
}
// We tried to fit hunk before text beginning and beyond text length, then
// hunk can't fit on the text. Return undefined
};
};
...@@ -109,6 +109,13 @@ export default class Editor { ...@@ -109,6 +109,13 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
attachMergeRequestModel(model) {
this.instance.setModel({
original: model.getTargetModel(),
modified: model.getModel(),
});
}
setupMonacoTheme() { setupMonacoTheme() {
this.monaco.editor.defineTheme( this.monaco.editor.defineTheme(
gitlabTheme.themeName, gitlabTheme.themeName,
......
...@@ -20,12 +20,19 @@ export default { ...@@ -20,12 +20,19 @@ export default {
return Promise.resolve(file.raw); return Promise.resolve(file.raw);
} }
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http
.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text()); .then(res => res.text());
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`); return Api.project(`${namespace}/${project}`);
}, },
getProjectMergeRequestData(projectId, mergeRequestId) {
return Api.mergeRequest(projectId, mergeRequestId);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
},
getBranchData(projectId, currentBranchId) { getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId); return Api.branchSingle(projectId, currentBranchId);
}, },
......
...@@ -119,3 +119,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -119,3 +119,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
export * from './actions/merge_request';
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils';
import { parsePatch, applyPatches } from 'diff';
import { revertPatch } from '../../lib/diff/revert_patch';
import flash from '~/flash'; import flash from '~/flash';
import eventHub from '../../eventhub'; import eventHub from '../../eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle, createTemp, findIndexOfFile } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => { export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path); const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
...@@ -46,53 +48,140 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -46,53 +48,140 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId); commit(types.SET_CURRENT_BRANCH, file.branchId);
}; };
export const getFileData = ({ state, commit, dispatch }, file) => { export const getFileData = ({ state, commit, dispatch }, path) => {
commit(types.TOGGLE_LOADING, { entry: file }); const file = state.entries[path];
return new Promise((resolve, reject) => {
return service commit(types.TOGGLE_LOADING, { entry: file });
.getFileData(file.url) service
.then(res => { .getFileData(file.url)
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); .then(res => {
const pageTitle = decodeURI(
setPageTitle(pageTitle); normalizeHeaders(res.headers)['PAGE-TITLE'],
);
return res.json();
}) setPageTitle(pageTitle);
.then(data => {
commit(types.SET_FILE_DATA, { data, file }); return res.json();
commit(types.TOGGLE_FILE_OPEN, file.path); })
dispatch('setFileActive', file.path); .then(data => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.SET_FILE_DATA, { data, file });
}) commit(types.TOGGLE_FILE_OPEN, path);
.catch(() => { dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
flash( })
'Error loading file data. Please try again.', .catch(err => {
'alert', console.log('Error : ', err);
document, commit(types.TOGGLE_LOADING, { entry: file });
null, flash(
false, 'Error loading file data. Please try again.',
true, 'alert',
); document,
}); null,
false,
true,
);
});
});
};
export const preloadFileTab = ({ state, commit, dispatch }, file) => {
return new Promise((resolve, reject) => {
commit(types.TOGGLE_LOADING, { entry: file });
service
.getFileData(file.url)
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
});
});
};
export const setFileTargetBranch = (
{ state, commit },
{ file, targetBranch },
) => {
commit(types.SET_FILE_TARGET_BRANCH, {
file,
targetBranch,
targetRawPath: file.rawPath.replace(file.branchId, targetBranch),
});
};
export const processFileMrDiff = ({ state, commit }, file) => {
const patchObj = parsePatch(file.mrDiff);
const transformedContent = applyPatch(file.raw, file.mrDiff);
debugger;
}; };
export const getRawFileData = ({ commit, dispatch }, file) => export const setFileMrDiff = ({ state, commit }, { file, mrDiff }) => {
service commit(types.SET_FILE_MR_DIFF, { file, mrDiff });
.getRawFileData(file) };
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); export const getRawFileData = ({ commit, dispatch }, file) => {
}) return new Promise((resolve, reject) => {
.catch(() => service
flash( .getRawFileData(file)
'Error loading file content. Please try again.', .then(raw => {
'alert', commit(types.SET_FILE_RAW_DATA, { file, raw });
document, if (file.mrDiff) {
null, const patchObj = parsePatch(file.mrDiff);
false, patchObj[0].hunks.forEach(hunk => {
true, console.log('H ', hunk);
), /*hunk.lines.forEach((line) => {
); if (line.substr(0, 1) === '+') {
line = '-' + line.substr(1);
} else if (line.substr(0, 1) === '-') {
line = '+' + line.substr(1);
}
})*/
});
console.log('PATCH OBJ : ' + JSON.stringify(patchObj));
const transformedContent = revertPatch(raw, patchObj, {
compareLine: (lineNumber, line, operation, patchContent) => {
const tempLine = line;
//line = patchContent;
//patchContent = tempLine;
if (operation === '-') {
operation = '+';
} else if (operation === '+') {
operation = '-';
}
console.log(
'COMPARE : ' + line + ' - ' + operation + ' - ' + patchContent,
);
return true;
},
});
console.log('TRANSFORMED : ', transformedContent);
commit(types.SET_FILE_TARGET_RAW_DATA, {
file,
raw: transformedContent,
});
resolve(raw);
} else {
resolve(raw);
}
})
.catch(() => {
flash('Error loading file content. Please try again.');
reject();
});
});
};
export const changeFileContent = ({ state, commit }, { path, content }) => { export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
......
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getMergeRequestData = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
.getProjectMergeRequestData(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
mergeRequest: data,
});
if (!state.currentMergeRequestId) {
commit(
types.SET_CURRENT_MERGE_REQUEST,
`${projectId}/${mergeRequestId}`,
);
}
resolve(data);
})
.catch(() => {
flash('Error loading merge request data. Please try again.');
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
}
});
// eslint-disable-next-line import/prefer-default-export
export const getMergeRequestChanges = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (
!state.projects[projectId].mergeRequests[mergeRequestId].changes ||
force
) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
changes: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request changes. Please try again.');
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
}
});
// eslint-disable-next-line import/prefer-default-export
export const getMergeRequestNotes = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (
!state.projects[projectId].mergeRequests[mergeRequestId].notes ||
force
) {
service
.getProjectMergeRequestNotes(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_NOTES, {
projectPath: projectId,
mergeRequestId,
notes: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request notes. Please try again.');
reject(new Error(`Merge Request Notes not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].notes);
}
});
...@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils'; ...@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash'; import flash from '~/flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { import { findEntry } from '../utils';
findEntry,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker'; import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => { export const toggleTreeOpen = ({ commit, dispatch }, path) => {
...@@ -21,24 +19,33 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { ...@@ -21,24 +19,33 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path); dispatch('setFileActive', row.path);
} else { } else {
dispatch('getFileData', row); dispatch('getFileData', row.path);
} }
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = (
{ state, commit, dispatch, getters },
tree = state,
) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath) service
.then((res) => { .getTreeLastCommit(tree.lastCommitPath)
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; .then(res => {
const lastCommitPath =
normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json(); return res.json();
}) })
.then((data) => { .then(data => {
data.forEach((lastCommit) => { data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name); const entry = findEntry(
tree.tree,
lastCommit.type,
lastCommit.file_name,
);
if (entry) { if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
...@@ -47,47 +54,62 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -47,47 +54,62 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree); dispatch('getLastCommitData', tree);
}) })
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); .catch(() =>
flash('Error fetching log data.', 'alert', document, null, false, true),
);
}; };
export const getFiles = ( export const getFiles = (
{ state, commit, dispatch }, { state, commit, dispatch },
{ projectId, branchId } = {}, { projectId, branchId } = {},
) => new Promise((resolve, reject) => { ) =>
if (!state.trees[`${projectId}/${branchId}`]) { new Promise((resolve, reject) => {
const selectedProject = state.projects[projectId]; if (!state.trees[`${projectId}/${branchId}`]) {
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId) service
.then(res => res.json()) .getFiles(selectedProject.web_url, branchId)
.then((data) => { .then(res => res.json())
const worker = new FilesDecoratorWorker(); .then(data => {
worker.addEventListener('message', (e) => { const worker = new FilesDecoratorWorker();
const { entries, treeList } = e.data; worker.addEventListener('message', e => {
const selectedTree = state.trees[`${projectId}/${branchId}`]; const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList }); commit(types.SET_ENTRIES, entries);
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false }); commit(types.SET_DIRECTORY_DATA, {
treePath: `${projectId}/${branchId}`,
worker.terminate(); data: treeList,
});
resolve(); commit(types.TOGGLE_LOADING, {
}); entry: selectedTree,
forceValue: false,
worker.postMessage({ });
data,
projectId, worker.terminate();
branchId,
resolve();
});
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch(e => {
flash(
'Error loading tree data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(e);
}); });
}) } else {
.catch((e) => { resolve();
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); }
reject(e); });
});
} else {
resolve();
}
});
...@@ -28,3 +28,5 @@ export const currentIcon = state => ...@@ -28,3 +28,5 @@ export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length; export const hasChanges = state => !!state.changedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
...@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT'; ...@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
export const SET_MERGE_REQUEST_NOTES = 'SET_MERGE_REQUEST_NOTES';
// Branch Mutation Types // Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
...@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA'; ...@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const SET_FILE_TARGET_RAW_DATA = 'SET_FILE_TARGET_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION'; export const SET_FILE_POSITION = 'SET_FILE_POSITION';
...@@ -39,5 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; ...@@ -39,5 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES'; export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const SET_FILE_MR_DIFF = 'SET_FILE_MR_DIFF';
export const SET_FILE_TARGET_BRANCH = 'SET_FILE_TARGET_BRANCH';
export const UPDATE_VIEWER = 'UPDATE_VIEWER'; export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
...@@ -100,6 +101,7 @@ export default { ...@@ -100,6 +101,7 @@ export default {
}); });
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
...branchMutations, ...branchMutations,
......
...@@ -35,6 +35,11 @@ export default { ...@@ -35,6 +35,11 @@ export default {
raw, raw,
}); });
}, },
[types.SET_FILE_TARGET_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
targetRaw: raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) { [types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw; const changed = content !== state.entries[path].raw;
...@@ -59,6 +64,16 @@ export default { ...@@ -59,6 +64,16 @@ export default {
editorColumn, editorColumn,
}); });
}, },
[types.SET_FILE_MR_DIFF](state, { file, mrDiff }) {
Object.assign(file, {
mrDiff,
});
},
[types.SET_FILE_TARGET_BRANCH](state, { file, targetBranch }) {
Object.assign(file, {
targetBranch,
});
},
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: state.entries[path].raw, content: state.entries[path].raw,
......
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
Object.assign(state, {
currentMergeRequestId,
});
},
[types.SET_MERGE_REQUEST](
state,
{ projectPath, mergeRequestId, mergeRequest },
) {
// Add client side properties
Object.assign(mergeRequest, {
active: true,
});
Object.assign(state.projects[projectPath], {
mergeRequests: {
[mergeRequestId]: mergeRequest,
},
});
},
[types.SET_MERGE_REQUEST_CHANGES](
state,
{ projectPath, mergeRequestId, changes },
) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
changes,
});
},
[types.SET_MERGE_REQUEST_NOTES](
state,
{ projectPath, mergeRequestId, notes },
) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
notes,
});
},
};
...@@ -11,6 +11,7 @@ export default { ...@@ -11,6 +11,7 @@ export default {
Object.assign(project, { Object.assign(project, {
tree: [], tree: [],
branches: {}, branches: {},
mergeRequests: {},
active: true, active: true,
}); });
......
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [], changedFiles: [],
endpoints: {}, endpoints: {},
lastCommitMsg: '', lastCommitMsg: '',
......
...@@ -38,7 +38,7 @@ export const dataStructure = () => ({ ...@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '', eol: '',
}); });
export const decorateData = (entity) => { export const decorateData = entity => {
const { const {
id, id,
projectId, projectId,
...@@ -57,7 +57,6 @@ export const decorateData = (entity) => { ...@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false, base64 = false,
file_lock, file_lock,
} = entity; } = entity;
return { return {
...@@ -80,17 +79,45 @@ export const decorateData = (entity) => { ...@@ -80,17 +79,45 @@ export const decorateData = (entity) => {
base64, base64,
file_lock, file_lock,
}; };
}; };
export const findEntry = (tree, type, name, prop = 'name') => tree.find( /*
f => f.type === type && f[prop] === name, Takes the multi-dimensional tree and returns a flattened array.
); This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state, treeId) => {
const baseTree = state.trees[treeId];
if (baseTree) {
const mapTree = arr =>
!arr.tree || !arr.tree.length
? []
: _.map(arr.tree, a => [a, mapTree(a)]);
return _.chain(baseTree.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
}
return [];
};
export const getTree = state => (namespace, projectId, branch) =>
state.trees[`${namespace}/${projectId}/${branch}`];
export const getTreeEntry = (store, treeId, path) => {
const fileList = treeList(store.state, treeId);
return fileList ? fileList.find(file => file.path === path) : null;
};
export const findEntry = (tree, type, name, prop = 'name') =>
tree.find(f => f.type === type && f[prop] === name);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); export const findIndexOfFile = (state, file) =>
state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => { export const setPageTitle = title => {
document.title = title; document.title = title;
}; };
...@@ -120,6 +147,11 @@ const sortTreesByTypeAndName = (a, b) => { ...@@ -120,6 +147,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0; return 0;
}; };
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, { export const sortTree = sortedTree =>
tree: entity.tree.length ? sortTree(entity.tree) : [], sortedTree
})).sort(sortTreesByTypeAndName); .map(entity =>
Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
}),
)
.sort(sortTreesByTypeAndName);
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
}, },
components: { },
icon, computed: {
clipboardButton, shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
}, },
props: { commitsText() {
mr: { return n__(
type: Object, '%d commit behind',
required: true, '%d commits behind',
}, this.mr.divergedCommitsCount,
);
}, },
computed: { branchNameClipboardData() {
shouldShowCommitsBehindText() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that
return this.mr.divergedCommitsCount > 0; // works around ClipboardJS limitations to allow the context-specific
}, // copy/pasting of plain text or GFM.
commitsText() { return JSON.stringify({
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); text: this.mr.sourceBranch,
}, gfm: `\`${this.mr.sourceBranch}\``,
branchNameClipboardData() { });
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
}, },
methods: { isSourceBranchLong() {
isBranchTitleLong(branchTitle) { return this.isBranchTitleLong(this.mr.sourceBranch);
return branchTitle.length > 32;
},
}, },
}; isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() {
return `${
gon.relative_url_root
}/-/ide/project${this.mr.statusPath.replace('.json', '')}`;
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script> </script>
<template> <template>
<div class="mr-source-target"> <div class="mr-source-target">
...@@ -96,6 +105,14 @@ ...@@ -96,6 +105,14 @@
</div> </div>
<div v-if="mr.isOpen"> <div v-if="mr.isOpen">
<a
:disabled="mr.sourceBranchRemoved"
:href="webIdePath"
class="btn btn-sm btn-default inline js-web-ide"
type="button"
>
{{ s__("mrWidget|Open in Web IDE") }}
</a>
<button <button
data-target="#modal_merge_info" data-target="#modal_merge_info"
data-toggle="modal" data-toggle="modal"
......
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