fix(core): git hashing handles "unusual" pathname characters

See: https://git-scm.com/docs/git-status#_pathname_format_notes_and_z
This commit is contained in:
Tasos Bekos 2021-03-14 12:57:27 +02:00 committed by Victor Savkin
parent 9d863a7073
commit 26a383912a
2 changed files with 39 additions and 28 deletions

View File

@ -1,5 +1,5 @@
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import { rmdirSync } from 'fs-extra'; import { removeSync } from 'fs-extra';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { getFileHashes } from './git-hasher'; import { getFileHashes } from './git-hasher';
@ -14,7 +14,7 @@ describe('git-hasher', () => {
}); });
afterEach(() => { afterEach(() => {
rmdirSync(dir, { recursive: true }); removeSync(dir);
}); });
it('should work', () => { it('should work', () => {
@ -112,6 +112,28 @@ describe('git-hasher', () => {
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/moda.txt`]); expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/moda.txt`]);
}); });
it('should handle special characters in filenames', () => {
run(`echo AAA > "a-ū".txt`);
run(`echo BBB > "b-ū".txt`);
run(`git add .`);
run(`git commit -am init`);
expect([...getFileHashes(dir).keys()]).toEqual([
`${dir}/a-ū.txt`,
`${dir}/b-ū.txt`,
]);
run(`mv a-ū.txt moda-ū.txt`);
run(`git add .`);
run(`echo modified >> moda-ū.txt`);
expect([...getFileHashes(dir).keys()]).toEqual([
`${dir}/b-ū.txt`,
`${dir}/moda-ū.txt`,
]);
run(`rm "moda-ū.txt"`);
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/b-ū.txt`]);
});
function run(command: string) { function run(command: string) {
return execSync(command, { cwd: dir, stdio: ['pipe', 'pipe', 'pipe'] }); return execSync(command, { cwd: dir, stdio: ['pipe', 'pipe', 'pipe'] });
} }

View File

@ -6,7 +6,7 @@ function parseGitLsTree(output: string): Map<string, string> {
const changes: Map<string, string> = new Map<string, string>(); const changes: Map<string, string> = new Map<string, string>();
if (output) { if (output) {
const gitRegex: RegExp = /([0-9]{6})\s(blob|commit)\s([a-f0-9]{40})\s*(.*)/; const gitRegex: RegExp = /([0-9]{6})\s(blob|commit)\s([a-f0-9]{40})\s*(.*)/;
output.split('\n').forEach((line) => { output.split('\0').forEach((line) => {
if (line) { if (line) {
const matches: RegExpMatchArray | null = line.match(gitRegex); const matches: RegExpMatchArray | null = line.match(gitRegex);
if (matches && matches[3] && matches[4]) { if (matches && matches[3] && matches[4]) {
@ -27,31 +27,18 @@ function parseGitStatus(output: string): Map<string, string> {
if (!output) { if (!output) {
return changes; return changes;
} }
output
.trim()
.split('\n')
.forEach((line) => {
const [changeType, ...filenames] = line
.trim()
.match(/(?:[^\s"]+|"[^"]*")+/g)
.map((r) => (r.startsWith('"') ? r.substring(1, r.length - 1) : r))
.filter((r) => !!r);
if (changeType && filenames && filenames.length > 0) { var chunks = output.split('\0');
// the before filename we mark as deleted, so we remove it from the map for (let i = 0; i < chunks.length; i++) {
// changeType can be A/D/R/RM etc const chunk = chunks[i];
// if it R and RM, we need to split the output into before and after if (chunk.length) {
// the before part gets marked as deleted const change = chunk[0];
if (changeType[0] === 'R') { if (change === 'R') {
changes.set(filenames[0], 'D'); changes.set(chunks[++i], 'D');
changes.set(filenames[filenames.length - 1], changeType);
} else if (changeType === '??') {
changes.set(filenames.join(' '), changeType);
} else {
changes.set(filenames[filenames.length - 1], changeType);
}
} }
}); changes.set(chunk.substring(2).trim(), change);
}
}
return changes; return changes;
} }
@ -92,7 +79,9 @@ function getGitHashForFiles(
} }
function gitLsTree(path: string): Map<string, string> { function gitLsTree(path: string): Map<string, string> {
return parseGitLsTree(spawnProcess('git', ['ls-tree', 'HEAD', '-r'], path)); return parseGitLsTree(
spawnProcess('git', ['ls-tree', 'HEAD', '-r', '-z'], path)
);
} }
function gitStatus( function gitStatus(
@ -101,7 +90,7 @@ function gitStatus(
const deletedFiles: string[] = []; const deletedFiles: string[] = [];
const filesToHash: string[] = []; const filesToHash: string[] = [];
parseGitStatus( parseGitStatus(
spawnProcess('git', ['status', '-s', '-u', '.'], path) spawnProcess('git', ['status', '-s', '-u', '-z', '.'], path)
).forEach((changeType: string, filename: string) => { ).forEach((changeType: string, filename: string) => {
if (changeType !== 'D') { if (changeType !== 'D') {
filesToHash.push(filename); filesToHash.push(filename);