diff options
author | Tim Zallmann <tzallmann@gitlab.com> | 2018-03-23 11:45:43 +0100 |
---|---|---|
committer | Tim Zallmann <tzallmann@gitlab.com> | 2018-03-28 12:17:02 +0200 |
commit | f62359c23b86bb8c8dc241ff4e09bab24e74ccbd (patch) | |
tree | f2ac4a95d433ce476fd5337cabed81c8bb524530 /app/assets/javascripts/ide | |
parent | 06afa5a3ff82e2d6edbcf668cc870e625784c09c (diff) | |
download | gitlab-ce-f62359c23b86bb8c8dc241ff4e09bab24e74ccbd.tar.gz |
Basic Setup for MR Showing
Diffstat (limited to 'app/assets/javascripts/ide')
21 files changed, 855 insertions, 212 deletions
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); |