Skip to content

Commit 7b15593

Browse files
glasserclaude
andcommitted
feat: add -b/--branch flag to specify branch name independently from worktree directory name
Closes #172 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff6264a commit 7b15593

3 files changed

Lines changed: 167 additions & 13 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A Git subcommand that makes `git worktree` simple.
88
$ git wt # List all worktrees
99
$ git wt --json # List all worktrees in JSON format
1010
$ git wt <branch|worktree|path> # Switch to worktree (create worktree/branch if needed)
11+
$ git wt -b <branch> <worktree> # Create worktree with a different branch name
1112
$ git wt -d <branch|worktree|path> # Delete worktree and branch (safe)
1213
$ git wt -D <branch|worktree|path> # Force delete worktree and branch
1314
```
@@ -19,6 +20,19 @@ The target can be specified as:
1920

2021
When deleting, the same target types apply: `git wt -d feature-branch`, `git wt -d .`, `git wt -d ../sibling`
2122

23+
Use `-b`/`--branch` to give the branch a different name from the worktree directory:
24+
25+
``` console
26+
$ git wt -b user/my-feature my-feature
27+
```
28+
29+
You can later switch to the worktree by either branch name or directory name:
30+
31+
``` console
32+
$ git wt user/my-feature # switch by branch name
33+
$ git wt my-feature # switch by directory name
34+
```
35+
2236
> [!NOTE]
2337
> The default branch (e.g., main, master) is protected from accidental deletion.
2438
> - If the default branch has a worktree, the worktree is deleted but the branch is preserved.

cmd/root.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var (
4040
forceDeleteFlag bool
4141
initShell string
4242
nocd bool
43+
branchFlag string
4344
// Config override flags.
4445
basedirFlag string
4546
copyignoredFlag bool
@@ -62,11 +63,12 @@ var rootCmd = &cobra.Command{
6263
Long: `git-wt is a Git subcommand that makes 'git worktree' simple.
6364
6465
Examples:
65-
git wt List all worktrees
66-
git wt <branch|worktree|path> Switch to worktree (create worktree/branch if needed)
66+
git wt List all worktrees
67+
git wt <branch|worktree|path> Switch to worktree (create worktree/branch if needed)
6768
git wt <branch|worktree|path> <start-point> Create worktree from start-point (e.g., origin/main)
68-
git wt -d <branch|worktree|path>... Delete worktree and branch (safe)
69-
git wt -D <branch|worktree|path>... Force delete worktree and branch
69+
git wt -b <branch> <worktree> Create worktree with a different branch name
70+
git wt -d <branch|worktree|path>... Delete worktree and branch (safe)
71+
git wt -D <branch|worktree|path>... Force delete worktree and branch
7072
7173
Note: The default branch (e.g., main, master) is protected from accidental deletion.
7274
- With worktree: worktree is deleted, but branch is preserved.
@@ -197,6 +199,7 @@ func init() {
197199
if err := rootCmd.Flags().MarkDeprecated("no-switch-directory", "use --nocd instead"); err != nil {
198200
panic(err) //nostyle:dontpanic
199201
}
202+
rootCmd.Flags().StringVarP(&branchFlag, "branch", "b", "", "Use a different branch name than the worktree directory name")
200203
// Config override flags.
201204
rootCmd.Flags().StringVar(&basedirFlag, "basedir", "", "Override wt.basedir config (worktree base directory)")
202205
rootCmd.Flags().BoolVar(&copyignoredFlag, "copyignored", false, "Override wt.copyignored config (copy .gitignore'd files)")
@@ -237,10 +240,16 @@ func runRoot(cmd *cobra.Command, args []string) error {
237240

238241
// Handle delete flags (multiple arguments allowed)
239242
if forceDeleteFlag {
243+
if branchFlag != "" {
244+
return fmt.Errorf("cannot use -b/--branch with -D/--force-delete")
245+
}
240246
args = uniqueArgs(args)
241247
return deleteWorktrees(ctx, cmd, args, true)
242248
}
243249
if deleteFlag {
250+
if branchFlag != "" {
251+
return fmt.Errorf("cannot use -b/--branch with -d/--delete")
252+
}
244253
args = uniqueArgs(args)
245254
return deleteWorktrees(ctx, cmd, args, false)
246255
}
@@ -251,14 +260,19 @@ func runRoot(cmd *cobra.Command, args []string) error {
251260
return fmt.Errorf("too many arguments: expected <branch> [<start-point>], got %d arguments", len(args))
252261
}
253262

254-
branch := args[0]
263+
wtName := args[0]
255264
var startPoint string
256265
if len(args) == 2 {
257266
startPoint = args[1]
258267
}
259268

269+
branchName := branchFlag
270+
if branchName == "" {
271+
branchName = wtName
272+
}
273+
260274
// Default: create or switch to worktree
261-
return handleWorktree(ctx, cmd, branch, startPoint)
275+
return handleWorktree(ctx, cmd, wtName, branchName, startPoint)
262276
}
263277

264278
// loadConfig loads config from git config and applies flag overrides.
@@ -704,7 +718,7 @@ func deleteWorktrees(ctx context.Context, cmd *cobra.Command, branches []string,
704718
return nil
705719
}
706720

707-
func handleWorktree(ctx context.Context, cmd *cobra.Command, branch, startPoint string) error {
721+
func handleWorktree(ctx context.Context, cmd *cobra.Command, wtName, branchName, startPoint string) error {
708722
// Load config with flag overrides
709723
cfg, err := loadConfig(ctx, cmd)
710724
if err != nil {
@@ -733,10 +747,17 @@ func handleWorktree(ctx context.Context, cmd *cobra.Command, branch, startPoint
733747
}
734748

735749
// Check if worktree already exists for this branch or directory name
736-
wt, err := git.FindWorktreeByBranchOrDir(ctx, branch)
750+
wt, err := git.FindWorktreeByBranchOrDir(ctx, branchName)
737751
if err != nil {
738752
return fmt.Errorf("failed to find worktree: %w", err)
739753
}
754+
if wt == nil && branchName != wtName {
755+
// Also try finding by worktree directory name
756+
wt, err = git.FindWorktreeByBranchOrDir(ctx, wtName)
757+
if err != nil {
758+
return fmt.Errorf("failed to find worktree: %w", err)
759+
}
760+
}
740761

741762
if wt != nil {
742763
// Worktree exists, print path to stdout
@@ -745,27 +766,27 @@ func handleWorktree(ctx context.Context, cmd *cobra.Command, branch, startPoint
745766
return nil
746767
}
747768

748-
// Get worktree path
749-
wtPath, err := git.WorktreePathFor(ctx, cfg.BaseDir, branch)
769+
// Get worktree path using the worktree name (not the branch name)
770+
wtPath, err := git.WorktreePathFor(ctx, cfg.BaseDir, wtName)
750771
if err != nil {
751772
return fmt.Errorf("failed to get worktree path: %w", err)
752773
}
753774

754775
// Check if branch exists
755-
exists, err := git.BranchExists(ctx, branch)
776+
exists, err := git.BranchExists(ctx, branchName)
756777
if err != nil {
757778
return fmt.Errorf("failed to check branch: %w", err)
758779
}
759780

760781
if exists {
761782
// Branch exists, create worktree with existing branch
762783
// start-point is ignored when using existing branch
763-
if err := git.AddWorktree(ctx, wtPath, branch, copyOpts); err != nil {
784+
if err := git.AddWorktree(ctx, wtPath, branchName, copyOpts); err != nil {
764785
return fmt.Errorf("failed to create worktree: %w", err)
765786
}
766787
} else {
767788
// Branch doesn't exist, create new branch and worktree
768-
if err := git.AddWorktreeWithNewBranch(ctx, wtPath, branch, startPoint, copyOpts); err != nil {
789+
if err := git.AddWorktreeWithNewBranch(ctx, wtPath, branchName, startPoint, copyOpts); err != nil {
769790
return fmt.Errorf("failed to create worktree with new branch: %w", err)
770791
}
771792
}

e2e/basic_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,125 @@ func TestE2E_CreateWorktree(t *testing.T) {
482482
t.Errorf("second worktree path = %q, want %q", wt2Path, expectedWt2Path)
483483
}
484484
})
485+
486+
t.Run("branch_flag_new_branch", func(t *testing.T) {
487+
t.Parallel()
488+
repo := testutil.NewTestRepo(t)
489+
repo.CreateFile("README.md", "# Test")
490+
repo.Commit("initial commit")
491+
492+
stdout, stderr, err := runGitWtStdout(t, binPath, repo.Root, "-b", "glasser/my-feature", "my-feature")
493+
if err != nil {
494+
t.Fatalf("git-wt -b failed: %v\nstderr: %s", err, stderr)
495+
}
496+
497+
wtPath := strings.TrimSpace(stdout)
498+
// Worktree directory should be named "my-feature", not "glasser/my-feature"
499+
expectedPath := filepath.Join(repo.Root, ".wt", "my-feature")
500+
if wtPath != expectedPath {
501+
t.Errorf("worktree path = %q, want %q", wtPath, expectedPath)
502+
}
503+
if _, err := os.Stat(wtPath); os.IsNotExist(err) {
504+
t.Fatalf("worktree directory was not created at %s", wtPath)
505+
}
506+
507+
// Verify the branch name is "glasser/my-feature"
508+
restore := repo.Chdir()
509+
defer restore()
510+
cmd := exec.Command("git", "branch", "--list", "glasser/my-feature")
511+
branchOut, err := cmd.Output()
512+
if err != nil {
513+
t.Fatalf("git branch --list failed: %v", err)
514+
}
515+
if !strings.Contains(string(branchOut), "glasser/my-feature") {
516+
t.Errorf("branch glasser/my-feature should exist, got: %s", branchOut)
517+
}
518+
})
519+
520+
t.Run("branch_flag_with_start_point", func(t *testing.T) {
521+
t.Parallel()
522+
repo := testutil.NewTestRepo(t)
523+
repo.CreateFile("README.md", "# Test")
524+
repo.Commit("initial commit")
525+
526+
repo.CreateFile("main-file.txt", "main content")
527+
repo.Commit("main commit")
528+
529+
repo.Git("branch", "old-base", "HEAD~1")
530+
531+
stdout, stderr, err := runGitWtStdout(t, binPath, repo.Root, "-b", "glasser/from-old", "from-old", "old-base")
532+
if err != nil {
533+
t.Fatalf("git-wt -b with start-point failed: %v\nstderr: %s", err, stderr)
534+
}
535+
536+
wtPath := strings.TrimSpace(stdout)
537+
expectedPath := filepath.Join(repo.Root, ".wt", "from-old")
538+
if wtPath != expectedPath {
539+
t.Errorf("worktree path = %q, want %q", wtPath, expectedPath)
540+
}
541+
542+
// Verify the worktree is based on old-base (should NOT have main-file.txt)
543+
mainFilePath := filepath.Join(wtPath, "main-file.txt")
544+
if _, err := os.Stat(mainFilePath); !os.IsNotExist(err) {
545+
t.Error("worktree should NOT have main-file.txt (should be based on old-base)")
546+
}
547+
})
548+
549+
t.Run("branch_flag_existing_branch", func(t *testing.T) {
550+
t.Parallel()
551+
repo := testutil.NewTestRepo(t)
552+
repo.CreateFile("README.md", "# Test")
553+
repo.Commit("initial commit")
554+
555+
// Create an existing branch
556+
repo.Git("branch", "glasser/existing")
557+
558+
stdout, stderr, err := runGitWtStdout(t, binPath, repo.Root, "-b", "glasser/existing", "existing")
559+
if err != nil {
560+
t.Fatalf("git-wt -b with existing branch failed: %v\nstderr: %s", err, stderr)
561+
}
562+
563+
wtPath := strings.TrimSpace(stdout)
564+
expectedPath := filepath.Join(repo.Root, ".wt", "existing")
565+
if wtPath != expectedPath {
566+
t.Errorf("worktree path = %q, want %q", wtPath, expectedPath)
567+
}
568+
if _, err := os.Stat(wtPath); os.IsNotExist(err) {
569+
t.Fatalf("worktree directory was not created at %s", wtPath)
570+
}
571+
})
572+
573+
t.Run("branch_flag_switch_by_branch", func(t *testing.T) {
574+
t.Parallel()
575+
repo := testutil.NewTestRepo(t)
576+
repo.CreateFile("README.md", "# Test")
577+
repo.Commit("initial commit")
578+
579+
// Create worktree with -b
580+
stdout1, stderr, err := runGitWtStdout(t, binPath, repo.Root, "-b", "glasser/feat", "feat")
581+
if err != nil {
582+
t.Fatalf("git-wt -b create failed: %v\nstderr: %s", err, stderr)
583+
}
584+
wtPath := strings.TrimSpace(stdout1)
585+
586+
// Switch to it by branch name
587+
stdout2, stderr, err := runGitWtStdout(t, binPath, repo.Root, "glasser/feat")
588+
if err != nil {
589+
t.Fatalf("git-wt switch by branch failed: %v\nstderr: %s", err, stderr)
590+
}
591+
if strings.TrimSpace(stdout2) != wtPath {
592+
t.Errorf("switch by branch returned %q, want %q", strings.TrimSpace(stdout2), wtPath)
593+
}
594+
595+
// Switch to it by directory name
596+
stdout3, stderr, err := runGitWtStdout(t, binPath, repo.Root, "feat")
597+
if err != nil {
598+
t.Fatalf("git-wt switch by dir failed: %v\nstderr: %s", err, stderr)
599+
}
600+
if strings.TrimSpace(stdout3) != wtPath {
601+
t.Errorf("switch by dir returned %q, want %q", strings.TrimSpace(stdout3), wtPath)
602+
}
603+
})
485604
}
486605

487606
func TestE2E_SwitchWorktree(t *testing.T) {

0 commit comments

Comments
 (0)