Skip to content

Commit fbb103a

Browse files
committed
Add worktree subcommand
Signed-off-by: Paulo Gomes <[email protected]>
1 parent 3733914 commit fbb103a

File tree

3 files changed

+199
-9
lines changed

3 files changed

+199
-9
lines changed

cmd/gogit/worktree.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/go-git/go-billy/v6/memfs"
10+
"github.com/go-git/go-billy/v6/osfs"
11+
"github.com/go-git/go-git/v6"
12+
"github.com/go-git/go-git/v6/plumbing"
13+
"github.com/go-git/go-git/v6/x/plumbing/worktree"
14+
xstorage "github.com/go-git/go-git/v6/x/storage"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
var (
19+
worktreeAddCommit string
20+
worktreeAddDetach bool
21+
)
22+
23+
func init() {
24+
worktreeAddCmd.Flags().StringVarP(&worktreeAddCommit, "commit", "c", "", "Commit hash to checkout in the new worktree")
25+
worktreeAddCmd.Flags().BoolVarP(&worktreeAddDetach, "detach", "d", false, "Create a detached HEAD worktree")
26+
worktreeCmd.AddCommand(worktreeAddCmd)
27+
worktreeCmd.AddCommand(worktreeListCmd)
28+
worktreeCmd.AddCommand(worktreeRemoveCmd)
29+
rootCmd.AddCommand(worktreeCmd)
30+
}
31+
32+
var worktreeCmd = &cobra.Command{
33+
Use: "worktree <command>",
34+
Short: "Manage repository worktrees",
35+
RunE: func(cmd *cobra.Command, _ []string) error {
36+
return cmd.Usage()
37+
},
38+
DisableFlagsInUseLine: true,
39+
}
40+
41+
var worktreeAddCmd = &cobra.Command{
42+
Use: "add <path>",
43+
Short: "Add a new linked worktree",
44+
Args: cobra.ExactArgs(1),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
path := args[0]
47+
name := filepath.Base(path)
48+
49+
r, err := git.PlainOpen(".")
50+
if err != nil {
51+
return fmt.Errorf("failed to open repository: %w", err)
52+
}
53+
54+
w, err := worktree.New(r.Storer)
55+
if err != nil {
56+
return fmt.Errorf("failed to create worktree manager: %w", err)
57+
}
58+
59+
wt := osfs.New(path)
60+
61+
var opts []worktree.Option
62+
if worktreeAddDetach {
63+
opts = append(opts, worktree.WithDetachedHead())
64+
}
65+
if worktreeAddCommit != "" {
66+
hash := plumbing.NewHash(worktreeAddCommit)
67+
if !hash.IsZero() {
68+
opts = append(opts, worktree.WithCommit(hash))
69+
}
70+
}
71+
72+
err = w.Add(wt, name, opts...)
73+
if err != nil {
74+
return fmt.Errorf("failed to add worktree: %w", err)
75+
}
76+
77+
fmt.Fprintf(cmd.OutOrStdout(), "Worktree '%s' created at '%s'\n", name, path)
78+
79+
return nil
80+
},
81+
DisableFlagsInUseLine: true,
82+
}
83+
84+
var worktreeListCmd = &cobra.Command{
85+
Use: "list",
86+
Short: "List all linked worktrees",
87+
Args: cobra.NoArgs,
88+
RunE: func(cmd *cobra.Command, _ []string) error {
89+
r, err := git.PlainOpen(".")
90+
if err != nil {
91+
return fmt.Errorf("failed to open repository: %w", err)
92+
}
93+
94+
w, err := worktree.New(r.Storer)
95+
if err != nil {
96+
return fmt.Errorf("failed to create worktree manager: %w", err)
97+
}
98+
99+
cwd, err := os.Getwd()
100+
if err != nil {
101+
return fmt.Errorf("failed to get current directory: %w", err)
102+
}
103+
104+
ref, err := r.Head()
105+
if err != nil {
106+
return fmt.Errorf("failed to get HEAD: %w", err)
107+
}
108+
109+
fmt.Fprintf(cmd.OutOrStdout(), "%-30s %s %s\n", cwd, ref.Hash().String()[:7], refName(ref))
110+
111+
worktrees, err := w.List()
112+
if err != nil {
113+
return fmt.Errorf("failed to list worktrees: %w", err)
114+
}
115+
116+
wts, ok := r.Storer.(xstorage.WorktreeStorer)
117+
if !ok {
118+
return errors.New("storer does not implement WorktreeStorer")
119+
}
120+
121+
commonDir := wts.Filesystem()
122+
for _, name := range worktrees {
123+
gitdirPath := filepath.Join(commonDir.Root(), "worktrees", name, "gitdir")
124+
gitdirData, err := os.ReadFile(gitdirPath)
125+
if err != nil || len(gitdirData) == 0 {
126+
continue
127+
}
128+
129+
wtPath := filepath.Dir(string(gitdirData[:len(gitdirData)-1]))
130+
wt := memfs.New()
131+
err = w.Init(wt, name)
132+
if err != nil {
133+
continue
134+
}
135+
136+
wtRepo, err := w.Open(wt)
137+
if err != nil {
138+
continue
139+
}
140+
141+
wtRef, err := wtRepo.Head()
142+
if err != nil {
143+
return fmt.Errorf("failed to get HEAD: %w", err)
144+
}
145+
146+
fmt.Fprintf(cmd.OutOrStdout(), "%-30s %s %s\n", wtPath, wtRef.Hash().String()[:7], refName(wtRef))
147+
}
148+
149+
return nil
150+
},
151+
DisableFlagsInUseLine: true,
152+
}
153+
154+
func refName(ref *plumbing.Reference) string {
155+
name := ref.Name()
156+
if name.IsBranch() {
157+
return fmt.Sprintf("[%s]", name.Short())
158+
}
159+
160+
return fmt.Sprintf("(detached %s)", string(ref.Name()))
161+
}
162+
163+
var worktreeRemoveCmd = &cobra.Command{
164+
Use: "remove <name>",
165+
Short: "Remove a linked worktree",
166+
Args: cobra.ExactArgs(1),
167+
RunE: func(cmd *cobra.Command, args []string) error {
168+
name := args[0]
169+
170+
r, err := git.PlainOpen(".")
171+
if err != nil {
172+
return fmt.Errorf("failed to open repository: %w", err)
173+
}
174+
175+
wt, err := worktree.New(r.Storer)
176+
if err != nil {
177+
return fmt.Errorf("failed to create worktree manager: %w", err)
178+
}
179+
180+
err = wt.Remove(name)
181+
if err != nil {
182+
return fmt.Errorf("failed to remove worktree: %w", err)
183+
}
184+
185+
fmt.Fprintf(cmd.OutOrStdout(), "Worktree '%s' removed\n", name)
186+
187+
return nil
188+
},
189+
DisableFlagsInUseLine: true,
190+
}

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ toolchain go1.25.4
66

