summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2018-03-23 11:45:43 +0100
committerTim Zallmann <tzallmann@gitlab.com>2018-03-28 12:17:02 +0200
commitf62359c23b86bb8c8dc241ff4e09bab24e74ccbd (patch)
treef2ac4a95d433ce476fd5337cabed81c8bb524530 /app/assets
parent06afa5a3ff82e2d6edbcf668cc870e625784c09c (diff)
downloadgitlab-ce-f62359c23b86bb8c8dc241ff4e09bab24e74ccbd.tar.gz
Basic Setup for MR Showing
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/api.js150
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue71
-rw-r--r--app/assets/javascripts/ide/components/ide.vue85
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue72
-rw-r--r--app/assets/javascripts/ide/ide_router.js81
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js14
-rw-r--r--app/assets/javascripts/ide/lib/diff/revert_patch.js183
-rw-r--r--app/assets/javascripts/ide/lib/editor.js7
-rw-r--r--app/assets/javascripts/ide/services/index.js9
-rw-r--r--app/assets/javascripts/ide/stores/actions.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js181
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js96
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js120
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js9
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutations/merge_request.js40
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js54
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue103
23 files changed, 1006 insertions, 314 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f..bed20b36868 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -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,
+ ),
});
},
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0..42b00b5d9df 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,36 @@
<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="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 015e750525a..f300afa24ac 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,51 +1,51 @@
<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"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index e73d1ce839f..d647d3b87fb 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -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) {
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 8ea64ddf84a..37e90d3ba7e 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,42 +1,47 @@
<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>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index db89c1d44db..f054a0a0364 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -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 => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 73cd684351c..8e16df99a03 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -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);
}
diff --git a/app/assets/javascripts/ide/lib/diff/revert_patch.js b/app/assets/javascripts/ide/lib/diff/revert_patch.js
new file mode 100644
index 00000000000..21c90adedd8
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/diff/revert_patch.js
@@ -0,0 +1,183 @@
+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
+ };
+};
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1..cdb277603f0 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -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,
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843..46a65c583e0 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -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);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7e920aa9f30..83e200f3cc0 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -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';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index ddc4b757bf9..57e036f57af 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,10 +1,12 @@
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];
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 00000000000..6da00e98f59
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,96 @@
+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);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a0325..4a960a12010 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -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();
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df..fab12e7e1c6 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -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;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e28f190897c..4edbc58ca40 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -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';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c..a3b9d0dac8c 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
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,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 2500f13db7c..1297d3aaf1f 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -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,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 00000000000..69abe010372
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,40 @@
+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,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a919..284b39a2c72 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
+ mergeRequests: {},
active: true,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c..e5cc8814000 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
currentProjectId: '',
currentBranchId: '',
+ currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e..cb0b1354665 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -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);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628..d3d5ef76d2e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,62 @@
<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"