'use strict' ; import * as path from 'path' ; import * as fs from 'fs' ; import { workspace , Uri , commands , WorkspaceFolder , Event , EventEmitter , Disposable , window } from 'vscode' ; import { spawn } from 'child_process' ; import { Tracer } from './tracer' ; const EntrySeparator = '471a2a19-885e-47f8-bff3-db43a3cdfaed' ; const FormatSeparator = 'e69fde18-a303-4529-963d-f5b63b7b1664' ; function formatDate ( timestamp ) { return ( new Date ( timestamp * 1000 ) ) . toDateString ( ) ; } export interface GitRepo { root : string ; } export enum GitRefType { Head , RemoteHead , Tag } export interface GitRef { type : GitRefType ; name ? : string ; commit ? : string ; } export interface GitLogEntry { subject : string ; hash : string ; ref : string ; author : string ; email : string ; date : string ; stat ? : string ; lineInfo ? : string ; } export interface GitCommittedFile { fileUri : Uri ; oldFileUri : Uri ; gitRelativePath : string ; gitRelativeOldPath : string ; status : string ; } class GitCommittedFileImpl implements GitCommittedFile { constructor ( private _repo , readonly gitRelativePath , readonly gitRelativeOldPath , readonly status ) { } get fileUri ( ) : Uri { return Uri . file ( path . join ( this . _repo . root , this . gitRelativePath ) ) ; } get oldFileUri ( ) : Uri { return Uri . file ( path . join ( this . _repo . root , this . gitRelativeOldPath ) ) ; } } export interface GitBlameItem { file : Uri ; line : number ; hash ? : string ; subject ? : string ; body ? : string ; author ? : string ; date ? : string ; relativeDate ? : string ; email ? : string ; stat ? : string ; } function singleLined ( value ) { return value . replace ( /\r?\n|\r/g , ' ' ) ; } export class GitService { private _gitRepos : GitRepo [ ] = [ ] ; private _onDidChangeGitRepositories = new EventEmitter < GitRepo [ ] > ( ) ; private _disposables : Disposable [ ] = [ ] ; private _gitPath : string = workspace . getConfiguration ( 'git' ) . get ( 'path' ) ; constructor ( ) { this . _disposables . push ( this . _onDidChangeGitRepositories ) ; } get onDidChangeGitRepositories ( ) : Event < GitRepo [ ] > { return this . _onDidChangeGitRepositories . event ; } dispose ( ) { this . _disposables . forEach ( d => d . dispose ( ) ) ; } updateGitRoots ( wsFolders ) { this . _gitRepos = [ ] ; commands . executeCommand ( 'setContext' , 'hasGitRepo' , false ) ; this . _onDidChangeGitRepositories . fire ( [ ] ) ; if ( wsFolders ) { wsFolders . forEach ( wsFolder => { this . getGitRepo ( wsFolder . uri ) ; const root = wsFolder . uri . fsPath ; this . _scanSubFolders ( root ) ; } ) ; } } getGitRepos ( ) { return this . _gitRepos ; } async getGitRepo ( uri ) { let fsPath = uri . fsPath ; if ( fs . statSync ( fsPath ) . isFile ( ) ) { fsPath = path . dirname ( fsPath ) ; } const repo = this . _gitRepos . find ( r => fsPath . startsWith ( r . root ) ) ; if ( repo ) { return repo ; } let root = ( await this . _exec ( [ 'rev-parse' , '--show-toplevel' ] , fsPath ) ) . trim ( ) ; if ( root ) { root = path . normalize ( root ) ; if ( this . _gitRepos . findIndex ( ( value ) => { return value . root == root ; } ) === - 1 ) { this . _gitRepos . push ( { root } ) ; commands . executeCommand ( 'setContext' , 'hasGitRepo' , true ) ; this . _onDidChangeGitRepositories . fire ( this . getGitRepos ( ) ) ; } } return root ? { root } : null ; } async getGitRelativePath ( file ) { const repo = await this . getGitRepo ( file ) ; if ( ! repo ) { return ; } let relative = path . relative ( repo . root , file . fsPath ) . replace ( /\\/g , '/' ) ; return relative === '' ? '.' : relative ; } async getCurrentBranch ( repo ) { if ( ! repo ) { return null ; } return ( await this . _exec ( [ 'rev-parse' , '--abbrev-ref' , 'HEAD' ] , repo . root ) ) . trim ( ) ; } async getCommitsCount ( repo , file ? , author ? ) { if ( ! repo ) { return 0 ; } let args = [ 'rev-list' , '--simplify-merges' , '--count' , 'HEAD' ] ; if ( author ) { args . push ( `--author=${ author }` ) ; } if ( file ) { args . push ( await this . getGitRelativePath ( file ) ) ; } return parseInt ( await this . _exec ( args , repo . root ) ) ; } async getRefs ( repo ) { if ( ! repo ) { return [ ] ; } const result = await this . _exec ( [ 'for-each-ref' , '--format' , '%(refname) %(objectname:short)' ] , repo . root ) ; const fn = ( line ) : GitRef | null => { let match ; if ( match = /^refs\/heads\/([^ ]+) ([0-9a-f]+)$/ . exec ( line ) ) { return { name : match [ 1 ] , commit : match [ 2 ] , type : GitRefType . Head } ; } else if ( match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]+)$/ . exec ( line ) ) { return { name : `${ match [ 1 ] }/${ match [ 2 ] }` , commit : match [ 3 ] , type : GitRefType . RemoteHead } ; } else if ( match = /^refs\/tags\/([^ ]+) ([0-9a-f]+)$/ . exec ( line ) ) { return { name : match [ 1 ] , commit : match [ 2 ] , type : GitRefType . Tag } ; } return null ; } ; return result . trim ( ) . split ( '\n' ) . filter ( line => ! ! line ) . map ( fn ) . filter ( ref => ! ! ref ) as GitRef [ ] ; } async getCommittedFiles ( repo , leftRef , rightRef , isStash ) { if ( ! repo ) { return [ ] ; } let args = [ 'show' , '--format=%h' , '--name-status' , rightRef ] ; if ( leftRef ) { args = [ 'diff' , '--name-status' , `${ leftRef }..${ rightRef }` ] ; } else if ( isStash ) { args . unshift ( 'stash' ) ; } const result = await this . _exec ( args , repo . root ) ; let files = [ ] ; result . split ( /\r?\n/g ) . forEach ( ( value , index ) => { if ( value ) { let info = value . split ( /\t/g ) ; if ( info . length < 2 ) { return ; } let gitRelativePath ; let gitRelativeOldPath ; const status = info [ 0 ] [ 0 ] . toLocaleUpperCase ( ) ; switch ( status ) { case 'M' : case 'A' : case 'D' : gitRelativeOldPath = info [ 1 ] ; gitRelativePath = info [ 1 ] ; break ; case 'R' : case 'C' : gitRelativeOldPath = info [ 1 ] ; gitRelativePath = info [ 2 ] ; break ; default : throw new Error ( 'Cannot parse ' + info ) ; } files . push ( new GitCommittedFileImpl ( repo , gitRelativePath , gitRelativeOldPath , status ) ) ; } } ) ; return files ; } async getLogEntries ( repo , express , start , count , branch , isStash ? , file ? , line ? , author ? ) { Tracer . info ( `Get entries. repo: ${ repo . root }, express: ${ express }, start: ${ start }, count: ${ count }, branch: ${ branch },` + `isStash: ${ isStash }, file: ${ file ? file . fsPath : '' }, line: ${ line }, author: ${ author }` ) ; if ( ! repo ) { return [ ] ; } let format = `--format=${ EntrySeparator }` ; if ( isStash ) { format += '%gd: ' ; } format += `%s${ FormatSeparator }%h${ FormatSeparator }%d${ FormatSeparator }%aN${ FormatSeparator }%ae${ FormatSeparator }%ct${ FormatSeparator }%cr${ FormatSeparator }` ; let args = [ format ] ; if ( ! express || ! ! line ) { args . push ( '--shortstat' ) ; } if ( isStash ) { args . unshift ( 'stash' , 'list' ) ; } else { args . unshift ( 'log' , `--skip=${ start }` , `--max-count=${ count }` , '--simplify-merges' , branch ) ; if ( author ) { args . push ( `--author=${ author }` ) ; } if ( file ) { const filePath = await this . getGitRelativePath ( file ) ; if ( line ) { args . push ( `-L ${ line },${ line }:${ filePath }` ) ; } else { args . push ( '--follow' , filePath ) ; } } } const result = await this . _exec ( args , repo . root ) ; let entries = [ ] ; result . split ( EntrySeparator ) . forEach ( entry => { if ( ! entry ) { return ; } let subject ; let hash ; let ref ; let author ; let email ; let date ; let stat ; let lineInfo ; entry . split ( FormatSeparator ) . forEach ( ( value , index ) => { switch ( index % 8 ) { case 0 : subject = singleLined ( value ) ; break ; case 1 : hash = value ; break ; case 2 : ref = value ; break ; case 3 : author = value ; break ; case 4 : email = value ; break ; case 5 : date = formatDate ( parseInt ( value ) ) ; break ; case 6 : date += ` (${ value })` ; break ; case 7 : if ( ! ! line ) { lineInfo = value . trim ( ) ; } else { stat = value . trim ( ) ; } entries . push ( { subject , hash , ref , author , email , date , stat , lineInfo } ) ; break ; } } ) ; } ) ; return entries ; } async getCommitDetails ( repo , ref , isStash ) { if ( ! repo ) { return null ; } const format = isStash ? `Stash:         %H
Author:        %aN <%aE>
AuthorDate:    %ad

%s
` : `Commit:        %H
Author:        %aN <%aE>
AuthorDate:    %ad
Commit:        %cN <%cE>
CommitDate:    %cd

%s
` ; let details = await this . _exec ( [ 'show' , `--format=${ format }` , '--no-patch' , '--date=local' , ref ] , repo . root ) ; const body = ( await this . _exec ( [ 'show' , '--format=%b' , '--no-patch' , ref ] , repo . root ) ) . trim ( ) ; if ( body ) { details += body + '\r\n\r\n' ; } details += '-----------------------------\r\n\r\n' ; details += await this . _exec ( [ 'show' , '--format=' , '--stat' , ref ] , repo . root ) ; return details ; } async getAuthors ( repo ) { if ( ! repo ) { return null ; } const result = ( await this . _exec ( [ 'shortlog' , '-se' , 'HEAD' ] , repo . root ) ) . trim ( ) ; return result . split ( /\r?\n/g ) . map ( item => { item = item . trim ( ) ; let start = item . search ( / |\t/ ) ; item = item . substr ( start + 1 ) . trim ( ) ; start = item . indexOf ( '<' ) ; const name = item . substring ( 0 , start ) ; const email = item . substring ( start + 1 , item . length - 1 ) ; return { name , email } ; } ) ; } async getBlameItem ( file , line ) { const repo = await this . getGitRepo ( file ) ; if ( ! repo ) { return null ; } const filePath = file . fsPath ; const result = await this . _exec ( [ 'blame' , `${ filePath }` , '-L' , `${ line + 1 },${ line + 1 }` , '--incremental' , '--root' ] , repo . root ) ; let hash ; let subject ; let author ; let date ; let email ; result . split ( /\r?\n/g ) . forEach ( ( line , index ) => { if ( index == 0 ) { hash = line . split ( ' ' ) [ 0 ] ; } else { const infoName = line . split ( ' ' ) [ 0 ] ; const info = line . substr ( infoName . length ) . trim ( ) ; if ( ! info ) { return ; } switch ( infoName ) { case 'author' : author = info ; break ; case 'committer-time' : date = ( new Date ( parseInt ( info ) * 1000 ) ) . toLocaleDateString ( ) ; break ; case 'author-mail' : email = info ; break ; case 'summary' : subject = singleLined ( info ) ; break ; default : break ; } } } ) ; if ( [ hash , subject , author , email , date ] . some ( v => ! v ) ) { Tracer . warning ( `Blame info missed. repo ${ repo . root } file ${ filePath }:${ line } ${ hash }` + ` author: ${ author }, mail: ${ email }, date: ${ date }, summary: ${ subject }` ) ; return null ; } const addition = await this . _exec ( [ 'show' , `--format=%h${ FormatSeparator }%cr${ FormatSeparator }%b${ FormatSeparator }` , '--stat' , `${ hash }` ] , repo . root ) ; const items = addition . split ( FormatSeparator ) ; hash = items [ 0 ] ; const relativeDate = items [ 1 ] ; const body = items [ 2 ] ? items [ 2 ] . trim ( ) : null ; const stat = ` ${ items [ 3 ] . trim ( ) }` ; return { file , line , subject , body , hash , author , date , email , relativeDate , stat } ; } private _scanSubFolders ( root ) { const children = fs . readdirSync ( root ) ; children . filter ( child => child !== '.git' ) . forEach ( async ( child ) => { const fullPath = path . join ( root , child ) ; if ( fs . statSync ( fullPath ) . isDirectory ( ) ) { if ( fs . readdirSync ( fullPath ) . find ( v => v === '.git' ) ) { let gitRoot = ( await this . _exec ( [ 'rev-parse' , '--show-toplevel' ] , fullPath ) ) . trim ( ) ; if ( gitRoot ) { gitRoot = path . normalize ( gitRoot ) ; if ( this . _gitRepos . findIndex ( ( value ) => { return value . root == gitRoot ; } ) === - 1 ) { this . _gitRepos . push ( { root : gitRoot } ) ; commands . executeCommand ( 'setContext' , 'hasGitRepo' , true ) ; this . _onDidChangeGitRepositories . fire ( this . getGitRepos ( ) ) ; } } } } } ) ; } private async _exec ( args , cwd ) { const start = Date . now ( ) ; let content = '' ; let gitPath = this . _gitPath ; if ( ! gitPath ) { gitPath = 'git' ; } let gitShow = spawn ( gitPath , args , { cwd } ) ; let out = gitShow . stdout ; out . setEncoding ( 'utf8' ) ; return new Promise < string > ( ( resolve , reject ) => { out . on ( 'data' , data => content += data ) ; out . on ( 'end' , ( ) => { resolve ( content ) ; Tracer . verbose ( `git command: git ${ args . join ( ' ' ) } (${ Date . now ( ) - start }ms)` ) ; } ) ; out . on ( 'error' , err => { reject ( err ) ; Tracer . error ( `git command failed: git ${ args . join ( ' ' ) } (${ Date . now ( ) - start }ms) ${ err . message }` ) ; } ) ; } ) ; } }