-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathworktree.go
More file actions
181 lines (162 loc) · 5.44 KB
/
worktree.go
File metadata and controls
181 lines (162 loc) · 5.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package discovery
import (
"os"
"os/exec"
"path/filepath"
"strings"
)
// Worktree represents a git worktree associated with a project.
type Worktree struct {
Name string `json:"name"` // directory name (e.g., "fancy-name")
Path string `json:"path"` // absolute filesystem path
Branch string `json:"branch"` // checked-out branch
IsMain bool `json:"isMain"` // true for the original clone
}
// DiscoverWorktrees returns all git worktrees for the project at projectPath.
// The main worktree is always first. Returns nil if the project is not a git repo
// or has no additional worktrees.
// E-PENPAL-WORKTREE-DISCOVERY: runs git worktree list --porcelain and parses the output.
func DiscoverWorktrees(projectPath string) []Worktree {
cmd := exec.Command("git", "-C", projectPath, "worktree", "list", "--porcelain")
out, err := cmd.Output()
if err != nil {
return nil
}
return parseWorktreeList(projectPath, string(out))
}
// parseWorktreeList parses `git worktree list --porcelain` output into Worktree structs.
// E-PENPAL-WORKTREE-DISCOVERY: parses porcelain output, strips refs/heads/ prefix, sets IsMain flag.
func parseWorktreeList(projectPath string, output string) []Worktree {
if output == "" {
return nil
}
var worktrees []Worktree
var current *Worktree
mainPath := filepath.Clean(projectPath)
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
if current != nil {
worktrees = append(worktrees, *current)
current = nil
}
continue
}
if strings.HasPrefix(line, "worktree ") {
wtPath := strings.TrimPrefix(line, "worktree ")
cleanPath := filepath.Clean(wtPath)
current = &Worktree{
Path: cleanPath,
Name: filepath.Base(cleanPath),
IsMain: cleanPath == mainPath,
}
} else if strings.HasPrefix(line, "branch ") {
if current != nil {
branch := strings.TrimPrefix(line, "branch ")
// Strip refs/heads/ prefix
branch = strings.TrimPrefix(branch, "refs/heads/")
current.Branch = branch
}
} else if line == "bare" {
// Skip bare repos
current = nil
}
}
// Handle last entry without trailing newline
if current != nil {
worktrees = append(worktrees, *current)
}
// Only return if there are additional worktrees beyond main
if len(worktrees) <= 1 {
return nil
}
return worktrees
}
// ResolveWorktree finds the worktree that contains the given absolute path.
// Returns the worktree name and the main project path, or empty strings if
// the path doesn't belong to any worktree.
func ResolveWorktree(projectPath string, absPath string) (worktreeName string, mainProjectPath string) {
absPath = filepath.Clean(absPath)
// First check if this path is inside the main project
mainPath := filepath.Clean(projectPath)
if strings.HasPrefix(absPath, mainPath+"/") || absPath == mainPath {
// Check if it's inside a worktree subdirectory
worktrees := DiscoverWorktrees(projectPath)
for _, wt := range worktrees {
if !wt.IsMain && (strings.HasPrefix(absPath, wt.Path+"/") || absPath == wt.Path) {
return wt.Name, mainPath
}
}
return "", mainPath
}
return "", ""
}
// FindMainWorktree returns the path to the main worktree for a given path
// that might be inside a worktree. It reads the .git file to find the
// gitdir and traces back to the main worktree.
func FindMainWorktree(path string) string {
cmd := exec.Command("git", "-C", path, "rev-parse", "--git-common-dir")
out, err := cmd.Output()
if err != nil {
return ""
}
commonDir := strings.TrimSpace(string(out))
if commonDir == "" || commonDir == "." {
return ""
}
// commonDir is the .git directory of the main worktree
// If it's relative, resolve it relative to the path
if !filepath.IsAbs(commonDir) {
// Get the actual git dir for this worktree first
cmd2 := exec.Command("git", "-C", path, "rev-parse", "--git-dir")
out2, err := cmd2.Output()
if err != nil {
return ""
}
gitDir := strings.TrimSpace(string(out2))
if !filepath.IsAbs(gitDir) {
gitDir = filepath.Join(path, gitDir)
}
commonDir = filepath.Join(gitDir, commonDir)
}
commonDir = filepath.Clean(commonDir)
// The main worktree is the parent of the .git directory
if filepath.Base(commonDir) == ".git" {
return filepath.Dir(commonDir)
}
return ""
}
// GitCommonDir returns the shared .git directory for the repository at
// projectPath (e.g. "/repo/.git"). Returns "" for non-git directories.
// Works for both main worktrees and linked worktrees.
// E-PENPAL-WORKTREE-WATCH: resolves the git common dir for fs watching.
func GitCommonDir(projectPath string) string {
cmd := exec.Command("git", "-C", projectPath, "rev-parse", "--git-common-dir")
out, err := cmd.Output()
if err != nil {
return ""
}
commonDir := strings.TrimSpace(string(out))
if commonDir == "" || commonDir == "." {
return ""
}
// --git-common-dir output is relative to the -C directory
if !filepath.IsAbs(commonDir) {
commonDir = filepath.Join(projectPath, commonDir)
}
return filepath.Clean(commonDir)
}
// GitWorktreesDir returns the path to the .git/worktrees/ directory for the
// repository that projectPath belongs to, or "" if it doesn't exist.
// E-PENPAL-WORKTREE-WATCH: resolves the worktrees metadata directory for fs watching.
func GitWorktreesDir(projectPath string) string {
commonDir := GitCommonDir(projectPath)
if commonDir == "" {
return ""
}
wtDir := filepath.Join(commonDir, "worktrees")
if info, err := os.Stat(wtDir); err == nil && info.IsDir() {
return wtDir
}
return ""
}