package sidecar import ( "fmt" "os" "os/exec" "path/filepath" "strings" "sync" "testing" ) func TestAppendCreatesPrivateUnrelatedSidecar(t *testing.T) { repo := testRepo(t) // Store must be independent of a user's restrictive auto-track setting. testJJ(t, repo, "config", "set", "--repo", "snapshot.auto-track", `"none()"`) testJJ(t, repo, "config", "set", "--repo", "git.private-commits", `"description(glob:'secret:*')"`) before := testJJ(t, repo, "status") store, err := Open(repo) if err != nil { t.Fatal(err) } if err := store.Append(func(current []byte) ([]byte, error) { if len(current) != 0 { t.Fatalf("new store contained %q", current) } return []byte("one\n"), nil }); err != nil { t.Fatal(err) } if err := store.Append(func(current []byte) ([]byte, error) { if string(current) != "one\n" { t.Fatalf("second append saw %q", current) } return []byte("two\n"), nil }); err != nil { t.Fatal(err) } got, err := store.Read() if err != nil { t.Fatal(err) } if string(got) != "one\ntwo\n" { t.Fatalf("sidecar log = %q", got) } if after := testJJ(t, repo, "status"); after != before { t.Fatalf("working copy changed:\n--- before\n%s--- after\n%s", before, after) } if _, err := os.Stat(filepath.Join(repo, LogFile)); !os.IsNotExist(err) { t.Fatalf("log materialized in the project working copy: %v", err) } alias := strings.TrimSpace(testJJ(t, repo, "config", "get", "revset-aliases."+Alias)) if alias == "" { t.Fatal("repo-local sidecar alias was not configured") } private := testJJ(t, repo, "config", "get", "git.private-commits") if !containsRevsetName(private, Alias) { t.Fatalf("private commits = %q, missing %s", private, Alias) } if !strings.Contains(private, "secret:*") { t.Fatalf("private commits = %q, previous setting was overwritten", private) } parent := strings.TrimSpace(testJJ(t, repo, "log", "-G", "-r", "exactly("+Alias+", 1)", "-T", "parents.map(|p| p.commit_id()).join(\"\")")) if parent != strings.Repeat("0", 40) { t.Fatalf("sidecar parent = %q, want virtual root", parent) } } func TestLegacyLogIsReadThenImportedOnFirstAppend(t *testing.T) { repo := testRepo(t) legacy := []byte("legacy\n") if err := os.WriteFile(filepath.Join(repo, LogFile), legacy, 0o644); err != nil { t.Fatal(err) } store, err := Open(repo) if err != nil { t.Fatal(err) } if got, err := store.Read(); err != nil || string(got) != string(legacy) { t.Fatalf("pre-migration read = %q, %v", got, err) } if err := store.Append(func(current []byte) ([]byte, error) { if string(current) != string(legacy) { t.Fatalf("migration callback saw %q", current) } return []byte("sidecar\n"), nil }); err != nil { t.Fatal(err) } if got, err := store.Read(); err != nil || string(got) != "legacy\nsidecar\n" { t.Fatalf("migrated log = %q, %v", got, err) } } func TestConcurrentAppendsAreSerialized(t *testing.T) { repo := testRepo(t) store, err := Open(repo) if err != nil { t.Fatal(err) } const writers = 6 errCh := make(chan error, writers) var wg sync.WaitGroup for range writers { wg.Add(1) go func() { defer wg.Done() errCh <- store.Append(func(current []byte) ([]byte, error) { id := len(strings.Fields(string(current))) + 1 return []byte(fmt.Sprintf("%d\n", id)), nil }) }() } wg.Wait() close(errCh) for err := range errCh { if err != nil { t.Fatal(err) } } got, err := store.Read() if err != nil { t.Fatal(err) } if string(got) != "1\n2\n3\n4\n5\n6\n" { t.Fatalf("serialized appends = %q", got) } } func testRepo(t *testing.T) string { t.Helper() if _, err := exec.LookPath("jj"); err != nil { t.Skip("jj not installed") } home := t.TempDir() t.Setenv("HOME", home) t.Setenv("JJ_USER", "test") t.Setenv("JJ_EMAIL", "test@example.com") repo := filepath.Join(home, "repo") if err := os.Mkdir(repo, 0o755); err != nil { t.Fatal(err) } cmd := exec.Command("jj", "git", "init") cmd.Dir = repo if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("jj git init: %v\n%s", err, out) } return repo } func testJJ(t *testing.T, repo string, args ...string) string { t.Helper() cmd := exec.Command("jj", append([]string{"--ignore-working-copy", "--no-pager"}, args...)...) cmd.Dir = repo out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("jj %s: %v\n%s", strings.Join(args, " "), err, out) } return string(out) }