diff --git a/cmd/worktree.go b/cmd/worktree.go index e213cb2..87718bb 100644 --- a/cmd/worktree.go +++ b/cmd/worktree.go @@ -15,6 +15,7 @@ import ( var ( wtBaseBranch string + wtDupBase string wtForce bool wtDeleteBranch bool wtDryRun bool @@ -40,20 +41,31 @@ bare repository (.bare) + worktree pattern. } var wtAddCmd = &cobra.Command{ - Use: "add [name...]", + Use: "add [name...]", Short: "Create new worktrees with new branches", Long: `Create one or more worktrees with new branches. The branch name will be the same as the worktree directory name. +When no name is given but -b is set, the worktree name is derived from +the base branch (useful for checking out an existing branch). Examples: gmc wt add feature-login # Create one worktree gmc wt add feat-a feat-b feat-c # Create multiple worktrees gmc wt add feature-login -b main # Create based on main branch gmc wt add feature-login --sync # Sync base branch before add - gmc wt add hotfix-bug123 -b release`, - Args: cobra.MinimumNArgs(1), + gmc wt add hotfix-bug123 -b release + gmc wt add -b feat/existing-branch # Name derived from -b`, + Args: func(_ *cobra.Command, args []string) error { + if len(args) == 0 && strings.TrimSpace(wtBaseBranch) == "" { + return errors.New("requires at least 1 arg or -b/--base flag") + } + return nil + }, RunE: func(_ *cobra.Command, args []string) error { + if len(args) == 0 { + args = []string{wtBaseBranch} + } wtClient := newWorktreeClient() return runWorktreeAdd(wtClient, args) }, @@ -210,13 +222,14 @@ func init() { wtCloneCmd.Flags().StringVar(&wtProjectName, "name", "", "Custom project directory name") // Flags for dup command - wtDupCmd.Flags().StringVarP(&wtBaseBranch, "base", "b", "main", "Base branch to create from") + wtDupCmd.Flags().StringVarP(&wtDupBase, "base", "b", "main", "Base branch to create from") // Flags for prune command wtPruneCmd.Flags().StringVarP(&wtPruneBase, "base", "b", "", "Base branch to check merge status against") wtPruneCmd.Flags().BoolVarP(&wtPruneForce, "force", "f", false, "Force removal even if worktree is dirty") wtPruneCmd.Flags().BoolVar(&wtPruneDryRun, "dry-run", false, "Preview what would be removed without making changes") - wtPruneCmd.Flags().BoolVar(&wtPrunePRAware, "pr-aware", false, "Check GitHub PR state before pruning (requires gh CLI)") + wtPruneCmd.Flags().BoolVar(&wtPrunePRAware, "pr-aware", false, + "Check GitHub PR state before pruning (requires gh CLI)") // Flags for pr-review command wtPrReviewCmd.Flags().StringVarP(&prRemote, "remote", "r", "", @@ -541,7 +554,7 @@ func printWorktreeJSON(wtClient *worktree.Client, worktrees []worktree.Info) err func runWorktreeDup(wtClient *worktree.Client, args []string) error { opts := worktree.DupOptions{ - BaseBranch: wtBaseBranch, + BaseBranch: wtDupBase, Count: 2, } diff --git a/cmd/worktree_test.go b/cmd/worktree_test.go index 4818e2d..3ad6f54 100644 --- a/cmd/worktree_test.go +++ b/cmd/worktree_test.go @@ -49,7 +49,8 @@ func TestRunWorktreeDefault_ShowsWorktreesInNonBareRepo(t *testing.T) { func TestWtHookRemoveCmd_RejectsTrailingCharactersInIndex(t *testing.T) { repoDir := initCmdTestRepo(t) - require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".git", "gmc-share.yml"), []byte("hooks:\n - cmd: echo ok\n"), 0o644)) + cfgPath := filepath.Join(repoDir, ".git", "gmc-share.yml") + require.NoError(t, os.WriteFile(cfgPath, []byte("hooks:\n - cmd: echo ok\n"), 0o644)) oldCwd, err := os.Getwd() require.NoError(t, err) @@ -99,9 +100,7 @@ func runGitCmd(t *testing.T, dir string, args ...string) string { return string(output) } -var execCommand = func(name string, args ...string) *exec.Cmd { - return exec.Command(name, args...) -} +var execCommand = exec.Command func TestRemoveAll_SkipsProtected(t *testing.T) { repoDir := initCmdTestRepo(t) diff --git a/internal/worktree/prune.go b/internal/worktree/prune.go index 71854ba..4bdfedc 100644 --- a/internal/worktree/prune.go +++ b/internal/worktree/prune.go @@ -49,7 +49,7 @@ var ghRunFunc = ghRunDefault func ghRunDefault(repoDir string, args ...string) ([]byte, error) { if _, err := exec.LookPath("gh"); err != nil { - return nil, fmt.Errorf("gh CLI not found: install from https://cli.github.com") + return nil, errors.New("gh CLI not found: install from https://cli.github.com") } cmd := exec.Command("gh", args...) cmd.Dir = repoDir @@ -167,7 +167,9 @@ func ghPRStates(repoDir string) (map[string]ghPRInfo, error) { return m, nil } -func (c *Client) prunePRAware(opts PruneOptions, candidates []pruneCandidate, repoDir string, result PruneResult) (PruneResult, error) { +func (c *Client) prunePRAware( + opts PruneOptions, candidates []pruneCandidate, repoDir string, result PruneResult, +) (PruneResult, error) { prMap, err := ghPRStates(repoDir) if err != nil { return result, err @@ -227,7 +229,9 @@ func (c *Client) prunePRAware(opts PruneOptions, candidates []pruneCandidate, re return result, nil } -func (c *Client) pruneClassic(opts PruneOptions, candidates []pruneCandidate, root, baseBranch, repoDir string, result PruneResult) (PruneResult, error) { +func (c *Client) pruneClassic( + opts PruneOptions, candidates []pruneCandidate, root, baseBranch, repoDir string, result PruneResult, +) (PruneResult, error) { var prunedAny bool for _, cand := range candidates { diff --git a/internal/worktree/resource.go b/internal/worktree/resource.go index 0babb87..2f5c90c 100644 --- a/internal/worktree/resource.go +++ b/internal/worktree/resource.go @@ -473,7 +473,9 @@ func (c *Client) NormalizeSharedResourcePath(path string) (string, error) { return trimmed, nil } -func (c *Client) resolveSharedPaths(repoRoot, targetRoot string, res SharedResource) (srcPath string, targetPath string, skip bool, err error) { +func (c *Client) resolveSharedPaths( + repoRoot, targetRoot string, res SharedResource, +) (srcPath string, targetPath string, skip bool, err error) { targetPath, err = sanitizeTargetRelativePath(res.Path) if err != nil { return "", "", false, err