@@ -9,9 +9,13 @@ package cmd
99import (
1010 "errors"
1111 "fmt"
12+ "os/exec"
1213 "testing"
1314
15+ "github.com/boneskull/gh-stack/internal/config"
16+ "github.com/boneskull/gh-stack/internal/git"
1417 "github.com/boneskull/gh-stack/internal/github"
18+ "github.com/boneskull/gh-stack/internal/style"
1519 "github.com/boneskull/gh-stack/internal/tree"
1620)
1721
@@ -319,6 +323,107 @@ func TestIsBaseBranchInvalidError(t *testing.T) {
319323 }
320324}
321325
326+ func setupTestRepo (t * testing.T ) * config.Config {
327+ t .Helper ()
328+ dir := t .TempDir ()
329+ if err := exec .Command ("git" , "init" , dir ).Run (); err != nil {
330+ t .Fatalf ("git init failed: %v" , err )
331+ }
332+ exec .Command ("git" , "-C" , dir , "config" , "user.email" , "test@test.com" ).Run () //nolint:errcheck
333+ exec .Command ("git" , "-C" , dir , "config" , "user.name" , "Test" ).Run () //nolint:errcheck
334+ cfg , err := config .Load (dir )
335+ if err != nil {
336+ t .Fatalf ("config.Load failed: %v" , err )
337+ }
338+ return cfg
339+ }
340+
341+ // setupTestRepoWithDir is like setupTestRepo but also returns the directory path
342+ // for callers that need to run git commands directly or construct a git.Git instance.
343+ func setupTestRepoWithDir (t * testing.T ) (* config.Config , string ) {
344+ t .Helper ()
345+ dir := t .TempDir ()
346+ if err := exec .Command ("git" , "init" , dir ).Run (); err != nil {
347+ t .Fatalf ("git init failed: %v" , err )
348+ }
349+ exec .Command ("git" , "-C" , dir , "config" , "user.email" , "test@test.com" ).Run () //nolint:errcheck
350+ exec .Command ("git" , "-C" , dir , "config" , "user.name" , "Test" ).Run () //nolint:errcheck
351+ // Create an initial commit so the repo has a HEAD and we can create branches.
352+ exec .Command ("git" , "-C" , dir , "commit" , "--allow-empty" , "-m" , "init" ).Run () //nolint:errcheck
353+ cfg , err := config .Load (dir )
354+ if err != nil {
355+ t .Fatalf ("config.Load failed: %v" , err )
356+ }
357+ return cfg , dir
358+ }
359+
360+ func TestIsTransitionToTrunk (t * testing.T ) {
361+ trunk := "main"
362+
363+ t .Run ("no_stored_base_returns_true" , func (t * testing.T ) {
364+ cfg := setupTestRepo (t )
365+ // No stored base — first run after PR creation; should prompt.
366+ if ! isTransitionToTrunk (cfg , "feat-a" , trunk ) {
367+ t .Error ("expected true when no stored base exists" )
368+ }
369+ })
370+
371+ t .Run ("stored_base_is_not_trunk_returns_true" , func (t * testing.T ) {
372+ cfg := setupTestRepo (t )
373+ // Branch previously targeted a non-trunk parent; now it targets trunk.
374+ if err := cfg .SetPRBase ("feat-a" , "feat-parent" ); err != nil {
375+ t .Fatalf ("SetPRBase failed: %v" , err )
376+ }
377+ if ! isTransitionToTrunk (cfg , "feat-a" , trunk ) {
378+ t .Error ("expected true when stored base is not trunk" )
379+ }
380+ })
381+
382+ t .Run ("stored_base_is_trunk_returns_false" , func (t * testing.T ) {
383+ cfg := setupTestRepo (t )
384+ // Branch was already targeting trunk on the previous run; don't re-prompt.
385+ if err := cfg .SetPRBase ("feat-a" , trunk ); err != nil {
386+ t .Fatalf ("SetPRBase failed: %v" , err )
387+ }
388+ if isTransitionToTrunk (cfg , "feat-a" , trunk ) {
389+ t .Error ("expected false when stored base is already trunk" )
390+ }
391+ })
392+
393+ t .Run ("different_branches_are_independent" , func (t * testing.T ) {
394+ cfg := setupTestRepo (t )
395+ // feat-a already targeting trunk; feat-b has no stored base.
396+ if err := cfg .SetPRBase ("feat-a" , trunk ); err != nil {
397+ t .Fatalf ("SetPRBase failed: %v" , err )
398+ }
399+ if isTransitionToTrunk (cfg , "feat-a" , trunk ) {
400+ t .Error ("feat-a: expected false when stored base is trunk" )
401+ }
402+ if ! isTransitionToTrunk (cfg , "feat-b" , trunk ) {
403+ t .Error ("feat-b: expected true when no stored base exists" )
404+ }
405+ })
406+
407+ t .Run ("custom_trunk_name_works" , func (t * testing.T ) {
408+ cfg := setupTestRepo (t )
409+ customTrunk := "master"
410+ // Stored as "master"; should not prompt.
411+ if err := cfg .SetPRBase ("feat-a" , customTrunk ); err != nil {
412+ t .Fatalf ("SetPRBase failed: %v" , err )
413+ }
414+ if isTransitionToTrunk (cfg , "feat-a" , customTrunk ) {
415+ t .Error ("expected false with custom trunk name already stored" )
416+ }
417+ // Stored as something else; should prompt.
418+ if err := cfg .SetPRBase ("feat-b" , "other-branch" ); err != nil {
419+ t .Fatalf ("SetPRBase failed: %v" , err )
420+ }
421+ if ! isTransitionToTrunk (cfg , "feat-b" , customTrunk ) {
422+ t .Error ("expected true when stored base is not the custom trunk" )
423+ }
424+ })
425+ }
426+
322427func TestApplyMustPushForSkippedAncestors (t * testing.T ) {
323428 main := & tree.Node {Name : "main" }
324429 featA := & tree.Node {Name : "feat-a" , Parent : main }
@@ -392,3 +497,84 @@ func TestApplyMustPushForSkippedAncestors(t *testing.T) {
392497 }
393498 })
394499}
500+
501+ func TestDeleteMergedBranchClearsPRBase (t * testing.T ) {
502+ cfg , dir := setupTestRepoWithDir (t )
503+ g := git .New (dir )
504+ s := style .New ()
505+
506+ trunk , err := g .CurrentBranch ()
507+ if err != nil {
508+ t .Fatalf ("CurrentBranch failed: %v" , err )
509+ }
510+ if err := cfg .SetTrunk (trunk ); err != nil {
511+ t .Fatalf ("SetTrunk failed: %v" , err )
512+ }
513+
514+ // Create feature-a with a commit so git can delete it later.
515+ if err := exec .Command ("git" , "-C" , dir , "checkout" , "-b" , "feature-a" ).Run (); err != nil {
516+ t .Fatalf ("create branch failed: %v" , err )
517+ }
518+ if err := exec .Command ("git" , "-C" , dir , "commit" , "--allow-empty" , "-m" , "feat" ).Run (); err != nil {
519+ t .Fatalf ("commit failed: %v" , err )
520+ }
521+ if err := exec .Command ("git" , "-C" , dir , "checkout" , trunk ).Run (); err != nil {
522+ t .Fatalf ("checkout trunk failed: %v" , err )
523+ }
524+
525+ if err := cfg .SetParent ("feature-a" , trunk ); err != nil {
526+ t .Fatalf ("SetParent failed: %v" , err )
527+ }
528+ if err := cfg .SetPR ("feature-a" , 10 ); err != nil {
529+ t .Fatalf ("SetPR failed: %v" , err )
530+ }
531+ if err := cfg .SetPRBase ("feature-a" , trunk ); err != nil {
532+ t .Fatalf ("SetPRBase failed: %v" , err )
533+ }
534+
535+ currentBranch := trunk
536+ deleteMergedBranch (g , cfg , "feature-a" , trunk , & currentBranch , s )
537+
538+ if v , err := cfg .GetPRBase ("feature-a" ); err == nil {
539+ t .Errorf ("expected stackPRBase to be removed after deleteMergedBranch, got %q" , v )
540+ }
541+ if v , err := cfg .GetPR ("feature-a" ); err == nil {
542+ t .Errorf ("expected stackPR to be removed after deleteMergedBranch, got %d" , v )
543+ }
544+ }
545+
546+ func TestOrphanMergedBranchClearsPRBase (t * testing.T ) {
547+ cfg , dir := setupTestRepoWithDir (t )
548+ g := git .New (dir )
549+ s := style .New ()
550+
551+ trunk , err := g .CurrentBranch ()
552+ if err != nil {
553+ t .Fatalf ("CurrentBranch failed: %v" , err )
554+ }
555+ if err := cfg .SetTrunk (trunk ); err != nil {
556+ t .Fatalf ("SetTrunk failed: %v" , err )
557+ }
558+
559+ if err := cfg .SetParent ("feature-a" , trunk ); err != nil {
560+ t .Fatalf ("SetParent failed: %v" , err )
561+ }
562+ if err := cfg .SetPR ("feature-a" , 11 ); err != nil {
563+ t .Fatalf ("SetPR failed: %v" , err )
564+ }
565+ if err := cfg .SetPRBase ("feature-a" , trunk ); err != nil {
566+ t .Fatalf ("SetPRBase failed: %v" , err )
567+ }
568+
569+ orphanMergedBranch (cfg , "feature-a" , s )
570+
571+ if v , err := cfg .GetPRBase ("feature-a" ); err == nil {
572+ t .Errorf ("expected stackPRBase to be removed after orphanMergedBranch, got %q" , v )
573+ }
574+ if v , err := cfg .GetPR ("feature-a" ); err == nil {
575+ t .Errorf ("expected stackPR to be removed after orphanMergedBranch, got %d" , v )
576+ }
577+ if v , err := cfg .GetParent ("feature-a" ); err == nil {
578+ t .Errorf ("expected stackParent to be removed after orphanMergedBranch, got %q" , v )
579+ }
580+ }
0 commit comments