always add worktree ignore rule to .gitignore · Entire

always add worktree ignore rule to .gitignore

e8a3390→main· pfleidi·1w ago·3 files·+28 added/-64 removed

Drop the interactive .gitignore/.git-info-exclude choice: the ignore rule for .entire/worktrees/ is now always appended to the repo-root .gitignore when not already ignored. Persisting the rule is the user's choice, made by committing or discarding the .gitignore change. This removes the prompt, the exclude writer, and --force's second meaning.

Sessions

01KX6N1F03DKKMQPC022K74FAFView transcript

Changes

3

12 unmodified lines

13
14
15
16
16
17
18
59 unmodified lines

78
79
80
82
83
84
85
81
82
83
84
85
86
5 unmodified lines

92
93
94
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
95
96
97
129
98
99
100
101
233 unmodified lines

335
336
337
369
338
339
340
341

12 unmodified lines

"strings"
    "time"

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

"github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"
59 unmodified lines

}

// ensureTrailWorktreeIgnoreRule makes sure .entire/worktrees/ is git-ignored.
// Already ignored (any mechanism) → silent no-op. Interactively it offers the
// shared .gitignore first; declining, aborting, --force, or a non-TTY all fall
// back to the local-only .git/info/exclude. Either write prints a notice.
func ensureTrailWorktreeIgnoreRule(ctx context.Context, w io.Writer, root string, force bool) error {
// Already ignored (any mechanism) → silent no-op. Otherwise the rule is
// appended to the repo-root .gitignore; committing it is the user's choice.
func ensureTrailWorktreeIgnoreRule(ctx context.Context, w io.Writer, root string) error {
    check := exec.CommandContext(ctx, "git", "check-ignore", "-q", trailWorktreesRelDir+"/")
    check.Dir = root
    err := check.Run()
5 unmodified lines

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

useGitignore := false
if !force && interactive.CanPromptInteractively() {
    confirmed := true
    form := NewAccessibleForm(
        huh.NewGroup(
            huh.NewConfirm().
                Title("Add .entire/worktrees/ to .gitignore?").
                Description("Choosing No adds a local-only rule to .git/info/exclude instead.").
                Value(&confirmed),
            ),
        )
    if err := form.Run(); err != nil {
        if !errors.Is(err, huh.ErrUserAborted) {
            return fmt.Errorf("failed to get confirmation: %w", err)
        }
        confirmed = false
    }
    useGitignore = confirmed
}

if useGitignore {
    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
}
    // 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 {
    if err := appendIgnoreRule(filepath.Join(root, ".gitignore")); err != nil {
        return err
    }
    fmt.Fprintln(w, "Added .entire/worktrees/ to .git/info/exclude (local to this clone).")
    fmt.Fprintln(w, "Added .entire/worktrees/ to .gitignore — commit it to keep the rule.")
    return nil
}

233 unmodified lines

return nil
}

if err := ensureTrailWorktreeIgnoreRule(ctx, w, root, force); err != nil {
if err := ensureTrailWorktreeIgnoreRule(ctx, w, root); err != nil {
return err
}

Mcmd/entire/cli/trail_checkout_worktree.go+6/-37


97 unmodified lines

98
99
100
101
101
102
103
104
1 unmodified line

106
107
108
109
109
110
111
112
112
113
114
114
115
116
117
117
118
119
120
121
122
123
119
120
121
122
123
2 unmodified lines

126
127
128
132
129
130
131
132
136
133
134
135
136
137
138
142
143
144
145
146
139
140
141
142
143
144
145
146
147
194 unmodified lines

342
343
344
347
345
346
349
347
348
351
352
349
350
351
352
353
39 unmodified lines

393
394
395
398
399
400
401
402
396
397
398
399
400

97 unmodified lines

}
}

func TestEnsureTrailWorktreeIgnoreRule_NonTTYWritesExclude(t *testing.T) {
func TestEnsureTrailWorktreeIgnoreRule_AppendsGitignore(t *testing.T) {
testutil.IsolateGitConfigEnv(t)

repoDir := t.TempDir()
1 unmodified line

t.Chdir(repoDir)

var out bytes.Buffer
if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir, false); err != nil {
if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir); err != nil {

t.Fatalf("ensureTrailWorktreeIgnoreRule: %v", err)
}
content, err := os.ReadFile(filepath.Join(repoDir, ".git", "info", "exclude"))
content, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))
if err != nil {
    t.Fatalf("read exclude: %v", err)
                                t.Fatalf("read .gitignore: %v", err)
}
if !strings.Contains(string(content), ".entire/worktrees/") {
    t.Fatalf("exclude = %q, want .entire/worktrees/ rule", string(content))
                            if !strings.Contains(out.String(), ".git/info/exclude") {
            t.Fatalf("output = %q, want notice mentioning .git/info/exclude", out.String())
}
if _, err := os.Stat(filepath.Join(repoDir, ".gitignore")); !os.IsNotExist(err) {
                            if !strings.Contains(out.String(), ".gitignore") {
                            t.Fatalf("output = %q, want notice mentioning .gitignore", out.String())
}
}

2 unmodified lines

repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
testutil.WriteFile(t, repoDir, ".gitignore", ".entire/worktrees/\n")
testutil.WriteFile(t, repoDir, ".gitignore", ".entire/\n")
t.Chdir(repoDir)

var out bytes.Buffer
if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir, false); err != nil {
if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir); err != nil {
    t.Fatalf("ensureTrailWorktreeIgnoreRule: %v", err)
}
if out.Len() != 0 {
    t.Fatalf("output = %q, want silence", out.String())
}
if _, err := os.Stat(filepath.Join(repoDir, ".git", "info", "exclude")); err == 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 .gitignore already covering it")
    }
content, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))
if err != nil {
    t.Fatalf("read .gitignore: %v", err)
    }
if got, want := string(content), ".entire/\n"; got != want {
    t.Fatalf(".gitignore = %q, want untouched %q", got, want)
}
}

194 unmodified lines

if _, err := os.Stat(filepath.Join(wantPath, ".env")); err != nil {
    t.Fatalf(".worktreeinclude copy missing: %v", err)
}
excludeContent, err := os.ReadFile(filepath.Join(repoDir, ".git", "info", "exclude"))
gitignoreContent, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))
if err != nil {
    t.Fatalf("read exclude: %v", err)
        t.Fatalf("read .gitignore: %v", err)
}
if !strings.Contains(string(excludeContent), ".entire/worktrees/") {
    t.Fatalf("exclude = %q, want .entire/worktrees/ rule", string(excludeContent))
    if !strings.Contains(string(gitignoreContent), ".entire/worktrees/") {
        t.Fatalf(".gitignore = %q, want .entire/worktrees/ rule", string(gitignoreContent))
}
}

39 unmodified lines

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")
}
if _, statErr := os.Stat(filepath.Join(repoDir, ".gitignore")); !os.IsNotExist(statErr) {
    t.Fatalf(".gitignore stat = %v, want no ignore rule written before the failure", statErr)
}
}

1261 unmodified lines

1262
1263
1264
1265
1265
1266
1267
1268