summaryrefslogtreecommitdiff
path: root/vrsite/jj.go
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 /vrsite/jj.go
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 'vrsite/jj.go')
-rw-r--r--vrsite/jj.go298
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
-}