@@ -347,6 +347,10 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
347
347
348
348
branch = branchMap . get ( tip ) ;
349
349
branchId = branch ?. id ?? getBranchId ( repoPath , false , tip ) ;
350
+
351
+ // Check if branch has commits that can be recomposed
352
+ const recomposable = await this . isBranchRecomposable ( branch , repoPath ) ;
353
+
350
354
context = {
351
355
webviewItem : `gitlens:branch${ head ? '+current' : '' } ${
352
356
branch ?. upstream != null ? '+tracking' : ''
@@ -358,7 +362,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
358
362
: ''
359
363
} ${ branch ?. starred ? '+starred' : '' } ${ branch ?. upstream ?. state . ahead ? '+ahead' : '' } ${
360
364
branch ?. upstream ?. state . behind ? '+behind' : ''
361
- } `,
365
+ } ${ recomposable ? '+recomposable' : '' } `,
362
366
webviewItemValue : {
363
367
type : 'branch' ,
364
368
ref : createReference ( tip , repoPath , {
@@ -617,6 +621,59 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
617
621
return getCommitsForGraphCore . call ( this , defaultLimit , selectSha , undefined , cancellation ) ;
618
622
}
619
623
624
+ private async isBranchRecomposable ( branch : GitBranch | undefined , repoPath : string ) : Promise < boolean > {
625
+ if ( ! branch || branch . remote ) return false ;
626
+
627
+ try {
628
+ const upstreamName = branch . upstream ?. name ;
629
+ const svc = this . container . git . getRepositoryService ( repoPath ) ;
630
+
631
+ // Get stored merge target configurations
632
+ const [ storedTargetResult , storedMergeBaseResult ] = await Promise . allSettled ( [
633
+ svc . branches . getStoredMergeTargetBranchName ?.( branch . name ) ,
634
+ svc . branches . getBaseBranchName ?.( branch . name ) ,
635
+ ] ) ;
636
+ const storedTarget = getSettledValue ( storedTargetResult ) ;
637
+ const validStoredTarget = storedTarget && storedTarget !== upstreamName ? storedTarget : undefined ;
638
+ const storedMergeBase = getSettledValue ( storedMergeBaseResult ) ;
639
+ const validStoredMergeBase =
640
+ storedMergeBase && storedMergeBase !== upstreamName ? storedMergeBase : undefined ;
641
+
642
+ // Select target with most recent common commit (closest to branch tip)
643
+ const validTargets = [ validStoredTarget , validStoredMergeBase ] ;
644
+ const targetCommit = await this . selectMostRecentMergeBase ( branch . name , validTargets , svc ) ;
645
+
646
+ return Boolean ( targetCommit && targetCommit !== branch . sha ) ;
647
+ } catch {
648
+ // If we can't determine, assume not recomposable
649
+ return false ;
650
+ }
651
+ }
652
+
653
+ private async selectMostRecentMergeBase (
654
+ branchName : string ,
655
+ targets : ( string | undefined ) [ ] ,
656
+ svc : ReturnType < typeof this . container . git . getRepositoryService > ,
657
+ ) : Promise < string | undefined > {
658
+ const mergeBaseResults = await Promise . allSettled (
659
+ targets . map ( target => target && svc . refs . getMergeBase ( branchName , target ) ) ,
660
+ ) ;
661
+ const isString = ( t : string | undefined ) : t is string => Boolean ( t ) ;
662
+ const mergeBases = mergeBaseResults . map ( result => getSettledValue ( result ) ) . filter ( isString ) ;
663
+
664
+ if ( mergeBases . length === 0 ) return undefined ;
665
+
666
+ let mostRecentMergeBase = mergeBases [ 0 ] ;
667
+ for ( let i = 1 ; i < mergeBases . length ; i ++ ) {
668
+ const isCurrentMoreRecent = await svc . commits . isAncestorOf ( mostRecentMergeBase , mergeBases [ i ] ) ;
669
+ if ( isCurrentMoreRecent ) {
670
+ mostRecentMergeBase = mergeBases [ i ] ;
671
+ }
672
+ }
673
+
674
+ return mostRecentMergeBase ;
675
+ }
676
+
620
677
@log < GraphGitSubProvider [ 'searchGraph' ] > ( {
621
678
args : {
622
679
1 : s =>
0 commit comments