diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 23:43:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-03 09:54:39 -0300 |
| commit | 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch) | |
| tree | f12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/jj.go | |
| parent | a9263daee9413c5eff3c1f1bebcd224442fd8685 (diff) | |
| download | notevi-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 'vrsite/jj.go')
| -rw-r--r-- | vrsite/jj.go | 298 |
1 files changed, 0 insertions, 298 deletions
diff --git a/vrsite/jj.go b/vrsite/jj.go deleted file mode 100644 index 0b5cbdf..0000000 --- a/vrsite/jj.go +++ /dev/null @@ -1,298 +0,0 @@ -// jj.go — every read of the repository goes through here. -// -// These helpers never modify project history or its working copy -// (--ignore-working-copy means they never even snapshot it). Sidecar mutation -// is isolated in the shared sidecar package. -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "time" -) - -// Field separator inside a templated line, record marker at its start. Both -// are ASCII control characters that cannot appear in ids, names or a -// description's first line, so parsing stays trivial even under a graph. -const ( - jjRec = "\x1e" // as jj prints it - jjSep = "\x1f" - // as it is written inside a jj template string literal - jjRecEsc = `\x1e` - jjSepEsc = `\x1f` -) - -const jjTimeout = 30 * time.Second - -// jjRun executes jj read-only in repo. stderr comes back inside the error so -// the page can show what jj complained about. -func jjRun(repo string, args ...string) ([]byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), jjTimeout) - defer cancel() - cmd := exec.CommandContext(ctx, "jj", append([]string{"--ignore-working-copy"}, args...)...) - cmd.Dir = repo - var errb strings.Builder - cmd.Stderr = &errb - out, err := cmd.Output() - if err != nil { - msg := strings.TrimSpace(errb.String()) - if msg == "" { - msg = err.Error() - } - return out, fmt.Errorf("jj %s: %s", strings.Join(args, " "), msg) - } - return out, nil -} - -// mustJJ is the static-build path: a jj failure there is fatal. -func mustJJ(repo string, args ...string) []byte { - out, err := jjRun(repo, args...) - if err != nil { - fmt.Fprintln(os.Stderr, err) - fatal("repository read failed") - } - return out -} - -// checkRevset rejects input that jj would read as a flag; revsets themselves -// are a query language, not a shell, and are passed as one argv entry. -func checkRevset(r string) error { - r = strings.TrimSpace(r) - if strings.HasPrefix(r, "-") { - return fmt.Errorf("revset must not start with %q", "-") - } - if len(r) > 512 { - return fmt.Errorf("revset too long") - } - return nil -} - -func resolveChange(repo, rev string) (string, error) { - change, _, err := resolveRevision(repo, rev) - return change, err -} - -func resolveRevision(repo, rev string) (change, commit string, err error) { - if rev == "" { - rev = "@" - } - if err := checkRevset(rev); err != nil { - return "", "", err - } - out, err := jjRun(repo, "log", "-r", "exactly(("+rev+"), 1)", "--no-graph", - "-T", "change_id ++ \"\\n\" ++ commit_id ++ \"\\n\"") - if err != nil { - return "", "", err - } - ids := strings.Fields(string(out)) - if len(ids) != 2 { - return "", "", fmt.Errorf("revset %q did not resolve to one change and commit", rev) - } - return ids[0], ids[1], nil -} - -// changeBrief resolves a revset to the one change it names, with everything a -// page says about it. One call answers both "which change" and "which commit -// is under it right now" — the second is what content is cached against, since -// a change id is a pointer and the commit beneath it is rewritten on every -// edit. -func changeBrief(repo, rev string) (Row, error) { - rows, err := LogRows(repo, rev, "", 1) - if err != nil { - return Row{}, err - } - for _, r := range rows { - if !r.Blank() { - return r, nil - } - } - return Row{}, fmt.Errorf("revset %q matches no change", rev) -} - -func fileList(repo, rev string) ([]string, error) { - out, err := jjRun(repo, "file", "list", "-r", rev) - if err != nil { - return nil, err - } - return strings.Split(strings.TrimRight(string(out), "\n"), "\n"), nil -} - -func fileShow(repo, rev, path string) ([]byte, error) { - return jjRun(repo, "file", "show", "-r", rev, "--", path) -} - -// ── log / op log rows ───────────────────────────────────────────────────── - -// A Row is one line of jj output: the graph art it was drawn with, plus the -// fields the inline template produced. Graph-only lines (│, ~) carry Graph -// alone and render as decoration. -type Row struct { - Graph string - ID, Short string - Commit, CommitShort string - Author, When, End string - Refs, Flags, Desc string - Op bool -} - -func (r Row) Blank() bool { return r.ID == "" } - -// tmplFields builds `"\x1e" ++ f1 ++ "\x1f" ++ f2 ... ++ "\n"`: one line per -// row, record-marked so the graph art in front of it is separable. -func tmplFields(fields ...string) string { - return `"` + jjRecEsc + `" ++ ` + strings.Join(fields, ` ++ "`+jjSepEsc+`" ++ `) + ` ++ "\n"` -} - -var ( - commitTmpl = tmplFields( - "change_id", - "change_id.short(8)", - "commit_id", - "commit_id.short(8)", - "author.email()", - `committer.timestamp().format("%Y-%m-%d %H:%M")`, - `bookmarks.join(" ")`, - `separate(" ", if(current_working_copy, "working copy"), if(empty, "empty"), if(conflict, "conflict"), if(divergent, "divergent"))`, - "description.first_line()", - ) - opTmpl = tmplFields( - "id", - "id.short(12)", - `""`, - `""`, - "user", - `time.start().format("%Y-%m-%d %H:%M:%S")`, - `""`, - `time.end().format("%H:%M:%S")`, - "description.first_line()", - ) -) - -func parseRows(out []byte, op bool) []Row { - var rows []Row - for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") { - i := strings.Index(line, jjRec) - if i < 0 { - if strings.TrimSpace(line) != "" { - rows = append(rows, Row{Graph: line, Op: op}) - } - continue - } - f := strings.Split(line[i+len(jjRec):], jjSep) - for len(f) < 9 { - f = append(f, "") - } - r := Row{Graph: line[:i], ID: f[0], Short: f[1], Commit: f[2], CommitShort: f[3], - Author: f[4], When: f[5], Refs: f[6], Flags: f[7], Desc: f[8], Op: op} - if op { - r.End, r.Flags = r.Flags, "" - } - rows = append(rows, r) - } - return rows -} - -// LogRows runs `jj log -r REVSET` with the inline template. atOp, when set, -// runs the query as of that operation — the debugging view. -func LogRows(repo, revset, atOp string, limit int) ([]Row, error) { - if err := checkRevset(revset); err != nil { - return nil, err - } - args := []string{"log", "-T", commitTmpl} - if strings.TrimSpace(revset) != "" { - args = append(args, "-r", revset) - } - if atOp != "" { - if err := checkRevset(atOp); err != nil { - return nil, err - } - args = append(args, "--at-operation", atOp) - } - if limit > 0 { - args = append(args, "-n", fmt.Sprint(limit)) - } - out, err := jjRun(repo, args...) - if err != nil { - return nil, err - } - return parseRows(out, false), nil -} - -func OpRows(repo string, limit int) ([]Row, error) { - args := []string{"op", "log", "-T", opTmpl} - if limit > 0 { - args = append(args, "-n", fmt.Sprint(limit)) - } - out, err := jjRun(repo, args...) - if err != nil { - return nil, err - } - return parseRows(out, true), nil -} - -// ── one change ──────────────────────────────────────────────────────────── - -type ChangeFile struct { - Status, Path string -} - -type Change struct { - Row Row - Desc string - Files []ChangeFile - Stat string - Parents []Row -} - -var statusName = map[string]string{"A": "added", "M": "modified", "D": "deleted", "R": "renamed", "C": "copied"} - -func LoadChange(repo, rev string) (*Change, error) { - rows, err := LogRows(repo, rev, "", 1) - if err != nil { - return nil, err - } - var row Row - for _, r := range rows { - if !r.Blank() { - row = r - break - } - } - if row.Blank() { - return nil, fmt.Errorf("no change matches %q", rev) - } - c := &Change{Row: row} - // full description separately: it is multi-line, so it cannot ride along - // in the one-line-per-row template - if out, err := jjRun(repo, "log", "-r", row.ID, "--no-graph", "-n", "1", "-T", "description"); err == nil { - c.Desc = strings.TrimRight(string(out), "\n") - } - if out, err := jjRun(repo, "diff", "-r", row.ID, "--summary"); err == nil { - for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") { - if len(line) > 2 { - st := statusName[line[:1]] - if st == "" { - st = line[:1] - } - c.Files = append(c.Files, ChangeFile{Status: st, Path: line[2:]}) - } - } - } - if out, err := jjRun(repo, "diff", "-r", row.ID, "--stat"); err == nil { - lines := strings.Split(strings.TrimRight(string(out), "\n"), "\n") - if n := len(lines); n > 0 { - c.Stat = strings.TrimSpace(lines[n-1]) - } - } - if rows, err := LogRows(repo, "parents("+row.ID+")", "", 8); err == nil { - for _, r := range rows { - if !r.Blank() { - c.Parents = append(c.Parents, r) - } - } - } - return c, nil -} |
