Skip to content

Commit e471d53

Browse files
committed
Refactors Graph commit streaming to improve perf
Reworks `Git.stream` to properly handle errors Adds `Git.stream` support to Live Share (hacky) Removes unused `Git.spawn` and `Git.logStreamTo` methods
1 parent 0229c2a commit e471d53

File tree

12 files changed

+187
-414
lines changed

12 files changed

+187
-414
lines changed

src/env/browser/providers.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,6 @@ export function git(_options: GitCommandOptions, ..._args: any[]): Promise<GitRe
1313
return Promise.resolve({ stdout: '', exitCode: 0 });
1414
}
1515

16-
export function gitLogStreamTo(
17-
_repoPath: string,
18-
_sha: string,
19-
_limit: number,
20-
_options?: { configs?: readonly string[]; stdin?: string },
21-
..._args: string[]
22-
): Promise<[data: string[], count: number]> {
23-
return Promise.resolve([[''], 0]);
24-
}
25-
2616
export function getSupportedGitProviders(container: Container): Promise<GitProvider[]> {
2717
return Promise.resolve([new GitHubGitProvider(container)]);
2818
}

src/env/node/git/git.ts

Lines changed: 70 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ChildProcess, SpawnOptions } from 'child_process';
1+
import type { SpawnOptions } from 'child_process';
22
import { spawn } from 'child_process';
33
import { accessSync } from 'fs';
44
import { join as joinPath } from 'path';
@@ -349,7 +349,7 @@ export class Git {
349349
}
350350
}
351351

