summaryrefslogtreecommitdiff
path: root/sidecar
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 23:43:08 -0300
committerGabriel Schneider <[email protected]>2026-08-03 09:54:39 -0300
commit5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch)
treef12f4b700fff4ff6f7e818d2a542672ba0b8d76e /sidecar
parenta9263daee9413c5eff3c1f1bebcd224442fd8685 (diff)
downloadnotevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.tar.gz
notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.zip
rebrand to notevi: one CLI over the jj sidecar
vr and vrsite become a single binary. vrsite/ folds into a web package in one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly what vrsite did, and "notevi read/grep/note/query" is unchanged. The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the description "private: notevi log". The pre-rebrand names are still recognized, so an old repository opens and reads; it is renamed in place on the first write, or up front with "notevi migrate DIR...". That rename cannot be a single mv inside jj run. jj only auto-tracks a *new* file in the run working copy below a size limit it does not take from the command line, so writing a whole log under a name the change has never held is silently dropped while jj reports success. ensureLogFile creates the file empty first and lets every later byte be a modification of a tracked file, which snapshots at any size; that also fixes the same latent bug when importing a large legacy vr-log.jsonl. Adds a bem-te-vi mark (favicon and nav brand) and a README. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'sidecar')
-rw-r--r--sidecar/store.go295
-rw-r--r--sidecar/store_test.go134
2 files changed, 362 insertions, 67 deletions
diff --git a/sidecar/store.go b/sidecar/store.go
index 76e626f..ce6c56f 100644
--- a/sidecar/store.go
+++ b/sidecar/store.go
@@ -1,4 +1,4 @@
-// Package sidecar stores vr's append-only JSONL log in a private jj change.
+// Package sidecar stores notevi's append-only JSONL log in a private jj change.
//
// The change is an anonymous direct child of root(), so it is visible and
// durable in the jj repository without becoming part of project history or the
@@ -18,18 +18,34 @@ import (
)
const (
- Alias = "vr_log"
- LogFile = "vr-log.jsonl"
- Description = "private: vr log"
+ Alias = "notevi_log"
+ LogFile = "notevi-log.jsonl"
+ Description = "private: notevi log"
+ lockFile = "notevi.lock"
)
-var ErrNoLog = errors.New("vr log has not been initialized")
+// naming is the set of repo-visible names one generation of the tool gave the
+// sidecar: the revset alias, the file inside the change, and the change's own
+// description.
+type naming struct{ alias, logFile, description string }
-// Store is a repository's vr log. Open is read-only with respect to jj; the
+var current = naming{Alias, LogFile, Description}
+
+// legacy is what the tool called itself before the notevi rebrand. Every read
+// path still recognizes it so an un-migrated repository opens; Migrate renames
+// it in place so nothing in the repository keeps the old name.
+var legacy = naming{"vr_log", "vr-log.jsonl", "private: vr log"}
+
+const legacyLockFile = "vr-agent-logger.lock"
+
+var ErrNoLog = errors.New("notevi log has not been initialized")
+
+// Store is a repository's notevi log. Open is read-only with respect to jj; the
// sidecar change and its config are created lazily by Append.
type Store struct {
- root string
- lockPath string
+ root string
+ configDir string
+ lockPath string
}
// Open finds the jj workspace containing dir.
@@ -43,32 +59,40 @@ func Open(dir string) (*Store, error) {
if err != nil {
return nil, err
}
+ configDir := filepath.Dir(strings.TrimSpace(configPath))
return &Store{
- root: root,
- lockPath: filepath.Join(filepath.Dir(strings.TrimSpace(configPath)), "vr-agent-logger.lock"),
+ root: root,
+ configDir: configDir,
+ lockPath: filepath.Join(configDir, lockFile),
}, nil
}
+// Descriptions lists every change subject that has ever named a notevi sidecar,
+// current first. Callers that discover sidecars by description need the older
+// ones too, so an un-migrated repository is still recognized as having a log.
+func Descriptions() []string { return []string{current.description, legacy.description} }
+
func (s *Store) Root() string { return s.root }
func (s *Store) String() string {
return fmt.Sprintf("jj sidecar %s:%s", Alias, LogFile)
}
-// Read returns the current log. Before the first sidecar write it falls back
-// to a repo-root legacy log, which makes migration transparent.
+// Read returns the current log. It reads a sidecar that still carries the
+// pre-rebrand names without migrating it, and before the first sidecar write it
+// falls back to a repo-root legacy log, which makes migration transparent.
func (s *Store) Read() ([]byte, error) {
var data []byte
err := s.withLock(syscall.LOCK_SH, func() error {
- rev, found, _, err := s.resolveExisting()
+ rev, n, found, _, err := s.locate()
if err != nil {
return err
}
if found {
- data, err = s.readRevision(rev)
+ data, err = s.readRevision(rev, n.logFile)
return err
}
- data, err = os.ReadFile(filepath.Join(s.root, LogFile))
+ data, err = os.ReadFile(filepath.Join(s.root, legacy.logFile))
if errors.Is(err, os.ErrNotExist) {
return ErrNoLog
}
@@ -77,12 +101,12 @@ func (s *Store) Read() ([]byte, error) {
return data, err
}
-// Generation is a cheap identity for the current contents, used by vrsite to
-// notice appends. A sidecar rewrite gets a new commit id.
+// Generation is a cheap identity for the current contents, used by the web app
+// to notice appends. A sidecar rewrite gets a new commit id.
func (s *Store) Generation() (string, error) {
var generation string
err := s.withLock(syscall.LOCK_SH, func() error {
- rev, found, _, err := s.resolveExisting()
+ rev, _, found, _, err := s.locate()
if err != nil {
return err
}
@@ -90,7 +114,7 @@ func (s *Store) Generation() (string, error) {
generation, err = s.logValue(rev, "commit_id")
return err
}
- info, err := os.Stat(filepath.Join(s.root, LogFile))
+ info, err := os.Stat(filepath.Join(s.root, legacy.logFile))
if errors.Is(err, os.ErrNotExist) {
generation = "empty"
return nil
@@ -104,7 +128,7 @@ func (s *Store) Generation() (string, error) {
return generation, err
}
-// Append serializes sidecar rewrites across vr processes. makePayload runs
+// Append serializes sidecar rewrites across notevi processes. makePayload runs
// while holding that lock and receives the complete current log, allowing note
// numbering and reply validation to be atomic with the append.
func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
@@ -113,17 +137,17 @@ func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
if err != nil {
return err
}
- sidecarData, err := s.readRevision(rev)
+ sidecarData, err := s.readRevision(rev, LogFile)
if err != nil {
return err
}
current := sidecarData
if len(current) == 0 {
- legacy, legacyErr := os.ReadFile(filepath.Join(s.root, LogFile))
- if legacyErr == nil {
- current = legacy
- } else if !errors.Is(legacyErr, os.ErrNotExist) {
- return legacyErr
+ old, oldErr := os.ReadFile(filepath.Join(s.root, legacy.logFile))
+ if oldErr == nil {
+ current = old
+ } else if !errors.Is(oldErr, os.ErrNotExist) {
+ return oldErr
}
}
payload, err := makePayload(current)
@@ -131,7 +155,7 @@ func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
return err
}
if len(payload) == 0 {
- return errors.New("refusing to append an empty vr log payload")
+ return errors.New("refusing to append an empty notevi log payload")
}
var appendData []byte
@@ -145,10 +169,97 @@ func (s *Store) Append(makePayload func(current []byte) ([]byte, error)) error {
if appendData[len(appendData)-1] != '\n' {
appendData = append(appendData, '\n')
}
- return s.runAppend(appendData)
+ return s.runAppend(rev, appendData)
})
}
+// Migrate renames a pre-rebrand sidecar in place: the file inside the change,
+// the change description, the repo-local revset alias, and the alias mentioned
+// in git.private-commits. It reports whether anything was renamed and is a
+// no-op on a repository that is already current or has no sidecar at all.
+func (s *Store) Migrate() (migrated bool, err error) {
+ err = s.withLock(syscall.LOCK_EX, func() error {
+ rev, n, found, configured, err := s.locate()
+ if err != nil {
+ return err
+ }
+ if !found || n != legacy {
+ // Still adopt an unconfigured current sidecar, so that a repo whose
+ // jj config was lost (moving a workspace orphans it) is repaired.
+ if found && !configured {
+ return s.configure(rev)
+ }
+ return nil
+ }
+ if err := s.rename(rev); err != nil {
+ return err
+ }
+ migrated = true
+ return nil
+ })
+ return migrated, err
+}
+
+// rename performs the legacy -> current renaming for an already-located sidecar.
+// The move is two runs, not one `mv`: see ensureLogFile.
+func (s *Store) rename(rev string) error {
+ listed, err := s.jj("file", "list", "-r", rev, "--", legacy.logFile)
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(listed) != "" {
+ if err := s.ensureLogFile(rev); err != nil {
+ return err
+ }
+ if _, err := s.jjRun(rev, "cat "+legacy.logFile+" >> "+LogFile+" && rm "+legacy.logFile); err != nil {
+ return err
+ }
+ }
+ if _, err := s.jj("describe", "-r", rev, "-m", Description); err != nil {
+ return err
+ }
+ if err := s.configure(rev); err != nil {
+ return err
+ }
+ if _, err := s.jj("config", "get", "revset-aliases."+legacy.alias); err == nil {
+ if _, err := s.jj("config", "unset", "--repo", "revset-aliases."+legacy.alias); err != nil {
+ return err
+ }
+ }
+ os.Remove(filepath.Join(s.configDir, legacyLockFile)) //nolint:errcheck
+ return nil
+}
+
+// configure points the repo-local alias at rev and refreshes the push guard.
+func (s *Store) configure(rev string) error {
+ if _, err := s.jj("config", "set", "--repo", "revset-aliases."+Alias, strconv.Quote(rev)); err != nil {
+ return err
+ }
+ return s.guard()
+}
+
+// guard keeps git.private-commits naming the current alias, rewriting a legacy
+// mention rather than leaving both names in the expression.
+func (s *Store) guard() error {
+ private, err := s.jj("config", "get", "git.private-commits")
+ if err != nil {
+ return err
+ }
+ expr := strings.TrimSpace(private)
+ next := expr
+ if containsRevsetName(next, legacy.alias) {
+ next = replaceRevsetName(next, legacy.alias, Alias)
+ }
+ if !containsRevsetName(next, Alias) {
+ next = fmt.Sprintf("(%s) | %s", next, Alias)
+ }
+ if next == expr {
+ return nil
+ }
+ _, err = s.jj("config", "set", "--repo", "git.private-commits", strconv.Quote(next))
+ return err
+}
+
func (s *Store) withLock(kind int, fn func() error) error {
if err := os.MkdirAll(filepath.Dir(s.lockPath), 0o755); err != nil {
return err
@@ -166,57 +277,64 @@ func (s *Store) withLock(kind int, fn func() error) error {
}
func (s *Store) ensure() (string, error) {
- rev, found, configured, err := s.resolveExisting()
+ rev, n, found, configured, err := s.locate()
if err != nil {
return "", err
}
+ if found && n == legacy {
+ // The first append to a pre-rebrand repository renames it, so no
+ // repository keeps writing under the old name once notevi touches it.
+ return rev, s.rename(rev)
+ }
if !found {
if _, err := s.jj("new", "--no-edit", "root()", "-m", Description); err != nil {
return "", err
}
- ids, err := s.descriptionMatches()
+ ids, err := s.descriptionMatches(Description)
if err != nil {
return "", err
}
if len(ids) != 1 {
return "", fmt.Errorf("created %q sidecar but found %d matching root children", Description, len(ids))
}
- rev = ids[0]
+ rev, configured = ids[0], false
}
if !configured {
- if _, err := s.jj("config", "set", "--repo", "revset-aliases."+Alias, strconv.Quote(rev)); err != nil {
- return "", err
- }
+ return rev, s.configure(rev)
}
- private, err := s.jj("config", "get", "git.private-commits")
- if err != nil {
- return "", err
+ return rev, s.guard()
+}
+
+// locate finds this repository's sidecar under the current names, falling back
+// to the pre-rebrand ones, and reports which generation named it.
+func (s *Store) locate() (rev string, n naming, found, configured bool, err error) {
+ rev, found, configured, err = s.resolveExisting(current)
+ if err != nil || found {
+ return rev, current, found, configured, err
}
- if !containsRevsetName(private, Alias) {
- expr := fmt.Sprintf("(%s) | %s", strings.TrimSpace(private), Alias)
- if _, err := s.jj("config", "set", "--repo", "git.private-commits", strconv.Quote(expr)); err != nil {
- return "", err
- }
+ rev, found, configured, err = s.resolveExisting(legacy)
+ if !found {
+ return rev, current, false, false, err
}
- return rev, nil
+ return rev, legacy, found, configured, err
}
-func (s *Store) resolveExisting() (rev string, found, configured bool, err error) {
- configuredValue, configErr := s.jj("config", "get", "revset-aliases."+Alias)
+func (s *Store) resolveExisting(n naming) (rev string, found, configured bool, err error) {
+ configuredValue, configErr := s.jj("config", "get", "revset-aliases."+n.alias)
if configErr == nil {
- rev, err = s.logValue("exactly("+Alias+", 1)", "change_id")
+ rev, err = s.logValue("exactly("+n.alias+", 1)", "change_id")
if err != nil {
- return "", false, true, fmt.Errorf("repo-local revset alias %s is invalid: %w", Alias, err)
+ return "", false, true, fmt.Errorf("repo-local revset alias %s is invalid: %w", n.alias, err)
}
- valid := fmt.Sprintf("exactly(%s & root()+ & subject(exact:%s), 1)", Alias, strconv.Quote(Description))
+ valid := fmt.Sprintf("exactly(%s & root()+ & subject(exact:%s), 1)", n.alias, strconv.Quote(n.description))
if _, err := s.logValue(valid, "change_id"); err != nil {
- return "", false, true, fmt.Errorf("repo-local revset alias %s=%q does not name vr's sidecar: %w",
- Alias, strings.TrimSpace(configuredValue), err)
+ return "", false, true, fmt.Errorf("repo-local revset alias %s=%q does not name notevi's sidecar: %w",
+ n.alias, strings.TrimSpace(configuredValue), err)
}
return rev, true, true, nil
}
- ids, err := s.descriptionMatches()
+ ids, err := s.descriptionMatches(n.description)
if err != nil {
return "", false, false, err
}
@@ -226,12 +344,12 @@ func (s *Store) resolveExisting() (rev string, found, configured bool, err error
case 1:
return ids[0], true, false, nil
default:
- return "", false, false, fmt.Errorf("found %d root children described %q; cannot choose a vr sidecar", len(ids), Description)
+ return "", false, false, fmt.Errorf("found %d root children described %q; cannot choose a notevi sidecar", len(ids), n.description)
}
}
-func (s *Store) descriptionMatches() ([]string, error) {
- revset := fmt.Sprintf("root()+ & subject(exact:%s)", strconv.Quote(Description))
+func (s *Store) descriptionMatches(description string) ([]string, error) {
+ revset := fmt.Sprintf("root()+ & subject(exact:%s)", strconv.Quote(description))
out, err := s.jj("log", "-G", "-r", revset, "-T", "change_id ++ \"\\n\"")
if err != nil {
return nil, err
@@ -255,20 +373,44 @@ func (s *Store) logValue(revset, template string) (string, error) {
return value, nil
}
-func (s *Store) readRevision(rev string) ([]byte, error) {
- listed, err := s.jj("file", "list", "-r", rev, "--", LogFile)
+func (s *Store) readRevision(rev, logFile string) ([]byte, error) {
+ listed, err := s.jj("file", "list", "-r", rev, "--", logFile)
if err != nil {
return nil, err
}
if strings.TrimSpace(listed) == "" {
return nil, nil
}
- out, err := s.jj("file", "show", "-r", rev, "--", LogFile)
+ out, err := s.jj("file", "show", "-r", rev, "--", logFile)
return []byte(out), err
}
-func (s *Store) runAppend(data []byte) error {
- f, err := os.CreateTemp(filepath.Dir(s.lockPath), "vr-log-append-*")
+// ensureLogFile creates the log file, empty, if the sidecar does not have it.
+//
+// This is load-bearing, not tidiness. jj only auto-tracks a *new* file in the
+// run working copy while it is under snapshot.max-new-file-size, and the run
+// working copy does not take that limit from the command line, so writing a
+// whole log under a name the change has never held is silently dropped — the
+// commit is rewritten with the file missing and jj reports success. Writing the
+// file empty first is always under the limit; from then on every byte is a
+// modification of a tracked file, which snapshots at any size.
+func (s *Store) ensureLogFile(rev string) error {
+ listed, err := s.jj("file", "list", "-r", rev, "--", LogFile)
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(listed) != "" {
+ return nil
+ }
+ _, err = s.jjRun(rev, ": > "+LogFile)
+ return err
+}
+
+func (s *Store) runAppend(rev string, data []byte) error {
+ if err := s.ensureLogFile(rev); err != nil {
+ return err
+ }
+ f, err := os.CreateTemp(s.configDir, "notevi-log-append-*")
if err != nil {
return err
}
@@ -281,13 +423,20 @@ func (s *Store) runAppend(data []byte) error {
if err := f.Close(); err != nil {
return err
}
- _, err = s.jj("--config", "snapshot.auto-track=\"all()\"",
- "--config", "snapshot.max-new-file-size=\"1GiB\"",
- "run", "--clean", "--root", "-r", "exactly("+Alias+", 1)", "--",
- "sh", "-c", "cat \"$1\" >> "+LogFile, "sh", tmp)
+ _, err = s.jjRun(rev, "cat \"$1\" >> "+LogFile, tmp)
return err
}
+// jjRun rewrites the sidecar change by running script against a clean checkout
+// of it. rev is a change id or alias; the working copy is never involved.
+func (s *Store) jjRun(rev, script string, scriptArgs ...string) (string, error) {
+ args := []string{"--config", "snapshot.auto-track=\"all()\"",
+ "--config", "snapshot.max-new-file-size=\"1GiB\"",
+ "run", "--clean", "--root", "-r", "exactly((" + rev + "), 1)", "--",
+ "sh", "-c", script, "sh"}
+ return s.jj(append(args, scriptArgs...)...)
+}
+
func (s *Store) jj(args ...string) (string, error) {
return jjOutput(s.root, args...)
}
@@ -324,6 +473,26 @@ func containsRevsetName(expr, name string) bool {
return false
}
+// replaceRevsetName swaps whole revset names, so that rewriting an expression
+// mentioning vr_log cannot corrupt an unrelated name that merely contains it.
+func replaceRevsetName(expr, name, replacement string) string {
+ var b strings.Builder
+ for i := 0; i < len(expr); {
+ if expr[i:min(i+len(name), len(expr))] == name {
+ before := i == 0 || !isNameByte(expr[i-1])
+ after := i+len(name) == len(expr) || !isNameByte(expr[i+len(name)])
+ if before && after {
+ b.WriteString(replacement)
+ i += len(name)
+ continue
+ }
+ }
+ b.WriteByte(expr[i])
+ i++
+ }
+ return b.String()
+}
+
func isNameByte(b byte) bool {
return b == '_' || b == '-' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9'
}
diff --git a/sidecar/store_test.go b/sidecar/store_test.go
index 85f50e1..8ce3192 100644
--- a/sidecar/store_test.go
+++ b/sidecar/store_test.go
@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strconv"
"strings"
"sync"
"testing"
@@ -72,19 +73,19 @@ func TestAppendCreatesPrivateUnrelatedSidecar(t *testing.T) {
func TestLegacyLogIsReadThenImportedOnFirstAppend(t *testing.T) {
repo := testRepo(t)
- legacy := []byte("legacy\n")
- if err := os.WriteFile(filepath.Join(repo, LogFile), legacy, 0o644); err != nil {
+ data := []byte("legacy\n")
+ if err := os.WriteFile(filepath.Join(repo, legacy.logFile), data, 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) {
+ if got, err := store.Read(); err != nil || string(got) != string(data) {
t.Fatalf("pre-migration read = %q, %v", got, err)
}
if err := store.Append(func(current []byte) ([]byte, error) {
- if string(current) != string(legacy) {
+ if string(current) != string(data) {
t.Fatalf("migration callback saw %q", current)
}
return []byte("sidecar\n"), nil
@@ -96,6 +97,131 @@ func TestLegacyLogIsReadThenImportedOnFirstAppend(t *testing.T) {
}
}
+// A repository written by the pre-rebrand tool must keep working, and must stop
+// carrying the old name once notevi has touched it.
+func TestPreRebrandSidecarIsRenamedInPlace(t *testing.T) {
+ repo := testRepo(t)
+ rev := writeLegacySidecar(t, repo, "old\n")
+
+ store, err := Open(repo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got, err := store.Read(); err != nil || string(got) != "old\n" {
+ t.Fatalf("read of an un-migrated sidecar = %q, %v", got, err)
+ }
+
+ migrated, err := store.Migrate()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !migrated {
+ t.Fatal("Migrate reported nothing to do for a legacy sidecar")
+ }
+
+ if got := strings.TrimSpace(testJJ(t, repo, "config", "get", "revset-aliases."+Alias)); got != rev {
+ t.Fatalf("alias %s = %q, want the original change %q", Alias, got, rev)
+ }
+ if out, err := exec.Command("jj", "--ignore-working-copy", "-R", repo,
+ "config", "get", "revset-aliases."+legacy.alias).CombinedOutput(); err == nil {
+ t.Fatalf("legacy alias %s survived: %s", legacy.alias, out)
+ }
+ private := testJJ(t, repo, "config", "get", "git.private-commits")
+ if !containsRevsetName(private, Alias) || containsRevsetName(private, legacy.alias) {
+ t.Fatalf("private commits = %q, want %s and not %s", private, Alias, legacy.alias)
+ }
+ files := testJJ(t, repo, "file", "list", "-r", "exactly("+Alias+", 1)")
+ if !strings.Contains(files, LogFile) || strings.Contains(files, legacy.logFile) {
+ t.Fatalf("sidecar files = %q, want only %s", files, LogFile)
+ }
+ subject := strings.TrimSpace(testJJ(t, repo, "log", "-G",
+ "-r", "exactly("+Alias+", 1)", "-T", "description"))
+ if subject != Description {
+ t.Fatalf("sidecar description = %q, want %q", subject, Description)
+ }
+
+ // The trace survived the rename and the store keeps appending to it.
+ if err := store.Append(func(current []byte) ([]byte, error) {
+ if string(current) != "old\n" {
+ t.Fatalf("append after migration saw %q", current)
+ }
+ return []byte("new\n"), nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := store.Read(); err != nil || string(got) != "old\nnew\n" {
+ t.Fatalf("migrated log = %q, %v", got, err)
+ }
+ if again, err := store.Migrate(); err != nil || again {
+ t.Fatalf("second Migrate = %v, %v; want no-op", again, err)
+ }
+}
+
+// A real trace is hundreds of KB. jj silently declines to auto-track a *new*
+// file in the run working copy past a size limit it does not take from the
+// command line, so a rename that writes the whole log under a name the change
+// has never held loses it — with jj reporting success. Small fixtures do not
+// catch that; this one does.
+func TestLargeLogSurvivesRenameAndAppend(t *testing.T) {
+ repo := testRepo(t)
+ var seed strings.Builder
+ for i := range 5000 {
+ fmt.Fprintf(&seed, `{"time":"2026-07-26T10:00:00Z","op":"read","file":"src/%d.rs","start":1,"end":400}`+"\n", i)
+ }
+ if seed.Len() < 256<<10 {
+ t.Fatalf("fixture is only %d bytes; too small to exercise the limit", seed.Len())
+ }
+ writeLegacySidecar(t, repo, seed.String())
+
+ store, err := Open(repo)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Migrate(); err != nil {
+ t.Fatal(err)
+ }
+ got, err := store.Read()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(got) != seed.String() {
+ t.Fatalf("renamed log is %d bytes, want %d", len(got), seed.Len())
+ }
+ if err := store.Append(func([]byte) ([]byte, error) { return []byte(`{"op":"note"}`), nil }); err != nil {
+ t.Fatal(err)
+ }
+ if got, err = store.Read(); err != nil || len(got) <= seed.Len() {
+ t.Fatalf("append after a large rename left %d bytes, %v", len(got), err)
+ }
+}
+
+// writeLegacySidecar builds the sidecar exactly as the pre-rebrand tool did and
+// returns its change id.
+func writeLegacySidecar(t *testing.T, repo, content string) string {
+ t.Helper()
+ testJJ(t, repo, "new", "--no-edit", "root()", "-m", legacy.description)
+ rev := strings.TrimSpace(testJJ(t, repo, "log", "-G", "-T", "change_id",
+ "-r", fmt.Sprintf("root()+ & subject(exact:%q)", legacy.description)))
+ tmp := filepath.Join(t.TempDir(), "seed")
+ if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ // Touch first, then fill: the same two steps the store uses, so a large
+ // fixture really lands in the sidecar instead of quietly seeding nothing.
+ testJJ(t, repo, "--config", `snapshot.auto-track="all()"`,
+ "run", "--clean", "--root", "-r", "exactly("+rev+", 1)", "--",
+ "sh", "-c", ": > "+legacy.logFile)
+ testJJ(t, repo, "--config", `snapshot.auto-track="all()"`,
+ "run", "--clean", "--root", "-r", "exactly("+rev+", 1)", "--",
+ "sh", "-c", "cat \"$1\" >> "+legacy.logFile, "sh", tmp)
+ if got := testJJ(t, repo, "file", "show", "-r", "exactly("+rev+", 1)", "--", legacy.logFile); got != content {
+ t.Fatalf("legacy fixture is %d bytes in the sidecar, want %d", len(got), len(content))
+ }
+ testJJ(t, repo, "config", "set", "--repo", "revset-aliases."+legacy.alias, strconv.Quote(rev))
+ testJJ(t, repo, "config", "set", "--repo", "git.private-commits", strconv.Quote("(none()) | "+legacy.alias))
+ return rev
+}
+
func TestConcurrentAppendsAreSerialized(t *testing.T) {
repo := testRepo(t)
store, err := Open(repo)