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/static.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/static.go')
| -rw-r--r-- | vrsite/static.go | 247 |
1 files changed, 0 insertions, 247 deletions
diff --git a/vrsite/static.go b/vrsite/static.go deleted file mode 100644 index 0a8eec0..0000000 --- a/vrsite/static.go +++ /dev/null @@ -1,247 +0,0 @@ -// static.go — the offline export: the same pages written out as files. -// -// Everything that needs a server (comments, live activity, jj queries) is -// omitted; the trace/notes toggle, the file finder and the note filters all -// keep working, since they run in the browser. -package main - -import ( - "bytes" - "fmt" - "html/template" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "time" -) - -func run(dir string, name string, args ...string) []byte { - cmd := exec.Command(name, args...) - cmd.Dir = dir - out, err := cmd.Output() - if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - os.Stderr.Write(ee.Stderr) - } - fatal("%s %s: %v", name, strings.Join(args, " "), err) - } - return out -} - -func buildSite(log logStore, repoAbs, out, title string, theme, light *Theme, themes *themeSet) { - start := time.Now() - entries := readLog(log) - ranked := buildModel(title, entries, "").Ranked - if len(ranked) == 0 { - fatal("log has no jj change ids") - } - // the most-logged change this repo actually has: a log can span repos, and - // the busiest change in it may belong to another one - var change string - var list []byte - for _, c := range ranked { - out, err := jjRun(repoAbs, "file", "list", "-r", c) - if err == nil { - change, list = c, out - break - } - } - if change == "" { - fatal("none of the log's %d changes are in %s — logs are per repository; point -repo at the one the trace was recorded in", - len(ranked), repoAbs) - } - // an export is a picture of one change, so it holds only what was - // recorded there; the notes page says how much of the log that leaves out - site := buildModel(title, entries, change) - site.Change = change - site.Theme, site.Light, site.Repo = theme, light, repoAbs - site.All = site.filesAt(strings.Split(strings.TrimRight(string(list), "\n"), "\n"), change) - - commit := strings.TrimSpace(string(mustJJ(repoAbs, "log", "-r", site.Change, "--no-graph", "-T", "commit_id"))) - tmp, err := os.MkdirTemp("", "vrsite-") - if err != nil { - fatal("%v", err) - } - defer os.RemoveAll(tmp) - archive := exec.Command("git", "-C", repoAbs, "archive", commit) - untar := exec.Command("tar", "-x", "-C", tmp) - untar.Stdin, _ = archive.StdoutPipe() - untar.Stderr = os.Stderr - if err := untar.Start(); err != nil { - fatal("tar: %v", err) - } - if err := archive.Run(); err != nil { - fatal("git archive %s: %v", short(commit), err) - } - if err := untar.Wait(); err != nil { - fatal("tar: %v", err) - } - - writeSite(site, out, tmp, themes) - stubs := 0 - for _, f := range site.All { - if f.Stub != "" { - stubs++ - } - } - fmt.Printf("vrsite: %d files (%d traced, %d not rendered), %d notes, %d agents in %s -> %s\n", - len(site.All), len(site.Order), stubs, len(site.Notes), len(site.Agents), - time.Since(start).Round(time.Millisecond), out) -} - -func writeSite(site *Site, out, srcDir string, themes *themeSet) { - if themes == nil { - themes = &themeSet{} - } - hl := newHighlighter(site.Theme, site.Light) - tpl := newTemplates(hl) - if err := os.MkdirAll(out, 0o755); err != nil { - fatal("%v", err) - } - - each := func(fn func(f *File)) { - sem := make(chan struct{}, runtime.NumCPU()) - var wg sync.WaitGroup - for _, f := range site.All { - wg.Add(1) - sem <- struct{}{} - go func(f *File) { - defer func() { <-sem; wg.Done() }() - fn(f) - }(f) - } - wg.Wait() - } - // phase A: content — coverage percentages must exist before any tree - // or index below is written - each(func(f *File) { - src, err := os.ReadFile(filepath.Join(srcDir, f.Path)) - switch { - case err != nil: - f.Stub = "not present in the archived commit" - case bytes.IndexByte(src, 0) >= 0: - f.Stub = "binary file — not rendered" - case len(src) > 2<<20: - f.Stub = fmt.Sprintf("file too large to render (%d KB)", len(src)/1024) - default: - f.render(src, hl) - } - }) - - cf := mustCreate(filepath.Join(out, "style.css")) - if err := renderCSS(cf, site); err != nil { - fatal("css: %v", err) - } - cf.Close() - for _, name := range []string{"app.js"} { - b, err := tfs.ReadFile("assets/" + name) - if err != nil { - fatal("%v", err) - } - if err := os.WriteFile(filepath.Join(out, name), b, 0o644); err != nil { - fatal("%v", err) - } - } - // the picker travels with the export: a <script src>/<link> pair loads - // from the filesystem, which is why the menu is a script and not JSON - for name, b := range map[string][]byte{"themes.css": themes.CSS, "themes.js": themes.JS} { - if err := os.WriteFile(filepath.Join(out, name), b, 0o644); err != nil { - fatal("%v", err) - } - } - - // shared tree fragment: the only nav tree, fetched by every page; "@@" is - // replaced with the page root by app.js, since the fragment is fetched - // from any page depth - treeFrag := renderTree(site.All, func(p string) string { return "@@code/" + p + ".html" }) - if err := os.WriteFile(filepath.Join(out, "tree.html"), []byte(treeFrag), 0o644); err != nil { - fatal("%v", err) - } - - facets := site.Facets() - var info *Row - if rows, err := LogRows(site.Repo, site.Change, "", 1); err == nil { - for _, row := range rows { - if !row.Blank() { - r := row - info = &r - break - } - } - } - render := func(path string, p *page) { - p.Site, p.Facets = site, facets - p.ChangeID, p.Default, p.ChangeInfo = site.Change, site.Change, info - p.Scope, p.Here = "change", site.NotesAt[site.Change] - p.Elsewhere = site.NotesTotal - p.Here - p.Files = site.All - f := mustCreate(path) - if err := tpl.Execute(f, p); err != nil { - fatal("render %s: %v", path, err) - } - f.Close() - } - - latest := site.FilterNotes(NoteFilter{}) - if len(latest) > 24 { - latest = latest[:24] - } - render(filepath.Join(out, "index.html"), &page{Kind: "index", Title: site.Title, Notes: latest}) - - fullTree := renderTree(site.All, func(p string) string { return "code/" + p + ".html" }) - render(filepath.Join(out, "files.html"), &page{Kind: "files", Title: "files — " + site.Title, - Tree: template.HTML(fullTree)}) - - render(filepath.Join(out, "notes.html"), &page{Kind: "notes", Title: "notes — " + site.Title, - Notes: site.FilterNotes(NoteFilter{})}) - - // phase B: every file page, in parallel - each(func(f *File) { - dst := filepath.Join(out, "code", f.Path+".html") - os.MkdirAll(filepath.Dir(dst), 0o755) - depth := strings.Count(f.Path, "/") + 1 - render(dst, &page{Kind: "file", Title: f.Path + " — " + site.Title, - Root: strings.Repeat("../", depth), Current: f.Path, File: f}) - }) - - // phase C: a page per note, so a permalink handed to someone else opens - // in the export exactly as it does in the live app. The source is read - // again rather than kept from phase A: an export is a batch job, and the - // alternative is holding every file in memory for the sake of a few - // hundred excerpts. - if err := os.MkdirAll(filepath.Join(out, "note"), 0o755); err != nil { - fatal("%v", err) - } - for _, n := range site.Notes { - p := &page{Kind: "note", Title: noteTitle(n) + " — " + site.Title, - Root: "../", Current: n.File, Note: n, Thread: site.NoteThread(n.ID)} - if n.File != "" && n.Start > 0 { - src, err := os.ReadFile(filepath.Join(srcDir, n.File)) - switch { - case err != nil: - p.ExErr = "not present in the archived commit" - case bytes.IndexByte(src, 0) >= 0: - p.ExErr = "binary file — not rendered" - default: - var cov []uint8 - if f, ok := site.Files[n.File]; ok { - cov = f.Cov - } - p.Excerpt, p.ExFrom, p.ExTo = excerptFor(n.File, src, cov, hl, n) - } - } - render(filepath.Join(out, "note", strconv.Itoa(n.ID)+".html"), p) - } -} - -func mustCreate(path string) *os.File { - f, err := os.Create(path) - if err != nil { - fatal("%v", err) - } - return f -} |
