Skip to content

Commit 8ffca11

Browse files
committed
fix: clarify external checkpoint discovery warning
Entire-Checkpoint: d2972e70b180
1 parent 2d03140 commit 8ffca11

2 files changed

Lines changed: 227 additions & 0 deletions

File tree

cmd/entire/cli/strategy/push_common.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"fmt"
77
"io"
88
"os"
9+
"os/exec"
910
"strings"
11+
"sync"
1012
"time"
1113

1214
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
@@ -115,6 +117,35 @@ func printCheckpointRemoteHint(target string) {
115117
fmt.Fprintln(os.Stderr, "[entire] Checkpoints are saved locally but not synced. Ensure you have access to the checkpoint remote.")
116118
}
117119

120+
// settingsHintOnce ensures the settings commit hint prints at most once per process.
121+
var settingsHintOnce sync.Once
122+
123+
// printSettingsCommitHint prints a hint after a successful checkpoint remote push
124+
// when .entire/settings.json is not tracked by git. entire.io needs the committed settings
125+
// to discover the external checkpoint repo. Uses sync.Once to avoid duplicates when
126+
// multiple branches/refs are pushed in a single pre-push invocation.
127+
func printSettingsCommitHint(ctx context.Context, target string) {
128+
if !isURL(target) {
129+
return
130+
}
131+
settingsHintOnce.Do(func() {
132+
if isSettingsTrackedByGit(ctx) {
133+
return
134+
}
135+
fmt.Fprintln(os.Stderr, "[entire] Note: Commit and push .entire/settings.json or entire.io may not find checkpoints on this remote.")
136+
})
137+
}
138+
139+
// isSettingsTrackedByGit returns true if .entire/settings.json is tracked by git.
140+
// Uses repo-root-relative pathspec (:/) to work correctly from any subdirectory.
141+
func isSettingsTrackedByGit(ctx context.Context) bool {
142+
cmd := exec.CommandContext(ctx, "git", "ls-files", ":/.entire/settings.json")
143+
output, err := cmd.Output()
144+
if err != nil {
145+
return false
146+
}
147+
return len(strings.TrimSpace(string(output))) > 0
148+
}
118149
// tryPushSessionsCommon attempts to push the sessions branch.
119150
func tryPushSessionsCommon(ctx context.Context, remote, branchName string) error {
120151
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)

cmd/entire/cli/strategy/push_common_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package strategy
22

