simplify trail worktree checkout internals · Entire

simplify trail worktree checkout internals

103c35c→main·

pfleidi·1w ago·2 files·+51 added/-85 removed

Reuse existing package helpers instead of local duplicates: shellQuote, parseWorktreeForBranch, remote.Fetch (regaining GIT_TERMINAL_PROMPT=0, --no-auto-gc, and the consistent fetch-timeout message), and the runGit test helper. Resolve the main worktree root once per invocation instead of re-spawning git rev-parse up to three times, and drop the constant rule parameter from appendIgnoreRule.

Sessions

01KX4RP62BZ6BE1GSVP4Q8CCXWView transcript

[?
Add trail checkout --worktree SupportClaude Code·2 steps](/content/gh/entireio/cli/session/c5215fe7-28db-4a83-9d2e-482217df292d#timeline-01KX4RP62BZ6BE1GSVP4Q8CCXW/index.html)

Changes

2

15 unmodified lines

16
17
18
19
20
21
22
24 unmodified lines

47
48
49
49
50
51
52
50
51
52
40 unmodified lines

93
94
95
99
96
97
98
15 unmodified lines

114
115
116
121
117
118
119
120
121
122
127
128
129
130
131
123
124
125
126
127
128
129
130
131
138
132
133
134
135
136
181 unmodified lines

318
319
320
326
321
322
323
324
325
326
327
328
329
2 unmodified lines

332
333
334
335
335
336
337
338
6 unmodified lines

345
346
347
348
349
350
351
348
349
350
13 unmodified lines

364
365
366
371
367
368
369
370
371
372
377
378
379
380
381
382
383
384
373
374
375
376
377
378
379
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
380
381
382
383
384
407
385
386
387
388
389
390
391
41 unmodified lines

433
434
435
455
456
436
437
438
439
440
441
442
443
444
445
446
447
448
449

15 unmodified lines

huh "charm.land/huh/v2"
    "github.com/go-git/go-git/v6/plumbing/format/gitignore"

"github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"
    "github.com/entireio/cli/cmd/entire/cli/interactive"
)

24 unmodified lines

return name
}

func shellQuotePath(path string) string {
    return "'" + strings.ReplaceAll(path, "'", `'\''`) + "'"
}

// gitCommonDirForTrailWorktree returns the absolute git common dir, which is
// the main repo's .git directory even when run from a linked worktree.
func gitCommonDirForTrailWorktree(ctx context.Context) (string, error) {
40 unmodified lines

return fmt.Errorf("failed to check ignore status of %s: %w", trailWorktreesRelDir, err)
}

const rule = trailWorktreesRelDir + "/"
useGitignore := false
if !force && interactive.CanPromptInteractively() {
    confirmed := true
15 unmodified lines

}

if useGitignore {
    if err := appendIgnoreRule(filepath.Join(root, ".gitignore"), rule); err != nil {
        if err := appendIgnoreRule(filepath.Join(root, ".gitignore")); err != nil {
            return err
        }
        fmt.Fprintln(w, "Added .entire/worktrees/ to .gitignore — commit this when convenient.")
        return nil
    }
    gitDir, err := gitCommonDirForTrailWorktree(ctx)
    if err != nil {
        return err
    }
    if err := appendIgnoreRule(filepath.Join(gitDir, "info", "exclude"), rule); err != nil {
        // root came from trailWorktreeBaseRoot, which guarantees <root>/.git is the
        // git common dir even when running from a linked worktree.
        if err := appendIgnoreRule(filepath.Join(root, ".git", "info", "exclude")); err != nil {
            return err
        }
        fmt.Fprintln(w, "Added .entire/worktrees/ to .git/info/exclude (local to this clone).")
        return nil
    }
}

func appendIgnoreRule(path, rule string) error { //nolint:unparam // rule is constant today; the writer serves both ignore files
func appendIgnoreRule(path string) error {
const rule = trailWorktreesRelDir + "/"
content, err := os.ReadFile(path) //nolint:gosec // path derived from repo root / git common dir
if err != nil && !errors.Is(err, os.ErrNotExist) {
    return fmt.Errorf("failed to read %s: %w", path, err)
181 unmodified lines

return err
}

existing, managed, found, err := findWorktreeForBranch(ctx, branch)
root, err := trailWorktreeBaseRoot(ctx)
if err != nil {
    return fmt.Errorf("failed to find main worktree root: %w", err)
}

existing, managed, found, err := findWorktreeForBranch(ctx, branch, root)
if err != nil {
    return err
}
2 unmodified lines

return fmt.Errorf("branch %q is already checked out at %s", branch, existing)
    }
    fmt.Fprintf(w, "Worktree already exists at %s\n", existing)
    fmt.Fprintf(w, "cd %s\n", shellQuotePath(existing))
    fmt.Fprintf(w, "cd %s\n", shellQuote(existing))
    return nil
}

6 unmodified lines

return nil
}

