From 00a1abe9127cf744fde7a73a64bc29aa7539688f Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 6 Jul 2018 16:37:53 +0200 Subject: [PATCH] cleanup git relative path calculation --- extensions/git/src/git.ts | 75 ++++++++++++++++++++++------- extensions/git/src/repository.ts | 16 ++++-- extensions/git/src/test/git.test.ts | 72 +++++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 26 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index c1fc82cb489..6cd7e5afa46 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -337,6 +337,7 @@ export const GitErrorCodes = { LocalChangesOverwritten: 'LocalChangesOverwritten', NoUpstreamBranch: 'NoUpstreamBranch', IsInSubmodule: 'IsInSubmodule', + WrongCase: 'WrongCase', }; function getGitErrorCode(stderr: string): string | undefined { @@ -642,6 +643,36 @@ export function parseGitCommit(raw: string): Commit | null { return { hash: match[1], message: match[3], parents }; } +interface LsTreeElement { + mode: string; + type: string; + object: string; + file: string; +} + +export function parseLsTree(raw: string): LsTreeElement[] { + return raw.split('\n') + .filter(l => !!l) + .map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!) + .filter(m => !!m) + .map(([, mode, type, object, file]) => ({ mode, type, object, file })); +} + +interface LsFilesElement { + mode: string; + object: string; + stage: string; + file: string; +} + +export function parseLsFiles(raw: string): LsFilesElement[] { + return raw.split('\n') + .filter(l => !!l) + .map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!) + .filter(m => !!m) + .map(([, mode, object, stage, file]) => ({ mode, object, stage, file })); +} + export interface DiffOptions { cached?: boolean; } @@ -710,13 +741,19 @@ export class Repository { return Promise.reject('Can\'t open file from git'); } - const { exitCode, stdout } = await exec(child); + const { exitCode, stdout, stderr } = await exec(child); if (exitCode) { - return Promise.reject(new GitError({ + const err = new GitError({ message: 'Could not show object.', exitCode - })); + }); + + if (/exists on disk, but not in/.test(stderr)) { + err.gitErrorCode = GitErrorCodes.WrongCase; + } + + return Promise.reject(err); } return stdout; @@ -751,25 +788,27 @@ export class Repository { return { mode, object, size: parseInt(size) }; } - async lstreeOutput(treeish: string, path: string): Promise { - if (!treeish) { // index - const { stdout } = await this.run(['ls-files', '--stage', '--', path]); - return stdout; - } + async lstree2(treeish: string, path: string): Promise { + const { stdout } = await this.run(['ls-tree', treeish, '--', path]); + return parseLsTree(stdout); + } - const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]); - return stdout; + async lsfiles(path: string): Promise { + const { stdout } = await this.run(['ls-files', '--stage', '--', path]); + return parseLsFiles(stdout); } - async relativePathToGitRelativePath(treeish: string, path: string): Promise { - let gitPath: string = path; - const pathPrefix = path.substring(0, path.lastIndexOf('/')); - const lstOutput = await this.lstreeOutput(treeish, pathPrefix + '/'); - const findResult = lstOutput.toUpperCase().indexOf(path.toUpperCase()); - if (findResult) { - gitPath = lstOutput.substr(findResult, path.length); + async getGitRelativePath(treeish: string, relativePath: string): Promise { + const relativePathLowercase = relativePath.toLowerCase(); + const dirname = path.posix.dirname(relativePath) + '/'; + const elements: { file: string; }[] = treeish ? await this.lstree2(treeish, dirname) : await this.lsfiles(dirname); + const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0]; + + if (!element) { + throw new GitError({ message: 'Git relative path not found.' }); } - return gitPath; + + return element.file; } async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index fa1daf8b36c..7c55a0ce22f 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -876,8 +876,8 @@ export class Repository implements Disposable { } async show(ref: string, filePath: string): Promise { - return this.run(Operation.Show, async () => { - let relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); + return await this.run(Operation.Show, async () => { + const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); @@ -886,8 +886,16 @@ export class Repository implements Disposable { ref = 'HEAD'; } - relativePath = await this.repository.relativePathToGitRelativePath(ref, relativePath); - return this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding); + try { + return await this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.WrongCase) { + const gitRelativePath = await this.repository.getGitRelativePath(ref, relativePath); + return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); + } + + throw err; + } }); } diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 781f91b6f00..76533949e34 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -6,7 +6,7 @@ 'use strict'; import 'mocha'; -import { GitStatusParser, parseGitCommit, parseGitmodules } from '../git'; +import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git'; import * as assert from 'assert'; suite('git', () => { @@ -177,7 +177,7 @@ suite('git', () => { }); suite('parseGitCommit', () => { - test('single parent commit', () => { + test('single parent commit', function () { const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1 8e5a374372b8393906c7e380dbb09349c5385554 This is a commit message.`; @@ -189,7 +189,7 @@ This is a commit message.`; }); }); - test('multiple parent commits', () => { + test('multiple parent commits', function () { const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 This is a commit message.`; @@ -201,7 +201,7 @@ This is a commit message.`; }); }); - test('no parent commits', async () => { + test('no parent commits', function () { const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 This is a commit message.`; @@ -213,4 +213,68 @@ This is a commit message.`; }); }); }); + + suite('parseLsTree', function () { + test('sample', function () { + const input = `040000 tree 0274a81f8ee9ca3669295dc40f510bd2021d0043 .vscode +100644 blob 1d487c1817262e4f20efbfa1d04c18f51b0046f6 Screen Shot 2018-06-01 at 14.48.05.png +100644 blob 686c16e4f019b734655a2576ce8b98749a9ffdb9 Screen Shot 2018-06-07 at 20.04.59.png +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 boom.txt +100644 blob 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 boomcaboom.txt +100644 blob a68b14060589b16d7ac75f67b905c918c03c06eb file.js +100644 blob f7bcfb05af46850d780f88c069edcd57481d822d file.md +100644 blob ab8b86114a051f6490f1ec5e3141b9a632fb46b5 hello.js +100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 what.js +100644 blob be859e3f412fa86513cd8bebe8189d1ea1a3e46d what.txt +100644 blob 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 what.txt2`; + + const output = parseLsTree(input); + + assert.deepEqual(output, [ + { mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', file: '.vscode' }, + { mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, + { mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, + { mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', file: 'boom.txt' }, + { mode: '100644', type: 'blob', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', file: 'boomcaboom.txt' }, + { mode: '100644', type: 'blob', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', file: 'file.js' }, + { mode: '100644', type: 'blob', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', file: 'file.md' }, + { mode: '100644', type: 'blob', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', file: 'hello.js' }, + { mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', file: 'what.js' }, + { mode: '100644', type: 'blob', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', file: 'what.txt' }, + { mode: '100644', type: 'blob', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', file: 'what.txt2' } + ]); + }); + }); + + suite('parseLsFiles', function () { + test('sample', function () { + const input = `100644 7a73a41bfdf76d6f793007240d80983a52f15f97 0 .vscode/settings.json +100644 1d487c1817262e4f20efbfa1d04c18f51b0046f6 0 Screen Shot 2018-06-01 at 14.48.05.png +100644 686c16e4f019b734655a2576ce8b98749a9ffdb9 0 Screen Shot 2018-06-07 at 20.04.59.png +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 boom.txt +100644 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 0 boomcaboom.txt +100644 a68b14060589b16d7ac75f67b905c918c03c06eb 0 file.js +100644 f7bcfb05af46850d780f88c069edcd57481d822d 0 file.md +100644 ab8b86114a051f6490f1ec5e3141b9a632fb46b5 0 hello.js +100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 what.js +100644 be859e3f412fa86513cd8bebe8189d1ea1a3e46d 0 what.txt +100644 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 0 what.txt2`; + + const output = parseLsFiles(input); + + assert.deepEqual(output, [ + { mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' }, + { mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' }, + { mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' }, + { mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'boom.txt' }, + { mode: '100644', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', stage: '0', file: 'boomcaboom.txt' }, + { mode: '100644', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', stage: '0', file: 'file.js' }, + { mode: '100644', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', stage: '0', file: 'file.md' }, + { mode: '100644', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', stage: '0', file: 'hello.js' }, + { mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'what.js' }, + { mode: '100644', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', stage: '0', file: 'what.txt' }, + { mode: '100644', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', stage: '0', file: 'what.txt2' }, + ]); + }); + }); }); \ No newline at end of file -- GitLab