@@ -5,12 +5,14 @@ import { GitErrorHandling } from '../../../../git/commandOptions';
55import type {
66 BranchContributionsOverview ,
77 GitBranchesSubProvider ,
8+ GitBranchMergedStatus ,
89 PagedResult ,
910 PagingOptions ,
1011} from '../../../../git/gitProvider' ;
1112import { GitBranch } from '../../../../git/models/branch' ;
1213import { getLocalBranchByUpstream , isDetachedHead } from '../../../../git/models/branch.utils' ;
1314import type { MergeConflict } from '../../../../git/models/mergeConflict' ;
15+ import type { GitBranchReference } from '../../../../git/models/reference' ;
1416import { createRevisionRange } from '../../../../git/models/revision.utils' ;
1517import { parseGitBranches } from '../../../../git/parsers/branchParser' ;
1618import { parseMergeTreeConflict } from '../../../../git/parsers/mergeTreeParser' ;
@@ -310,6 +312,74 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
310312 await this . git . branch ( repoPath , name , ref ) ;
311313 }
312314
315+ @log ( )
316+ async getBranchMergedStatus (
317+ repoPath : string ,
318+ branch : GitBranchReference ,
319+ into : GitBranchReference ,
320+ ) : Promise < GitBranchMergedStatus > {
321+ const result = await this . getBranchMergedStatusCore ( repoPath , branch , into ) ;
322+ if ( result . merged ) return result ;
323+
324+ // If the branch we are checking is a remote branch, check if it has been merged into its local branch (if there is one)
325+ if ( into . remote ) {
326+ const localIntoBranch = await this . getLocalBranchByUpstream ( repoPath , into . name ) ;
327+ // If there is a local branch and it is not the branch we are checking, check if it has been merged into it
328+ if ( localIntoBranch != null && localIntoBranch . name !== branch . name ) {
329+ const result = await this . getBranchMergedStatusCore ( repoPath , branch , localIntoBranch ) ;
330+ if ( result . merged ) {
331+ return {
332+ ...result ,
333+ localBranchOnly : { name : localIntoBranch . name } ,
334+ } ;
335+ }
336+ }
337+ }
338+
339+ return { merged : false } ;
340+ }
341+
342+ private async getBranchMergedStatusCore (
343+ repoPath : string ,
344+ branch : GitBranchReference ,
345+ into : GitBranchReference ,
346+ ) : Promise < Exclude < GitBranchMergedStatus , 'localBranchOnly' > > {
347+ const scope = getLogScope ( ) ;
348+
349+ try {
350+ // Check if branch is direct ancestor (handles FF merges)
351+ try {
352+ await this . git . exec (
353+ { cwd : repoPath , errors : GitErrorHandling . Throw } ,
354+ 'merge-base' ,
355+ '--is-ancestor' ,
356+ branch . name ,
357+ into . name ,
358+ ) ;
359+ return { merged : true , confidence : 'highest' } ;
360+ } catch { }
361+
362+ // Cherry-pick detection (handles cherry-picks, rebases, etc)
363+ const data = await this . git . exec < string > (
364+ { cwd : repoPath } ,
365+ 'cherry' ,
366+ '--abbrev' ,
367+ '-v' ,
368+ into . name ,
369+ branch . name ,
370+ ) ;
371+ // Check if there are no lines or all lines startwith a `-` (i.e. likely merged)
372+ if ( ! data || data . split ( '\n' ) . every ( l => l . startsWith ( '-' ) ) ) {
373+ return { merged : true , confidence : 'high' } ;
374+ }
375+
376+ return { merged : false } ;
377+ } catch ( ex ) {
378+ Logger . error ( ex , scope ) ;
379+ return { merged : false } ;
380+ }
381+ }
382+
313383 @log ( )
314384 async getLocalBranchByUpstream ( repoPath : string , remoteBranchName : string ) : Promise < GitBranch | undefined > {
315385 const branches = new PageableResult < GitBranch > ( p =>
@@ -425,7 +495,12 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
425495 if ( match != null && match . length === 2 ) {
426496 let name : string | undefined = match [ 1 ] ;
427497 if ( name !== 'HEAD' ) {
428- name = await this . getValidatedBranchName ( repoPath , options ?. upstream ? `${ name } @{u}` : name ) ;
498+ if ( options ?. upstream ) {
499+ const upstream = await this . getValidatedBranchName ( repoPath , `${ name } @{u}` ) ;
500+ if ( upstream ) return upstream ;
501+ }
502+
503+ name = await this . getValidatedBranchName ( repoPath , name ) ;
429504 if ( name ) return name ;
430505 }
431506 }
@@ -438,13 +513,17 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
438513 `--grep-reflog=checkout: moving from .* to ${ ref . replace ( 'refs/heads/' , '' ) } ` ,
439514 ) ;
440515 entries = data . split ( '\n' ) . filter ( entry => Boolean ( entry ) ) ;
441-
442516 if ( ! entries . length ) return undefined ;
443517
444518 match = entries [ entries . length - 1 ] . match ( / c h e c k o u t : m o v i n g f r o m ( [ ^ \s ] + ) \s / ) ;
445519 if ( match != null && match . length === 2 ) {
446520 let name : string | undefined = match [ 1 ] ;
447- name = await this . getValidatedBranchName ( repoPath , options ?. upstream ? `${ name } @{u}` : name ) ;
521+ if ( options ?. upstream ) {
522+ const upstream = await this . getValidatedBranchName ( repoPath , `${ name } @{u}` ) ;
523+ if ( upstream ) return upstream ;
524+ }
525+
526+ name = await this . getValidatedBranchName ( repoPath , name ) ;
448527 if ( name ) return name ;
449528 }
450529 } catch { }
0 commit comments