@@ -5,12 +5,14 @@ import { GitErrorHandling } from '../../../../git/commandOptions';
5
5
import type {
6
6
BranchContributionsOverview ,
7
7
GitBranchesSubProvider ,
8
+ GitBranchMergedStatus ,
8
9
PagedResult ,
9
10
PagingOptions ,
10
11
} from '../../../../git/gitProvider' ;
11
12
import { GitBranch } from '../../../../git/models/branch' ;
12
13
import { getLocalBranchByUpstream , isDetachedHead } from '../../../../git/models/branch.utils' ;
13
14
import type { MergeConflict } from '../../../../git/models/mergeConflict' ;
15
+ import type { GitBranchReference } from '../../../../git/models/reference' ;
14
16
import { createRevisionRange } from '../../../../git/models/revision.utils' ;
15
17
import { parseGitBranches } from '../../../../git/parsers/branchParser' ;
16
18
import { parseMergeTreeConflict } from '../../../../git/parsers/mergeTreeParser' ;
@@ -310,6 +312,74 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
310
312
await this . git . branch ( repoPath , name , ref ) ;
311
313
}
312
314
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
+
313
383
@log ( )
314
384
async getLocalBranchByUpstream ( repoPath : string , remoteBranchName : string ) : Promise < GitBranch | undefined > {
315
385
const branches = new PageableResult < GitBranch > ( p =>
@@ -425,7 +495,12 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
425
495
if ( match != null && match . length === 2 ) {
426
496
let name : string | undefined = match [ 1 ] ;
427
497
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 ) ;
429
504
if ( name ) return name ;
430
505
}
431
506
}
@@ -438,13 +513,17 @@ export class BranchesGitSubProvider implements GitBranchesSubProvider {
438
513
`--grep-reflog=checkout: moving from .* to ${ ref . replace ( 'refs/heads/' , '' ) } ` ,
439
514
) ;
440
515
entries = data . split ( '\n' ) . filter ( entry => Boolean ( entry ) ) ;
441
-
442
516
if ( ! entries . length ) return undefined ;
443
517
444
518
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 / ) ;
445
519
if ( match != null && match . length === 2 ) {
446
520
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 ) ;
448
527
if ( name ) return name ;
449
528
}
450
529
} catch { }
0 commit comments