/********************************************************************************
 * Copyright (C) 2018 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import * as fs from '@theia/core/shared/fs-extra' ; import * as Path from 'path' ; import { injectable , inject , postConstruct } from '@theia/core/shared/inversify' ; import { git } from 'dugite-extra/lib/core/git' ; import { push } from 'dugite-extra/lib/command/push' ; import { pull } from 'dugite-extra/lib/command/pull' ; import { clone } from 'dugite-extra/lib/command/clone' ; import { fetch } from 'dugite-extra/lib/command/fetch' ; import { stash } from 'dugite-extra/lib/command/stash' ; import { merge } from 'dugite-extra/lib/command/merge' ; import { FileUri } from '@theia/core/lib/node/file-uri' ; import { getStatus } from 'dugite-extra/lib/command/status' ; import { createCommit } from 'dugite-extra/lib/command/commit' ; import { stage , unstage } from 'dugite-extra/lib/command/stage' ; import { reset , GitResetMode } from 'dugite-extra/lib/command/reset' ; import { getTextContents , getBlobContents } from 'dugite-extra/lib/command/show' ; import { checkoutBranch , checkoutPaths } from 'dugite-extra/lib/command/checkout' ; import { createBranch , deleteBranch , renameBranch , listBranch } from 'dugite-extra/lib/command/branch' ; import { IStatusResult , IAheadBehind , AppFileStatus , WorkingDirectoryStatus as DugiteStatus , FileChange as DugiteFileChange } from 'dugite-extra/lib/model/status' ; import { Branch as DugiteBranch } from 'dugite-extra/lib/model/branch' ; import { Commit as DugiteCommit , CommitIdentity as DugiteCommitIdentity } from 'dugite-extra/lib/model/commit' ; import { ILogger } from '@theia/core' ; import { Deferred } from '@theia/core/lib/common/promise-util' ; import * as strings from '@theia/core/lib/common/strings' ; import { Git , GitUtils , Repository , WorkingDirectoryStatus , GitFileChange , GitFileStatus , Branch , Commit , CommitIdentity , GitResult , CommitWithChanges , GitFileBlame , CommitLine , GitError , Remote , StashEntry } from '../common' ; import { GitRepositoryManager } from './git-repository-manager' ; import { GitLocator } from './git-locator/git-locator-protocol' ; import { GitExecProvider } from './git-exec-provider' ; import { GitEnvProvider } from './env/git-env-provider' ; import { GitInit } from './init/git-init' ; /**
 * Parsing and converting raw Git output into Git model instances.
 */ @ injectable ( ) export abstract class OutputParser < T > { /** This is the `NUL` delimiter. Equals wih `%x00`. */ static readonly LINE_DELIMITER = '\0' ; abstract parse ( repositoryUri , raw , delimiter ? ) ; abstract parse ( repositoryUri , items ) ; abstract parse ( repositoryUri , input , delimiter ? ) ; protected toUri ( repositoryUri , pathSegment ) { return FileUri . create ( Path . join ( FileUri . fsPath ( repositoryUri ) , pathSegment ) ) . toString ( ) ; } protected split ( input , delimiter ) { return ( Array . isArray ( input ) ? input : input . split ( delimiter ) ) . filter ( item => item && item . length > 0 ) ; } } /**
 * Status parser for converting raw Git `--name-status` output into file change objects.
 */ @ injectable ( ) export class NameStatusParser extends OutputParser < GitFileChange > { parse ( repositoryUri , input , delimiter = OutputParser . LINE_DELIMITER ) { const items = this . split ( input , delimiter ) ; const changes = [ ] ; let index = 0 ; while ( index < items . length ) { const rawStatus = items [ index ] ; const status = GitUtils . mapStatus ( rawStatus ) ; if ( GitUtils . isSimilarityStatus ( rawStatus ) ) { const uri = this . toUri ( repositoryUri , items [ index + 2 ] ) ; const oldUri = this . toUri ( repositoryUri , items [ index + 1 ] ) ; changes . push ( { status , uri , oldUri , staged : true } ) ; index = index + 3 ; } else { const uri = this . toUri ( repositoryUri , items [ index + 1 ] ) ; changes . push ( { status , uri , staged : true } ) ; index = index + 2 ; } } return changes ; } } /**
 * Built-in Git placeholders for tuning the `--format` option for `git diff` or `git log`.
 */ export enum CommitPlaceholders { HASH = '%H' , SHORT_HASH = '%h' , AUTHOR_EMAIL = '%aE' , AUTHOR_NAME = '%aN' , AUTHOR_DATE = '%aI' , AUTHOR_RELATIVE_DATE = '%ar' , SUBJECT = '%s' , BODY = '%b' } /**
 * Parser for converting raw, Git commit details into `CommitWithChanges` instances.
 */ @ injectable ( ) export class CommitDetailsParser extends OutputParser < CommitWithChanges > { static readonly ENTRY_DELIMITER = '\x01' ; static readonly COMMIT_CHUNK_DELIMITER = '\x02' ; static readonly DEFAULT_PLACEHOLDERS = [ CommitPlaceholders . HASH , CommitPlaceholders . AUTHOR_EMAIL , CommitPlaceholders . AUTHOR_NAME , CommitPlaceholders . AUTHOR_DATE , CommitPlaceholders . AUTHOR_RELATIVE_DATE , CommitPlaceholders . SUBJECT , CommitPlaceholders . BODY ] ; @ inject ( NameStatusParser ) protected readonly nameStatusParser : NameStatusParser ; parse ( repositoryUri , input , delimiter = CommitDetailsParser . COMMIT_CHUNK_DELIMITER ) { const chunks = this . split ( input , delimiter ) ; const changes = [ ] ; for ( const chunk of chunks ) { const [ sha , email , name , timestamp , authorDateRelative , summary , body , rawChanges ] = chunk . trim ( ) . split ( CommitDetailsParser . ENTRY_DELIMITER ) ; const fileChanges = this . nameStatusParser . parse ( repositoryUri , ( rawChanges || '' ) . trim ( ) ) ; changes . push ( { sha , author : { timestamp , email , name } , authorDateRelative , summary , body , fileChanges } ) ; } return changes ; } getFormat ( ... placeholders ) { return '%x02' + placeholders . join ( '%x01' ) + '%x01' ; } } @ injectable ( ) export class GitBlameParser { async parse ( fileUri , gitBlameOutput , commitBody ) { if ( ! gitBlameOutput ) { return undefined ; } const parsedEntries = this . parseEntries ( gitBlameOutput ) ; return this . createFileBlame ( fileUri , parsedEntries , commitBody ) ; } protected parseEntries ( rawOutput ) { const result = [ ] ; let current ; for ( const line of strings . split ( rawOutput , '\n' ) ) { if ( current === undefined ) { current = { } ; } if ( GitBlameParser . pumpEntry ( current , line ) ) { result . push ( current ) ; current = undefined ; } } return result ; } protected async createFileBlame ( uri , blameEntries , commitBody ) { const commits = new Map < string , Commit > ( ) ; const lines = [ ] ; for ( const entry of blameEntries ) { const sha = entry . sha ! ; let commit = commits . get ( sha ) ; if ( ! commit ) { commit = < Commit > { sha , author : { name : entry . author , email : entry . authorMail , timestamp : entry . authorTime ? new Date ( entry . authorTime * 1000 ) . toISOString ( ) : '' , } , summary : entry . summary , body : await commitBody ( sha ) } ; commits . set ( sha , commit ) ; } const lineCount = entry . lineCount ! ; for ( let lineOffset = 0 ; lineOffset < lineCount ; lineOffset ++ ) { const line = < CommitLine > { sha , line : entry . line ! + lineOffset } ; lines [ line . line ] = line ; } } const fileBlame = < GitFileBlame > { uri , commits : Array . from ( commits . values ( ) ) , lines } ; return fileBlame ; } } export namespace GitBlameParser { export interface Entry { fileName ? : string ; sha ? : string ; previousSha ? : string ; line ? : number ; lineCount ? : number ; author ? : string ; authorMail ? : string ; authorTime ? : number ; summary ? : string ; } export function isUncommittedSha ( sha ) { return ( sha || '' ) . startsWith ( '0000000' ) ; } export function pumpEntry ( entry , outputLine ) { const parts = outputLine . split ( ' ' ) ; if ( parts . length < 2 ) { return false ; } const uncommitted = isUncommittedSha ( entry . sha ) ; const firstPart = parts [ 0 ] ; if ( entry . sha === undefined ) { entry . sha = firstPart ; entry . line = parseInt ( parts [ 2 ] , 10 ) - 1 ; entry . lineCount = parseInt ( parts [ 3 ] , 10 ) ; } else if ( firstPart === 'author' ) { entry . author = uncommitted ? 'You' : parts . slice ( 1 ) . join ( ' ' ) ; } else if ( firstPart === 'author-mail' ) { const rest = parts . slice ( 1 ) . join ( ' ' ) ; const matches = rest . match ( /(<(.*)>)/ ) ; entry . authorMail = matches ? matches [ 2 ] : rest ; } else if ( firstPart === 'author-time' ) { entry . authorTime = parseInt ( parts [ 1 ] , 10 ) ; } else if ( firstPart === 'summary' ) { let summary = parts . slice ( 1 ) . join ( ' ' ) ; if ( summary . startsWith ( '"' ) && summary . endsWith ( '"' ) ) { summary = summary . substr ( 1 , summary . length - 2 ) ; } entry . summary = uncommitted ? 'uncommitted' : summary ; } else if ( firstPart === 'previous' ) { entry . previousSha = parts [ 1 ] ; } else if ( firstPart === 'filename' ) { entry . fileName = parts . slice ( 1 ) . join ( ' ' ) ; return true ; } return false ; } } /**
 * `dugite-extra` based Git implementation.
 */ @ injectable ( ) export class DugiteGit implements Git { protected readonly limit = 1000 ; @ inject ( ILogger ) protected readonly logger : ILogger ; @ inject ( GitLocator ) protected readonly locator : GitLocator ; @ inject ( GitRepositoryManager ) protected readonly manager : GitRepositoryManager ; @ inject ( NameStatusParser ) protected readonly nameStatusParser : NameStatusParser ; @ inject ( CommitDetailsParser ) protected readonly commitDetailsParser : CommitDetailsParser ; @ inject ( GitBlameParser ) protected readonly blameParser : GitBlameParser ; @ inject ( GitExecProvider ) protected readonly execProvider : GitExecProvider ; @ inject ( GitEnvProvider ) protected readonly envProvider : GitEnvProvider ; @ inject ( GitInit ) protected readonly gitInit : GitInit ; protected ready : Deferred < void > = new Deferred ( ) ; protected gitEnv : Deferred < Object > = new Deferred ( ) ; @ postConstruct ( ) protected init ( ) { this . envProvider . getEnv ( ) . then ( env => this . gitEnv . resolve ( env ) ) ; this . gitInit . init ( ) . catch ( err => { this . logger . error ( 'An error occurred during the Git initialization.' , err ) ; this . ready . resolve ( ) ; } ) . then ( ( ) => this . ready . resolve ( ) ) ; } dispose ( ) { this . locator . dispose ( ) ; this . execProvider . dispose ( ) ; this . gitInit . dispose ( ) ; } async clone ( remoteUrl , options ) { await this . ready . promise ; const { localUri , branch } = options ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; await clone ( remoteUrl , this . getFsPath ( localUri ) , { branch } , { exec , env } ) ; return { localUri } ; } async repositories ( workspaceRootUri , options ) { await this . ready . promise ; const workspaceRootPath = this . getFsPath ( workspaceRootUri ) ; const repositories = [ ] ; const containingPath = await this . resolveContainingPath ( workspaceRootPath ) ; if ( containingPath ) { repositories . push ( { localUri : this . getUri ( containingPath ) } ) ; } const maxCount = typeof options . maxCount === 'number' ? options . maxCount - repositories . length : undefined ; if ( typeof maxCount === 'number' && maxCount <= 0 ) { return repositories ; } for ( const repositoryPath of await this . locator . locate ( workspaceRootPath , { maxCount } ) ) { if ( containingPath !== repositoryPath ) { repositories . push ( { localUri : this . getUri ( repositoryPath ) } ) ; } } return repositories ; } async status ( repository ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; const dugiteStatus = await getStatus ( repositoryPath , true , this . limit , { exec , env } ) ; return this . mapStatus ( dugiteStatus , repository ) ; } async add ( repository , uri ) { await this . ready . promise ; const paths = ( Array . isArray ( uri ) ? uri : [ uri ] ) . map ( FileUri . fsPath ) ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => stage ( this . getFsPath ( repository ) , paths , { exec , env } ) ) ; } async unstage ( repository , uri , options ? ) { await this . ready . promise ; const paths = ( Array . isArray ( uri ) ? uri : [ uri ] ) . map ( FileUri . fsPath ) ; const treeish = options && options . treeish ? options . treeish : undefined ; const where = options && options . reset ? options . reset : undefined ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => unstage ( this . getFsPath ( repository ) , paths , treeish , where , { exec , env } ) ) ; } async branch ( repository , options ) ; async branch ( repository , options ) ; async branch ( repository , options ) ; async branch ( repository , options ) { await this . ready . promise ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; const repositoryPath = this . getFsPath ( repository ) ; if ( GitUtils . isBranchList ( options ) ) { if ( options . type === 'current' ) { const currentBranch = await listBranch ( repositoryPath , options . type , { exec , env } ) ; return currentBranch ? this . mapBranch ( currentBranch ) : undefined ; } const branches = await listBranch ( repositoryPath , options . type , { exec , env } ) ; return Promise . all ( branches . map ( branch => this . mapBranch ( branch ) ) ) ; } return this . manager . run ( repository , ( ) => { if ( GitUtils . isBranchCreate ( options ) ) { return createBranch ( repositoryPath , options . toCreate , { startPoint : options . startPoint } , { exec , env } ) ; } if ( GitUtils . isBranchRename ( options ) ) { return renameBranch ( repositoryPath , options . newName , options . newName , { force : ! ! options . force } , { exec , env } ) ; } if ( GitUtils . isBranchDelete ( options ) ) { return deleteBranch ( repositoryPath , options . toDelete , { force : ! ! options . force , remote : ! ! options . remote } , { exec , env } ) ; } return this . fail ( repository , `Unexpected git branch options: ${ options }.` ) ; } ) ; } async checkout ( repository , options ) { await this . ready . promise ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => { const repositoryPath = this . getFsPath ( repository ) ; if ( GitUtils . isBranchCheckout ( options ) ) { return checkoutBranch ( repositoryPath , options . branch , { exec , env } ) ; } if ( GitUtils . isWorkingTreeFileCheckout ( options ) ) { const paths = ( Array . isArray ( options . paths ) ? options . paths : [ options . paths ] ) . map ( FileUri . fsPath ) ; return checkoutPaths ( repositoryPath , paths , { exec , env } ) ; } return this . fail ( repository , `Unexpected git checkout options: ${ options }.` ) ; } ) ; } async commit ( repository , message ? , options ? ) { await this . ready . promise ; const signOff = options && options . signOff ; const amend = options && options . amend ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => createCommit ( this . getFsPath ( repository ) , message || '' , signOff , amend , { exec , env } ) ) ; } async fetch ( repository , options ? ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const r = await this . getDefaultRemote ( repositoryPath , options ? options . remote : undefined ) ; if ( r ) { const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => fetch ( repositoryPath , r ! , { exec , env } ) ) ; } this . fail ( repository , 'No remote repository specified. Please, specify either a URL or a remote name from which new revisions should be fetched.' ) ; } async push ( repository , { remote , localBranch , remoteBranch , setUpstream , force } = { } ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const currentRemote = await this . getDefaultRemote ( repositoryPath , remote ) ; if ( currentRemote === undefined ) { this . fail ( repository , 'No configured push destination.' ) ; } const branch = await this . getCurrentBranch ( repositoryPath , localBranch ) ; const branchName = typeof branch === 'string' ? branch : branch . name ; if ( setUpstream || force ) { const args = [ 'push' ] ; if ( force ) { args . push ( '--force' ) ; } if ( setUpstream ) { args . push ( '--set-upstream' ) ; } if ( currentRemote ) { args . push ( currentRemote ) ; } args . push ( branchName + ( remoteBranch ? `:${ remoteBranch }` : '' ) ) ; await this . exec ( repository , args ) ; } else { const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => push ( repositoryPath , currentRemote ! , branchName , remoteBranch , { exec , env } ) ) ; } } async pull ( repository , { remote , branch , rebase } = { } ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const currentRemote = await this . getDefaultRemote ( repositoryPath , remote ) ; if ( currentRemote === undefined ) { this . fail ( repository , 'No remote repository specified. Please, specify either a URL or a remote name from which new revisions should be fetched.' ) ; } if ( rebase ) { const args = [ 'pull' ] ; if ( rebase ) { args . push ( '-r' ) ; } if ( currentRemote ) { args . push ( currentRemote ) ; } if ( branch ) { args . push ( branch ) ; } await this . exec ( repository , args ) ; } else { const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => pull ( repositoryPath , currentRemote ! , branch , { exec , env } ) ) ; } } async reset ( repository , options ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const mode = this . getResetMode ( options . mode ) ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => reset ( repositoryPath , mode , options . ref ? options . ref : 'HEAD' , { exec , env } ) ) ; } async merge ( repository , options ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; return this . manager . run ( repository , ( ) => merge ( repositoryPath , options . branch , { exec , env } ) ) ; } async show ( repository , uri , options ? ) { await this . ready . promise ; const encoding = options ? options . encoding || 'utf8' : 'utf8' ; const commitish = this . getCommitish ( options ) ; const repositoryPath = this . getFsPath ( repository ) ; const path = this . getFsPath ( uri ) ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; if ( encoding === 'binary' ) { return ( await getBlobContents ( repositoryPath , commitish , path , { exec , env } ) ) . toString ( ) ; } return ( await getTextContents ( repositoryPath , commitish , path , { exec , env } ) ) . toString ( ) ; } async stash ( repository , options ? ) ; async stash ( repository , options ) ; async stash ( repository , options ) ; async stash ( repository , options ) ; async stash ( repository , options ? ) { const repositoryPath = this . getFsPath ( repository ) ; try { if ( ! options || ( options && ! options . action ) ) { await stash . push ( repositoryPath , options ? options . message : undefined ) ; return ; } switch ( options . action ) { case 'push' : await stash . push ( repositoryPath , options . message ) ; break ; case 'apply' : await stash . apply ( repositoryPath , options . id ) ; break ; case 'pop' : await stash . pop ( repositoryPath , options . id ) ; break ; case 'list' : const stashList = await stash . list ( repositoryPath ) ; const stashes = [ ] ; stashList . forEach ( stashItem => { const splitIndex = stashItem . indexOf ( ':' ) ; stashes . push ( { id : stashItem . substring ( 0 , splitIndex ) , message : stashItem . substring ( splitIndex + 1 ) } ) ; } ) ; return stashes ; case 'drop' : await stash . drop ( repositoryPath , options . id ) ; break ; } } catch ( err ) { this . fail ( repository , err ) ; } } async remote ( repository ) ; async remote ( repository , options ) ; async remote ( repository , options ? ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; const remotes = await this . getRemotes ( repositoryPath ) ; const names = remotes . map ( a => a . name ) ; return ( options && options . verbose === true ) ? remotes : names ; } async exec ( repository , args , options ? ) { await this . ready . promise ; const repositoryPath = this . getFsPath ( repository ) ; return this . manager . run ( repository , async ( ) => { const name = options && options . name ? options . name : '' ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; let opts = { } ; if ( options ) { opts = { ... options } ; if ( options . successExitCodes ) { opts = { ... opts , successExitCodes : new Set ( options . successExitCodes ) } ; } if ( options . expectedErrors ) { opts = { ... opts , expectedErrors : new Set ( options . expectedErrors ) } ; } } opts = { ... opts , exec , env } ; return git ( args , repositoryPath , name , opts ) ; } ) ; } async diff ( repository , options ? ) { await this . ready . promise ; const args = [ 'diff' , '--name-status' , '-C' , '-M' , '-z' ] ; args . push ( this . mapRange ( ( options || { } ) . range ) ) ; if ( options && options . uri ) { const relativePath = Path . relative ( this . getFsPath ( repository ) , this . getFsPath ( options . uri ) ) ; args . push ( ... [ '--' , relativePath !== '' ? relativePath : '.' ] ) ; } const result = await this . exec ( repository , args ) ; return this . nameStatusParser . parse ( repository . localUri , result . stdout . trim ( ) ) ; } async log ( repository , options ? ) { await this . ready . promise ; const args = [ 'log' ] ; if ( options && options . branch ) { args . push ( options . branch ) ; } const range = this . mapRange ( ( options || { } ) . range ) ; args . push ( ... [ range , '-C' , '-M' , '-m' , '--first-parent' ] ) ; const maxCount = options && options . maxCount ? options . maxCount : 0 ; if ( Number . isInteger ( maxCount ) && maxCount > 0 ) { args . push ( ... [ '-n' , `${ maxCount }` ] ) ; } const placeholders = options && options . shortSha ? [ CommitPlaceholders . SHORT_HASH , ... CommitDetailsParser . DEFAULT_PLACEHOLDERS . slice ( 1 ) ] : CommitDetailsParser . DEFAULT_PLACEHOLDERS ; args . push ( ... [ '--name-status' , '--date=unix' , `--format=${ this . commitDetailsParser . getFormat ( ... placeholders ) }` , '-z' , '--' ] ) ; if ( options && options . uri ) { const file = Path . relative ( this . getFsPath ( repository ) , this . getFsPath ( options . uri ) ) || '.' ; args . push ( ... [ file ] ) ; } const successExitCodes = [ 0 , 128 ] ; let result = await this . exec ( repository , args , { successExitCodes } ) ; if ( result . exitCode !== 0 ) { const rangeInvolvesHead = ! options || ! options . range || options . range . toRevision === 'HEAD' ; const repositoryHasNoHead = ! await this . revParse ( repository , { ref : 'HEAD' } ) ; if ( rangeInvolvesHead && repositoryHasNoHead ) { return [ ] ; } result = await this . exec ( repository , args ) ; } return this . commitDetailsParser . parse ( repository . localUri , result . stdout . trim ( ) . split ( CommitDetailsParser . COMMIT_CHUNK_DELIMITER ) . filter ( item => item && item . length > 0 ) ) ; } async revParse ( repository , options ) { const ref = options . ref ; const successExitCodes = [ 0 , 128 ] ; const result = await this . exec ( repository , [ 'rev-parse' , ref ] , { successExitCodes } ) ; if ( result . exitCode === 0 ) { return result . stdout ; } } async blame ( repository , uri , options ? ) { await this . ready . promise ; const args = [ 'blame' , '--root' , '--incremental' ] ; const file = Path . relative ( this . getFsPath ( repository ) , this . getFsPath ( uri ) ) ; const repositoryPath = this . getFsPath ( repository ) ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; const status = await getStatus ( repositoryPath , true , this . limit , { exec , env } ) ; const isUncommitted = ( change ) => change . status === AppFileStatus . New && change . path === file ; const changes = status . workingDirectory . files ; if ( changes . some ( isUncommitted ) ) { return undefined ; } const stdin = options ? options . content : undefined ; if ( stdin ) { args . push ( '--contents' , '-' ) ; } const gitResult = await this . exec ( repository , [ ... args , '--' , file ] , { stdin } ) ; const output = gitResult . stdout . trim ( ) ; const commitBodyReader = async ( sha ) => { if ( GitBlameParser . isUncommittedSha ( sha ) ) { return '' ; } const revResult = await this . exec ( repository , [ 'rev-list' , '--format=%B' , '--max-count=1' , sha ] ) ; const revOutput = revResult . stdout ; let nl = revOutput . indexOf ( '\n' ) ; if ( nl > 0 ) { nl = revOutput . indexOf ( '\n' , nl + 1 ) ; } return revOutput . substr ( Math . max ( 0 , nl ) ) . trim ( ) ; } ; const blame = await this . blameParser . parse ( uri , output , commitBodyReader ) ; return blame ; } async lsFiles ( repository , uri , options ? ) { await this . ready . promise ; const args = [ 'ls-files' ] ; const relativePath = Path . relative ( this . getFsPath ( repository ) , this . getFsPath ( uri ) ) ; const file = ( relativePath === '' ) ? '.' : relativePath ; if ( options && options . errorUnmatch ) { args . push ( '--error-unmatch' , file ) ; const successExitCodes = [ 0 , 1 ] ; const expectedErrors = [ GitError . OutsideRepository ] ; const result = await this . exec ( repository , args , { successExitCodes , expectedErrors } ) ; const { exitCode } = result ; return exitCode === 0 ; } } private getCommitish ( options ? ) { if ( options && options . commitish ) { return 'index' === options . commitish ? '' : options . commitish ; } return '' ; } private async resolveContainingPath ( repositoryPath ) { await this . ready . promise ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; const options = { successExitCodes : new Set ( [ 0 , 128 ] ) , exec , env } ; const result = await git ( [ 'rev-parse' , '--show-toplevel' ] , repositoryPath , 'rev-parse' , options ) ; const out = result . stdout ; if ( out && out . length !== 0 ) { try { return fs . realpathSync ( out . trim ( ) ) ; } catch ( e ) { this . logger . error ( e ) ; return undefined ; } } return undefined ; } private async getRemotes ( repositoryPath ) { await this . ready . promise ; const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; const result = await git ( [ 'remote' , '-v' ] , repositoryPath , 'remote' , { exec , env } ) ; const out = result . stdout || '' ; const results = out . trim ( ) . match ( /\S+/g ) ; if ( results ) { const values = [ ] ; for ( let i = 0 ; i < results . length ; i += 6 ) { values . push ( { name : results [ i ] , fetch : results [ i + 1 ] , push : results [ i + 4 ] } ) ; } return values ; } else { return [ ] ; } } private async getDefaultRemote ( repositoryPath , remote ? ) { if ( remote === undefined ) { const remotes = await this . getRemotes ( repositoryPath ) ; const name = remotes . map ( a => a . name ) ; return name . shift ( ) ; } return remote ; } private async getCurrentBranch ( repositoryPath , localBranch ? ) { await this . ready . promise ; if ( localBranch !== undefined ) { return localBranch ; } const [ exec , env ] = await Promise . all ( [ this . execProvider . exec ( ) , this . gitEnv . promise ] ) ; const branch = await listBranch ( repositoryPath , 'current' , { exec , env } ) ; if ( branch === undefined ) { return this . fail ( repositoryPath , 'No current branch.' ) ; } if ( Array . isArray ( branch ) ) { return this . fail ( repositoryPath , `Implementation error. Listing branch with the 'current' flag must return with single value. Was: ${ branch }` ) ; } return this . mapBranch ( branch ) ; } private getResetMode ( mode ) { switch ( mode ) { case 'hard' : return GitResetMode . Hard ; case 'soft' : return GitResetMode . Soft ; case 'mixed' : return GitResetMode . Mixed ; default : throw new Error ( `Unexpected Git reset mode: ${ mode }.` ) ; } } private async mapBranch ( toMap ) { const tip = await this . mapTip ( toMap . tip ) ; return { name : toMap . name , nameWithoutRemote : toMap . nameWithoutRemote , remote : toMap . remote , type : toMap . type , upstream : toMap . upstream , upstreamWithoutRemote : toMap . upstreamWithoutRemote , tip } ; } private async mapTip ( toMap ) { const author = await this . mapCommitIdentity ( toMap . author ) ; return { author , body : toMap . body , parentSHAs : [ ... toMap . parentSHAs ] , sha : toMap . sha , summary : toMap . summary } ; } private async mapCommitIdentity ( toMap ) { return { timestamp : toMap . date . toISOString ( ) , email : toMap . email , name : toMap . name , } ; } private async mapStatus ( toMap , repository ) { const repositoryPath = this . getFsPath ( repository ) ; const [ aheadBehind , changes ] = await Promise . all ( [ this . mapAheadBehind ( toMap . branchAheadBehind ) , this . mapFileChanges ( toMap . workingDirectory , repositoryPath ) ] ) ; return { exists : toMap . exists , branch : toMap . currentBranch , upstreamBranch : toMap . currentUpstreamBranch , aheadBehind , changes , currentHead : toMap . currentTip , incomplete : toMap . incomplete } ; } private async mapAheadBehind ( toMap ) { return toMap ? { ... toMap } : undefined ; } private async mapFileChanges ( toMap , repositoryPath ) { return Promise . all ( toMap . files . filter ( file => ! this . isNestedGitRepository ( file ) ) . map ( file => this . mapFileChange ( file , repositoryPath ) ) ) ; } private isNestedGitRepository ( fileChange ) { return fileChange . path . endsWith ( '/' ) ; } private async mapFileChange ( toMap , repositoryPath ) { const [ uri , status , oldUri ] = await Promise . all ( [ this . getUri ( Path . join ( repositoryPath , toMap . path ) ) , this . mapFileStatus ( toMap . status ) , toMap . oldPath ? this . getUri ( Path . join ( repositoryPath , toMap . oldPath ) ) : undefined ] ) ; return { uri , status , oldUri , staged : toMap . staged } ; } private mapFileStatus ( toMap ) { switch ( toMap ) { case AppFileStatus . Conflicted : return GitFileStatus . Conflicted ; case AppFileStatus . Copied : return GitFileStatus . Copied ; case AppFileStatus . Deleted : return GitFileStatus . Deleted ; case AppFileStatus . Modified : return GitFileStatus . Modified ; case AppFileStatus . New : return GitFileStatus . New ; case AppFileStatus . Renamed : return GitFileStatus . Renamed ; default : throw new Error ( `Unexpected application file status: ${ toMap }` ) ; } } private mapRange ( toMap ) { let range = 'HEAD' ; if ( toMap ) { if ( typeof toMap . fromRevision === 'number' ) { const toRevision = toMap . toRevision || 'HEAD' ; range = `${ toRevision }~${ toMap . fromRevision }..${ toRevision }` ; } else if ( typeof toMap . fromRevision === 'string' ) { range = `${ toMap . fromRevision }${ toMap . toRevision ? '..' + toMap . toRevision : '' }` ; } else if ( toMap . toRevision ) { range = toMap . toRevision ; } } return range ; } private getFsPath ( repository ) { const uri = typeof repository === 'string' ? repository : repository . localUri ; return FileUri . fsPath ( uri ) ; } private getUri ( path ) { return FileUri . create ( path ) . toString ( ) ; } private fail ( repository , message ? ) { const p = typeof repository === 'string' ? repository : repository . localUri ; const m = message ? `${ message } ` : '' ; throw new Error ( `${ m }[${ p }]` ) ; } }