import * as cp from 'child_process' ; import { CancellationToken , Disposable , disposeAll , OutputChannel } from 'coc.nvim' ; import iconv from 'iconv-lite' ; import path from 'path' ; import { cpErrorHandler , IGit , onceEvent } from '../util' ; export interface SpawnOptions extends cp . SpawnOptions { input ? : string ; encoding ? : string ; log ? : boolean ; cancellationToken ? : CancellationToken ; } export interface IExecutionResult < T extends string | Buffer > { exitCode : number ; stdout : T ; stderr : string ; } export default class Git { constructor ( private gitInfo , private channel ) { } public async getRepositoryRoot ( repositoryPath ) { const result = await this . exec ( repositoryPath , [ 'rev-parse' , '--show-toplevel' ] ) ; return path . normalize ( result . stdout . trim ( ) ) ; } public async exec ( cwd , args , options = { } ) { options = Object . assign ( { cwd } , options || { } ) ; return await this . _exec ( args , options ) ; } public stream ( cwd , args , options = { } ) { options = Object . assign ( { cwd } , options || { } ) ; return this . spawn ( args , options ) ; } private async _exec ( args , options = { } ) { const child = this . spawn ( args , options ) ; if ( options . input ) { child . stdin . end ( options . input , 'utf8' ) ; } const bufferResult = await exec ( child , options . cancellationToken ) ; if ( options . log !== false && bufferResult . stderr . length > 0 ) { this . log ( `${ bufferResult . stderr }\n` ) ; } let encoding = options . encoding || 'utf8' ; encoding = iconv . encodingExists ( encoding ) ? encoding : 'utf8' ; const result = { exitCode : bufferResult . exitCode , stdout : iconv . decode ( bufferResult . stdout , encoding ) , stderr : bufferResult . stderr } ; if ( bufferResult . exitCode ) { this . channel . appendLine ( `Error ${ result . exitCode } on: 'git ${ args . join ( ' ' ) }' in ${ options . cwd }` ) ; this . channel . append ( result . stderr ) ; this . channel . append ( result . stdout ) ; return Promise . reject ( new Error ( 'Failed to execute git' ) ) ; } return result ; } private spawn ( args , options = { } ) { if ( ! options ) { options = { } ; } if ( ! options . stdio && ! options . input ) { options . stdio = [ 'ignore' , null , null ] ; } options . env = Object . assign ( { } , process . env , options . env || { } , { LC_ALL : 'en_US.UTF-8' , LANG : 'en_US.UTF-8' } ) ; if ( options . log !== false ) { this . log ( `> git ${ args . join ( ' ' ) }\n` ) ; } return cp . spawn ( this . gitInfo . path , args , options ) ; } private log ( output ) { this . channel . append ( output ) ; } } async function exec ( child , cancellationToken ? ) { if ( ! child . stdout || ! child . stderr ) { throw new Error ( 'Failed to get stdout or stderr from git process.' ) ; } if ( cancellationToken && cancellationToken . isCancellationRequested ) { throw new Error ( 'Cancelled' ) ; } const disposables = [ ] ; const once = ( ee , name , fn ) => { ee . once ( name , fn ) ; disposables . push ( Disposable . create ( ( ) => ee . removeListener ( name , fn ) ) ) ; } ; const on = ( ee , name , fn ) => { ee . on ( name , fn ) ; disposables . push ( Disposable . create ( ( ) => ee . removeListener ( name , fn ) ) ) ; } ; let result = Promise . all < any > ( [ new Promise < number > ( ( c , e ) => { once ( child , 'error' , cpErrorHandler ( e ) ) ; once ( child , 'exit' , c ) ; } ) , new Promise < Buffer > ( c => { const buffers = [ ] ; on ( child . stdout , 'data' , ( b ) => buffers . push ( b ) ) ; once ( child . stdout , 'close' , ( ) => c ( Buffer . concat ( buffers ) ) ) ; } ) , new Promise < string > ( c => { const buffers = [ ] ; on ( child . stderr , 'data' , ( b ) => buffers . push ( b ) ) ; once ( child . stderr , 'close' , ( ) => c ( Buffer . concat ( buffers ) . toString ( 'utf8' ) ) ) ; } ) ] ) as Promise < [ number , Buffer , string ] > ; if ( cancellationToken ) { const cancellationPromise = new Promise < [ number , Buffer , string ] > ( ( _ , e ) => { onceEvent ( cancellationToken . onCancellationRequested ) ( ( ) => { try { child . kill ( ) ; } catch ( err ) { } e ( new Error ( 'Cancelled' ) ) ; } ) ; } ) ; result = Promise . race ( [ result , cancellationPromise ] ) ; } try { const [ exitCode , stdout , stderr ] = await result ; return { exitCode , stdout , stderr } ; } finally { disposeAll ( disposables ) ; } }