1- import type { ChildProcess , SpawnOptions } from 'child_process' ;
1+ import type { SpawnOptions } from 'child_process' ;
22import { spawn } from 'child_process' ;
33import { accessSync } from 'fs' ;
44import { 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