-
Notifications
You must be signed in to change notification settings - Fork 4
Add worktree subcommand
#32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,190 @@ | ||||||||||||||||||||||||||||||||||
| package main | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| "github.com/go-git/go-billy/v6/memfs" | ||||||||||||||||||||||||||||||||||
| "github.com/go-git/go-billy/v6/osfs" | ||||||||||||||||||||||||||||||||||
| "github.com/go-git/go-git/v6" | ||||||||||||||||||||||||||||||||||
| "github.com/go-git/go-git/v6/plumbing" | ||||||||||||||||||||||||||||||||||
| "github.com/go-git/go-git/v6/x/plumbing/worktree" | ||||||||||||||||||||||||||||||||||
| xstorage "github.com/go-git/go-git/v6/x/storage" | ||||||||||||||||||||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||||
| worktreeAddCommit string | ||||||||||||||||||||||||||||||||||
| worktreeAddDetach bool | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||||||||||||||
| worktreeAddCmd.Flags().StringVarP(&worktreeAddCommit, "commit", "c", "", "Commit hash to checkout in the new worktree") | ||||||||||||||||||||||||||||||||||
| worktreeAddCmd.Flags().BoolVarP(&worktreeAddDetach, "detach", "d", false, "Create a detached HEAD worktree") | ||||||||||||||||||||||||||||||||||
| worktreeCmd.AddCommand(worktreeAddCmd) | ||||||||||||||||||||||||||||||||||
| worktreeCmd.AddCommand(worktreeListCmd) | ||||||||||||||||||||||||||||||||||
| worktreeCmd.AddCommand(worktreeRemoveCmd) | ||||||||||||||||||||||||||||||||||
| rootCmd.AddCommand(worktreeCmd) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var worktreeCmd = &cobra.Command{ | ||||||||||||||||||||||||||||||||||
| Use: "worktree <command>", | ||||||||||||||||||||||||||||||||||
| Short: "Manage repository worktrees", | ||||||||||||||||||||||||||||||||||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||||||||||||||||||||||||||||||||||
| return cmd.Usage() | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| DisableFlagsInUseLine: true, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var worktreeAddCmd = &cobra.Command{ | ||||||||||||||||||||||||||||||||||
| Use: "add <path>", | ||||||||||||||||||||||||||||||||||
| Short: "Add a new linked worktree", | ||||||||||||||||||||||||||||||||||
| Args: cobra.ExactArgs(1), | ||||||||||||||||||||||||||||||||||
| RunE: func(cmd *cobra.Command, args []string) error { | ||||||||||||||||||||||||||||||||||
| path := args[0] | ||||||||||||||||||||||||||||||||||
| name := filepath.Base(path) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| r, err := git.PlainOpen(".") | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to open repository: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| w, err := worktree.New(r.Storer) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to create worktree manager: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| wt := osfs.New(path) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var opts []worktree.Option | ||||||||||||||||||||||||||||||||||
| if worktreeAddDetach { | ||||||||||||||||||||||||||||||||||
| opts = append(opts, worktree.WithDetachedHead()) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if worktreeAddCommit != "" { | ||||||||||||||||||||||||||||||||||
| hash := plumbing.NewHash(worktreeAddCommit) | ||||||||||||||||||||||||||||||||||
| if !hash.IsZero() { | ||||||||||||||||||||||||||||||||||
| opts = append(opts, worktree.WithCommit(hash)) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| err = w.Add(wt, name, opts...) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to add worktree: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| fmt.Fprintf(cmd.OutOrStdout(), "Worktree '%s' created at '%s'\n", name, path) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| DisableFlagsInUseLine: true, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| var worktreeListCmd = &cobra.Command{ | ||||||||||||||||||||||||||||||||||
| Use: "list", | ||||||||||||||||||||||||||||||||||
| Short: "List all linked worktrees", | ||||||||||||||||||||||||||||||||||
| Args: cobra.NoArgs, | ||||||||||||||||||||||||||||||||||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||||||||||||||||||||||||||||||||||
| r, err := git.PlainOpen(".") | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to open repository: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| w, err := worktree.New(r.Storer) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to create worktree manager: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| cwd, err := os.Getwd() | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to get current directory: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| ref, err := r.Head() | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to get HEAD: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| fmt.Fprintf(cmd.OutOrStdout(), "%-30s %s %s\n", cwd, ref.Hash().String()[:7], refName(ref)) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| worktrees, err := w.List() | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to list worktrees: %w", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| wts, ok := r.Storer.(xstorage.WorktreeStorer) | ||||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||||
| return errors.New("storer does not implement WorktreeStorer") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| commonDir := wts.Filesystem() | ||||||||||||||||||||||||||||||||||
| for _, name := range worktrees { | ||||||||||||||||||||||||||||||||||
| gitdirPath := filepath.Join(commonDir.Root(), "worktrees", name, "gitdir") | ||||||||||||||||||||||||||||||||||
| gitdirData, err := os.ReadFile(gitdirPath) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| wtPath := filepath.Dir(string(gitdirData[:len(gitdirData)-1])) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| wt := memfs.New() | ||||||||||||||||||||||||||||||||||
| err = w.Init(wt, name) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| r, err := w.Open(wt) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| ref, err := r.Head() | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+141
|
||||||||||||||||||||||||||||||||||
| r, err := w.Open(wt) | |
| if err != nil { | |
| continue | |
| } | |
| ref, err := r.Head() | |
| wtRepo, err := w.Open(wt) | |
| if err != nil { | |
| continue | |
| } | |
| ref, err := wtRepo.Head() |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent error handling in the loop. While errors from os.ReadFile, w.Init, and w.Open are silently ignored with 'continue', an error from r.Head() returns immediately with an error. This inconsistency could make the command fail unexpectedly instead of just skipping problematic worktrees like the earlier error cases.
| return fmt.Errorf("failed to get HEAD: %w", err) | |
| continue |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable shadowing issue: the variable 'ref' is being redeclared here, shadowing the HEAD reference 'ref' from line 104. This makes the outer 'ref' variable inaccessible within this scope and could lead to confusion.
| ref, err := r.Head() | |
| if err != nil { | |
| return fmt.Errorf("failed to get HEAD: %w", err) | |
| } | |
| fmt.Fprintf(cmd.OutOrStdout(), "%-30s %s %s\n", wtPath, ref.Hash().String()[:7], refName(ref)) | |
| wtRef, err := r.Head() | |
| if err != nil { | |
| return fmt.Errorf("failed to get HEAD: %w", err) | |
| } | |
| fmt.Fprintf(cmd.OutOrStdout(), "%-30s %s %s\n", wtPath, wtRef.Hash().String()[:7], refName(wtRef)) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fmt.Sprint call is unnecessarily verbose. Using fmt.Sprintf with proper formatting would be more idiomatic and clearer.
| return fmt.Sprint("[", name.Short(), "]") | |
| } | |
| return fmt.Sprint("(detached ", string(ref.Name()), ")") | |
| return fmt.Sprintf("[%s]", name.Short()) | |
| } | |
| return fmt.Sprintf("(detached %s)", string(ref.Name())) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fmt.Sprint call is unnecessarily verbose. Using fmt.Sprintf with proper formatting would be more idiomatic and clearer.
| return fmt.Sprint("[", name.Short(), "]") | |
| } | |
| return fmt.Sprint("(detached ", string(ref.Name()), ")") | |
| return fmt.Sprintf("[%s]", name.Short()) | |
| } | |
| return fmt.Sprintf("(detached %s)", ref.Name()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No validation is performed on the commit hash provided by the user. If the user provides an invalid hash string, plumbing.NewHash() will silently return a zero hash, and the WithCommit option won't be added. This could lead to unexpected behavior where the user thinks they're checking out a specific commit but they're not.