77
require (
88
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd
9-
github.com/go-git/go-git-fixtures/v5 v5.1.1
10-
github.com/go-git/go-git/v6 v6.0.0-20251123162143-36fa81975a20
9+
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251217024734-e267099c9ed5
10+
github.com/go-git/go-git/v6 v6.0.0-20251218224324-ede584db67a4
1111
github.com/spf13/cobra v1.10.2
1212
golang.org/x/crypto v0.46.0
1313
golang.org/x/term v0.38.0
@@ -27,6 +27,6 @@ require (
2727
github.com/pjbgf/sha1cd v0.5.0 // indirect
2828
github.com/sergi/go-diff v1.4.0 // indirect
2929
github.com/spf13/pflag v1.0.9 // indirect
30-
golang.org/x/net v0.47.0 // indirect
30+
golang.org/x/net v0.48.0 // indirect
3131
golang.org/x/sys v0.39.0 // indirect
3232
)

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
2424
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
2525
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
2626
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
27-
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
28-
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
29-
github.com/go-git/go-git/v6 v6.0.0-20251123162143-36fa81975a20 h1:T1iYBFBFcB5Kjpq5BjCET47+Xc+blVhFDJ4cM7SJImw=
30-
github.com/go-git/go-git/v6 v6.0.0-20251123162143-36fa81975a20/go.mod h1:82JGB4xCU6W8toVHjEcv4KH4GSiB+MhjFTCGQxPOLdM=
27+
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251217024734-e267099c9ed5 h1:3PN91izCLX3c2mMqXDHpF9ift/yVticGUTPu/eGfX0M=
28+
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251217024734-e267099c9ed5/go.mod h1:0mL/38Gupl98cI/OAAF+inDRhyKM9ic5FLuIY+zCTCE=
29+
github.com/go-git/go-git/v6 v6.0.0-20251218224324-ede584db67a4 h1:M8aO2/N4F0dLkFRhEK+SN/kdxEQr0tD5UlCMiXxlFqc=
30+
github.com/go-git/go-git/v6 v6.0.0-20251218224324-ede584db67a4/go.mod h1:JpR+9QlEMKtBgshm5dybpsz1cjmXU8u7ZnMnI3glMIo=
3131
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
3232
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
3333
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -57,8 +57,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
5757
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
5858
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
5959
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
60-
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
61-
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
60+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
61+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
6262
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
6363
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
6464
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=

0 commit comments

Comments
 (0)