import chalk from 'chalk' ; import { getCommitterDate } from '../lib/utils' ; import { getTrunk } from '../lib/utils/trunk' ; import { Commit } from '../wrapper-classes' ; import Branch from '../wrapper-classes/branch' ; import { TBranchPRInfo } from '../wrapper-classes/metadata_ref' ; type TPrintStackConfig = { currentBranch : Branch | null ; offTrunk : boolean ; visited : string [ ] ; } ; export function printStack ( args ) { args . config . visited . push ( args . baseBranch . name ) ; const children = args . baseBranch . getChildrenFromGit ( ) ; const currPrefix = getPrefix ( args . indentLevel , args . config ) ; /**
     * TODO(nicholasyan): we need to improve how we handle merges.
     *
     * C
     * |\
     * | B
     * |/
     * A
     *
     * For example in the above case, our logic will print the subtrees headed
     * by both B and C - which means that the subtree headed by C gets printed
     * twice.
     *
     * This is a short-term workaround to at least prevent duplicate printing
     * in the near-term: we mark already-visited nodes and make sure if we
     * hit an already-visited node, we just filter it out and skip it.
     */ const unvisitedChildren = children . filter ( ( child ) => ! child . isTrunk ( ) && ! args . config . visited . includes ( child . name ) ) ; unvisitedChildren . forEach ( ( child , i ) => { printStack ( { baseBranch : child , indentLevel : args . indentLevel + i , config : args . config , } ) ; } ) ; const numChildren = unvisitedChildren . length ; if ( numChildren > 1 ) { let newBranchOffshoots = '│' ; for ( let i = 1 ; i < numChildren ; i ++ ) { if ( i < numChildren - 1 ) { newBranchOffshoots += '──┴' ; } else { newBranchOffshoots += '──┘' ; } } console . log ( currPrefix + newBranchOffshoots ) ; console . log ( currPrefix + '│' ) ; } const branchInfo = getBranchInfo ( args . baseBranch , args . config ) ; branchInfo . forEach ( ( line ) => console . log ( currPrefix + line ) ) ; console . log ( currPrefix + ( ! args . config . offTrunk && args . baseBranch . name === getTrunk ( ) . name ? '․' : '│' ) ) ; } function getPrefix ( indentLevel , config ) { let prefix = '' ; for ( let i = 0 ; i < indentLevel ; i ++ ) { if ( i === 0 ) { prefix += config . offTrunk ? '│  ' : '․  ' ; } else { prefix += '│  ' ; } } return prefix ; } function getBranchInfo ( branch , config ) { let branchInfoLines = [ ] ; branchInfoLines . push ( getBranchTitle ( branch , config ) ) ; const prInfo = branch . getPRInfo ( ) ; const prTitle = prInfo ?. title ; if ( prTitle !== undefined ) { branchInfoLines . push ( prTitle ) ; } branchInfoLines . push ( `${ chalk . dim ( getCommitterDate ( { revision : branch . name , timeFormat : 'RELATIVE_READABLE' , } ) ) }` ) ; if ( ! branch . isTrunk ( ) ) { const commits = branch . getCommitSHAs ( ) ; if ( commits . length !== 0 ) { commits . forEach ( ( commitSHA ) => { const commit = new Commit ( commitSHA ) ; branchInfoLines . push ( chalk . gray ( `* ${ commit . sha . slice ( 0 , 6 ) } - ${ commit . messageSubject ( ) }` ) ) ; } ) ; } } branchInfoLines = dimMergedOrClosedBranches ( { lines : branchInfoLines , branch : branch , } ) ; branchInfoLines = prefixWithBranchStem ( { branch : branch , config : config , lines : branchInfoLines , } ) ; return branchInfoLines ; } export function getBranchTitle ( branch , config ) { const prInfo = branch . getPRInfo ( ) ; const branchName = config . currentBranch ?. name === branch . name ? chalk . cyan ( `${ branch . name } (current)` ) : branch . name ; const prNumber = prInfo !== undefined ? `PR #${ prInfo . number }` : '' ; if ( prInfo ?. state === 'MERGED' ) { return `${ branchName } ${ prNumber } ${ getPRState ( prInfo ) ?? '' }` ; } else if ( prInfo ?. state === 'CLOSED' ) { return `${ chalk . strikethrough ( `${ branchName } ${ prNumber }` ) } ${ getPRState ( prInfo ) ?? '' }` ; } else { return `${ chalk . blueBright ( branchName ) } ${ chalk . yellow ( prNumber ) } ${ getPRState ( prInfo ) ?? '' }` ; } } function getPRState ( prInfo ) { if ( prInfo === undefined ) { return '' ; } if ( prInfo . state === undefined && prInfo . reviewDecision === undefined ) { return chalk . dim ( 'Syncing PR Info...' ) ; } if ( getMergedOrClosed ( prInfo ) ) { switch ( prInfo . state ) { case 'CLOSED' : return chalk . gray ( '(Abandoned)' ) ; case 'MERGED' : return chalk . gray ( '(Merged)' ) ; default : } } if ( prInfo . isDraft ) { return chalk . gray ( '(Draft)' ) ; } const reviewDecision = prInfo . reviewDecision ; switch ( reviewDecision ) { case 'APPROVED' : return chalk . green ( '(Approved)' ) ; case 'CHANGES_REQUESTED' : return chalk . magenta ( '(Changes Requested)' ) ; case 'REVIEW_REQUIRED' : return chalk . yellow ( '(Review Required)' ) ; default : } return '' ; } /**
 * Prefixes a set of lines with the appropriate branch stem.
 *
 * Before:
 * [
 *  "foo",
 *  "bar",
 *  "baz",
 * ]
 *
 * After:
 * [
 *  "◉ foo",
 *  "│ bar",
 *  "│ baz",
 * ]
 *
 */ function prefixWithBranchStem ( args ) { const isCurrentBranch = args . config . currentBranch ?. name === args . branch . name ; const dot = isCurrentBranch ? chalk . cyan ( '◉' ) : '◯' ; return args . lines . map ( ( line , index ) => index === 0 ? `${ dot } ${ line }` : `│ ${ line }` ) ; } function dimMergedOrClosedBranches ( args ) { const isBranchMergedOrClosed = getMergedOrClosed ( args . branch . getPRInfo ( ) ) ; if ( isBranchMergedOrClosed ) { return args . lines . map ( ( line ) => chalk . dim . gray ( line ) ) ; } return args . lines ; } function getMergedOrClosed ( prInfo ) { const state = prInfo ?. state ; return state === 'MERGED' || state === 'CLOSED' ; }