Commit f94ce61e authored by Himanshu Kapoor's avatar Himanshu Kapoor

Handle edge cases in stage and unstage mutations

Modify stage and unstage mutations to handle edge cases involving
the ability to stage empty content in Web IDE. Also handle cases
of files deleted and readded.
parent 9ade8e07
...@@ -134,12 +134,14 @@ export const scrollToTab = () => { ...@@ -134,12 +134,14 @@ export const scrollToTab = () => {
}); });
}; };
export const stageAllChanges = ({ state, commit, dispatch }) => { export const stageAllChanges = ({ state, commit, dispatch, getters }) => {
const openFile = state.openFiles[0]; const openFile = state.openFiles[0];
commit(types.SET_LAST_COMMIT_MSG, ''); commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); state.changedFiles.forEach(file =>
commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }),
);
dispatch('openPendingTab', { dispatch('openPendingTab', {
file: state.stagedFiles.find(f => f.path === openFile.path), file: state.stagedFiles.find(f => f.path === openFile.path),
...@@ -147,10 +149,12 @@ export const stageAllChanges = ({ state, commit, dispatch }) => { ...@@ -147,10 +149,12 @@ export const stageAllChanges = ({ state, commit, dispatch }) => {
}); });
}; };
export const unstageAllChanges = ({ state, commit, dispatch }) => { export const unstageAllChanges = ({ state, commit, dispatch, getters }) => {
const openFile = state.openFiles[0]; const openFile = state.openFiles[0];
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path)); state.stagedFiles.forEach(file =>
commit(types.UNSTAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }),
);
dispatch('openPendingTab', { dispatch('openPendingTab', {
file: state.changedFiles.find(f => f.path === openFile.path), file: state.changedFiles.find(f => f.path === openFile.path),
......
...@@ -214,11 +214,11 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = ...@@ -214,11 +214,11 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
}; };
export const stageChange = ({ commit, state, dispatch }, path) => { export const stageChange = ({ commit, state, dispatch, getters }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
const openFile = state.openFiles.find(f => f.path === path); const openFile = state.openFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path); commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
commit(types.SET_LAST_COMMIT_MSG, ''); commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) { if (stagedFile) {
...@@ -235,10 +235,10 @@ export const stageChange = ({ commit, state, dispatch }, path) => { ...@@ -235,10 +235,10 @@ export const stageChange = ({ commit, state, dispatch }, path) => {
} }
}; };
export const unstageChange = ({ commit, dispatch, state }, path) => { export const unstageChange = ({ commit, dispatch, state, getters }, path) => {
const openFile = state.openFiles.find(f => f.path === path); const openFile = state.openFiles.find(f => f.path === path);
commit(types.UNSTAGE_CHANGE, path); commit(types.UNSTAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
if (openFile && openFile.active) { if (openFile && openFile.active) {
const file = state.changedFiles.find(f => f.path === path); const file = state.changedFiles.find(f => f.path === path);
......
...@@ -164,31 +164,32 @@ export default { ...@@ -164,31 +164,32 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path), changedFiles: state.changedFiles.filter(f => f.path !== path),
}); });
}, },
[types.STAGE_CHANGE](state, path) { [types.STAGE_CHANGE](state, { path, diffInfo }) {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
Object.assign(state, { Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path), changedFiles: state.changedFiles.filter(f => f.path !== path),
entries: Object.assign(state.entries, { entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], { [path]: Object.assign(state.entries[path], {
staged: true, staged: diffInfo.exists,
changed: diffInfo.changed,
tempFile: diffInfo.tempFile,
deleted: diffInfo.deleted,
}), }),
}), }),
}); });
if (stagedFile) { if (stagedFile) {
Object.assign(stagedFile, { Object.assign(stagedFile, { ...state.entries[path] });
...state.entries[path],
});
} else { } else {
Object.assign(state, { state.stagedFiles = [...state.stagedFiles, { ...state.entries[path] }];
stagedFiles: state.stagedFiles.concat({ }
...state.entries[path],
}), if (!diffInfo.exists) {
}); state.stagedFiles = state.stagedFiles.filter(f => f.path !== path);
} }
}, },
[types.UNSTAGE_CHANGE](state, path) { [types.UNSTAGE_CHANGE](state, { path, diffInfo }) {
const changedFile = state.changedFiles.find(f => f.path === path); const changedFile = state.changedFiles.find(f => f.path === path);
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
...@@ -201,9 +202,11 @@ export default { ...@@ -201,9 +202,11 @@ export default {
changed: true, changed: true,
}); });
Object.assign(state, { state.changedFiles = state.changedFiles.concat(state.entries[path]);
changedFiles: state.changedFiles.concat(state.entries[path]), }
});
if (!diffInfo.exists) {
state.changedFiles = state.changedFiles.filter(f => f.path !== path);
} }
Object.assign(state, { Object.assign(state, {
...@@ -211,6 +214,9 @@ export default { ...@@ -211,6 +214,9 @@ export default {
entries: Object.assign(state.entries, { entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], { [path]: Object.assign(state.entries[path], {
staged: false, staged: false,
changed: diffInfo.changed,
tempFile: diffInfo.tempFile,
deleted: diffInfo.deleted,
}), }),
}), }),
}); });
......
---
title: "!21542 Part 3: Handle edge cases in stage and unstage mutations"
merge_request: 21676
author:
type: fixed
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores'; import { createStore } from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file'; import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services'; import service from '~/ide/services';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers'; import { file } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
const RELATIVE_URL_ROOT = '/gitlab'; const RELATIVE_URL_ROOT = '/gitlab';
describe('IDE store file actions', () => { describe('IDE store file actions', () => {
let mock; let mock;
let originalGon; let originalGon;
let store;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -24,12 +24,15 @@ describe('IDE store file actions', () => { ...@@ -24,12 +24,15 @@ describe('IDE store file actions', () => {
relative_url_root: RELATIVE_URL_ROOT, relative_url_root: RELATIVE_URL_ROOT,
}; };
store = createStore();
jest.spyOn(store, 'commit');
jest.spyOn(store, 'dispatch');
jest.spyOn(router, 'push').mockImplementation(() => {}); jest.spyOn(router, 'push').mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
resetStore(store);
window.gon = originalGon; window.gon = originalGon;
}); });
...@@ -663,31 +666,32 @@ describe('IDE store file actions', () => { ...@@ -663,31 +666,32 @@ describe('IDE store file actions', () => {
}); });
describe('stageChange', () => { describe('stageChange', () => {
it('calls STAGE_CHANGE with file path', done => { it('calls STAGE_CHANGE with file path', () => {
testAction( const f = { ...file('path'), content: 'old' };
actions.stageChange,
'path', store.state.entries[f.path] = f;
store.state,
[ actions.stageChange(store, 'path');
{ type: types.STAGE_CHANGE, payload: 'path' },
{ type: types.SET_LAST_COMMIT_MSG, payload: '' }, expect(store.commit.calls.allArgs()).toEqual([
], [types.STAGE_CHANGE, jest.objectContaining({ path: 'path' })],
[], [types.SET_LAST_COMMIT_MSG, ''],
done, ]);
);
}); });
}); });
describe('unstageChange', () => { describe('unstageChange', () => {
it('calls UNSTAGE_CHANGE with file path', done => { it('calls UNSTAGE_CHANGE with file path', () => {
testAction( const f = { ...file('path'), content: 'old' };
actions.unstageChange,
'path', store.state.entries[f.path] = f;
store.state, store.state.stagedFiles.push({ f, content: 'new' });
[{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
[], actions.unstageChange(store, 'path');
done,
); expect(store.commit.calls.allArgs()).toEqual([
[types.UNSTAGE_CHANGE, jest.objectContaining({ path: 'path' })],
]);
}); });
}); });
......
import mutations from '~/ide/stores/mutations/file'; import mutations from '~/ide/stores/mutations/file';
import state from '~/ide/stores/state'; import { createStore } from '~/ide/stores';
import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants'; import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants';
import { file } from '../../helpers'; import { file } from '../../helpers';
describe('IDE store file mutations', () => { describe('IDE store file mutations', () => {
let localState; let localState;
let localStore;
let localFile; let localFile;
beforeEach(() => { beforeEach(() => {
localState = state(); localStore = createStore();
localState = localStore.state;
localFile = { ...file(), type: 'blob' }; localFile = { ...file(), type: 'blob' };
localState.entries[localFile.path] = localFile; localState.entries[localFile.path] = localFile;
...@@ -333,44 +335,154 @@ describe('IDE store file mutations', () => { ...@@ -333,44 +335,154 @@ describe('IDE store file mutations', () => {
}); });
}); });
describe('STAGE_CHANGE', () => { describe.each`
mutationName | mutation | addedTo | removedFrom | staged | changedFilesCount | stagedFilesCount
${'STAGE_CHANGE'} | ${mutations.STAGE_CHANGE} | ${'stagedFiles'} | ${'changedFiles'} | ${true} | ${0} | ${1}
${'UNSTAGE_CHANGE'} | ${mutations.UNSTAGE_CHANGE} | ${'changedFiles'} | ${'stagedFiles'} | ${false} | ${1} | ${0}
`(
'$mutationName',
({ mutation, changedFilesCount, removedFrom, addedTo, staged, stagedFilesCount }) => {
let unstagedFile;
let stagedFile;
beforeEach(() => { beforeEach(() => {
mutations.STAGE_CHANGE(localState, localFile.path); unstagedFile = {
...file('file'),
type: 'blob',
raw: 'original content',
content: 'changed content',
};
stagedFile = {
...unstagedFile,
content: 'staged content',
staged: true,
};
localState.changedFiles.push(unstagedFile);
localState.stagedFiles.push(stagedFile);
localState.entries[unstagedFile.path] = unstagedFile;
}); });
it('adds file into stagedFiles array', () => { it('removes all changes of a file if staged and unstaged change contents are equal', () => {
expect(localState.stagedFiles.length).toBe(1); unstagedFile.content = 'original content';
expect(localState.stagedFiles[0]).toEqual(localFile);
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
}); });
it('updates stagedFile if it is already staged', () => { expect(localState.entries.file).toEqual(
localFile.raw = 'testing 123'; expect.objectContaining({
content: 'original content',
staged: false,
changed: false,
}),
);
expect(localState.stagedFiles.length).toBe(0);
expect(localState.changedFiles.length).toBe(0);
});
mutations.STAGE_CHANGE(localState, localFile.path); it('removes all changes of a file if a file is deleted and a new file with same content is added', () => {
stagedFile.deleted = true;
unstagedFile.tempFile = true;
unstagedFile.content = 'original content';
expect(localState.stagedFiles.length).toBe(1); mutation(localState, {
expect(localState.stagedFiles[0].raw).toEqual('testing 123'); path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
}); });
expect(localState.stagedFiles.length).toBe(0);
expect(localState.changedFiles.length).toBe(0);
expect(localState.entries.file).toEqual(
expect.objectContaining({
content: 'original content',
deleted: false,
tempFile: false,
}),
);
}); });
describe('UNSTAGE_CHANGE', () => { it('merges deleted and added file into a changed file if the contents differ', () => {
let f; stagedFile.deleted = true;
unstagedFile.tempFile = true;
unstagedFile.content = 'hello';
beforeEach(() => { mutation(localState, {
f = { ...file(), type: 'blob', staged: true }; path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
localState.stagedFiles.push(f); expect(localState.stagedFiles.length).toBe(stagedFilesCount);
localState.changedFiles.push(f); expect(localState.changedFiles.length).toBe(changedFilesCount);
localState.entries[f.path] = f;
expect(unstagedFile).toEqual(
expect.objectContaining({
content: 'hello',
staged,
deleted: false,
tempFile: false,
changed: true,
}),
);
}); });
it('removes from stagedFiles array', () => { it('does not remove file from stagedFiles and changedFiles if the file was renamed, even if the contents are equal', () => {
mutations.UNSTAGE_CHANGE(localState, f.path); unstagedFile.content = 'original content';
unstagedFile.prevPath = 'old_file';
expect(localState.stagedFiles.length).toBe(0); mutation(localState, {
expect(localState.changedFiles.length).toBe(1); path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.entries.file).toEqual(
expect.objectContaining({
content: 'original content',
staged,
changed: false,
prevPath: 'old_file',
}),
);
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
});
it(`removes file from ${removedFrom} array and adds it into ${addedTo} array`, () => {
localState.stagedFiles.length = 0;
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
const f = localState.stagedFiles[0] || localState.changedFiles[0];
expect(f).toEqual(unstagedFile);
});
it(`updates file in ${addedTo} array if it is was already present in it`, () => {
unstagedFile.raw = 'testing 123';
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
}); });
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
const f = localState.stagedFiles[0] || localState.changedFiles[0];
expect(f.raw).toEqual('testing 123');
}); });
},
);
describe('TOGGLE_FILE_CHANGED', () => { describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => { it('updates file changed status', () => {
......
...@@ -18,19 +18,19 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -18,19 +18,19 @@ import axios from '~/lib/utils/axios_utils';
import { createStore } from '~/ide/stores'; import { createStore } from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers'; import { file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub'; import eventHub from '~/ide/eventhub';
const store = createStore();
describe('Multi-file store actions', () => { describe('Multi-file store actions', () => {
let store;
beforeEach(() => { beforeEach(() => {
spyOn(router, 'push'); store = createStore();
});
afterEach(() => { spyOn(store, 'commit').and.callThrough();
resetStore(store); spyOn(store, 'dispatch').and.callThrough();
spyOn(router, 'push');
}); });
describe('redirectToUrl', () => { describe('redirectToUrl', () => {
...@@ -390,58 +390,43 @@ describe('Multi-file store actions', () => { ...@@ -390,58 +390,43 @@ describe('Multi-file store actions', () => {
}); });
}); });
describe('stageAllChanges', () => { describe('stage/unstageAllChanges', () => {
it('adds all files from changedFiles to stagedFiles', done => { let file1;
const openFile = { ...file(), path: 'test' }; let file2;
store.state.openFiles.push(openFile); beforeEach(() => {
store.state.stagedFiles.push(openFile); file1 = { ...file('test'), content: 'changed test', raw: 'test' };
store.state.changedFiles.push(openFile, file('new')); file2 = { ...file('test2'), content: 'changed test2', raw: 'test2' };
testAction( store.state.openFiles = [file1];
stageAllChanges, store.state.changedFiles = [file1];
null, store.state.stagedFiles = [{ ...file2, content: 'staged test' }];
store.state,
[ store.state.entries = {
{ type: types.SET_LAST_COMMIT_MSG, payload: '' }, [file1.path]: { ...file1 },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, [file2.path]: { ...file2 },
{ type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, };
], });
[
{ describe('stageAllChanges', () => {
type: 'openPendingTab', it('adds all files from changedFiles to stagedFiles', () => {
payload: { file: openFile, keyPrefix: 'staged' }, stageAllChanges(store);
},
], expect(store.commit.calls.allArgs()).toEqual([
done, [types.SET_LAST_COMMIT_MSG, ''],
); [types.STAGE_CHANGE, jasmine.objectContaining({ path: file1.path })],
]);
}); });
}); });
describe('unstageAllChanges', () => { describe('unstageAllChanges', () => {
it('removes all files from stagedFiles after unstaging', done => { it('removes all files from stagedFiles after unstaging', () => {
const openFile = { ...file(), path: 'test' }; unstageAllChanges(store);
store.state.openFiles.push(openFile); expect(store.commit.calls.allArgs()).toEqual([
store.state.changedFiles.push(openFile); [types.UNSTAGE_CHANGE, jasmine.objectContaining({ path: file2.path })],
store.state.stagedFiles.push(openFile, file('new')); ]);
});
testAction(
unstageAllChanges,
null,
store.state,
[
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
],
[
{
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'unstaged' },
},
],
done,
);
}); });
}); });
...@@ -752,10 +737,6 @@ describe('Multi-file store actions', () => { ...@@ -752,10 +737,6 @@ describe('Multi-file store actions', () => {
}); });
}); });
afterEach(() => {
resetStore(store);
});
it('by default renames an entry and adds to changed', done => { it('by default renames an entry and adds to changed', done => {
testAction( testAction(
renameEntry, renameEntry,
...@@ -966,7 +947,10 @@ describe('Multi-file store actions', () => { ...@@ -966,7 +947,10 @@ describe('Multi-file store actions', () => {
describe('error', () => { describe('error', () => {
let dispatch; let dispatch;
const callParams = [ let callParams;
beforeEach(() => {
callParams = [
{ {
commit() {}, commit() {},
state: store.state, state: store.state,
...@@ -976,8 +960,6 @@ describe('Multi-file store actions', () => { ...@@ -976,8 +960,6 @@ describe('Multi-file store actions', () => {
branchId: 'master-testing', branchId: 'master-testing',
}, },
]; ];
beforeEach(() => {
dispatch = jasmine.createSpy('dispatchSpy'); dispatch = jasmine.createSpy('dispatchSpy');
document.body.innerHTML += '<div class="flash-container"></div>'; document.body.innerHTML += '<div class="flash-container"></div>';
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment