Skip to content

Commit 00a1abe

Browse files
committed
cleanup git relative path calculation
1 parent 3f9e717 commit 00a1abe

3 files changed

Lines changed: 137 additions & 26 deletions

File tree

extensions/git/src/git.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ export const GitErrorCodes = {
337337
LocalChangesOverwritten: 'LocalChangesOverwritten',
338338
NoUpstreamBranch: 'NoUpstreamBranch',
339339
IsInSubmodule: 'IsInSubmodule',
340+
WrongCase: 'WrongCase',
340341
};
341342

342343
function getGitErrorCode(stderr: string): string | undefined {
@@ -642,6 +643,36 @@ export function parseGitCommit(raw: string): Commit | null {
642643
return { hash: match[1], message: match[3], parents };
643644
}
644645

646+
interface LsTreeElement {
647+
mode: string;
648+
type: string;
649+
object: string;
650+
file: string;
651+
}
652+
653+
export function parseLsTree(raw: string): LsTreeElement[] {
654+
return raw.split('\n')
655+
.filter(l => !!l)
656+
.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
657+
.filter(m => !!m)
658+
.map(([, mode, type, object, file]) => ({ mode, type, object, file }));
659+
}
660+
661+
interface LsFilesElement {
662+
mode: string;
663+
object: string;
664+
stage: string;
665+
file: string;
666+
}
667+
668+
export function parseLsFiles(raw: string): LsFilesElement[] {
669+
return raw.split('\n')
670+
.filter(l => !!l)
671+
.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
672+
.filter(m => !!m)
673+
.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
674+
}
675+
645676
export interface DiffOptions {
646677
cached?: boolean;
647678
}
@@ -710,13 +741,19 @@ export class Repository {
710741
return Promise.reject<Buffer>('Can\'t open file from git');
711742
}
712743

713-
const { exitCode, stdout } = await exec(child);
744+
const { exitCode, stdout, stderr } = await exec(child);
714745

715746
if (exitCode) {
716-
return Promise.reject<Buffer>(new GitError({
747+
const err = new GitError({
717748
message: 'Could not show object.',
718749
exitCode
719-
}));
750+
});
751+
752+
if (/exists on disk, but not in/.test(stderr)) {
753+
err.gitErrorCode = GitErrorCodes.WrongCase;
754+
}
755+
756+
return Promise.reject<Buffer>(err);
720757
}
721758

722759
return stdout;
@@ -751,25 +788,27 @@ export class Repository {
751788
return { mode, object, size: parseInt(size) };
752789
}
753790

754-
async lstreeOutput(treeish: string, path: string): Promise<string> {
755-
if (!treeish) { // index
756-
const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
757-
return stdout;
758-
}
791+
async lstree2(treeish: string, path: string): Promise<LsTreeElement[]> {
792+
const { stdout } = await this.run(['ls-tree', treeish, '--', path]);
793+
return parseLsTree(stdout);
794+
}
759795

760-
const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]);
761-
return stdout;
796+
async lsfiles(path: string): Promise<LsFilesElement[]> {
797+
const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
798+
return parseLsFiles(stdout);
762799
}
763800

