import chalk from 'chalk' ; import { execSync } from 'child_process' ; import prompts from 'prompts' ; import { cache } from '../lib/config' ; import { DeleteBranchesStackFrameT , MergeConflictCallstackT , } from '../lib/config/merge_conflict_callstack_config' ; import { KilledError } from '../lib/errors' ; import { checkoutBranch , getTrunk , logInfo } from '../lib/utils' ; import Branch from '../wrapper-classes/branch' ; import { deleteBranchAction } from './delete_branch' ; import { ontoAction } from './onto' ; /**
 * This method is assumed to be idempotent -- if a merge conflict interrupts
 * execution of this method, we simply restart the method upon running `gt
 * continue`.
 */ export async function deleteMergedBranches ( opts ) { const trunkChildren = getTrunk ( ) . getChildrenFromMeta ( ) ; /**
     * To find and delete all of the merged branches, we traverse all of the
     * stacks off of trunk, greedily deleting the merged-in base branches and
     * rebasing the remaining branches.
     *
     * To greedily delete the branches, we keep track of the branches we plan
     * to delete as well as a live snapshot of their children. When a branch
     * we plan to delete has no more children, we know that it is safe to
     * eagerly delete.
     *
     * This eager deletion doesn't matter much in small repos, but matters
     * a lot if a repo has a lot of branches to delete. Whereas previously
     * any error in `repo sync` would throw away all of the work the command did
     * to determine what could and couldn't be deleted, now we take advantage
     * of that work as soon as we can.
     */ let toProcess = trunkChildren ; const branchesToDelete = { } ; /**
     * Since we're doing a DFS, assuming rather even distribution of stacks off
     * of trunk children, we can trace the progress of the DFS through the trunk
     * children to give the user a sense of how far the repo sync has progressed.
     * Note that we only do this if the user has a large number of branches off
     * of trunk (> 50).
     */ const trunkChildrenProgressMarkers = { } ; if ( opts . frame . showDeleteProgress ) { trunkChildren . forEach ( ( child , i ) => { if ( i === 0 ) { return ; } trunkChildrenProgressMarkers [ child . name ] = `${ + ( ( ( i / ( trunkChildren . length + 1 ) ) * 100 ) . toFixed ( 2 ) ) }%` ; } ) ; } do { const branch = toProcess . shift ( ) ; if ( branch === undefined ) { break ; } if ( branch . name in branchesToDelete ) { continue ; } if ( opts . frame . showDeleteProgress && branch . name in trunkChildrenProgressMarkers ) { logInfo ( `${ trunkChildrenProgressMarkers [ branch . name ] } done searching for merged branches to delete...` ) ; } const shouldDelete = await shouldDeleteBranch ( { branch : branch , force : opts . frame . force , } ) ; if ( shouldDelete ) { const children = branch . getChildrenFromMeta ( ) ; toProcess = children . concat ( toProcess ) ; branchesToDelete [ branch . name ] = { branch : branch , children : children , } ; } else { const parent = branch . getParentFromMeta ( ) ; const parentName = parent ?. name ; if ( parentName !== undefined && parentName in branchesToDelete ) { checkoutBranch ( branch . name ) ; logInfo ( `upstacking (${ branch . name }) onto (${ getTrunk ( ) . name })` ) ; await ontoAction ( { onto : getTrunk ( ) . name , mergeConflictCallstack : { frame : opts . frame , parent : opts . parent , } , } ) ; branchesToDelete [ parentName ] . children = branchesToDelete [ parentName ] . children . filter ( ( child ) => child . name !== branch . name ) ; } } checkoutBranch ( getTrunk ( ) . name ) ; let branchToDeleteName ; do { branchToDeleteName = Object . keys ( branchesToDelete ) . find ( ( branchToDelete ) => branchesToDelete [ branchToDelete ] . children . length === 0 ) ; if ( branchToDeleteName === undefined ) { continue ; } const branch = branchesToDelete [ branchToDeleteName ] . branch ; const parentName = branch . getParentFromMeta ( ) ?. name ; if ( parentName !== undefined && parentName in branchesToDelete ) { branchesToDelete [ parentName ] . children = branchesToDelete [ parentName ] . children . filter ( ( child ) => child . name !== branch . name ) ; } await deleteBranch ( branch ) ; delete branchesToDelete [ branchToDeleteName ] ; } while ( branchToDeleteName !== undefined ) ; } while ( toProcess . length > 0 ) ; } async function shouldDeleteBranch ( args ) { const merged = branchMerged ( args . branch ) ; if ( ! merged ) { return false ; } if ( args . force ) { return true ; } const githubMergedBase = args . branch . getPRInfo ( ) ?. state === 'MERGED' ? args . branch . getPRInfo ( ) ?. base : undefined ; const mergedBase = githubMergedBase ?? getTrunk ( ) . name ; const response = await prompts ( { type : 'confirm' , name : 'value' , message : `Delete (${ chalk . green ( args . branch . name ) }), which has been merged into (${ mergedBase })?` , initial : true , } , { onCancel : ( ) => { throw new KilledError ( ) ; } , } ) ; return response . value === true ; } function branchMerged ( branch ) { const prMerged = branch . getPRInfo ( ) ?. state === 'MERGED' ; if ( prMerged ) { return true ; } const branchName = branch . name ; const trunk = getTrunk ( ) . name ; const cherryCheckProvesMerged = execSync ( `mergeBase=$(git merge-base ${ trunk } ${ branchName }) && git cherry ${ trunk } $(git commit-tree $(git rev-parse "${ branchName }^{tree}") -p $mergeBase -m _)` ) . toString ( ) . trim ( ) . startsWith ( '-' ) ; if ( cherryCheckProvesMerged ) { return true ; } const diffCheckProvesMerged = execSync ( `git diff ${ branchName } ${ trunk } | wc -l` ) . toString ( ) . trim ( ) === '0' ; if ( diffCheckProvesMerged ) { return true ; } return false ; } async function deleteBranch ( branch ) { logInfo ( `Deleting (${ chalk . red ( branch . name ) })` ) ; await deleteBranchAction ( { branchName : branch . name , force : true , } ) ; cache . clearAll ( ) ; }