Commit 21398b3e authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '39498-part-3' into 'master'

!21542 Part 2 & 3: Handle edge cases in stage and unstage mutations

See merge request gitlab-org/gitlab!21676
parents fa47aded a82aec39
...@@ -134,28 +134,40 @@ export const scrollToTab = () => { ...@@ -134,28 +134,40 @@ 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', { const file = getters.getStagedFile(openFile.path);
file: state.stagedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.staged, if (file) {
}); dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.staged,
});
}
}; };
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', { const file = getters.getChangedFile(openFile.path);
file: state.changedFiles.find(f => f.path === openFile.path),
keyPrefix: stageKeys.unstaged, if (file) {
}); dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.unstaged,
});
}
}; };
export const updateViewer = ({ commit }, viewer) => { export const updateViewer = ({ commit }, viewer) => {
......
...@@ -214,20 +214,20 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = ...@@ -214,20 +214,20 @@ 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, dispatch, getters }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = getters.getStagedFile(path);
const openFile = state.openFiles.find(f => f.path === path); const openFile = getters.getOpenFile(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) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
} }
if (openFile && openFile.active) { const file = getters.getStagedFile(path);
const file = state.stagedFiles.find(f => f.path === path);
if (openFile && openFile.active && file) {
dispatch('openPendingTab', { dispatch('openPendingTab', {
file, file,
keyPrefix: stageKeys.staged, keyPrefix: stageKeys.staged,
...@@ -235,14 +235,14 @@ export const stageChange = ({ commit, state, dispatch }, path) => { ...@@ -235,14 +235,14 @@ export const stageChange = ({ commit, state, dispatch }, path) => {
} }
}; };
export const unstageChange = ({ commit, dispatch, state }, path) => { export const unstageChange = ({ commit, dispatch, getters }, path) => {
const openFile = state.openFiles.find(f => f.path === path); const openFile = getters.getOpenFile(path);
commit(types.UNSTAGE_CHANGE, path); commit(types.UNSTAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
if (openFile && openFile.active) { const file = getters.getChangedFile(path);
const file = state.changedFiles.find(f => f.path === path);
if (openFile && openFile.active && file) {
dispatch('openPendingTab', { dispatch('openPendingTab', {
file, file,
keyPrefix: stageKeys.unstaged, keyPrefix: stageKeys.unstaged,
......
...@@ -64,6 +64,7 @@ export const allBlobs = state => ...@@ -64,6 +64,7 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path); export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const getOpenFile = state => path => state.openFiles.find(f => f.path === path);
export const lastOpenedFile = state => export const lastOpenedFile = state =>
[...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0]; [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
......
...@@ -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,30 +666,33 @@ describe('IDE store file actions', () => { ...@@ -663,30 +666,33 @@ 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).toHaveBeenCalledWith(
], types.STAGE_CHANGE,
[], expect.objectContaining({ path: 'path' }),
done,
); );
expect(store.commit).toHaveBeenCalledWith(types.SET_LAST_COMMIT_MSG, '');
}); });
}); });
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).toHaveBeenCalledWith(
types.UNSTAGE_CHANGE,
expect.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`
beforeEach(() => { mutationName | mutation | addedTo | removedFrom | staged | changedFilesCount | stagedFilesCount
mutations.STAGE_CHANGE(localState, localFile.path); ${'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(() => {
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);
});
it('updates stagedFile if it is already staged', () => { mutation(localState, {
localFile.raw = 'testing 123'; path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
mutations.STAGE_CHANGE(localState, localFile.path); expect(localState.entries.file).toEqual(
expect.objectContaining({
content: 'original content',
staged: false,
changed: false,
}),
);
expect(localState.stagedFiles.length).toBe(1); expect(localState.stagedFiles.length).toBe(0);
expect(localState.stagedFiles[0].raw).toEqual('testing 123'); expect(localState.changedFiles.length).toBe(0);
}); });
});
describe('UNSTAGE_CHANGE', () => { it('removes all changes of a file if a file is deleted and a new file with same content is added', () => {
let f; stagedFile.deleted = true;
unstagedFile.tempFile = true;
unstagedFile.content = 'original content';
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(0);
localState.changedFiles.push(f); expect(localState.changedFiles.length).toBe(0);
localState.entries[f.path] = f;
});
it('removes from stagedFiles array', () => { expect(localState.entries.file).toEqual(
mutations.UNSTAGE_CHANGE(localState, f.path); expect.objectContaining({
content: 'original content',
deleted: false,
tempFile: false,
}),
);
});
expect(localState.stagedFiles.length).toBe(0); it('merges deleted and added file into a changed file if the contents differ', () => {
expect(localState.changedFiles.length).toBe(1); stagedFile.deleted = true;
}); unstagedFile.tempFile = true;
}); unstagedFile.content = 'hello';
mutation(localState, {
path: unstagedFile.path,
diffInfo: localStore.getters.getDiffInfo(unstagedFile.path),
});
expect(localState.stagedFiles.length).toBe(stagedFilesCount);
expect(localState.changedFiles.length).toBe(changedFilesCount);
expect(unstagedFile).toEqual(
expect.objectContaining({
content: 'hello',
staged,
deleted: false,
tempFile: false,
changed: true,
}),
);
});
it('does not remove file from stagedFiles and changedFiles if the file was renamed, even if the contents are equal', () => {
unstagedFile.content = 'original content';
unstagedFile.prevPath = 'old_file';
mutation(localState, {
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,82 @@ describe('Multi-file store actions', () => { ...@@ -390,58 +390,82 @@ 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 }, };
],
[
{
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'staged' },
},
],
done,
);
}); });
});
describe('unstageAllChanges', () => { describe('stageAllChanges', () => {
it('removes all files from stagedFiles after unstaging', done => { it('adds all files from changedFiles to stagedFiles', () => {
const openFile = { ...file(), path: 'test' }; stageAllChanges(store);
store.state.openFiles.push(openFile); expect(store.commit.calls.allArgs()).toEqual([
store.state.changedFiles.push(openFile); [types.SET_LAST_COMMIT_MSG, ''],
store.state.stagedFiles.push(openFile, file('new')); [types.STAGE_CHANGE, jasmine.objectContaining({ path: file1.path })],
]);
});
testAction( it('opens pending tab if a change exists in that file', () => {
unstageAllChanges, stageAllChanges(store);
null,
store.state, expect(store.dispatch.calls.allArgs()).toEqual([
[ [
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path }, 'openPendingTab',
{ type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path }, { file: { ...file1, staged: true, changed: true }, keyPrefix: 'staged' },
], ],
[ ]);
{ });
type: 'openPendingTab',
payload: { file: openFile, keyPrefix: 'unstaged' }, it('does not open pending tab if no change exists in that file', () => {
}, store.state.entries[file1.path].content = 'test';
], store.state.stagedFiles = [file1];
done, store.state.changedFiles = [store.state.entries[file1.path]];
);
stageAllChanges(store);
expect(store.dispatch).not.toHaveBeenCalled();
});
});
describe('unstageAllChanges', () => {
it('removes all files from stagedFiles after unstaging', () => {
unstageAllChanges(store);
expect(store.commit.calls.allArgs()).toEqual([
[types.UNSTAGE_CHANGE, jasmine.objectContaining({ path: file2.path })],
]);
});
it('opens pending tab if a change exists in that file', () => {
unstageAllChanges(store);
expect(store.dispatch.calls.allArgs()).toEqual([
['openPendingTab', { file: file1, keyPrefix: 'unstaged' }],
]);
});
it('does not open pending tab if no change exists in that file', () => {
store.state.entries[file1.path].content = 'test';
store.state.stagedFiles = [file1];
store.state.changedFiles = [store.state.entries[file1.path]];
unstageAllChanges(store);
expect(store.dispatch).not.toHaveBeenCalled();
});
}); });
}); });
...@@ -752,10 +776,6 @@ describe('Multi-file store actions', () => { ...@@ -752,10 +776,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,18 +986,19 @@ describe('Multi-file store actions', () => { ...@@ -966,18 +986,19 @@ describe('Multi-file store actions', () => {
describe('error', () => { describe('error', () => {
let dispatch; let dispatch;
const callParams = [ let callParams;
{
commit() {},
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
];
beforeEach(() => { beforeEach(() => {
callParams = [
{
commit() {},
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
];
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