import { execSync } from 'child_process' ; import { repoConfig } from '../lib/config' ; import { ExitFailedError , PreconditionsFailedError } from '../lib/errors' ; import { getBranchChildrenOrParentsFromGit , getRef , otherBranchesWithSameCommit , } from '../lib/git-refs' ; import { getCommitterDate , getTrunk , gpExecSync , logDebug } from '../lib/utils' ; import Commit from './commit' ; import MetadataRef , { TBranchPRInfo , TMeta } from './metadata_ref' ; type TBranchFilters = { useMemoizedResults ? : boolean ; maxDaysBehindTrunk ? : number ; maxBranches ? : number ; sort ? : '-committerdate' ; } ; let memoizedMetaChildren ; export default class Branch { name : string ; shouldUseMemoizedResults : boolean ; constructor ( name , opts ? ) { this . name = name ; this . shouldUseMemoizedResults = opts ?. useMemoizedResults || false ; } /**
     * Uses memoized results for some of the branch calculations. Only turn this
     * on if the git tree should not change at all during the current invoked
     * command.
     */ public useMemoizedResults ( ) { this . shouldUseMemoizedResults = true ; return this ; } public toString ( ) { return this . name ; } stackByTracingMetaParents ( branch ? ) { const curBranch = branch || this ; const metaParent = curBranch . getParentFromMeta ( ) ; if ( metaParent ) { return this . stackByTracingMetaParents ( metaParent ) . concat ( [ curBranch . name , ] ) ; } else { return [ curBranch . name ] ; } } stackByTracingGitParents ( branch ? ) { const curBranch = branch || this ; const gitParents = curBranch . getParentsFromGit ( ) ; if ( gitParents . length === 1 ) { return this . stackByTracingGitParents ( gitParents [ 0 ] ) . concat ( [ curBranch . name , ] ) ; } else { return [ curBranch . name ] ; } } getParentFromMeta ( ) { if ( this . name === getTrunk ( ) . name ) { return undefined ; } let parentName = MetadataRef . getMeta ( this . name ) ?. parentBranchName ; if ( ! parentName ) { return undefined ; } if ( ! Branch . exists ( parentName ) ) { while ( parentName && ! Branch . exists ( parentName ) ) { parentName = MetadataRef . getMeta ( parentName ) ?. parentBranchName ; } if ( parentName ) { this . setParentBranchName ( parentName ) ; } else { this . clearParentMetadata ( ) ; return undefined ; } } if ( parentName === this . name ) { this . clearParentMetadata ( ) ; throw new ExitFailedError ( `Branch (${ this . name }) has itself listed as a parent in the meta. Deleting (${ this . name }) parent metadata and exiting.` ) ; } return new Branch ( parentName ) ; } public getChildrenFromMeta ( ) { logDebug ( `Meta Children (${ this . name }): start` ) ; if ( this . shouldUseMemoizedResults ) { if ( memoizedMetaChildren === undefined ) { logDebug ( `Meta Children (${ this . name }): initialize memoization | finding all branches...` ) ; const metaChildren = { } ; const allBranches = Branch . allBranches ( { useMemoizedResults : this . shouldUseMemoizedResults , } ) ; logDebug ( `Meta Children: intiialize memoization | sifting through branches...` ) ; allBranches . forEach ( ( branch , i ) => { logDebug ( `               Branch ${ i }/${ allBranches . length } (${ branch . name })` ) ; const parentBranchName = MetadataRef . getMeta ( branch . name ) ?. parentBranchName ; if ( parentBranchName === undefined ) { return ; } if ( parentBranchName in metaChildren ) { metaChildren [ parentBranchName ] . push ( branch ) ; } else { metaChildren [ parentBranchName ] = [ branch ] ; } } ) ; logDebug ( `Meta Children (${ this . name }): initialize memoization | done` ) ; memoizedMetaChildren = metaChildren ; } logDebug ( `Meta Children (${ this . name }): end (memoized)` ) ; return memoizedMetaChildren [ this . name ] ?? [ ] ; } const children = Branch . allBranches ( ) . filter ( ( b ) => MetadataRef . getMeta ( b . name ) ?. parentBranchName === this . name ) ; logDebug ( `Git Children (${ this . name }): end` ) ; return children ; } public isUpstreamOf ( commitRef ) { const downstreamRef = gpExecSync ( { command : `git merge-base ${ this . name } ${ commitRef }` , } ) . toString ( ) . trim ( ) ; return downstreamRef !== this . ref ( ) ; } public ref ( ) { return getRef ( this ) ; } public getMetaMergeBase ( ) { const parent = this . getParentFromMeta ( ) ; if ( ! parent ) { return undefined ; } const curParentRef = parent . getCurrentRef ( ) ; const prevParentRef = parent . getMetaPrevRef ( ) ; const curParentMergeBase = execSync ( `git merge-base ${ curParentRef } ${ this . name }` ) . toString ( ) . trim ( ) ; if ( ! prevParentRef ) { return curParentMergeBase ; } const prevParentMergeBase = execSync ( `git merge-base ${ prevParentRef } ${ this . name }` ) . toString ( ) . trim ( ) ; const closestMergeBase = execSync ( `git merge-base ${ prevParentMergeBase } ${ curParentMergeBase }` ) . toString ( ) . trim ( ) === curParentMergeBase ? prevParentMergeBase : curParentMergeBase ; return closestMergeBase ; } public static exists ( branchName ) { try { execSync ( `git show-ref --quiet refs/heads/${ branchName }` , { stdio : 'ignore' , } ) ; } catch { return false ; } return true ; } private getMeta ( ) { return MetadataRef . getMeta ( this . name ) ; } private writeMeta ( meta ) { MetadataRef . updateOrCreate ( this . name , meta ) ; } public getMetaPrevRef ( ) { return MetadataRef . getMeta ( this . name ) ?. prevRef ; } public getCurrentRef ( ) { return execSync ( `git rev-parse ${ this . name }` ) . toString ( ) . trim ( ) ; } public clearMetadata ( ) { this . writeMeta ( { } ) ; return this ; } public clearParentMetadata ( ) { const meta = this . getMeta ( ) || { } ; delete meta . parentBranchName ; this . writeMeta ( meta ) ; } public setParentBranchName ( parentBranchName ) { const meta = this . getMeta ( ) || { } ; meta . parentBranchName = parentBranchName ; this . writeMeta ( meta ) ; } public resetParentBranch ( ) { const meta = this . getMeta ( ) || { } ; meta . parentBranchName = undefined ; this . writeMeta ( meta ) ; } public setMetaPrevRef ( prevRef ) { const meta = this . getMeta ( ) || { } ; meta . prevRef = prevRef ; this . writeMeta ( meta ) ; } public lastCommitTime ( ) { return parseInt ( gpExecSync ( { command : `git log -1 --format=%ct ${ this . name } --` } ) . toString ( ) . trim ( ) ) ; } public isTrunk ( ) { return this . name === getTrunk ( ) . name ; } static async branchWithName ( name ) { const branch = Branch . allBranches ( ) . find ( ( b ) => b . name === name ) ; if ( ! branch ) { throw new Error ( `Failed to find branch named ${ name }` ) ; } return new Branch ( name ) ; } static getCurrentBranch ( ) { const name = gpExecSync ( { command : `git rev-parse --abbrev-ref HEAD` , } , ( e ) => { return Buffer . alloc ( 0 ) ; } ) . toString ( ) . trim ( ) ; return name . length > 0 && name !== 'HEAD' ? new Branch ( name ) : null ; } private static allBranchesImpl ( opts ? ) { const sortString = opts ?. sort === undefined ? '' : `--sort='${ opts ?. sort }'` ; return execSync ( `git for-each-ref --format='%(refname:short)' ${ sortString } refs/heads/` ) . toString ( ) . trim ( ) . split ( '\n' ) . filter ( ( name ) => name . length > 0 && ! repoConfig . branchIsIgnored ( name ) ) . map ( ( name ) => new Branch ( name ) ) ; } static allBranches ( opts ? ) { return Branch . allBranchesWithFilter ( { filter : ( ) => true , opts : opts , } ) ; } static allBranchesWithFilter ( args ) { let branches = Branch . allBranchesImpl ( { sort : args . opts ?. maxDaysBehindTrunk !== undefined ? '-committerdate' : args . opts ?. sort , } ) ; if ( args . opts ?. useMemoizedResults ) { branches = branches . map ( ( branch ) => branch . useMemoizedResults ( ) ) ; } const maxDaysBehindTrunk = args . opts ?. maxDaysBehindTrunk ; let minUnixTimestamp = undefined ; if ( maxDaysBehindTrunk ) { const trunkUnixTimestamp = parseInt ( getCommitterDate ( { revision : getTrunk ( ) . name , timeFormat : 'UNIX_TIMESTAMP' , } ) ) ; const secondsInDay = 24 * 60 * 60 ; minUnixTimestamp = trunkUnixTimestamp - maxDaysBehindTrunk * secondsInDay ; } const maxBranches = args . opts ?. maxBranches ; const filteredBranches = [ ] ; for ( let i = 0 ; i < branches . length ; i ++ ) { if ( filteredBranches . length === maxBranches ) { break ; } if ( minUnixTimestamp !== undefined ) { const committed = parseInt ( getCommitterDate ( { revision : branches [ i ] . name , timeFormat : 'UNIX_TIMESTAMP' , } ) ) ; if ( committed < minUnixTimestamp ) { break ; } } if ( args . filter ( branches [ i ] ) ) { filteredBranches . push ( branches [ i ] ) ; } } return filteredBranches ; } static async getAllBranchesWithoutParents ( opts ? ) { return this . allBranchesWithFilter ( { filter : ( branch ) => { if ( opts ?. excludeTrunk && branch . name === getTrunk ( ) . name ) { return false ; } return branch . getParentsFromGit ( ) . length === 0 ; } , opts : opts , } ) ; } static async getAllBranchesWithParents ( opts ? ) { return this . allBranchesWithFilter ( { filter : ( branch ) => branch . getParentsFromGit ( ) . length > 0 , opts : opts , } ) ; } public head ( ) { return new Commit ( execSync ( `git rev-parse ${ this . name }` ) . toString ( ) . trim ( ) ) ; } public base ( ) { const parentBranchName = this . getMeta ( ) ?. parentBranchName ; if ( ! parentBranchName ) { return undefined ; } return new Commit ( execSync ( `git merge-base ${ parentBranchName } ${ this . name }` ) . toString ( ) . trim ( ) ) ; } public getChildrenFromGit ( ) { logDebug ( `Git Children (${ this . name }): start` ) ; const kids = getBranchChildrenOrParentsFromGit ( this , { direction : 'children' , useMemoizedResults : this . shouldUseMemoizedResults , } ) ; logDebug ( `Git Children (${ this . name }): end` ) ; return kids . sort ( this . sortBranchesAlphabetically ) ; } private sortBranchesAlphabetically ( a , b ) { if ( a . name === b . name ) { return 0 ; } else if ( a . name < b . name ) { return - 1 ; } else { return 1 ; } } public getParentsFromGit ( ) { if ( this . name === getTrunk ( ) . name ) { return [ ] ; } else if ( this . pointsToSameCommitAs ( getTrunk ( ) ) ) { return [ getTrunk ( ) ] ; } return getBranchChildrenOrParentsFromGit ( this , { direction : 'parents' , useMemoizedResults : this . shouldUseMemoizedResults , } ) . sort ( this . sortBranchesAlphabetically ) ; } private pointsToSameCommitAs ( branch ) { return ! ! otherBranchesWithSameCommit ( branch ) . find ( ( b ) => b . name === branch . name ) ; } public branchesWithSameCommit ( ) { return otherBranchesWithSameCommit ( this ) ; } public setPriorSubmitTitle ( title ) { const meta = this . getMeta ( ) || { } ; meta . priorSubmitInfo = { ... meta . priorSubmitInfo , title : title , } ; this . writeMeta ( meta ) ; } public getPriorSubmitTitle ( ) { return this . getMeta ( ) ?. priorSubmitInfo ?. title ; } public setPriorSubmitBody ( body ) { const meta = this . getMeta ( ) || { } ; meta . priorSubmitInfo = { ... meta . priorSubmitInfo , body : body , } ; this . writeMeta ( meta ) ; } public getPriorSubmitBody ( ) { return this . getMeta ( ) ?. priorSubmitInfo ?. body ; } public setPRInfo ( prInfo ) { const meta = this . getMeta ( ) || { } ; meta . prInfo = prInfo ; this . writeMeta ( meta ) ; } public clearPRInfo ( ) { const meta = this . getMeta ( ) || { } ; meta . prInfo = undefined ; this . writeMeta ( meta ) ; } public getPRInfo ( ) { return this . getMeta ( ) ?. prInfo ; } public isBaseSameAsRemotePr ( ) { const parent = this . getParentFromMeta ( ) ; if ( parent === undefined ) { throw new PreconditionsFailedError ( `Could not find parent for branch ${ this . name } to submit PR against. Please checkout ${ this . name } and run \`gt upstack onto <parent_branch>\` to set its parent.` ) ; } return parent . name !== this . getPRInfo ( ) ?. base ; } public getCommitSHAs ( ) { const parent = this . getParentFromMeta ( ) ; if ( parent === undefined ) { return [ ] ; } const shas = new Set ( ) ; const commits = gpExecSync ( { command : `git rev-list ${ parent }..${ this . name }` , } , ( _ ) => { return Buffer . alloc ( 0 ) ; } ) . toString ( ) . trim ( ) ; if ( commits . length === 0 ) { return [ ] ; } commits . split ( /[\r\n]+/ ) . forEach ( ( sha ) => { shas . add ( sha ) ; } ) ; return [ ... shas ] ; } }