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
cmd/entire/cli
Mtrail_checkout_worktree.go+6/-37
Mtrail_checkout_worktree_test.go+21/-26
Mtrail_cmd.go+1/-1
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