root, err := trailWorktreeBaseRoot(ctx)
if err != nil {
    return fmt.Errorf("failed to find main worktree root: %w", err)
}
if err := ensureTrailWorktreeIgnoreRule(ctx, w, root, force); err != nil {
    return err
}
13 unmodified lines

fmt.Fprintf(w, "Worktree ready at %s\n", worktreePath)
    fmt.Fprintf(w, "cd %s\n", shellQuotePath(worktreePath))
    fmt.Fprintf(w, "cd %s\n", shellQuote(worktreePath))
    return nil
}

// findWorktreeForBranch reports whether branch is already checked out in any
// worktree, and whether that worktree is managed under
// <main-root>/.entire/worktrees.
func findWorktreeForBranch(ctx context.Context, branch string) (path string, managed bool, found bool, err error) {
root, err := trailWorktreeBaseRoot(ctx)
if err != nil {
    return "", false, false, fmt.Errorf("failed to find main worktree root: %w", err)
}
managedRoot := normalizeWorktreePath(filepath.Join(root, filepath.FromSlash(trailWorktreesRelDir)));

// <root>/.entire/worktrees.
func findWorktreeForBranch(ctx context.Context, branch, root string) (path string, managed bool, found bool, err error) {
cmd := exec.CommandContext(ctx, "git", "worktree", "list", "--porcelain")
output, err := cmd.Output()
if err != nil {
    return "", false, false, fmt.Errorf("failed to list worktrees: %w", err)
}
var currentPath string
for _, line := range strings.Split(string(output), "\n") {
    line = strings.TrimSpace(line)
    if line == "" {
        currentPath = ""
        continue
    }
    if strings.HasPrefix(line, "worktree ") {
        currentPath = strings.TrimSpace(strings.TrimPrefix(line, "worktree "))
        continue
    }
    if strings.TrimPrefix(line, "branch ") == "refs/heads/"+branch && currentPath != "" {
        normalized := normalizeWorktreePath(currentPath)
        isManaged := normalized == managedRoot || strings.HasPrefix(normalized, managedRoot+string(filepath.Separator))
        return currentPath, isManaged, true, nil
    }
// Empty currentRoot: match any worktree, including the current checkout.
path, found = parseWorktreeForBranch(string(output), branch, "")
if !found {
    return "", false, false, nil
}
return "", false, false, nil
managedRoot := normalizeWorktreePath(filepath.Join(root, filepath.FromSlash(trailWorktreesRelDir)))
normalized := normalizeWorktreePath(path)
managed = normalized == managedRoot || strings.HasPrefix(normalized, managedRoot+string(filepath.Separator))
return path, managed, true, nil
}

// ensureTrailWorktreeBranchAvailable makes sure branch exists locally,
41 unmodified lines

defer cancel()

refSpec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)
cmd := exec.CommandContext(ctx, "git", "fetch", "origin", refSpec)
if output, err := cmd.CombinedOutput(); err != nil {
    // NoFilter: the worktree checkout needs full branch content; a partial
    // clone would leave blobs missing.
    output, err := remote.Fetch(ctx, remote.FetchOptions{
        Remote:   "origin",
        RefSpecs: []string{refSpec},
        NoFilter: true,
    })
    if err != nil {
        if errors.Is(ctx.Err(), context.DeadlineExceeded) {
            return errors.New("fetch timed out after 2 minutes")
        }
        return fmt.Errorf("failed to fetch branch from origin: %s: %w", strings.TrimSpace(string(output)), err)
    }
    return nil
}

Mcmd/entire/cli/trail_checkout_worktree.go+36/-46

5 unmodified lines

6
7
8
9
10
11
12
45 unmodified lines

58
59
60
60
61
62
63
64
65
66
67
68
69
61
62
63
64
65
75
66
67
68
69
16 unmodified lines

86
87
88
98
89
90
91
92
67 unmodified lines

160
161
162
172
163
164
165
175
176
177
178
179
166
167
168
9 unmodified lines

178
179
180
195
181
182
183
184
119 unmodified lines

304
305
306
321
322
323
324
325
326
327
328
329
330
307
308
309
7 unmodified lines

317
318
319
344
320
321
322
323
8 unmodified lines

332
333
334
359
335
336
337
338
16 unmodified lines

355
356
357
382
383
358
359
360
361
362
42 unmodified lines

405
406
407
432
408
409
410
411
18 unmodified lines

430
431
432
457
433
434
435
436
437
462
438
439
440
441
466
467
468
442
443
444
445
446
447

5 unmodified lines

"os"
    "os/exec"
    "path/filepath"
    "slices"
    "strings"
    "testing"

45 unmodified lines

}
}

func TestShellQuotePath(t *testing.T) {
    t.Parallel()

got := shellQuotePath("/tmp/it's here")
    want := `'/tmp/it'\''s here'`
    if got != want {
        t.Fatalf("shellQuotePath() = %q, want %q", got, want)
    }
}

