Commit c6c52893 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '29451-webide-currentsha' into 'master'

Use current commit SHA when requesting IDE file tree and contents

See merge request gitlab-org/gitlab!19348
parents f27e4a9c d0972c99
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { escapeFileUrl } from '../stores/utils';
import Api from '~/api'; import Api from '~/api';
export default { export default {
...@@ -23,18 +25,25 @@ export default { ...@@ -23,18 +25,25 @@ export default {
.then(({ data }) => data); .then(({ data }) => data);
}, },
getBaseRawFileData(file, sha) { getBaseRawFileData(file, sha) {
if (file.tempFile) { if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw);
return Promise.resolve(file.baseRaw);
}
if (file.baseRaw) { // if files are renamed, their base path has changed
return Promise.resolve(file.baseRaw); const filePath =
} file.mrChange && file.mrChange.renamed_file ? file.mrChange.old_path : file.path;
return axios return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), { .get(
transformResponse: [f => f], joinPaths(
}) gon.relative_url_root || '/',
file.projectId,
'raw',
sha,
escapeFileUrl(filePath),
),
{
transformResponse: [f => f],
},
)
.then(({ data }) => data); .then(({ data }) => data);
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
...@@ -58,8 +67,8 @@ export default { ...@@ -58,8 +67,8 @@ export default {
commit(projectId, payload) { commit(projectId, payload) {
return Api.commitMultiple(projectId, payload); return Api.commitMultiple(projectId, payload);
}, },
getFiles(projectUrl, branchId) { getFiles(projectUrl, ref) {
const url = `${projectUrl}/files/${branchId}`; const url = `${projectUrl}/files/${ref}`;
return axios.get(url, { params: { format: 'json' } }); return axios.get(url, { params: { format: 'json' } });
}, },
lastCommitPipelines({ getters }) { lastCommitPipelines({ getters }) {
......
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths } from '~/lib/utils/url_utility';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import eventHub from '../../eventhub'; import eventHub from '../../eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle, replaceFileUrl, addFinalNewlineIfNeeded } from '../utils'; import { escapeFileUrl, addFinalNewlineIfNeeded, setPageTitleForFile } from '../utils';
import { viewerTypes, stageKeys } from '../../constants'; import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
...@@ -58,7 +57,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -58,7 +57,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
}; };
export const getFileData = ( export const getFileData = (
{ state, commit, dispatch }, { state, commit, dispatch, getters },
{ path, makeFileActive = true, openFile = makeFileActive }, { path, makeFileActive = true, openFile = makeFileActive },
) => { ) => {
const file = state.entries[path]; const file = state.entries[path];
...@@ -67,15 +66,18 @@ export const getFileData = ( ...@@ -67,15 +66,18 @@ export const getFileData = (
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
const url = file.prevPath ? replaceFileUrl(file.url, file.path, file.prevPath) : file.url; const url = joinPaths(
gon.relative_url_root || '/',
state.currentProjectId,
file.type,
getters.lastCommit && getters.lastCommit.id,
escapeFileUrl(file.prevPath || file.path),
);
return service return service
.getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/'))) .getFileData(url)
.then(({ data, headers }) => { .then(({ data }) => {
const normalizedHeaders = normalizeHeaders(headers); setPageTitleForFile(state, file);
let title = normalizedHeaders['PAGE-TITLE'];
title = file.prevPath ? title.replace(file.prevPath, file.path) : title;
setPageTitle(decodeURI(title));
if (data) commit(types.SET_FILE_DATA, { data, file }); if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path); if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
......
...@@ -152,15 +152,17 @@ export const openMergeRequest = ( ...@@ -152,15 +152,17 @@ export const openMergeRequest = (
.then(mr => { .then(mr => {
dispatch('setCurrentBranchId', mr.source_branch); dispatch('setCurrentBranchId', mr.source_branch);
dispatch('getBranchData', { // getFiles needs to be called after getting the branch data
// since files are fetched using the last commit sha of the branch
return dispatch('getBranchData', {
projectId, projectId,
branchId: mr.source_branch, branchId: mr.source_branch,
}); }).then(() =>
dispatch('getFiles', {
return dispatch('getFiles', { projectId,
projectId, branchId: mr.source_branch,
branchId: mr.source_branch, }),
}); );
}) })
.then(() => .then(() =>
dispatch('getMergeRequestVersions', { dispatch('getMergeRequestVersions', {
......
...@@ -46,7 +46,7 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL ...@@ -46,7 +46,7 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL
}); });
}; };
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => export const getFiles = ({ state, commit, dispatch, getters }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if ( if (
!state.trees[`${projectId}/${branchId}`] || !state.trees[`${projectId}/${branchId}`] ||
...@@ -54,10 +54,11 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = ...@@ -54,10 +54,11 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
state.trees[`${projectId}/${branchId}`].tree.length === 0) state.trees[`${projectId}/${branchId}`].tree.length === 0)
) { ) {
const selectedProject = state.projects[projectId]; const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); const selectedBranch = getters.findBranch(projectId, branchId);
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service service
.getFiles(selectedProject.web_url, branchId) .getFiles(selectedProject.web_url, selectedBranch.commit.id)
.then(({ data }) => { .then(({ data }) => {
const { entries, treeList } = decorateFiles({ const { entries, treeList } = decorateFiles({
data, data,
......
...@@ -34,7 +34,9 @@ export const currentMergeRequest = state => { ...@@ -34,7 +34,9 @@ export const currentMergeRequest = state => {
return null; return null;
}; };
export const currentProject = state => state.projects[state.currentProjectId]; export const findProject = state => projectId => state.projects[projectId];
export const currentProject = (state, getters) => getters.findProject(state.currentProjectId);
export const emptyRepo = state => export const emptyRepo = state =>
state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo; state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo;
...@@ -94,8 +96,14 @@ export const lastCommit = (state, getters) => { ...@@ -94,8 +96,14 @@ export const lastCommit = (state, getters) => {
return branch ? branch.commit : null; return branch ? branch.commit : null;
}; };
export const findBranch = (state, getters) => (projectId, branchId) => {
const project = getters.findProject(projectId);
return project && project.branches[branchId];
};
export const currentBranch = (state, getters) => export const currentBranch = (state, getters) =>
getters.currentProject && getters.currentProject.branches[state.currentBranchId]; getters.findBranch(state.currentProjectId, state.currentBranchId);
export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name; export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name;
......
...@@ -113,6 +113,11 @@ export const setPageTitle = title => { ...@@ -113,6 +113,11 @@ export const setPageTitle = title => {
document.title = title; document.title = title;
}; };
export const setPageTitleForFile = (state, file) => {
const title = [file.path, state.currentBranchId, state.currentProjectId, 'GitLab'].join(' · ');
setPageTitle(title);
};
export const commitActionForFile = file => { export const commitActionForFile = file => {
if (file.prevPath) { if (file.prevPath) {
return commitActionTypes.move; return commitActionTypes.move;
......
---
title: 'Resolve: Web IDE Throws Error When Viewing Diff for Renamed Files'
merge_request: 19348
author:
type: fixed
---
title: Use initial commit SHA instead of branch id to request IDE files and contents
merge_request: 19348
author: David Palubin
type: fixed
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import services from '~/ide/services'; import services from '~/ide/services';
import Api from '~/api'; import Api from '~/api';
import { escapeFileUrl } from '~/ide/stores/utils';
jest.mock('~/api'); jest.mock('~/api');
const TEST_PROJECT_ID = 'alice/wonderland'; const TEST_PROJECT_ID = 'alice/wonderland';
const TEST_BRANCH = 'master-patch-123'; const TEST_BRANCH = 'master-patch-123';
const TEST_COMMIT_SHA = '123456789'; const TEST_COMMIT_SHA = '123456789';
const TEST_FILE_PATH = 'README2.md';
const TEST_FILE_OLD_PATH = 'OLD_README2.md';
const TEST_FILE_PATH_SPECIAL = 'READM?ME/abc';
const TEST_FILE_CONTENTS = 'raw file content';
describe('IDE services', () => { describe('IDE services', () => {
describe('commit', () => { describe('commit', () => {
...@@ -28,4 +35,80 @@ describe('IDE services', () => { ...@@ -28,4 +35,80 @@ describe('IDE services', () => {
expect(Api.commitMultiple).toHaveBeenCalledWith(TEST_PROJECT_ID, payload); expect(Api.commitMultiple).toHaveBeenCalledWith(TEST_PROJECT_ID, payload);
}); });
}); });
describe('getBaseRawFileData', () => {
let file;
let mock;
beforeEach(() => {
file = {
mrChange: null,
projectId: TEST_PROJECT_ID,
path: TEST_FILE_PATH,
};
jest.spyOn(axios, 'get');
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('gives back file.baseRaw for files with that property present', () => {
file.baseRaw = TEST_FILE_CONTENTS;
return services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
expect(content).toEqual(TEST_FILE_CONTENTS);
});
});
it('gives back file.baseRaw for files for temp files', () => {
file.tempFile = true;
file.baseRaw = TEST_FILE_CONTENTS;
return services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
expect(content).toEqual(TEST_FILE_CONTENTS);
});
});
describe.each`
relativeUrlRoot | filePath | isRenamed
${''} | ${TEST_FILE_PATH} | ${false}
${''} | ${TEST_FILE_OLD_PATH} | ${true}
${''} | ${TEST_FILE_PATH_SPECIAL} | ${false}
${''} | ${TEST_FILE_PATH_SPECIAL} | ${true}
${'gitlab'} | ${TEST_FILE_OLD_PATH} | ${true}
`(
'with relativeUrlRoot ($relativeUrlRoot) and filePath ($filePath) and isRenamed ($isRenamed)',
({ relativeUrlRoot, filePath, isRenamed }) => {
beforeEach(() => {
if (isRenamed) {
file.mrChange = {
renamed_file: true,
old_path: filePath,
};
} else {
file.path = filePath;
}
gon.relative_url_root = relativeUrlRoot;
mock
.onGet(
`${relativeUrlRoot}/${TEST_PROJECT_ID}/raw/${TEST_COMMIT_SHA}/${escapeFileUrl(
filePath,
)}`,
)
.reply(200, TEST_FILE_CONTENTS);
});
it('fetches file content', () =>
services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
expect(content).toEqual(TEST_FILE_CONTENTS);
}));
},
);
});
}); });
...@@ -182,13 +182,25 @@ describe('IDE store file actions', () => { ...@@ -182,13 +182,25 @@ describe('IDE store file actions', () => {
spyOn(service, 'getFileData').and.callThrough(); spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`); localFile = file(`newCreate-${Math.random()}`);
localFile.url = `project/getFileDataURL`;
store.state.entries[localFile.path] = localFile; store.state.entries[localFile.path] = localFile;
store.state.currentProjectId = 'test/test';
store.state.currentBranchId = 'master';
store.state.projects['test/test'] = {
branches: {
master: {
commit: {
id: '7297abc',
},
},
},
};
}); });
describe('success', () => { describe('success', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce( mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).replyOnce(
200, 200,
{ {
blame_path: 'blame_path', blame_path: 'blame_path',
...@@ -210,7 +222,7 @@ describe('IDE store file actions', () => { ...@@ -210,7 +222,7 @@ describe('IDE store file actions', () => {
.dispatch('getFileData', { path: localFile.path }) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(service.getFileData).toHaveBeenCalledWith( expect(service.getFileData).toHaveBeenCalledWith(
`${RELATIVE_URL_ROOT}/project/getFileDataURL`, `${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`,
); );
done(); done();
...@@ -229,12 +241,11 @@ describe('IDE store file actions', () => { ...@@ -229,12 +241,11 @@ describe('IDE store file actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('sets document title', done => { it('sets document title with the branchId', done => {
store store
.dispatch('getFileData', { path: localFile.path }) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(document.title).toBe('testing getFileData'); expect(document.title).toBe(`${localFile.path} · master · test/test · GitLab`);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
...@@ -283,7 +294,7 @@ describe('IDE store file actions', () => { ...@@ -283,7 +294,7 @@ describe('IDE store file actions', () => {
localFile.path = 'new-shiny-file'; localFile.path = 'new-shiny-file';
store.state.entries[localFile.path] = localFile; store.state.entries[localFile.path] = localFile;
mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce( mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/old-dull-file`).replyOnce(
200, 200,
{ {
blame_path: 'blame_path', blame_path: 'blame_path',
...@@ -304,7 +315,7 @@ describe('IDE store file actions', () => { ...@@ -304,7 +315,7 @@ describe('IDE store file actions', () => {
store store
.dispatch('getFileData', { path: localFile.path }) .dispatch('getFileData', { path: localFile.path })
.then(() => { .then(() => {
expect(document.title).toBe('testing new-shiny-file'); expect(document.title).toBe(`new-shiny-file · master · test/test · GitLab`);
done(); done();
}) })
...@@ -314,14 +325,17 @@ describe('IDE store file actions', () => { ...@@ -314,14 +325,17 @@ describe('IDE store file actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(`project/getFileDataURL`).networkError(); mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).networkError();
}); });
it('dispatches error action', done => { it('dispatches error action', done => {
const dispatch = jasmine.createSpy('dispatch'); const dispatch = jasmine.createSpy('dispatch');
actions actions
.getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path }) .getFileData(
{ state: store.state, commit() {}, dispatch, getters: store.getters },
{ path: localFile.path },
)
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
text: 'An error occurred whilst loading the file.', text: 'An error occurred whilst loading the file.',
......
...@@ -356,8 +356,30 @@ describe('IDE store merge request actions', () => { ...@@ -356,8 +356,30 @@ describe('IDE store merge request actions', () => {
changes: [], changes: [],
}; };
store.state.entries = { store.state.entries = {
foo: {}, foo: {
bar: {}, type: 'blob',
},
bar: {
type: 'blob',
},
};
store.state.currentProjectId = 'test/test';
store.state.currentBranchId = 'master';
store.state.projects['test/test'] = {
branches: {
master: {
commit: {
id: '7297abc',
},
},
abcbranch: {
commit: {
id: '29020fc',
},
},
},
}; };
const originalDispatch = store.dispatch; const originalDispatch = store.dispatch;
...@@ -415,9 +437,11 @@ describe('IDE store merge request actions', () => { ...@@ -415,9 +437,11 @@ describe('IDE store merge request actions', () => {
it('updates activity bar view and gets file data, if changes are found', done => { it('updates activity bar view and gets file data, if changes are found', done => {
store.state.entries.foo = { store.state.entries.foo = {
url: 'test', url: 'test',
type: 'blob',
}; };
store.state.entries.bar = { store.state.entries.bar = {
url: 'test', url: 'test',
type: 'blob',
}; };
testMergeRequestChanges.changes = [ testMergeRequestChanges.changes = [
......
...@@ -31,7 +31,10 @@ describe('Multi-file store tree actions', () => { ...@@ -31,7 +31,10 @@ describe('Multi-file store tree actions', () => {
web_url: '', web_url: '',
branches: { branches: {
master: { master: {
workingReference: '1', workingReference: '12345678',
commit: {
id: '12345678',
},
}, },
}, },
}; };
...@@ -61,7 +64,7 @@ describe('Multi-file store tree actions', () => { ...@@ -61,7 +64,7 @@ describe('Multi-file store tree actions', () => {
store store
.dispatch('getFiles', basicCallParameters) .dispatch('getFiles', basicCallParameters)
.then(() => { .then(() => {
expect(service.getFiles).toHaveBeenCalledWith('', 'master'); expect(service.getFiles).toHaveBeenCalledWith('', '12345678');
done(); done();
}) })
...@@ -99,8 +102,18 @@ describe('Multi-file store tree actions', () => { ...@@ -99,8 +102,18 @@ describe('Multi-file store tree actions', () => {
store.state.projects = { store.state.projects = {
'abc/def': { 'abc/def': {
web_url: `${gl.TEST_HOST}/files`, web_url: `${gl.TEST_HOST}/files`,
branches: {
'master-testing': {
commit: {
id: '12345',
},
},
},
}, },
}; };
const getters = {
findBranch: () => store.state.projects['abc/def'].branches['master-testing'],
};
mock.onGet(/(.*)/).replyOnce(500); mock.onGet(/(.*)/).replyOnce(500);
...@@ -109,6 +122,7 @@ describe('Multi-file store tree actions', () => { ...@@ -109,6 +122,7 @@ describe('Multi-file store tree actions', () => {
commit() {}, commit() {},
dispatch, dispatch,
state: store.state, state: store.state,
getters,
}, },
{ {
projectId: 'abc/def', projectId: 'abc/def',
......
...@@ -163,20 +163,57 @@ describe('IDE store getters', () => { ...@@ -163,20 +163,57 @@ describe('IDE store getters', () => {
describe('currentBranch', () => { describe('currentBranch', () => {
it('returns current projects branch', () => { it('returns current projects branch', () => {
const localGetters = { localState.currentProjectId = 'abcproject';
currentProject: { localState.currentBranchId = 'master';
branches: { localState.projects.abcproject = {
master: { name: 'abcproject',
name: 'master', branches: {
}, master: {
name: 'master',
}, },
}, },
}; };
const localGetters = {
findBranch: jasmine.createSpy('findBranchSpy'),
};
getters.currentBranch(localState, localGetters);
expect(localGetters.findBranch).toHaveBeenCalledWith('abcproject', 'master');
});
});
describe('findProject', () => {
it('returns the project matching the id', () => {
localState.currentProjectId = 'abcproject';
localState.projects.abcproject = {
name: 'abcproject',
};
expect(getters.findProject(localState)('abcproject').name).toBe('abcproject');
});
});
describe('findBranch', () => {
let result;
it('returns the selected branch from a project', () => {
localState.currentProjectId = 'abcproject';
localState.currentBranchId = 'master'; localState.currentBranchId = 'master';
localState.projects.abcproject = {
name: 'abcproject',
branches: {
master: {
name: 'master',
},
},
};
const localGetters = {
findProject: () => localState.projects.abcproject,
};
expect(getters.currentBranch(localState, localGetters)).toEqual({ result = getters.findBranch(localState, localGetters)('abcproject', 'master');
name: 'master',
}); expect(result.name).toBe('master');
}); });
}); });
......
...@@ -11,6 +11,23 @@ describe('Multi-file store utils', () => { ...@@ -11,6 +11,23 @@ describe('Multi-file store utils', () => {
}); });
}); });
describe('setPageTitleForFile', () => {
it('sets the document page title for the file passed', () => {
const f = {
path: 'README.md',
};
const state = {
currentBranchId: 'master',
currentProjectId: 'test/test',
};
utils.setPageTitleForFile(state, f);
expect(document.title).toBe('README.md · master · test/test · GitLab');
});
});
describe('findIndexOfFile', () => { describe('findIndexOfFile', () => {
let localState; let localState;
......
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