352-
async spawn(options: GitSpawnOptions, ...args: readonly (string | undefined)[]): Promise<ChildProcess> {
352+
async *stream(options: GitSpawnOptions, ...args: readonly (string | undefined)[]): AsyncGenerator<string> {
353353
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
354354

355355
const start = hrtime();
@@ -389,10 +389,21 @@ export class Git {
389389
runArgs.unshift('-c', 'core.longpaths=true');
390390
}
391391

392-
if (cancellation) {
392+
let disposable: Disposable | undefined;
393+
if (cancellation != null) {
393394
const aborter = new AbortController();
395+
const onAbort = () => aborter.abort();
396+
397+
const signal = spawnOpts.signal;
398+
disposable = {
399+
dispose: () => {
400+
cancellation?.onCancellationRequested(onAbort);
401+
signal?.removeEventListener('abort', onAbort);
402+
},
403+
};
404+
405+
spawnOpts.signal?.addEventListener('abort', onAbort);
394406
spawnOpts.signal = aborter.signal;
395-
cancellation.onCancellationRequested(() => aborter.abort());
396407
}
397408

398409
const command = await this.path();
@@ -402,111 +413,73 @@ export class Git {
402413
}
403414

404415
let exception: Error | undefined;
405-
proc.once('error', ex => {
406-
if (ex?.name === 'AbortError') {
407-
exception = new CancellationError(new CancelledRunError(command, true));
408-
} else {
409-
exception = new GitError(ex);
410-
}
411-
});
412-
proc.once('exit', () => this.logGitCommand(gitCommand, exception, getDurationMilliseconds(start), false));
413-
return proc;
414-
}
415-
416-
async *stream(options: GitSpawnOptions, ...args: readonly (string | undefined)[]): AsyncGenerator<string> {
417-
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
418-
419-
const start = hrtime();
420-
421-
const { cancellation, configs, stdin, stdinEncoding, ...opts } = options;
422-
const runArgs = args.filter(a => a != null);
423416

424-
const spawnOpts: SpawnOptions = {
425-
// Unless provided, ignore stdin and leave default streams for stdout and stderr
426-
stdio: [stdin ? 'pipe' : 'ignore', null, null],
427-
...opts,
428-
// Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581
429-
// Shouldn't *really* be needed but better safe than sorry
430-
env: {
431-
...process.env,
432-
...this._gitEnv,
433-
...(options.env ?? emptyObj),
434-
GCM_INTERACTIVE: 'NEVER',
435-
GCM_PRESERVE_CREDS: 'TRUE',
436-
LC_ALL: 'C',
437-
},
438-
};
439-
440-
const gitCommand = `(spawn) [${spawnOpts.cwd as string}] git ${runArgs.join(' ')}`;
441-
442-
// Fixes https://github.com/gitkraken/vscode-gitlens/issues/73 & https://github.com/gitkraken/vscode-gitlens/issues/161
443-
// See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x
444-
runArgs.unshift(
445-
'-c',
446-
'core.quotepath=false',
447-
'-c',
448-
'color.ui=false',
449-
...(configs !== undefined ? configs : emptyArray),
450-
);
451-
452-
if (process.platform === 'win32') {
453-
runArgs.unshift('-c', 'core.longpaths=true');
454-
}
417+
const promise = new Promise<void>((resolve, reject) => {
418+
const stderrChunks: string[] = [];
419+
if (proc.stderr) {
420+
proc.stderr?.setEncoding('utf8');
421+
proc.stderr.on('data', chunk => stderrChunks.push(chunk));
422+
}
455423

456-
const aborter = new AbortController();
457-
spawnOpts.signal = aborter.signal;
458-
cancellation?.onCancellationRequested(() => aborter.abort());
424+
proc.once('error', ex => {
425+
if (ex?.name === 'AbortError') return;
459426

460-
const command = await this.path();
461-
const proc = spawn(command, runArgs, spawnOpts);
462-
if (stdin) {
463-
proc.stdin?.end(stdin, (stdinEncoding ?? 'utf8') as BufferEncoding);
464-
}
427+
exception = new GitError(ex);
428+
});
429+
proc.once('close', (code, signal) => {
430+
// If the process exited normally or the caller didn't iterate over the complete stream, just resolve
431+
if (code === 0 || code === 141 /* SIGPIPE */) {
432+
resolve();
433+
return;
434+
}
465435

466-
let completed = false;
467-
let exception: Error | undefined;
436+
if (signal === 'SIGTERM') {
437+
reject(
438+
new CancellationError(
439+
new CancelledRunError(proc.spawnargs.join(' '), true, code ?? undefined, signal),
440+
),
441+
);
442+
return;
443+
}
468444

469-
proc.once(
470-
'error',
471-
ex =>
472-
(exception =
473-
ex?.name === 'AbortError'
474-
? new CancellationError(new CancelledRunError(command, true))
475-
: new GitError(ex)),
476-
);
477-
proc.once('exit', (code, signal) => {
478-
if (signal === 'SIGTERM') {
479-
exception = new CancellationError(new CancelledRunError(command, true, code ?? undefined, signal));
480-
}
481-
this.logGitCommand(gitCommand, exception, getDurationMilliseconds(start), false);
445+
const stderr = stderrChunks.join('').trim();
446+
reject(
447+
new GitError(
448+
new RunError(
449+
{
450+
message: `Error (${code}): ${stderr || 'Unknown'}`,
451+
cmd: proc.spawnargs.join(' '),
452+
killed: proc.killed,
453+
code: proc.exitCode,
454+
},
455+
'',
456+
stderr,
457+
),
458+
),
459+
);
460+
});
482461
});
483462

484463
try {
485-
if (!proc.stdout) {
486-
aborter.abort();
487-
throw new Error('Spawned Git process has no stdout');
488-
}
489-
proc.stdout.setEncoding('utf8');
490-
491-
for await (const chunk of proc.stdout) {
492-
if (exception != null) {
493-
if (isCancellationError(exception)) {
494-
// TODO: Should we throw here?
495-
break;
496-
} else {
497-
throw exception;
464+
try {
465+
if (proc.stdout) {
466+
proc.stdout.setEncoding('utf8');
467+
for await (const chunk of proc.stdout) {
468+
yield chunk;
498469
}
499470
}
500-
501-
yield chunk;
471+
} finally {
472+
// I have NO idea why this HAS to be in a finally block, but it does
473+
await promise;
502474
}
503-
504-
completed = true;
475+
} catch (ex) {
476+
exception = ex;
477+
throw ex;
505478
} finally {
506-
// If we didn't complete the iteration, then abort the process
507-
if (!completed) {
508-
aborter.abort();
509-
}
479+
disposable?.dispose();
480+
proc.removeAllListeners();
481+
482+
this.logGitCommand(gitCommand, exception, getDurationMilliseconds(start), false);
510483
}
511484
}
512485

@@ -1101,93 +1074,6 @@ export class Git {
11011074
}
11021075
}
11031076

1104-
async logStreamTo(
1105-
repoPath: string,
1106-
sha: string,
1107-
limit: number,
1108-
options?: { cancellation?: CancellationToken; configs?: readonly string[]; stdin?: string },
1109-
...args: string[]
1110-
): Promise<[data: string[], count: number]> {
1111-
const params = ['log', ...args];
1112-
if (options?.stdin) {
1113-
params.push('--stdin');
1114-
}
1115-
1116-
const proc = await this.spawn(
1117-
{
1118-
cwd: repoPath,
1119-
cancellation: options?.cancellation,
1120-
configs: options?.configs ?? gitLogDefaultConfigs,
1121-
stdin: options?.stdin,
1122-
},
1123-
...params,
1124-
'--',
1125-
);
1126-
1127-
// \x1E = ASCII Record Separator character
1128-
// \x1D = ASCII Group Separator character
1129-
const shaMatch = `\x1E${sha}\x1D`;
1130-
// eslint-disable-next-line no-control-regex
1131-
const shaMatchRegex = /\x1E.+?\x1D/g;
1132-
let found = false;
1133-
let count = 0;
1134-
1135-
return new Promise<[data: string[], count: number]>((resolve, reject) => {
1136-
const errData: string[] = [];
1137-
const data: string[] = [];
1138-
1139-
function onErrData(s: string) {
1140-
errData.push(s);
1141-
}
1142-
1143-
function onError(e: Error) {
1144-
reject(e);
1145-
}
1146-
1147-
function onExit(exitCode: number) {
1148-
if (exitCode !== 0) {
1149-
reject(new Error(errData.join('')));
1150-
}
1151-
1152-
resolve([data, count]);
1153-
}
1154-
1155-
function onData(s: string) {
1156-
data.push(s);
1157-
1158-
const matches = s.match(shaMatchRegex);
1159-
count += matches?.length ?? 0;
1160-
1161-
if (!found && matches?.includes(shaMatch)) {
1162-
found = true;
1163-
// Buffer a bit past the sha we are looking for
1164-
if (count > limit) {
1165-
limit = count + 50;
1166-
}
1167-
}
1168-
1169-
if (!found || count <= limit) return;
1170-
1171-
proc.removeListener('exit', onExit);
1172-
proc.removeListener('error', onError);
1173-
proc.stdout!.removeListener('data', onData);
1174-
proc.stderr!.removeListener('data', onErrData);
1175-
proc.kill();
1176-
1177-
resolve([data, count]);
1178-
}
1179-
1180-
proc.on('error', onError);
1181-
proc.on('exit', onExit);
1182-
1183-
proc.stdout!.setEncoding('utf8');
1184-
proc.stdout!.on('data', onData);
1185-
1186-
proc.stderr!.setEncoding('utf8');
1187-
proc.stderr!.on('data', onErrData);
1188-
});
1189-
}
1190-
11911077
async ls_files(
11921078
repoPath: string,
11931079
fileName: string,

0 commit comments

Comments
 (0)