返回 DeepSeek-Reasonix
worktree_test.go
根目录 / internal / worktree / worktree_test.go
1 package worktree
2
3 import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11 "time"
12 )
13
14 func requireGit(t *testing.T) {
15 t.Helper()
16 if _, err := exec.LookPath("git"); err != nil {
17 t.Skip("git is unavailable")
18 }
19 }
20
21 func initRepo(t *testing.T) string {
22 t.Helper()
23 repo := t.TempDir()
24 git := func(args ...string) string {
25 t.Helper()
26 cmd := exec.Command("git", append([]string{"-C", repo}, args...)...)
27 cmd.Env = append(os.Environ(),
28 "GIT_CONFIG_NOSYSTEM=1",
29 "GIT_AUTHOR_NAME=Reasonix Test", "GIT_AUTHOR_EMAIL=reasonix@example.invalid",
30 "GIT_COMMITTER_NAME=Reasonix Test", "GIT_COMMITTER_EMAIL=reasonix@example.invalid")
31 out, err := cmd.CombinedOutput()
32 if err != nil {
33 t.Fatalf("git %v: %v\n%s", args, err, out)
34 }
35 return strings.TrimSpace(string(out))
36 }
37 git("init")
38 if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("base\n"), 0o644); err != nil {
39 t.Fatal(err)
40 }
41 git("add", "README.md")
42 git("commit", "-m", "initial")
43 return repo
44 }
45
46 func TestCreateManagedWorktreeFromRepositoryFolder(t *testing.T) {
47 requireGit(t)
48 repo := initRepo(t)
49 managed := t.TempDir()
50 result, err := Create(context.Background(), repo, managed)
51 if err != nil {
52 t.Fatal(err)
53 }
54 wantSource, err := filepath.EvalSymlinks(repo)
55 if err != nil {
56 t.Fatal(err)
57 }
58 if result.SourceRoot != wantSource {
59 t.Fatalf("source root = %q, want %q", result.SourceRoot, wantSource)
60 }
61 if result.WorkspaceRoot != result.WorktreeRoot {
62 t.Fatalf("workspace root = %q, worktree root = %q", result.WorkspaceRoot, result.WorktreeRoot)
63 }
64 if !strings.HasPrefix(result.Branch, "reasonix/delivery-") {
65 t.Fatalf("branch = %q", result.Branch)
66 }
67 if _, err := os.Stat(filepath.Join(result.WorktreeRoot, "README.md")); err != nil {
68 t.Fatalf("created worktree missing committed file: %v", err)
69 }
70 if _, err := os.Stat(filepath.Join(result.WorktreeRoot, ".reasonix-source")); !os.IsNotExist(err) {
71 t.Fatalf("worktree must not contain a Reasonix marker, stat err = %v", err)
72 }
73 branchCmd := exec.Command("git", "-C", result.WorktreeRoot, "branch", "--show-current")
74 branchOut, err := branchCmd.Output()
75 if err != nil || strings.TrimSpace(string(branchOut)) != result.Branch {
76 t.Fatalf("worktree branch = %q err=%v, want %q", strings.TrimSpace(string(branchOut)), err, result.Branch)
77 }
78 }
79
80 func TestCreatePreservesSelectedRepositorySubdirectory(t *testing.T) {
81 requireGit(t)
82 repo := initRepo(t)
83 subdir := filepath.Join(repo, "packages", "app")
84 if err := os.MkdirAll(subdir, 0o755); err != nil {
85 t.Fatal(err)
86 }
87 if err := os.WriteFile(filepath.Join(subdir, "app.txt"), []byte("app\n"), 0o644); err != nil {
88 t.Fatal(err)
89 }
90 cmd := exec.Command("git", "-C", repo, "add", ".")
91 if out, err := cmd.CombinedOutput(); err != nil {
92 t.Fatalf("git add: %v %s", err, out)
93 }
94 cmd = exec.Command("git", "-C", repo, "-c", "user.name=Reasonix Test", "-c", "user.email=reasonix@example.invalid", "commit", "-m", "subdir")
95 if out, err := cmd.CombinedOutput(); err != nil {
96 t.Fatalf("git commit: %v %s", err, out)
97 }
98 result, err := Create(context.Background(), subdir, t.TempDir())
99 if err != nil {
100 t.Fatal(err)
101 }
102 want := filepath.Join(result.WorktreeRoot, "packages", "app")
103 if result.WorkspaceRoot != want {
104 t.Fatalf("workspace root = %q, want %q", result.WorkspaceRoot, want)
105 }
106 }
107
108 func TestInspectRejectsUncommittedSelectedSubdirectoryWithoutGitMutation(t *testing.T) {
109 requireGit(t)
110 repo := initRepo(t)
111 untracked := filepath.Join(repo, "untracked-project")
112 if err := os.MkdirAll(untracked, 0o755); err != nil {
113 t.Fatal(err)
114 }
115 before, _, err := runGit(context.Background(), repo, "for-each-ref", "--format=%(refname)", "refs/heads/reasonix/delivery-")
116 if err != nil {
117 t.Fatal(err)
118 }
119 got := Inspect(context.Background(), untracked)
120 if got.Available || !strings.Contains(got.Reason, "committed HEAD") {
121 t.Fatalf("availability = %+v", got)
122 }
123 after, _, err := runGit(context.Background(), repo, "for-each-ref", "--format=%(refname)", "refs/heads/reasonix/delivery-")
124 if err != nil {
125 t.Fatal(err)
126 }
127 if after != before {
128 t.Fatalf("availability probe mutated Git refs: before=%q after=%q", before, after)
129 }
130 }
131
132 func TestCreateFromExistingLinkedWorktree(t *testing.T) {
133 requireGit(t)
134 repo := initRepo(t)
135 linked := filepath.Join(t.TempDir(), "already-linked-worktree")
136 cmd := exec.Command("git", "-C", repo, "worktree", "add", "-b", "test/existing-linked", linked, "HEAD")
137 if out, err := cmd.CombinedOutput(); err != nil {
138 t.Fatalf("git worktree add: %v %s", err, out)
139 }
140
141 result, err := Create(context.Background(), linked, t.TempDir())
142 if err != nil {
143 t.Fatal(err)
144 }
145 wantSource, err := filepath.EvalSymlinks(linked)
146 if err != nil {
147 t.Fatal(err)
148 }
149 if result.SourceRoot != wantSource {
150 t.Fatalf("source root = %q, want linked worktree %q", result.SourceRoot, wantSource)
151 }
152 if result.WorkspaceRoot != result.WorktreeRoot {
153 t.Fatalf("workspace root = %q, worktree root = %q", result.WorkspaceRoot, result.WorktreeRoot)
154 }
155 if result.WorktreeRoot == wantSource {
156 t.Fatal("managed worktree reused the already-open linked worktree")
157 }
158 cmd = exec.Command("git", "-C", linked, "branch", "--show-current")
159 out, err := cmd.Output()
160 if err != nil || strings.TrimSpace(string(out)) != "test/existing-linked" {
161 t.Fatalf("source linked-worktree branch changed: %q err=%v", strings.TrimSpace(string(out)), err)
162 }
163 }
164
165 func TestCreateDoesNotCopyOrChangeDirtySource(t *testing.T) {
166 requireGit(t)
167 repo := initRepo(t)
168 dirtyPath := filepath.Join(repo, "README.md")
169 if err := os.WriteFile(dirtyPath, []byte("uncommitted\n"), 0o644); err != nil {
170 t.Fatal(err)
171 }
172 result, err := Create(context.Background(), repo, t.TempDir())
173 if err != nil {
174 t.Fatal(err)
175 }
176 if !result.SourceDirty {
177 t.Fatal("dirty source was not reported")
178 }
179 got, err := os.ReadFile(filepath.Join(result.WorktreeRoot, "README.md"))
180 if err != nil {
181 t.Fatal(err)
182 }
183 // Git for Windows may materialize the committed LF as CRLF according to
184 // core.autocrlf. Compare the file's semantic content so this assertion only
185 // detects the behavior under test: copying the dirty source value.
186 if strings.TrimSpace(string(got)) != "base" {
187 t.Fatalf("worktree copied uncommitted content: %q", got)
188 }
189 source, _ := os.ReadFile(dirtyPath)
190 if string(source) != "uncommitted\n" {
191 t.Fatalf("source was modified: %q", source)
192 }
193 }
194
195 func TestInspectRejectsNonRepositoryAndUnbornRepository(t *testing.T) {
196 requireGit(t)
197 if got := Inspect(context.Background(), t.TempDir()); got.Available || !strings.Contains(got.Reason, "not inside a Git repository") {
198 t.Fatalf("non-repo availability = %+v", got)
199 }
200 unborn := t.TempDir()
201 if out, err := exec.Command("git", "-C", unborn, "init").CombinedOutput(); err != nil {
202 t.Fatalf("git init: %v %s", err, out)
203 }
204 if got := Inspect(context.Background(), unborn); got.Available || !strings.Contains(got.Reason, "initial commit") {
205 t.Fatalf("unborn availability = %+v", got)
206 }
207 }
208
209 func TestInspectWithoutGitExplainsSafeFallback(t *testing.T) {
210 if runtime.GOOS == "windows" {
211 t.Setenv("PATH", t.TempDir())
212 } else {
213 t.Setenv("PATH", t.TempDir())
214 }
215 got := Inspect(context.Background(), t.TempDir())
216 if got.Available || !strings.Contains(got.Reason, "Git is not installed") || !strings.Contains(got.Reason, "serialize writes") {
217 t.Fatalf("no-Git availability = %+v", got)
218 }
219 }
220
221 func TestManagedPathBoundary(t *testing.T) {
222 managed := filepath.Join(t.TempDir(), "worktrees")
223 inside := filepath.Join(managed, "repo", "id")
224 sibling := managed + "-backup"
225 if !IsManagedPath(inside, managed) {
226 t.Fatal("inside path not recognized")
227 }
228 if IsManagedPath(managed, managed) || IsManagedPath(sibling, managed) {
229 t.Fatal("managed root or prefix sibling was incorrectly recognized")
230 }
231 }
232
233 func TestSafePathComponentHandlesWindowsNames(t *testing.T) {
234 for input, want := range map[string]string{
235 "repo:demo": "repo-demo",
236 "CON": "_CON",
237 "lpt1.txt": "_lpt1.txt",
238 "a<b>c": "a-b-c",
239 } {
240 if got := safePathComponent(input); got != want {
241 t.Errorf("safePathComponent(%q) = %q, want %q", input, got, want)
242 }
243 }
244 }
245
246 // Argument construction — including the Windows-only core.longpaths override —
247 // now lives in internal/gitcmd and is covered by that package's tests.
248
249 func TestGitWorktreeAddUsesExtendedTimeout(t *testing.T) {
250 if got := gitTimeout([]string{"status", "--porcelain=v1"}); got != gitProbeTimeout {
251 t.Fatalf("status timeout = %v, want %v", got, gitProbeTimeout)
252 }
253 got := gitTimeout([]string{"worktree", "add", "-b", "branch", "destination", "HEAD"})
254 if got != gitWorktreeAddTimeout {
255 t.Fatalf("worktree add timeout = %v, want %v", got, gitWorktreeAddTimeout)
256 }
257 if got < 2*time.Minute || got <= gitProbeTimeout {
258 t.Fatalf("worktree add timeout = %v, want a checkout-safe timeout longer than probes", got)
259 }
260 }
261
262 func TestCreateSupportsPathsWithSpaces(t *testing.T) {
263 requireGit(t)
264 parent := t.TempDir()
265 repo := filepath.Join(parent, "repo with spaces")
266 if err := os.MkdirAll(repo, 0o755); err != nil {
267 t.Fatal(err)
268 }
269 git := func(args ...string) {
270 cmd := exec.Command("git", append([]string{"-C", repo}, args...)...)
271 cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=Reasonix Test", "GIT_AUTHOR_EMAIL=reasonix@example.invalid", "GIT_COMMITTER_NAME=Reasonix Test", "GIT_COMMITTER_EMAIL=reasonix@example.invalid")
272 if out, err := cmd.CombinedOutput(); err != nil {
273 t.Fatalf("git %v: %v %s", args, err, out)
274 }
275 }
276 git("init")
277 if err := os.WriteFile(filepath.Join(repo, "file.txt"), []byte("x"), 0o644); err != nil {
278 t.Fatal(err)
279 }
280 git("add", ".")
281 git("commit", "-m", "initial")
282 result, err := Create(context.Background(), repo, filepath.Join(parent, "managed worktrees"))
283 if err != nil {
284 t.Fatal(err)
285 }
286 if runtime.GOOS != "windows" && !strings.Contains(result.WorktreeRoot, "managed worktrees") {
287 t.Fatalf("unexpected worktree root %q", result.WorktreeRoot)
288 }
289 }
290
290 lines GO