764-
async relativePathToGitRelativePath(treeish: string, path: string): Promise<string> {
765-
let gitPath: string = path;
766-
const pathPrefix = path.substring(0, path.lastIndexOf('/'));
767-
const lstOutput = await this.lstreeOutput(treeish, pathPrefix + '/');
768-
const findResult = lstOutput.toUpperCase().indexOf(path.toUpperCase());
769-
if (findResult) {
770-
gitPath = lstOutput.substr(findResult, path.length);
801+
async getGitRelativePath(treeish: string, relativePath: string): Promise<string> {
802+
const relativePathLowercase = relativePath.toLowerCase();
803+
const dirname = path.posix.dirname(relativePath) + '/';
804+
const elements: { file: string; }[] = treeish ? await this.lstree2(treeish, dirname) : await this.lsfiles(dirname);
805+
const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0];
806+
807+
if (!element) {
808+
throw new GitError({ message: 'Git relative path not found.' });
771809
}
772-
return gitPath;
810+
811+
return element.file;
773812
}
774813

775814
async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {

extensions/git/src/repository.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -876,8 +876,8 @@ export class Repository implements Disposable {
876876
}
877877

878878
async show(ref: string, filePath: string): Promise<string> {
879-
return this.run(Operation.Show, async () => {
880-
let relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
879+
return await this.run(Operation.Show, async () => {
880+
const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
881881
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
882882
const defaultEncoding = configFiles.get<string>('encoding');
883883
const autoGuessEncoding = configFiles.get<boolean>('autoGuessEncoding');
@@ -886,8 +886,16 @@ export class Repository implements Disposable {
886886
ref = 'HEAD';
887887
}
888888

889-
relativePath = await this.repository.relativePathToGitRelativePath(ref, relativePath);
890-
return this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding);
889+
try {
890+
return await this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding);
891+
} catch (err) {
892+
if (err.gitErrorCode === GitErrorCodes.WrongCase) {
893+
const gitRelativePath = await this.repository.getGitRelativePath(ref, relativePath);
894+
return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding);
895+
}
896+
897+
throw err;
898+
}
891899
});
892900
}
893901

extensions/git/src/test/git.test.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'use strict';
77

88
import 'mocha';
9-
import { GitStatusParser, parseGitCommit, parseGitmodules } from '../git';
9+
import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
1010
import * as assert from 'assert';
1111

1212
suite('git', () => {
@@ -177,7 +177,7 @@ suite('git', () => {
177177
});
178178

179179
suite('parseGitCommit', () => {
180-
test('single parent commit', () => {
180+
test('single parent commit', function () {
181181
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
182182
8e5a374372b8393906c7e380dbb09349c5385554
183183
This is a commit message.`;
@@ -189,7 +189,7 @@ This is a commit message.`;
189189
});
190190
});
191191

192-
test('multiple parent commits', () => {
192+
test('multiple parent commits', function () {
193193
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
194194
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
195195
This is a commit message.`;
@@ -201,7 +201,7 @@ This is a commit message.`;
201201
});
202202
});
203203

204-
test('no parent commits', async () => {
204+
test('no parent commits', function () {
205205
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
206206
207207
This is a commit message.`;
@@ -213,4 +213,68 @@ This is a commit message.`;
213213
});
214214
});
215215
});
216+
217+
suite('parseLsTree', function () {
218+
test('sample', function () {
219+
const input = `040000 tree 0274a81f8ee9ca3669295dc40f510bd2021d0043 .vscode
220+
100644 blob 1d487c1817262e4f20efbfa1d04c18f51b0046f6 Screen Shot 2018-06-01 at 14.48.05.png
221+
100644 blob 686c16e4f019b734655a2576ce8b98749a9ffdb9 Screen Shot 2018-06-07 at 20.04.59.png
222+
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 boom.txt
223+
100644 blob 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 boomcaboom.txt
224+
100644 blob a68b14060589b16d7ac75f67b905c918c03c06eb file.js
225+
100644 blob f7bcfb05af46850d780f88c069edcd57481d822d file.md
226+
100644 blob ab8b86114a051f6490f1ec5e3141b9a632fb46b5 hello.js
227+
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 what.js
228+
100644 blob be859e3f412fa86513cd8bebe8189d1ea1a3e46d what.txt
229+
100644 blob 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 what.txt2`;
230+
231+
const output = parseLsTree(input);
232+
233+
assert.deepEqual(output, [
234+
{ mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', file: '.vscode' },
235+
{ mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', file: 'Screen Shot 2018-06-01 at 14.48.05.png' },
236+
{ mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', file: 'Screen Shot 2018-06-07 at 20.04.59.png' },
237+
{ mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', file: 'boom.txt' },
238+
{ mode: '100644', type: 'blob', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', file: 'boomcaboom.txt' },
239+
{ mode: '100644', type: 'blob', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', file: 'file.js' },
240+
{ mode: '100644', type: 'blob', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', file: 'file.md' },
241+
{ mode: '100644', type: 'blob', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', file: 'hello.js' },
242+
{ mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', file: 'what.js' },
243+
{ mode: '100644', type: 'blob', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', file: 'what.txt' },
244+
{ mode: '100644', type: 'blob', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', file: 'what.txt2' }
245+
]);
246+
});
247+
});
248+
249+
suite('parseLsFiles', function () {
250+
test('sample', function () {
251+
const input = `100644 7a73a41bfdf76d6f793007240d80983a52f15f97 0 .vscode/settings.json
252+
100644 1d487c1817262e4f20efbfa1d04c18f51b0046f6 0 Screen Shot 2018-06-01 at 14.48.05.png
253+
100644 686c16e4f019b734655a2576ce8b98749a9ffdb9 0 Screen Shot 2018-06-07 at 20.04.59.png
254+
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 boom.txt
255+
100644 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 0 boomcaboom.txt
256+
100644 a68b14060589b16d7ac75f67b905c918c03c06eb 0 file.js
257+
100644 f7bcfb05af46850d780f88c069edcd57481d822d 0 file.md
258+
100644 ab8b86114a051f6490f1ec5e3141b9a632fb46b5 0 hello.js
259+
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 what.js
260+
100644 be859e3f412fa86513cd8bebe8189d1ea1a3e46d 0 what.txt
261+
100644 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 0 what.txt2`;
262+
263+
const output = parseLsFiles(input);
264+
265+
assert.deepEqual(output, [
266+
{ mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' },
267+
{ mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' },
268+
{ mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' },
269+
{ mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'boom.txt' },
270+
{ mode: '100644', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', stage: '0', file: 'boomcaboom.txt' },
271+
{ mode: '100644', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', stage: '0', file: 'file.js' },
272+
{ mode: '100644', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', stage: '0', file: 'file.md' },
273+
{ mode: '100644', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', stage: '0', file: 'hello.js' },
274+
{ mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'what.js' },
275+
{ mode: '100644', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', stage: '0', file: 'what.txt' },
276+
{ mode: '100644', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', stage: '0', file: 'what.txt2' },
277+
]);
278+
});
279+
});
216280
});

0 commit comments

Comments
 (0)