import * as fs from 'fs-extra' ; import { inject , injectable } from 'inversify' ; import * as path from 'path' ; import * as tmp from 'tmp' ; import * as vscode from 'vscode' ; import { IWorkspaceService } from '../../application/types/workspace' ; import { cache } from '../../common/cache' ; import { IServiceContainer } from '../../ioc/types' ; import { ActionedUser , Branch , CommittedFile , Hash , IGitService , LogEntries , LogEntry , Ref , FsUri } from '../../types' ; import { IGitCommandExecutor } from '../exec' ; import { IFileStatParser , ILogParser } from '../parsers/types' ; import { ITEM_ENTRY_SEPARATOR , LOG_ENTRY_SEPARATOR , LOG_FORMAT_ARGS } from './constants' ; import { Repository , RefType } from './git.d' ; import { GitOriginType } from './index' ; import { IGitArgsService } from './types' ; import { GitRemoteService } from './gitRemoteService' ; import { captureTelemetry } from '../../common/telemetry' ; @ injectable ( ) export class Git implements IGitService { private refHashesMap : Map < string , string > = new Map < string , string > ( ) ; private readonly remotesService : GitRemoteService ; constructor ( private repo , @ inject ( IServiceContainer ) private serviceContainer , @ inject ( IGitCommandExecutor ) private gitCmdExecutor , @ inject ( ILogParser ) private logParser , @ inject ( IGitArgsService ) private gitArgsService ) { this . remotesService = new GitRemoteService ( repo , this . gitCmdExecutor ) ; } /**
     * Used to differentiate between repository being cached using the @cache decorator
     */ public getHashCode ( ) { return `:${ this . getGitRoot ( ) }` ; } public getGitRoot ( ) { return this . repo . rootUri . fsPath ; } public getGitRelativePath ( file ) { if ( ! path . isAbsolute ( file . fsPath ) ) { return file . fsPath ; } const gitRoot = this . getGitRoot ( ) ; const filerealpath = fs . realpathSync ( file . fsPath ) ; return path . relative ( gitRoot , filerealpath ) . replace ( /\\/g , '/' ) ; } public getHeadHashes ( ) { return this . repo . state . refs . filter ( x => x . type <= 1 ) . map ( x => { return { ref : x . name , hash : x . commit } ; } ) ; } public getDetachedHash ( ) { return this . repo . state . HEAD ! . name === undefined ? this . repo . state . HEAD ! . commit : undefined ; } @ captureTelemetry ( ) public async getBranches ( withRemote = false ) { const gitRoot = this . getGitRoot ( ) ; const branches = await this . repo . getBranches ( { remote : withRemote } ) ; return Promise . all ( branches . map ( async ( r ) => { let remoteUrl = undefined ; const remoteName = ( await this . repo . getBranch ( r . name || 'master' ) ) . upstream ?. remote ; if ( remoteName ) { remoteUrl = this . repo . state . remotes . find ( x => x . name === remoteName ) ?. fetchUrl ; } const originType = await this . remotesService . getOriginType ( remoteUrl ) ; return { gitRoot : gitRoot , name : r . name , type : r . type . valueOf ( ) , remote : remoteUrl , remoteType : originType , current : this . getCurrentBranch ( ) === r . name , } as Branch ; } ) ) ; } public getCurrentBranch ( ) { return this . repo . state . HEAD ! . name || '' ; } @ cache ( 'IGitService' , 60 * 1000 ) @ captureTelemetry ( ) public async getAuthors ( ) { const authorArgs = this . gitArgsService . getAuthorsArgs ( ) ; const authors = await this . exec ( ... authorArgs ) ; const dict = new Set < string > ( ) ; return authors . split ( /\r?\n/g ) . map ( line => line . trim ( ) ) . filter ( line => line . trim ( ) . length > 0 ) . map ( line => line . substring ( line . indexOf ( '\t' ) + 1 ) ) . map ( line => { const indexOfEmailSeparator = line . indexOf ( '<' ) ; if ( indexOfEmailSeparator === - 1 ) { return { name : line . trim ( ) , email : '' , } ; } else { const nameParts = line . split ( '<' ) ; const name = nameParts . shift ( ) ! . trim ( ) ; const email = nameParts [ 0 ] . substring ( 0 , nameParts [ 0 ] . length - 1 ) . trim ( ) ; return { name , email , } ; } } ) . filter ( item => { if ( dict . has ( item . name ) ) { return false ; } dict . add ( item . name ) ; return true ; } ) . sort ( ( a , b ) => ( a . name > b . name ? 1 : - 1 ) ) ; } @ captureTelemetry ( ) public async getOriginType ( url ? ) { return this . remotesService . getOriginType ( url ) ; } @ captureTelemetry ( ) public async getOriginUrl ( branchName ? ) { return this . remotesService . getOriginUrl ( branchName ) ; } /**
     * Used to load dereferenced hashes for (annotation) tags
     *
     * This is used in vscode versions prior to 1.52 because
     * annotation references to not correct point to the assocated commit
     *
     * See https://github.com/microsoft/vscode/issues/92146
     */ private async loadDereferenceHashes ( ) { const vsCodeVersionNumber = parseFloat ( vscode . version ) ; if ( vsCodeVersionNumber >= 1.52 ) { return ; } this . refHashesMap . clear ( ) ; const tags = this . repo . state . refs . filter ( x => x . type === RefType . Tag ) ; if ( tags . length === 0 ) { return ; } const tagNames = tags . map ( x => { return x . name ! + '^{}' ; } ) ; const maxTagNamesLength = 500 ; let result ; if ( tagNames . length > maxTagNamesLength ) { const tagNamesChunks = [ ] ; while ( tagNames . length ) { tagNamesChunks . push ( tagNames . splice ( 0 , maxTagNamesLength ) ) ; } result = await Promise . all ( tagNamesChunks . map ( tagNamesChunk => this . exec ( ... [ 'rev-parse' , ... tagNamesChunk ] ) ) ) . then ( resultChunks => resultChunks . join ( '' ) ) ; } else { result = await this . exec ( ... [ 'rev-parse' , ... tagNames ] ) ; } result . trim ( ) . split ( /\r\n|\n/ ) . forEach ( ( x , i ) => { this . refHashesMap . set ( tags [ i ] . name ! , x ) ; } ) ; } public getRefsContainingCommit ( hash ) { return this . repo . state . refs . filter ( x => ( x . type === RefType . Tag && this . refHashesMap . get ( x . name ! ) === hash ) || x . commit === hash ) . map ( x => { return { type : x . type as any , name : x . name } as Ref ; } ) ; } @ captureTelemetry ( ) public async getLogEntries ( pageIndex = 0 , pageSize = 0 , branches = [ ] , searchText = '' , file ? , lineNumber ? , author ? ) { branches = Array . isArray ( branches ) ? branches : [ ] ; if ( pageSize <= 0 ) { const workspace = this . serviceContainer . get < IWorkspaceService > ( IWorkspaceService ) ; pageSize = workspace . getConfiguration ( 'gitHistory' ) . get < number > ( 'pageSize' , 100 ) ; } const relativePath = file ? this . getGitRelativePath ( file ) : undefined ; const args = this . gitArgsService . getLogArgs ( pageIndex , pageSize , branches , searchText , relativePath , lineNumber , author ) ; const gitRepoPath = this . getGitRoot ( ) ; const countPromise = lineNumber ? Promise . resolve ( - 1 ) : this . exec ( ... args . counterArgs ) . then ( value => parseInt ( value ) ) ; `` ; const [ output ] = await Promise . all ( [ this . exec ( ... args . logArgs ) , this . loadDereferenceHashes ( ) ] ) ; const items = output . split ( LOG_ENTRY_SEPARATOR ) . map ( entry => { if ( entry . length === 0 ) { return ; } return this . logParser . parse ( gitRepoPath , entry , ITEM_ENTRY_SEPARATOR , LOG_FORMAT_ARGS ) ; } ) . filter ( logEntry => logEntry !== undefined ) . map ( logEntry => { logEntry ! . refs = this . getRefsContainingCommit ( logEntry ! . hash . full ) ; return logEntry ! ; } ) ; const headHashes = this . getHeadHashes ( ) ; const count = await countPromise ; const headHashesOnly = headHashes . map ( item => item . hash ) ; items . filter ( x => headHashesOnly . indexOf ( x . hash . full ) > - 1 ) . forEach ( item => { item . isLastCommit = true ; } ) ; return { items , count , branches , file , pageIndex , pageSize , searchText , } as LogEntries ; } @ captureTelemetry ( ) public async getCommit ( hash , withRefs = false ) { const commitArgs = this . gitArgsService . getCommitArgs ( hash ) ; const nameStatusArgs = this . gitArgsService . getCommitNameStatusArgsForMerge ( hash ) ; const gitRootPath = this . getGitRoot ( ) ; const commitOutput = await this . exec ( ... commitArgs ) ; const filesWithNumStat = commitOutput . slice ( commitOutput . lastIndexOf ( ITEM_ENTRY_SEPARATOR ) + ITEM_ENTRY_SEPARATOR . length ) ; const filesWithNameStatus = await this . exec ( ... nameStatusArgs ) ; const entries = commitOutput . split ( LOG_ENTRY_SEPARATOR ) . map ( entry => { if ( entry . trim ( ) . length === 0 ) { return undefined ; } return this . logParser . parse ( gitRootPath , entry , ITEM_ENTRY_SEPARATOR , LOG_FORMAT_ARGS , filesWithNumStat , filesWithNameStatus ) ; } ) . filter ( entry => entry !== undefined ) . map ( entry => entry ! ) ; if ( entries . length > 0 ) { if ( withRefs ) { entries [ 0 ] . refs = this . getRefsContainingCommit ( hash ) ; } return entries [ 0 ] ; } return undefined ; } @ cache ( 'IGitService' ) @ captureTelemetry ( ) public async getCommitFile ( hash , file ) { const filePath = typeof file === 'string' ? file : file . path . toString ( ) ; const content = await this . repo . buffer ( hash , filePath ) ; return new Promise < vscode . Uri > ( ( resolve , reject ) => { tmp . file ( { postfix : path . extname ( filePath ) } , async ( err , tmpPath ) => { if ( err ) { return reject ( err ) ; } try { const tmpFilePath = path . join ( path . dirname ( tmpPath ) , `${ hash }${ new Date ( ) . getTime ( ) }${ path . basename ( tmpPath ) }` ) . replace ( /\\/g , '/' ) ; const tmpFile = path . join ( tmpFilePath , path . basename ( filePath ) ) ; await fs . ensureDir ( tmpFilePath ) ; await fs . writeFile ( tmpFile , content ) ; resolve ( vscode . Uri . file ( tmpFile ) ) ; } catch ( ex ) { console . error ( 'Git History: failed to get file contents (again)' ) ; console . error ( ex ) ; reject ( ex ) ; } } ) ; } ) ; } @ cache ( 'IGitService' ) @ captureTelemetry ( ) public async getDifferences ( hash1 , hash2 ) { const numStartArgs = this . gitArgsService . getDiffCommitWithNumStatArgs ( hash1 , hash2 ) ; const nameStatusArgs = this . gitArgsService . getDiffCommitNameStatusArgs ( hash1 , hash2 ) ; const gitRootPathPromise = this . getGitRoot ( ) ; const filesWithNumStatPromise = this . exec ( ... numStartArgs ) ; const filesWithNameStatusPromise = this . exec ( ... nameStatusArgs ) ; const values = await Promise . all ( [ gitRootPathPromise , filesWithNumStatPromise , filesWithNameStatusPromise ] ) ; const gitRootPath = values [ 0 ] ; const filesWithNumStat = values [ 1 ] ; const filesWithNameStatus = values [ 2 ] ; const fileStatParser = this . serviceContainer . get < IFileStatParser > ( IFileStatParser ) ; return fileStatParser . parse ( gitRootPath , filesWithNumStat . split ( /\r?\n/g ) , filesWithNameStatus . split ( /\r?\n/g ) ) ; } @ cache ( 'IGitService' ) @ captureTelemetry ( ) public async getPreviousCommitHashForFile ( hash , file ) { const gitRootPath = await this . getGitRoot ( ) ; const relativeFilePath = path . relative ( gitRootPath , file . path ) ; const args = this . gitArgsService . getPreviousCommitHashForFileArgs ( hash , relativeFilePath ) ; const output = await this . exec ( ... args ) ; const hashes = output . split ( /\r?\n/g ) . filter ( item => item . length > 0 ) [ 0 ] . split ( '-' ) ; return { short : hashes [ 1 ] , full : hashes [ 0 ] , } ; } @ captureTelemetry ( ) public async cherryPick ( hash ) { await this . exec ( 'cherry-pick' , hash ) ; } @ captureTelemetry ( ) public async reset ( hash , hard = false ) { await this . exec ( 'reset' , hard ? '--hard' : '--soft' , hash ) ; } @ captureTelemetry ( ) public async checkout ( hash ) { await this . repo . checkout ( hash ) ; } @ captureTelemetry ( ) public async revertCommit ( hash ) { await this . exec ( 'revert' , '--no-edit' , hash ) ; } @ captureTelemetry ( ) public async createBranch ( branchName , hash ) { try { await this . repo . createBranch ( branchName , false , hash ) ; } catch ( ex ) { throw ( ex as any ) . stderr ; } } @ captureTelemetry ( ) public async createTag ( tagName , hash ) { const result = await this . exec ( 'tag' , '-a' , tagName , '-m' , tagName , hash ) ; await this . repo . status ( ) ; return result ; } @ captureTelemetry ( ) public async removeTag ( tagName ) { await this . exec ( 'tag' , '-d' , tagName ) ; await this . repo . status ( ) ; this . refHashesMap . delete ( tagName ) ; } @ captureTelemetry ( ) public async removeBranch ( branchName ) { try { await this . repo . deleteBranch ( branchName ) ; } catch ( ex ) { throw ( ex as any ) . stderr ; } } @ captureTelemetry ( ) public async removeRemoteBranch ( remoteBranchName ) { const pathes = remoteBranchName . split ( '/' ) ; const remote = pathes . shift ( ) as string ; const branchName = pathes . join ( '/' ) ; await this . repo . push ( remote , `:${ branchName }` ) ; } @ captureTelemetry ( ) public async merge ( hash ) { await this . exec ( 'merge' , hash ) ; } @ captureTelemetry ( ) public async rebase ( hash ) { await this . exec ( 'rebase' , hash ) ; } private async exec ( ... args ) { const gitRootPath = this . getGitRoot ( ) ; return this . gitCmdExecutor . exec ( gitRootPath , ... args ) ; } }