func TestAppendIgnoreRule(t *testing.T) {
    t.Parallel()

path := filepath.Join(t.TempDir(), "sub", "exclude")
    for range 2 {
        if err := appendIgnoreRule(path, ".entire/worktrees/"); err != nil {
        if err := appendIgnoreRule(path); err != nil {
            t.Fatalf("appendIgnoreRule: %v", err)
        }
    }
16 unmodified lines

if err := os.WriteFile(path, []byte("node_modules"), 0o600); err != nil {
        t.Fatalf("seed: %v", err)
    }
    if err := appendIgnoreRule(path, ".entire/worktrees/"); err != nil {
    if err := appendIgnoreRule(path); err != nil {
        t.Fatalf("appendIgnoreRule: %v", err)
    }
    content, err := os.ReadFile(path)
67 unmodified lines

}
}

func TestLoadWorktreeIncludePatterns(t *testing.T) {
    9 unmodified lines

t.Fatalf("loadWorktreeIncludePatterns: %v", err)
    }
    want := []string{".env", "*.local"}
    if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
    if !slices.Equal(got, want) {
        t.Fatalf("patterns = %v, want %v", got, want)
    }
}
119 unmodified lines

return repoDir
}

func runTrailWorktreeGit(t *testing.T, dir string, args ...string) {
    t.Helper()
    cmd := exec.CommandContext(context.Background(), "git", args...)
    cmd.Dir = dir
    output, err := cmd.CombinedOutput()
    if err != nil {
        t.Fatalf("git %s failed: %v\n%s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
    }
}

func currentBranchInDir(t *testing.T, dir string) string {
    t.Helper()
    cmd := exec.CommandContext(context.Background(), "git", "branch", "--show-current")
7 unmodified lines

func TestCheckoutTrailWorktree_CreatesWorktree(t *testing.T) {
    repoDir := newTrailWorktreeTestRepo(t)
    runTrailWorktreeGit(t, repoDir, "branch", "feature/test")
    runGit(t, repoDir, "branch", "feature/test")
    testutil.WriteFile(t, repoDir, ".worktreeinclude", ".env\n")
    testutil.WriteFile(t, repoDir, ".env", "SECRET=1\n")
    testutil.WriteFile(t, repoDir, ".gitignore", ".env\n")
8 unmodified lines

}

wantPath := filepath.Join(repoDir, ".entire", "worktrees", "trail-7-feature-test")
    if !strings.Contains(out.String(), "cd "+shellQuotePath(wantPath)) {
    if !strings.Contains(out.String(), "cd "+shellQuote(wantPath)) {
        t.Fatalf("output = %q, want cd hint for %q", out.String(), wantPath)
    }
    if got := currentBranchInDir(t, repoDir); got != startBranch {
16 unmodified lines

func TestCheckoutTrailWorktree_FromLinkedWorktreeCreatesSibling(t *testing.T) {
    repoDir := newTrailWorktreeTestRepo(t)
    runTrailWorktreeGit(t, repoDir, "branch", "feature/first")
    runTrailWorktreeGit(t, repoDir, "branch", "feature/second")
    runGit(t, repoDir, "branch", "feature/first")
    runGit(t, repoDir, "branch", "feature/second")
    t.Chdir(repoDir)

var out1, err1 bytes.Buffer
    42 unmodified lines

func TestCheckoutTrailWorktree_ReusesExistingWorktree(t *testing.T) {
    repoDir := newTrailWorktreeTestRepo(t)
    runTrailWorktreeGit(t, repoDir, "branch", "feature/reuse")
    runGit(t, repoDir, "branch", "feature/reuse")
    t.Chdir(repoDir)

var out1, err1 bytes.Buffer
    18 unmodified lines

originDir := filepath.Join(tmp, "origin.git")
    seedDir := filepath.Join(tmp, "seed")
    repoDir := filepath.Join(tmp, "local")
    runTrailWorktreeGit(t, tmp, "init", "--bare", originDir)
    runGit(t, tmp, "init", "--bare", originDir)
    testutil.InitRepo(t, seedDir)
    testutil.WriteFile(t, seedDir, "README.md", "test\n")
    testutil.GitAdd(t, seedDir, "README.md")
    testutil.GitCommit(t, seedDir, "initial")
    runTrailWorktreeGit(t, seedDir, "checkout", "-b", "feature/remote")
    runGit(t, seedDir, "checkout", "-b", "feature/remote")
    testutil.WriteFile(t, seedDir, "remote.txt", "remote\n")
    testutil.GitAdd(t, seedDir, "remote.txt")
    testutil.GitCommit(t, seedDir, "remote branch")
    runTrailWorktreeGit(t, seedDir, "remote", "add", "origin", originDir)
    runTrailWorktreeGit(t, seedDir, "push", "origin", "--all")
    runTrailWorktreeGit(t, tmp, "clone", originDir, repoDir)
    runGit(t, seedDir, "remote", "add", "origin", originDir)
    runGit(t, seedDir, "push", "origin", "--all")
    runGit(t, tmp, "clone", originDir, repoDir)
    t.Chdir(repoDir)

var out, errOut bytes.Buffer