33
import (
4+
"bytes"
45
"context"
6+
"os"
57
"os/exec"
68
"path/filepath"
9+
"sync"
710
"testing"
811

912
"github.com/entireio/cli/cmd/entire/cli/paths"
@@ -172,3 +175,196 @@ func TestPushBranchIfNeeded_LocalBareRepo_PushesSuccessfully(t *testing.T) {
172175
t.Errorf("branch should exist on bare remote after push: %v\n%s", err, output)
173176
}
174177
}
178+
// TestIsSettingsTrackedByGit verifies detection of .entire/settings.json tracking status.
179+
// Not parallel: uses t.Chdir().
180+
func TestIsSettingsTrackedByGit(t *testing.T) {
181+
t.Run("untracked", func(t *testing.T) {
182+
tmpDir := t.TempDir()
183+
testutil.InitRepo(t, tmpDir)
184+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
185+
testutil.GitAdd(t, tmpDir, "f.txt")
186+
testutil.GitCommit(t, tmpDir, "init")
187+
188+
// Create .entire/settings.json but don't track it
189+
entireDir := filepath.Join(tmpDir, ".entire")
190+
require.NoError(t, os.MkdirAll(entireDir, 0o755))
191+
require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{}`), 0o644))
192+
193+
t.Chdir(tmpDir)
194+
assert.False(t, isSettingsTrackedByGit(context.Background()))
195+
})
196+
197+
t.Run("tracked", func(t *testing.T) {
198+
tmpDir := t.TempDir()
199+
testutil.InitRepo(t, tmpDir)
200+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
201+
testutil.GitAdd(t, tmpDir, "f.txt")
202+
testutil.GitCommit(t, tmpDir, "init")
203+
204+
// Create and track .entire/settings.json
205+
entireDir := filepath.Join(tmpDir, ".entire")
206+
require.NoError(t, os.MkdirAll(entireDir, 0o755))
207+
require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{}`), 0o644))
208+
testutil.GitAdd(t, tmpDir, ".entire/settings.json")
209+
testutil.GitCommit(t, tmpDir, "add settings")
210+
211+
t.Chdir(tmpDir)
212+
assert.True(t, isSettingsTrackedByGit(context.Background()))
213+
})
214+
215+
t.Run("works from subdirectory", func(t *testing.T) {
216+
tmpDir := t.TempDir()
217+
testutil.InitRepo(t, tmpDir)
218+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
219+
testutil.GitAdd(t, tmpDir, "f.txt")
220+
testutil.GitCommit(t, tmpDir, "init")
221+
222+
// Create and track .entire/settings.json
223+
entireDir := filepath.Join(tmpDir, ".entire")
224+
require.NoError(t, os.MkdirAll(entireDir, 0o755))
225+
require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{}`), 0o644))
226+
testutil.GitAdd(t, tmpDir, ".entire/settings.json")
227+
testutil.GitCommit(t, tmpDir, "add settings")
228+
229+
// Run from a subdirectory
230+
subDir := filepath.Join(tmpDir, "subdir")
231+
require.NoError(t, os.MkdirAll(subDir, 0o755))
232+
t.Chdir(subDir)
233+
assert.True(t, isSettingsTrackedByGit(context.Background()), "should detect tracked file from subdirectory")
234+
})
235+
}
236+
237+
// TestPrintSettingsCommitHint verifies the hint only prints for URL targets
238+
// with untracked settings, and only once per process via sync.Once.
239+
// Not parallel: uses t.Chdir() and resets package-level settingsHintOnce.
240+
func TestPrintSettingsCommitHint(t *testing.T) {
241+
t.Run("no hint for non-URL target", func(t *testing.T) {
242+
// Reset the sync.Once for this test
243+
settingsHintOnce = sync.Once{}
244+
245+
tmpDir := t.TempDir()
246+
testutil.InitRepo(t, tmpDir)
247+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
248+
testutil.GitAdd(t, tmpDir, "f.txt")
249+
testutil.GitCommit(t, tmpDir, "init")
250+
t.Chdir(tmpDir)
251+
252+
// Capture stderr
253+
old := os.Stderr
254+
r, w, err := os.Pipe()
255+
require.NoError(t, err)
256+
os.Stderr = w
257+
258+
printSettingsCommitHint(context.Background(), "origin")
259+
260+
w.Close()
261+
var buf bytes.Buffer
262+
if _, readErr := buf.ReadFrom(r); readErr != nil {
263+
t.Fatalf("read pipe: %v", readErr)
264+
}
265+
os.Stderr = old
266+
267+
assert.Empty(t, buf.String(), "should not print hint for non-URL target")
268+
})
269+
270+
t.Run("hint for URL target with untracked settings", func(t *testing.T) {
271+
settingsHintOnce = sync.Once{}
272+
273+
tmpDir := t.TempDir()
274+
testutil.InitRepo(t, tmpDir)
275+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
276+
testutil.GitAdd(t, tmpDir, "f.txt")
277+
testutil.GitCommit(t, tmpDir, "init")
278+
279+
// Create .entire/settings.json but don't track it
280+
entireDir := filepath.Join(tmpDir, ".entire")
281+
require.NoError(t, os.MkdirAll(entireDir, 0o755))
282+
require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{}`), 0o644))
283+
t.Chdir(tmpDir)
284+
285+
old := os.Stderr
286+
r, w, err := os.Pipe()
287+
require.NoError(t, err)
288+
os.Stderr = w
289+
290+
printSettingsCommitHint(context.Background(), "git@github.com:org/repo.git")
291+
292+
w.Close()
293+
var buf bytes.Buffer
294+
if _, readErr := buf.ReadFrom(r); readErr != nil {
295+
t.Fatalf("read pipe: %v", readErr)
296+
}
297+
os.Stderr = old
298+
299+
assert.Contains(t, buf.String(), "Commit and push .entire/settings.json")
300+
assert.Contains(t, buf.String(), "entire.io may not find checkpoints on this remote")
301+
})
302+
303+
t.Run("no hint when settings is tracked", func(t *testing.T) {
304+
settingsHintOnce = sync.Once{}
305+
306+
tmpDir := t.TempDir()
307+
testutil.InitRepo(t, tmpDir)
308+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
309+
testutil.GitAdd(t, tmpDir, "f.txt")
310+
testutil.GitCommit(t, tmpDir, "init")
311+
312+
// Create and track .entire/settings.json
313+
entireDir := filepath.Join(tmpDir, ".entire")
314+
require.NoError(t, os.MkdirAll(entireDir, 0o755))
315+
require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{}`), 0o644))
316+
testutil.GitAdd(t, tmpDir, ".entire/settings.json")
317+
testutil.GitCommit(t, tmpDir, "add settings")
318+
t.Chdir(tmpDir)
319+
320+
old := os.Stderr
321+
r, w, err := os.Pipe()
322+
require.NoError(t, err)
323+
os.Stderr = w
324+
325+
printSettingsCommitHint(context.Background(), "git@github.com:org/repo.git")
326+
327+
w.Close()
328+
var buf bytes.Buffer
329+
if _, readErr := buf.ReadFrom(r); readErr != nil {
330+
t.Fatalf("read pipe: %v", readErr)
331+
}
332+
os.Stderr = old
333+
334+
assert.Empty(t, buf.String(), "should not print hint when settings.json is tracked")
335+
})
336+
337+
t.Run("prints only once per process", func(t *testing.T) {
338+
settingsHintOnce = sync.Once{}
339+
340+
tmpDir := t.TempDir()
341+
testutil.InitRepo(t, tmpDir)
342+
testutil.WriteFile(t, tmpDir, "f.txt", "init")
343+
testutil.GitAdd(t, tmpDir, "f.txt")
344+
testutil.GitCommit(t, tmpDir, "init")
345+
346+
entireDir := filepath.Join(tmpDir, ".entire")
347+
require.NoError(t, os.MkdirAll(entireDir, 0o755))
348+
require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{}`), 0o644))
349+
t.Chdir(tmpDir)
350+
351+
old := os.Stderr
352+
r, w, err := os.Pipe()
353+
require.NoError(t, err)
354+
os.Stderr = w
355+
356+
// Call twice — should only print once
357+
printSettingsCommitHint(context.Background(), "git@github.com:org/repo.git")
358+
printSettingsCommitHint(context.Background(), "git@github.com:org/repo.git")
359+
360+
w.Close()
361+
var buf bytes.Buffer
362+
if _, readErr := buf.ReadFrom(r); readErr != nil {
363+
t.Fatalf("read pipe: %v", readErr)
364+
}
365+
os.Stderr = old
366+
367+
count := bytes.Count(buf.Bytes(), []byte("Commit and push .entire/settings.json"))
368+
assert.Equal(t, 1, count, "hint should print exactly once, got %d", count)
369+
})
370+
}

0 commit comments

Comments
 (0)