harden trail worktree checkout edge cases · Entire
harden trail worktree checkout edge cases
93556b5→main·
pfleidi·1w ago·3 files·+97 added/-24 removed
Address final-review findings: fail early with a clear error when the trail's branch is already checked out in a non-managed worktree (previously a raw git fatal after the ignore rule was written), confine .worktreeinclude copies to the new worktree via os.OpenRoot so tracked symlinked directories cannot redirect writes, bound the branch fetch with the same 2-minute timeout as other remote operations, and document the extra --force meaning in the flag help.
Sessions
01KX4QZZKB1JWMA2FYET5ZK1CCView transcript
[?
Add trail checkout --worktree SupportClaude Code·1 step](/content/gh/entireio/cli/session/c5215fe7-28db-4a83-9d2e-482217df292d#timeline-01KX4QZZKB1JWMA2FYET5ZK1CC/index.html)
Changes
3
cmd/entire/cli
Mtrail_checkout_worktree.go+47/-23
Mtrail_checkout_worktree_test.go+49
Mtrail_cmd.go+1/-1
10 unmodified lines
11
12
13
14
15
16
17
158 unmodified lines
176
177
178
178
179
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
73 unmodified lines
269
270
271
259
272
273
274
275
276
277
278
13 unmodified lines
292
293
294
279
295
296
297
282
298
299
300
301
302
303
288
289
304
305
306
307
292
293
308
309
310
295
296
297
311
312
313
314
315
316
6 unmodified lines
323
324
325
310
326
327
328
312
329
330
331
332
333
334
335
336
35 unmodified lines
372
373
374
354
355
356
375
376
377
378
379
380
359
381
382
383
384
385
386
387
366
388
389
390
391
8 unmodified lines
400
401
402
381
382
383
403
404
405
406
386
407
408
409
410
37 unmodified lines
448
449
450
451
452
453
454
455
456
10 unmodified lines
"os/exec"
"path/filepath"
"strings"
"time"
huh "charm.land/huh/v2"
"github.com/go-git/go-git/v6/plumbing/format/gitignore"
158 unmodified lines
if err != nil {
return err
}
for _, rel := range matchIncludePatterns(patterns, ignored) {
if err := copyIncludedFile(filepath.Join(root, rel), filepath.Join(dest, rel)); err != nil {
...
}
}
return clean, true
}
func copyIncludedFile(src, dst string) error {
...
}
func TestCopyWorktreeIncludeFiles_RefusesSymlinkedDirEscape(t *testing.T) {
testutil.IsolateGitConfigEnv(t)
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
testutil.WriteFile(t, repoDir, ".gitignore", "sub/"+testEnvFile+"\n")
testutil.WriteFile(t, repoDir, ".worktreeinclude", "sub/"+testEnvFile+"\n")
testutil.WriteFile(t, repoDir, "sub/"+testEnvFile, "SECRET=1\n")
dest := t.TempDir()
outside := t.TempDir()
if err := os.Symlink(outside, filepath.Join(dest, "sub")); err != nil {
t.Skipf("symlinks unsupported: %v", err)
}
var errOut bytes.Buffer
if err := copyWorktreeIncludeFiles(context.Background(), &errOut, repoDir, dest); err != nil {
t.Fatalf("copyWorktreeIncludeFiles: %v", err)
}
if !strings.Contains(errOut.String(), "warning: skipped sub/"+testEnvFile) {
t.Fatalf("stderr = %q, want skip warning for sub/%s", errOut.String(), testEnvFile)
}
if _, err := os.Stat(filepath.Join(outside, testEnvFile)); !os.IsNotExist(err) {
t.Fatalf("%s stat in outside dir = %v, want not exist", testEnvFile, err)
}
}
func newTrailWorktreeTestRepo(t *testing.T) string {
...
}
func TestCheckoutTrailWorktree_BranchCheckedOutInMainWorktree(t *testing.T) {
repoDir := newTrailWorktreeTestRepo(t)
startBranch := currentBranchInDir(t, repoDir)
t.Chdir(repoDir)
var out, errOut bytes.Buffer
err := checkoutTrailWorktree(context.Background(), &out, &errOut, startBranch, false, 1)
if err == nil || !strings.Contains(err.Error(), "already checked out at") {
t.Fatalf("error = %v, want already-checked-out error", err)
}
if _, statErr := os.Stat(filepath.Join(repoDir, ".entire", "worktrees")); !os.IsNotExist(statErr) {
t.Fatalf(".entire/worktrees stat = %v, want not exist", statErr)
}
if _, statErr := os.Stat(filepath.Join(repoDir, ".git", "info", "exclude")); statErr == nil {
content, readErr := os.ReadFile(filepath.Join(repoDir, ".git", "info", "exclude"))
if readErr == nil && strings.Contains(string(content), ".entire/worktrees/") {
t.Fatalf("exclude gained the rule despite failing before the ignore-rule write")
}
}
}
func TestCheckoutTrailWorktree_ReusesExistingWorktree(t *testing.T) {
repoDir := newTrailWorktreeTestRepo(t)
runTrailWorktreeGit(t, repoDir, "branch", "feature/reuse")
Mcmd/entire/cli/trail_checkout_worktree_test.go+49
1261 unmodified lines
1262
1263
1264
1265
1265
1266
1267
1268
1261 unmodified lines
}
cmd.Flags().StringVar(&trailSelector, "trail", "", "Trail to check out (number, id, or branch; defaults to the current branch's trail)")
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip the prompt before fetching a remote-only branch")
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip the prompt before fetching a remote-only branch (with --worktree, also skip the ignore-rule prompt)")
cmd.Flags().BoolVar(&worktree, "worktree", false, "Check out the trail branch in a worktree under .entire/worktrees instead of switching this checkout")
return cmd
Mcmd/entire/cli/trail_cmd.go+1/-1