Commit f62359c2 authored by Tim Zallmann's avatar Tim Zallmann

Basic Setup for MR Showing

parent 06afa5a3
......@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
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',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
......@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
.replace(':id', groupId);
return axios.get(url)
.then(({ data }) => {
callback(data);
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
return axios.get(url).then(({ data }) => {
callback(data);
return data;
});
return data;
});
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, {
params: Object.assign({
search: query,
per_page: 20,
}, options),
})
return axios
.get(url, {
params: Object.assign(
{
search: query,
per_page: 20,
},
options,
),
})
.then(({ data }) => {
callback(data);
......@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
return axios.get(url, {
params: {
search: query,
per_page: 20,
},
})
return axios
.get(url, {
params: {
search: query,
per_page: 20,
},
})
.then(({ data }) => callback(data));
},
......@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
return axios.get(url, {
params: Object.assign(defaults, options),
})
return axios
.get(url, {
params: Object.assign(defaults, options),
})
.then(({ data }) => {
callback(data);
......@@ -85,8 +92,28 @@ const Api = {
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath)
.replace(':id', encodeURIComponent(projectPath));
const url = Api.buildUrl(Api.projectPath).replace(
':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);
},
......@@ -99,33 +126,39 @@ const Api = {
.replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath);
} else {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
url = Api.buildUrl(Api.groupLabelsPath).replace(
':namespace_path',
namespacePath,
);
}
return axios.post(url, {
label: data,
})
return axios
.post(url, {
label: data,
})
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', groupId);
return axios.get(url, {
params: {
search: query,
per_page: 20,
},
})
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
return axios
.get(url, {
params: {
search: query,
per_page: 20,
},
})
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', encodeURIComponent(id));
const url = Api.buildUrl(Api.commitPath).replace(
':id',
encodeURIComponent(id),
);
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
......@@ -136,39 +169,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
.replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
return axios.get(url, {
params: data,
})
const url = Api.buildUrl(Api.licensePath).replace(':key', key);
return axios
.get(url, {
params: data,
})
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath)
.replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath)
.replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
......@@ -177,7 +205,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
return axios.get(url)
return axios
.get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
......@@ -185,10 +214,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
params: Object.assign({
search: query,
per_page: 20,
}, options),
params: Object.assign(
{
search: query,
per_page: 20,
},
options,
),
});
},
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
hasMergeRequest: {
type: Boolean,
required: false,
default: false,
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
viewer: {
type: String,
required: true,
},
};
showShadow: {
type: Boolean,
required: true,
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script>
<template>
......@@ -43,7 +48,10 @@
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
<template v-if="viewer === 'mrdiff'">
{{ __('Reviewing (merge request)') }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
......@@ -57,6 +65,21 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<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>
<a
href="#"
......
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapGetters(['activeFile', 'hasChanges']),
committedStateSvgPath: {
type: String,
required: true,
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer']),
...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, {
returnValue,
});
return returnValue;
};
},
};
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
......@@ -63,6 +63,7 @@
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:has-merge-request="hasMergeRequest"
/>
<repo-editor
class="multi-file-edit-pane-content"
......
......@@ -70,7 +70,9 @@ export default {
this.getRawFileData(this.file)
.then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor')
: Promise.resolve();
return viewerPromise;
})
......@@ -78,8 +80,15 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
.catch(err => {
flash(
'Error setting up monaco. Please try again.',
'alert',
document,
null,
false,
true,
);
throw err;
});
},
......@@ -101,9 +110,13 @@ export default {
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;
if (file.active) {
......
<script>
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default {
components: {
RepoTab,
EditorMode,
export default {
components: {
RepoTab,
EditorMode,
},
props: {
files: {
type: Array,
required: true,
},
props: {
files: {
type: Array,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
viewer: {
type: String,
required: true,
},
data() {
return {
showShadow: false,
};
hasChanges: {
type: Boolean,
required: true,
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
hasMergeRequest: {
type: Boolean,
required: true,
default: false,
},
};
},
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
......@@ -55,6 +60,7 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:has-merge-request="hasMergeRequest"
@click="updateViewer"
/>
</div>
......
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
import { getTreeEntry } from './stores/utils';
Vue.use(VueRouter);
......@@ -44,7 +45,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
path: 'mr/:mrid',
path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
......@@ -96,6 +97,84 @@ router.beforeEach((to, from, next) => {
);
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 => {
......
......@@ -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.updateContent = this.updateContent.bind(this);
......@@ -58,6 +68,10 @@ export default class Model {
return this.originalModel;
}
getTargetModel() {
return this.targetModel;
}
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 {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
attachMergeRequestModel(model) {
this.instance.setModel({
original: model.getTargetModel(),
modified: model.getModel(),
});
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(
gitlabTheme.themeName,
......
......@@ -20,12 +20,19 @@ export default {
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());
},
getProjectData(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) {
return Api.branchSingle(projectId, currentBranchId);
},
......
......@@ -119,3 +119,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { parsePatch, applyPatches } from 'diff';
import { revertPatch } from '../../lib/diff/revert_patch';
import flash from '~/flash';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
import { setPageTitle, createTemp, findIndexOfFile } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
......@@ -46,53 +48,140 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file });
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
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 getFileData = ({ state, commit, dispatch }, path) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
commit(types.TOGGLE_LOADING, { entry: file });
service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(
normalizeHeaders(res.headers)['PAGE-TITLE'],
);
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(err => {
console.log('Error : ', err);
commit(types.TOGGLE_LOADING, { entry: file });
flash(
'Error loading file data. Please try again.',
'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) =>
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
export const setFileMrDiff = ({ state, commit }, { file, mrDiff }) => {
commit(types.SET_FILE_MR_DIFF, { file, mrDiff });
};
export const getRawFileData = ({ commit, dispatch }, file) => {
return new Promise((resolve, reject) => {
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrDiff) {
const patchObj = parsePatch(file.mrDiff);
patchObj[0].hunks.forEach(hunk => {
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 }) => {
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';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
import {
findEntry,
} from '../utils';
import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
......@@ -21,24 +19,33 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} 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;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
service
.getTreeLastCommit(tree.lastCommitPath)
.then(res => {
const lastCommitPath =
normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
.then(data => {
data.forEach(lastCommit => {
const entry = findEntry(
tree.tree,
lastCommit.type,
lastCommit.file_name,
);
if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
......@@ -47,47 +54,62 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
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 = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
) => new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
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.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
worker.terminate();
resolve();
});
worker.postMessage({
data,
projectId,
branchId,
) =>
new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then(data => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', e => {
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.TOGGLE_LOADING, {
entry: selectedTree,
forceValue: false,
});
worker.terminate();
resolve();
});
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch(e => {
flash(
'Error loading tree data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(e);
});
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
});
} else {
resolve();
}
});
} else {
resolve();
}
});
......@@ -28,3 +28,5 @@ export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
......@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
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
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
......@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
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 SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
......@@ -39,5 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
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_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
......@@ -100,6 +101,7 @@ export default {
});
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
......
......@@ -35,6 +35,11 @@ export default {
raw,
});
},
[types.SET_FILE_TARGET_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
targetRaw: raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
......@@ -59,6 +64,16 @@ export default {
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) {
Object.assign(state.entries[path], {
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 {
Object.assign(project, {
tree: [],
branches: {},
mergeRequests: {},
active: true,
});
......
export default () => ({
currentProjectId: '',
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
......
......@@ -38,7 +38,7 @@ export const dataStructure = () => ({
eol: '',
});
export const decorateData = (entity) => {
export const decorateData = entity => {
const {
id,
projectId,
......@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
base64 = false,
file_lock,
} = entity;
return {
......@@ -80,17 +79,45 @@ export const decorateData = (entity) => {
base64,
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;
};
......@@ -120,6 +147,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
})).sort(sortTreesByTypeAndName);
export const sortTree = sortedTree =>
sortedTree
.map(entity =>
Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
}),
)
.sort(sortTreesByTypeAndName);
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
},
components: {
icon,
clipboardButton,
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
props: {
mr: {
type: Object,
required: true,
},
commitsText() {
return n__(
'%d commit behind',
'%d commits behind',
this.mr.divergedCommitsCount,
);
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
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);
},
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}\``,
});
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
};
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>
<template>
<div class="mr-source-target">
......@@ -96,6 +105,14 @@
</div>
<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
data-target="#modal_merge_info"
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