diff options
Diffstat (limited to 'app/assets/javascripts')
19 files changed, 279 insertions, 154 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index b308cd9c236..db3ad0bb4c9 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -337,6 +337,7 @@ class GfmAutoComplete { }, // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${title}', + limit: 20, callbacks: { ...this.getDefaultCallbacks(), beforeSave(merges) { diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index 11d5d9639b6..6b2ef34c960 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -43,7 +43,12 @@ export default { <template> <div class="d-flex ide-commit-editor-header align-items-center"> <file-icon :file-name="activeFile.name" :size="16" class="mr-2" /> - <strong class="mr-2"> {{ activeFile.path }} </strong> + <strong class="mr-2"> + <template v-if="activeFile.prevPath && activeFile.prevPath !== activeFile.path"> + {{ activeFile.prevPath }} → + </template> + {{ activeFile.path }} + </strong> <changed-file-icon :file="activeFile" :is-centered="false" /> <div class="ml-auto"> <button diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 47b205f0a75..230dfaf047b 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -110,6 +110,9 @@ export default { > <span class="multi-file-commit-list-file-path d-flex align-items-center"> <file-icon :file-name="file.name" class="append-right-8" /> + <template v-if="file.prevName && file.prevName !== file.name"> + {{ file.prevName }} → + </template> {{ file.name }} </span> <div class="ml-auto d-flex align-items-center"> diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 5819999a459..f0bedcfbd6b 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -34,6 +34,9 @@ export default { 'getUnstagedFilesCountForPath', 'getStagedFilesCountForPath', ]), + isTree() { + return this.file.type === 'tree'; + }, folderUnstagedCount() { return this.getUnstagedFilesCountForPath(this.file.path); }, @@ -58,10 +61,13 @@ export default { }); }, showTreeChangesCount() { - return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened; + return this.isTree && this.changesCount > 0 && !this.file.opened; + }, + isModified() { + return this.file.changed || this.file.tempFile || this.file.staged || this.file.prevPath; }, showChangedFileIcon() { - return this.file.changed || this.file.tempFile || this.file.staged; + return !this.isTree && this.isModified; }, }, }; diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 1af86a94482..95782b2c88a 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -30,9 +30,6 @@ export default { showLoading() { return !this.currentTree || this.currentTree.loading; }, - actualTreeList() { - return this.currentTree.tree.filter(entry => !entry.moved); - }, }, mounted() { this.updateViewer(this.viewerType); @@ -57,9 +54,9 @@ export default { <slot name="header"></slot> </header> <div class="ide-tree-body h-100"> - <template v-if="actualTreeList.length"> + <template v-if="currentTree.tree.length"> <file-row - v-for="file in actualTreeList" + v-for="file in currentTree.tree" :key="file.key" :file="file" :level="0" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a2dd31aebd4..d2ed1fe3e55 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -91,7 +91,6 @@ export default { this.renameEntry({ path: this.entryModal.entry.path, name: entryName, - entryPath: null, parentPath, }), ) diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 802b7f1fa6f..3bf8308ccea 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -155,15 +155,7 @@ export default { this.editor.clearEditor(); - this.getFileData({ - path: this.file.path, - makeFileActive: false, - }) - .then(() => - this.getRawFileData({ - path: this.file.path, - }), - ) + this.fetchFileData() .then(() => { this.createEditorInstance(); }) @@ -179,6 +171,20 @@ export default { throw err; }); }, + fetchFileData() { + if (this.file.tempFile) { + return Promise.resolve(); + } + + return this.getFileData({ + path: this.file.path, + makeFileActive: false, + }).then(() => + this.getRawFileData({ + path: this.file.path, + }), + ); + }, createEditorInstance() { this.editor.dispose(); diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index 51278640b5b..e86dac20104 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -1,7 +1,5 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; -import { decorateData, sortTree } from '../stores/utils'; - -export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); +import { decorateData, sortTree, escapeFileUrl } from '../stores/utils'; export const splitParent = path => { const idx = path.lastIndexOf('/'); diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 8c0119a1fed..4e18ec58feb 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -9,6 +9,7 @@ import { decorateFiles } from '../lib/files'; import { stageKeys } from '../constants'; import service from '../services'; import router from '../ide_router'; +import eventHub from '../eventhub'; export const redirectToUrl = (self, url) => visitUrl(url); @@ -171,8 +172,10 @@ export const setCurrentBranchId = ({ commit }, currentBranchId) => { export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => { commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); - if (file.parentPath) { - dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile }); + const parent = file.parentPath && state.entries[file.parentPath]; + + if (parent) { + dispatch('updateTempFlagForEntry', { file: parent, tempFile }); } }; @@ -199,51 +202,71 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { export const deleteEntry = ({ commit, dispatch, state }, path) => { const entry = state.entries[path]; - + const { prevPath, prevName, prevParentPath } = entry; + const isTree = entry.type === 'tree'; + + if (prevPath) { + dispatch('renameEntry', { + path, + name: prevName, + parentPath: prevParentPath, + }); + dispatch('deleteEntry', prevPath); + return; + } if (state.unusedSeal) dispatch('burstUnusedSeal'); if (entry.opened) dispatch('closeFile', entry); - if (entry.type === 'tree') { + if (isTree) { entry.tree.forEach(f => dispatch('deleteEntry', f.path)); } commit(types.DELETE_ENTRY, path); - dispatch('stageChange', path); + + // Only stage if we're not a directory or a new file + if (!isTree && !entry.tempFile) { + dispatch('stageChange', path); + } dispatch('triggerFilesChange'); }; export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); -export const renameEntry = ( - { dispatch, commit, state }, - { path, name, entryPath = null, parentPath }, -) => { - const entry = state.entries[entryPath || path]; +export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => { + const entry = state.entries[path]; + const newPath = parentPath ? `${parentPath}/${name}` : name; - commit(types.RENAME_ENTRY, { path, name, entryPath, parentPath }); + commit(types.RENAME_ENTRY, { path, name, parentPath }); if (entry.type === 'tree') { - const slashedParentPath = parentPath ? `${parentPath}/` : ''; - const targetEntry = entryPath ? entryPath.split('/').pop() : name; - const newParentPath = `${slashedParentPath}${targetEntry}`; - - state.entries[entryPath || path].tree.forEach(f => { + state.entries[newPath].tree.forEach(f => { dispatch('renameEntry', { - path, - name, - entryPath: f.path, - parentPath: newParentPath, + path: f.path, + name: f.name, + parentPath: newPath, }); }); } else { - const newPath = parentPath ? `${parentPath}/${name}` : name; const newEntry = state.entries[newPath]; - commit(types.TOGGLE_FILE_CHANGED, { file: newEntry, changed: true }); + const isRevert = newPath === entry.prevPath; + const isReset = isRevert && !newEntry.changed && !newEntry.tempFile; + const isInChanges = state.changedFiles + .concat(state.stagedFiles) + .some(({ key }) => key === newEntry.key); + + if (isReset) { + commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry); + } else if (!isInChanges) { + commit(types.ADD_FILE_TO_CHANGED, newPath); + } + + if (!newEntry.tempFile) { + eventHub.$emit(`editor.update.model.dispose.${entry.key}`); + } - if (entry.opened) { + if (newEntry.opened) { router.push(`/project${newEntry.url}`); - commit(types.TOGGLE_FILE_OPEN, entry.path); } } diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 7627b6e03af..59445afc7a4 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -5,7 +5,7 @@ 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, replaceFileUrl } from '../utils'; import { viewerTypes, stageKeys } from '../../constants'; export const closeFile = ({ commit, state, dispatch }, file) => { @@ -67,7 +67,7 @@ export const getFileData = ( commit(types.TOGGLE_LOADING, { entry: file }); - const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url; + const url = file.prevPath ? replaceFileUrl(file.url, file.path, file.prevPath) : file.url; return service .getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/'))) @@ -186,11 +186,6 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = dispatch('restoreTree', file.parentPath); } - if (file.movedPath) { - commit(types.DISCARD_FILE_CHANGES, file.movedPath); - commit(types.REMOVE_FILE_FROM_CHANGED, file.movedPath); - } - commit(types.DISCARD_FILE_CHANGES, path); commit(types.REMOVE_FILE_FROM_CHANGED, path); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index dd8f17e4f3a..20887e7d0ac 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -92,13 +92,27 @@ export const showEmptyState = ({ commit, state }, { projectId, branchId }) => { }); }; -export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => { - dispatch('setCurrentBranchId', branchId); +export const loadFile = ({ dispatch, state }, { basePath }) => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; - if (getters.emptyRepo) { - return dispatch('showEmptyState', { projectId, branchId }); + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } else { + dispatch('createTempEntry', { + name: path, + type: 'blob', + }); + } } - return dispatch('getBranchData', { +}; + +export const loadBranch = ({ dispatch }, { projectId, branchId }) => + dispatch('getBranchData', { projectId, branchId, }) @@ -107,42 +121,38 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, projectId, branchId, }); - dispatch('getFiles', { + return dispatch('getFiles', { projectId, branchId, - }) - .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(state.entries).find( - key => key === path && !state.entries[key].pending, - ); - const treeEntry = state.entries[treeEntryKey]; - - if (treeEntry) { - dispatch('handleTreeEntryAction', treeEntry); - } else { - dispatch('createTempEntry', { - name: path, - type: 'blob', - }); - } - } - }) - .catch( - () => - new Error( - sprintf( - __('An error occurred whilst getting files for - %{branchId}'), - { - branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, - }, - false, - ), - ), - ); + }); }) .catch(() => { dispatch('showBranchNotFoundError', branchId); + return Promise.reject(); }); + +export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => { + const currentProject = state.projects[projectId]; + if (getters.emptyRepo) { + return dispatch('showEmptyState', { projectId, branchId }); + } + if (!currentProject || !currentProject.branches[branchId]) { + dispatch('setCurrentBranchId', branchId); + + return dispatch('loadBranch', { projectId, branchId }) + .then(() => dispatch('loadFile', { basePath })) + .catch( + () => + new Error( + sprintf( + __('An error occurred whilst getting files for - %{branchId}'), + { + branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + }, + false, + ), + ), + ); + } + return Promise.resolve(dispatch('loadFile', { basePath })); }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index f767ca92a56..e89ed49318b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -154,8 +154,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo .then(() => { commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); - commit(rootTypes.CLEAR_REPLACED_FILES, null, { root: true }); - setTimeout(() => { commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index f021729c451..f0b4718d025 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -59,8 +59,7 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES'; export const STAGE_CHANGE = 'STAGE_CHANGE'; export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; - -export const CLEAR_REPLACED_FILES = 'CLEAR_REPLACED_FILES'; +export const REMOVE_FILE_FROM_STAGED_AND_CHANGED = 'REMOVE_FILE_FROM_STAGED_AND_CHANGED'; export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; @@ -79,5 +78,6 @@ export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; export const DELETE_ENTRY = 'DELETE_ENTRY'; export const RENAME_ENTRY = 'RENAME_ENTRY'; +export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY'; export const RESTORE_TREE = 'RESTORE_TREE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index ea125214ebb..2587b57a817 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -5,7 +5,14 @@ import mergeRequestMutation from './mutations/merge_request'; import fileMutations from './mutations/file'; import treeMutations from './mutations/tree'; import branchMutations from './mutations/branch'; -import { sortTree } from './utils'; +import { + sortTree, + replaceFileUrl, + swapInParentTreeWithSorting, + updateFileCollections, + removeFromParentTree, + pathsAreEqual, +} from './utils'; export default { [types.SET_INITIAL_DATA](state, data) { @@ -56,11 +63,6 @@ export default { stagedFiles: [], }); }, - [types.CLEAR_REPLACED_FILES](state) { - Object.assign(state, { - replacedFiles: [], - }); - }, [types.SET_ENTRIES](state, entries) { Object.assign(state, { entries, @@ -157,9 +159,14 @@ export default { changed: Boolean(changedFile), staged: false, replaces: false, - prevPath: '', - moved: false, lastCommitSha: lastCommit.commit.id, + + prevId: undefined, + prevPath: undefined, + prevName: undefined, + prevUrl: undefined, + prevKey: undefined, + prevParentPath: undefined, }); if (prevPath) { @@ -209,7 +216,9 @@ export default { entry.deleted = true; - parent.tree = parent.tree.filter(f => f.path !== entry.path); + if (parent) { + parent.tree = parent.tree.filter(f => f.path !== entry.path); + } if (entry.type === 'blob') { if (tempFile) { @@ -219,51 +228,61 @@ export default { } } }, - [types.RENAME_ENTRY](state, { path, name, entryPath = null, parentPath }) { - const oldEntry = state.entries[entryPath || path]; - const slashedParentPath = parentPath ? `${parentPath}/` : ''; - const newPath = entryPath - ? `${slashedParentPath}${oldEntry.name}` - : `${slashedParentPath}${name}`; + [types.RENAME_ENTRY](state, { path, name, parentPath }) { + const oldEntry = state.entries[path]; + const newPath = parentPath ? `${parentPath}/${name}` : name; + const isRevert = newPath === oldEntry.prevPath; - Vue.set(state.entries, newPath, { + const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath); + + const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath); + + const baseProps = { ...oldEntry, + name, id: newPath, - key: `${newPath}-${oldEntry.type}-${oldEntry.path}`, path: newPath, - name: entryPath ? oldEntry.name : name, - tempFile: true, - prevPath: oldEntry.tempFile ? null : oldEntry.path, - url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), - tree: [], - raw: '', - opened: false, - parentPath, - }); + url: newUrl, + key: newKey, + parentPath: parentPath || '', + }; - oldEntry.moved = true; - oldEntry.movedPath = newPath; + const prevProps = + oldEntry.tempFile || isRevert + ? { + prevId: undefined, + prevPath: undefined, + prevName: undefined, + prevUrl: undefined, + prevKey: undefined, + prevParentPath: undefined, + } + : { + prevId: oldEntry.prevId || oldEntry.id, + prevPath: oldEntry.prevPath || oldEntry.path, + prevName: oldEntry.prevName || oldEntry.name, + prevUrl: oldEntry.prevUrl || oldEntry.url, + prevKey: oldEntry.prevKey || oldEntry.key, + prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath, + }; - const parent = parentPath - ? state.entries[parentPath] - : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - const newEntry = state.entries[newPath]; - - parent.tree = sortTree(parent.tree.concat(newEntry)); + Vue.set(state.entries, newPath, { + ...baseProps, + ...prevProps, + }); - if (newEntry.type === 'blob') { - state.changedFiles = state.changedFiles.concat(newEntry); + if (pathsAreEqual(oldEntry.parentPath, parentPath)) { + swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath); + } else { + removeFromParentTree(state, oldEntry.key, oldEntry.parentPath); + swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath); } - if (oldEntry.tempFile) { - const filterMethod = f => f.path !== oldEntry.path; - - state.openFiles = state.openFiles.filter(filterMethod); - state.changedFiles = state.changedFiles.filter(filterMethod); - parent.tree = parent.tree.filter(filterMethod); - - Vue.delete(state.entries, oldEntry.path); + if (oldEntry.type === 'blob') { + updateFileCollections(state, oldEntry.key, newPath); } + + Vue.delete(state.entries, oldEntry.path); }, ...projectMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 1442ea7dbfa..8caeb2d73b2 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -138,8 +138,6 @@ export default { content: stagedFile ? stagedFile.content : state.entries[path].raw, changed: false, deleted: false, - moved: false, - movedPath: '', }); if (deleted) { @@ -179,11 +177,6 @@ export default { }); if (stagedFile) { - Object.assign(state, { - replacedFiles: state.replacedFiles.concat({ - ...stagedFile, - }), - }); Object.assign(stagedFile, { ...state.entries[path], }); @@ -252,4 +245,15 @@ export default { openFiles: state.openFiles.filter(f => f.key !== file.key), }); }, + [types.REMOVE_FILE_FROM_STAGED_AND_CHANGED](state, file) { + Object.assign(state, { + changedFiles: state.changedFiles.filter(f => f.key !== file.key), + stagedFiles: state.stagedFiles.filter(f => f.key !== file.key), + }); + + Object.assign(state.entries[file.path], { + changed: false, + staged: false, + }); + }, }; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index c4da482bf0a..d400b9831a9 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -6,7 +6,6 @@ export default () => ({ currentMergeRequestId: '', changedFiles: [], stagedFiles: [], - replacedFiles: [], endpoints: {}, lastCommitMsg: '', lastCommitPath: '', diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 52200ce7847..a8d8ff31afe 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -50,9 +50,7 @@ export const dataStructure = () => ({ lastOpenedAt: 0, mrChange: null, deleted: false, - prevPath: '', - movedPath: '', - moved: false, + prevPath: undefined, }); export const decorateData = entity => { @@ -129,7 +127,7 @@ export const commitActionForFile = file => { export const getCommitFiles = stagedFiles => stagedFiles.reduce((acc, file) => { - if (file.moved || file.type === 'tree') return acc; + if (file.type === 'tree') return acc; return acc.concat({ ...file, @@ -148,9 +146,9 @@ export const createCommitPayload = ({ commit_message: state.commitMessage || getters.preBuiltCommitMessage, actions: getCommitFiles(rootState.stagedFiles).map(f => ({ action: commitActionForFile(f), - file_path: f.moved ? f.movedPath : f.path, - previous_path: f.prevPath === '' ? undefined : f.prevPath, - content: f.prevPath ? null : f.content || undefined, + file_path: f.path, + previous_path: f.prevPath || undefined, + content: f.prevPath && !f.changed ? null : f.content || undefined, encoding: f.base64 ? 'base64' : 'text', last_commit_id: newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha, @@ -213,3 +211,61 @@ export const mergeTrees = (fromTree, toTree) => { return toTree; }; + +export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); + +export const replaceFileUrl = (url, oldPath, newPath) => { + // Add `/-/` so that we don't accidentally replace project path + const result = url.replace(`/-/${escapeFileUrl(oldPath)}`, `/-/${escapeFileUrl(newPath)}`); + + return result; +}; + +export const swapInStateArray = (state, arr, key, entryPath) => + Object.assign(state, { + [arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)), + }); + +export const getEntryOrRoot = (state, path) => + path ? state.entries[path] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + +export const swapInParentTreeWithSorting = (state, oldKey, newPath, parentPath) => { + if (!newPath) { + return; + } + + const parent = getEntryOrRoot(state, parentPath); + + if (parent) { + const tree = parent.tree + // filter out old entry && new entry + .filter(({ key, path }) => key !== oldKey && path !== newPath) + // concat new entry + .concat(state.entries[newPath]); + + parent.tree = sortTree(tree); + } +}; + +export const removeFromParentTree = (state, oldKey, parentPath) => { + const parent = getEntryOrRoot(state, parentPath); + + if (parent) { + parent.tree = sortTree(parent.tree.filter(({ key }) => key !== oldKey)); + } +}; + +export const updateFileCollections = (state, key, entryPath) => { + ['openFiles', 'changedFiles', 'stagedFiles'].forEach(fileCollection => { + swapInStateArray(state, fileCollection, key, entryPath); + }); +}; + +export const cleanTrailingSlash = path => path.replace(/\/$/, ''); + +export const pathsAreEqual = (a, b) => { + const cleanA = a ? cleanTrailingSlash(a) : ''; + const cleanB = b ? cleanTrailingSlash(b) : ''; + + return cleanA === cleanB; +}; diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 54cd0c9c642..75c3c544c77 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -70,7 +70,13 @@ export default { return undefined; }, showIcon() { - return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted; + return ( + this.file.changed || + this.file.tempFile || + this.file.staged || + this.file.deleted || + this.file.prevPath + ); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index f49e69c473b..341c9534763 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -131,7 +131,7 @@ export default { </script> <template> - <div v-if="!file.moved"> + <div> <file-header v-if="file.isHeader" :path="file.path" /> <div v-else |