@@ -426,6 +426,165 @@ func TestBuildPlansDelete(t *testing.T) {
426426 }
427427}
428428
429+ // TestBuildPlansPrunePreservesUnrelatedBranchesUnderFilter is a regression
430+ // guard for the prune-scoping rule in planner.go: --prune deletes orphan
431+ // target branches only when the user has not narrowed the source ref set
432+ // with --branch or --map. With either filter present, branches that exist
433+ // only on the target are out of scope and must be preserved.
434+ func TestBuildPlansPrunePreservesUnrelatedBranchesUnderFilter (t * testing.T ) {
435+ t .Parallel ()
436+
437+ mainHash := plumbing .NewHash ("1111111111111111111111111111111111111111" )
438+ releaseHash := plumbing .NewHash ("2222222222222222222222222222222222222222" )
439+
440+ mainRef := plumbing .NewBranchReferenceName ("main" )
441+ stableRef := plumbing .NewBranchReferenceName ("stable" )
442+ releaseRef := plumbing .NewBranchReferenceName ("release" )
443+
444+ sourceRefs := map [plumbing.ReferenceName ]plumbing.Hash {
445+ mainRef : mainHash ,
446+ }
447+
448+ tests := []struct {
449+ name string
450+ cfg PlanConfig
451+ targetRefs map [plumbing.ReferenceName ]plumbing.Hash
452+ wantManaged plumbing.ReferenceName
453+ preservedRef plumbing.ReferenceName
454+ }{
455+ {
456+ name : "branch filter --branch main --prune" ,
457+ cfg : PlanConfig {
458+ Branches : []string {"main" },
459+ Prune : true ,
460+ },
461+ targetRefs : map [plumbing.ReferenceName ]plumbing.Hash {
462+ mainRef : mainHash ,
463+ releaseRef : releaseHash ,
464+ },
465+ wantManaged : mainRef ,
466+ preservedRef : releaseRef ,
467+ },
468+ {
469+ name : "rename mapping --map main:stable --prune" ,
470+ cfg : PlanConfig {
471+ Mappings : []RefMapping {{Source : "main" , Target : "stable" }},
472+ Prune : true ,
473+ },
474+ targetRefs : map [plumbing.ReferenceName ]plumbing.Hash {
475+ stableRef : mainHash ,
476+ releaseRef : releaseHash ,
477+ },
478+ wantManaged : stableRef ,
479+ preservedRef : releaseRef ,
480+ },
481+ }
482+
483+ for _ , tt := range tests {
484+ t .Run (tt .name , func (t * testing.T ) {
485+ t .Parallel ()
486+
487+ desired , managed , err := BuildDesiredRefs (sourceRefs , tt .cfg )
488+ if err != nil {
489+ t .Fatalf ("BuildDesiredRefs: %v" , err )
490+ }
491+
492+ plans , err := BuildPlans (nil , desired , tt .targetRefs , managed , tt .cfg )
493+ if err != nil {
494+ t .Fatalf ("BuildPlans: %v" , err )
495+ }
496+
497+ for _ , p := range plans {
498+ if p .TargetRef == tt .preservedRef {
499+ t .Fatalf ("unrelated target branch %s emitted plan %+v; --prune must preserve it under filtered scope" , tt .preservedRef , p )
500+ }
501+ if p .Action == ActionDelete && p .TargetRef != tt .preservedRef {
502+ t .Fatalf ("unexpected delete plan for %s: %+v" , p .TargetRef , p )
503+ }
504+ }
505+
506+ if _ , ok := managed [tt .preservedRef ]; ok {
507+ t .Fatalf ("managed map leaked unrelated target ref %s under filtered prune scope" , tt .preservedRef )
508+ }
509+ if _ , ok := managed [tt .wantManaged ]; ! ok {
510+ t .Fatalf ("expected managed scope to include %s, got %+v" , tt .wantManaged , managed )
511+ }
512+ })
513+ }
514+ }
515+
516+ // TestBuildReplicationPlansPrunePreservesUnrelatedBranchesUnderFilter is the
517+ // replicate-mode counterpart to the sync test above. The prune-scoping rule
518+ // must hold whether the operation is sync or replicate.
519+ func TestBuildReplicationPlansPrunePreservesUnrelatedBranchesUnderFilter (t * testing.T ) {
520+ t .Parallel ()
521+
522+ mainHash := plumbing .NewHash ("1111111111111111111111111111111111111111" )
523+ releaseHash := plumbing .NewHash ("2222222222222222222222222222222222222222" )
524+
525+ mainRef := plumbing .NewBranchReferenceName ("main" )
526+ stableRef := plumbing .NewBranchReferenceName ("stable" )
527+ releaseRef := plumbing .NewBranchReferenceName ("release" )
528+
529+ sourceRefs := map [plumbing.ReferenceName ]plumbing.Hash {
530+ mainRef : mainHash ,
531+ }
532+
533+ tests := []struct {
534+ name string
535+ cfg PlanConfig
536+ targetRefs map [plumbing.ReferenceName ]plumbing.Hash
537+ preservedRef plumbing.ReferenceName
538+ }{
539+ {
540+ name : "branch filter --branch main --prune" ,
541+ cfg : PlanConfig {
542+ Branches : []string {"main" },
543+ Prune : true ,
544+ },
545+ targetRefs : map [plumbing.ReferenceName ]plumbing.Hash {
546+ mainRef : mainHash ,
547+ releaseRef : releaseHash ,
548+ },
549+ preservedRef : releaseRef ,
550+ },
551+ {
552+ name : "rename mapping --map main:stable --prune" ,
553+ cfg : PlanConfig {
554+ Mappings : []RefMapping {{Source : "main" , Target : "stable" }},
555+ Prune : true ,
556+ },
557+ targetRefs : map [plumbing.ReferenceName ]plumbing.Hash {
558+ stableRef : mainHash ,
559+ releaseRef : releaseHash ,
560+ },
561+ preservedRef : releaseRef ,
562+ },
563+ }
564+
565+ for _ , tt := range tests {
566+ t .Run (tt .name , func (t * testing.T ) {
567+ t .Parallel ()
568+
569+ desired , managed , err := BuildDesiredRefs (sourceRefs , tt .cfg )
570+ if err != nil {
571+ t .Fatalf ("BuildDesiredRefs: %v" , err )
572+ }
573+
574+ plans , err := BuildReplicationPlans (desired , tt .targetRefs , managed , tt .cfg )
575+ if err != nil {
576+ t .Fatalf ("BuildReplicationPlans: %v" , err )
577+ }
578+
579+ for _ , p := range plans {
580+ if p .TargetRef == tt .preservedRef {
581+ t .Fatalf ("unrelated target branch %s emitted plan %+v; --prune must preserve it under filtered scope" , tt .preservedRef , p )
582+ }
583+ }
584+ })
585+ }
586+ }
587+
429588func TestBuildPlansTagBlock (t * testing.T ) {
430589 repo , err := git .Init (memory .NewStorage (), nil )
431590 if err != nil {
0 commit comments