261026.10 · cb28c9556139b6aaddb064d0ae5f117d33a2e8b1
Stardates that could mislead now say what they name
Unpushed commits share their day and x, so a history's and the timeline's rows add each one's short hash, eg 261026.x 6376dd4. A repo whose main branch is neither main nor master follows the branch origin's HEAD names, after main and master, so no numbered repo is renumbered. A day alone, eg 261024, is refused as a commit's name, though six digits also read as hex. A declaration's diff page shows the full hash, as the commit page does.
reading declarations
@@ -1,6 +1,6 @@ # History -Add `?log` to a path for its git history: the commits that touched it, newest first, one line each with its stardate and subject. The stardate leads, so the stardates line up, and the version menu orders its rows the same way. A line opens to the commit's full message and its diff, limited to the path; the diff loads when the line first opens. A commit with no message shows its day alone. Each commit shows its stardate, eg 261024.4, the fourth commit of 24 Oct 2026 on the repo's main line: the first-parent line of origin's main or master, else the local main or master. A commit a merge brought in is named after the merge, eg 261024.4.2. A commit on the local main or master that origin's line has not reached yet shows its day and x, eg 261026.x: its number comes with the push, so a rebase before it renames nothing. Its version's address uses the hash. A commit on no main line, eg on another branch, shows its short hash. Hover shows the hash and the exact time. A row is one target: a click anywhere on it opens or closes the fold. Inside, the full hash links the commit's own page and browse links its version. A directory's history covers everything below it. A listing whose directory has commits opens its history from its first entry, `<(git log)`. A doc or code page with commits opens with a line that says where the reader is: current when the page matches its last commit, changed when it has edits since. Hover names the commit by stardate and subject. www compares git's id for the file with the one HEAD keeps, which takes microseconds, and asks `git status` only when they differ. A ▸ leads the line, the browser's own sign that it opens, and turns to ▾ while open. A click anywhere on the line opens the timeline, a menu of every version of the page, newest first, then the history; `?timeline` serves it as a page. The version shown is marked. The newest row leads to the current page and says current, or, when the page has uncommitted changes, a row of its own above it says so. The menu loads when the pointer first passes over the line, so it opens full. Up and Down walk it, Enter opens a version, and Escape or a click outside closes it. On a share, where there are no versions, its rows open the commits. A page outside a repo shows git's complaint.+Add `?log` to a path for its git history: the commits that touched it, newest first, one line each with its stardate and subject. The stardate leads, so the stardates line up, and the version menu orders its rows the same way. A line opens to the commit's full message and its diff, limited to the path; the diff loads when the line first opens. A commit with no message shows its day alone. Each commit shows its stardate, eg 261024.4, the fourth commit of 24 Oct 2026 on the repo's main line: the first-parent line of origin's main or master, else of the branch origin's HEAD names, eg origin/trunk, else of the local main or master. A commit a merge brought in is named after the merge, eg 261024.4.2. A commit on the local main or master that origin's line has not reached yet shows its day and x, eg 261026.x: its number comes with the push, so a rebase before it renames nothing. Several commits can share a day and x, so a history or the timeline adds each one's short hash, eg 261026.x 6376dd4. Its version's address uses the hash. A commit on no main line, eg on another branch, shows its short hash. Hover shows the hash and the exact time. A row is one target: a click anywhere on it opens or closes the fold. Inside, the full hash links the commit's own page and browse links its version. A directory's history covers everything below it. A listing whose directory has commits opens its history from its first entry, `<(git log)`. A doc or code page with commits opens with a line that says where the reader is: current when the page matches its last commit, changed when it has edits since. Hover names the commit by stardate and subject. www compares git's id for the file with the one HEAD keeps, which takes microseconds, and asks `git status` only when they differ. A ▸ leads the line, the browser's own sign that it opens, and turns to ▾ while open. A click anywhere on the line opens the timeline, a menu of every version of the page, newest first, then the history; `?timeline` serves it as a page. The version shown is marked. The newest row leads to the current page and says current, or, when the page has uncommitted changes, a row of its own above it says so. The menu loads when the pointer first passes over the line, so it opens full. Up and Down walk it, Enter opens a version, and Escape or a click outside closes it. On a share, where there are no versions, its rows open the commits. A page outside a repo shows git's complaint. A history lists the 500 newest commits. Bodies wrapped by hand join into paragraphs; a list item or an indented line keeps its own line. A commit's hash in a history's fold, or commit at the right end of a version's top line, opens the commit on a page of its own: its stardate and full hash as plain text, to select and give to git, then its message and its diff, limited to the file or directory the history was for, with a link to all the files it changed in the tree. Added lines sit on a green tint and removed ones on a red. The diff stops at 1 MiB. From the terminal, `www --commit=261024.4` prints a commit's full hash, stardate and subject as keyval. @@ -12,4 +12,4 @@ www runs `git log`, `git show` and `git rev-parse` in the directory of what the A shared server shows history and diffs as loopback does, with four differences. History appears only when other users could read the repo's git directory, its `HEAD`, `objects` and `refs`; nobody reads through www what git run as another user would not show them. Each message loses its trailer block, which carries emails and session links in lines such as `Signed-off-by:` and `Claude-Session:`. As in git, the last paragraph is a trailer block when a quarter of its lines are `Key: value` lines, so a cherry-pick note or a wrapped trailer does not keep it. A page in a repo the share keeps private never reloads on a commit. Diffs leave out files whose names begin with a dot, as listings do. A share never shows git's errors, which name paths on the machine, and says only `No git history here.` No history page shows authors, on a share or on loopback. -Each commit in a history has a browse link, inside its fold, to the page as it was in that commit. Below a tree's address, `@` and a commit's hash serve the tree as of that commit, eg `@5430ac3/` for its root or `@5430ac3/doc/history.md` for one doc. A stardate works the same, eg `@261024.4/`, and a hash of a commit with a stardate redirects to it. Docs, code pages, listings and search work there as on the tree itself, read from git rather than the disk, and relative links stay in the same version. Any length of hash from four characters up works and redirects to git's short form. The top line names the commit that last changed the page as of that version, stardate first as in histories, and opens the same timeline with that version marked. At the line's right end, commit opens the commit's page, and current leads to the page as it is, as does the timeline's first row. The breadcrumbs and the tab title carry the hash, eg `www @ 5430ac3`. A version never changes, so it has no live reload, no Delete lock button and no path to copy, and its docs list no backlinks. www's script still runs there, so keys, the search box and line marks work as on the tree. A tree that holds an entry named like a version, eg a directory `@cafe`, serves that entry instead. Shares and the web address have no versions.+Each commit in a history has a browse link, inside its fold, to the page as it was in that commit. Below a tree's address, `@` and a commit's hash serve the tree as of that commit, eg `@5430ac3/` for its root or `@5430ac3/doc/history.md` for one doc. A stardate works the same, eg `@261024.4/`, and a hash of a commit with a stardate redirects to it. A day alone, eg `@261024/`, names no commit and says so, though its six digits would also read as a hash. Docs, code pages, listings and search work there as on the tree itself, read from git rather than the disk, and relative links stay in the same version. Any length of hash from four characters up works and redirects to git's short form. The top line names the commit that last changed the page as of that version, stardate first as in histories, and opens the same timeline with that version marked. At the line's right end, commit opens the commit's page, and current leads to the page as it is, as does the timeline's first row. The breadcrumbs and the tab title carry the hash, eg `www @ 5430ac3`. A version never changes, so it has no live reload, no Delete lock button and no path to copy, and its docs list no backlinks. www's script still runs there, so keys, the search box and line marks work as on the tree. A tree that holds an entry named like a version, eg a directory `@cafe`, serves that entry instead. Shares and the web address have no versions.
@@ -379,10 +379,10 @@ func (t *tree) gitHead(name string) (gitDir, state string) { return r.gitDir, r.gitDir + "\n" + r.head(ctx) } -// commitLine is a commit's stardate, as a history and a commit page show-// it, with its hash and exact time on hover.-func commitLine(s *stardates, c *commit) string {- return fmt.Sprintf("<span class=\"date\" title=\"%s %s\">%s</span>", template.HTMLEscapeString(c.short), exact(c.date), template.HTMLEscapeString(s.label(c)))+// commitLine is label, a commit's stardate as a history or a commit page+// shows it, with its hash and exact time on hover.+func commitLine(label string, c *commit) string {+ return fmt.Sprintf("<span class=\"date\" title=\"%s %s\">%s</span>", template.HTMLEscapeString(c.short), exact(c.date), template.HTMLEscapeString(label)) } // starsFor are the stardates of the repo name lies in. A version reads@@ -557,7 +557,7 @@ func (t *tree) serveTimeline(w http.ResponseWriter, r *http.Request, name string // Each row leads with its stardate, so they line up down the menu. labels := make([]string, len(log)) for i := range log {- labels[i] = stars.label(&log[i])+ labels[i] = stars.rowLabel(&log[i]) } body.WriteString("<ol class=\"versions\"" + leadStyle(labels) + ">\n") current := (&url.URL{Path: t.prefix + rest}).String()@@ -632,7 +632,7 @@ func (t *tree) serveLog(w http.ResponseWriter, r *http.Request, name string, dir // Each row leads with its stardate, so they line up down the history. labels := make([]string, len(log)) for i := range log {- labels[i] = stars.label(&log[i])+ labels[i] = stars.rowLabel(&log[i]) } body.WriteString("<ol class=\"log\"" + leadStyle(labels) + ">\n") for i := range log {@@ -644,7 +644,7 @@ func (t *tree) serveLog(w http.ResponseWriter, r *http.Request, name string, dir } fmt.Fprintf(&body, "<li><details data-diff=\"%s\">\n<summary><span class=\"own lead\">%s</span> <span class=\"subject\">%s</span></summary>\n"+ "<p class=\"own links\">commit <a href=\"%s\">%s</a>%s</p>\n",- show, commitLine(stars, c), esc(c.subject), show, esc(c.hash), then)+ show, commitLine(labels[i], c), esc(c.subject), show, esc(c.hash), then) for _, para := range paragraphs(c.body) { body.WriteString("<p>" + para + "</p>\n") }@@ -838,7 +838,7 @@ func (t *tree) serveCommit(w http.ResponseWriter, r *http.Request, name string, var body strings.Builder // The commit's full hash follows its stardate as plain text, so it // can be selected and given to git.- fmt.Fprintf(&body, "<div class=\"commit\">\n<p class=\"own\">%s · <span class=\"hash\">%s</span></p>\n<p class=\"subject\">%s</p>\n", commitLine(t.starsFor(r.Context(), name), c), esc(c.hash), esc(c.subject))+ fmt.Fprintf(&body, "<div class=\"commit\">\n<p class=\"own\">%s · <span class=\"hash\">%s</span></p>\n<p class=\"subject\">%s</p>\n", commitLine(t.starsFor(r.Context(), name).label(c), c), esc(c.hash), esc(c.subject)) for _, para := range paragraphs(c.body) { body.WriteString("<p>" + para + "</p>\n") }
@@ -35,4 +35,9 @@ func TestLookupCommit(t *testing.T) { t.Errorf("%s in %s: exit %d, printed %q; want E2 and nothing", c.commit, c.dir, code, b.String()) } }+ // A day alone names many commits, though six digits are hex too.+ var b strings.Builder+ if code := lookupCommit(&b, config{dir: dir, commit: star[:6]}); code != int(syscall.EINVAL) {+ t.Errorf("%s: exit %d, want E22", star[:6], code)+ } }
@@ -809,7 +809,7 @@ func (t *tree) serveDecl(w http.ResponseWriter, r *http.Request, name string, di } } var body strings.Builder- fmt.Fprintf(&body, "<div class=\"commit\">\n<p class=\"own\">%s</p>\n<p class=\"subject\">%s</p>\n</div>\n", commitLine(t.starsFor(ctx, name), c), esc(c.subject))+ fmt.Fprintf(&body, "<div class=\"commit\">\n<p class=\"own\">%s · <span class=\"hash\">%s</span></p>\n<p class=\"subject\">%s</p>\n</div>\n", commitLine(t.starsFor(ctx, name).label(c), c), esc(c.hash), esc(c.subject)) fmt.Fprintf(&body, "<div class=\"diffs\">\n<p class=\"own file\">%s %s in %s%s", esc(strings.Join(ch.what, ", ")), esc(declName), esc(file), esc(ch.whence())) if len(links) > 0 { body.WriteString(" · " + strings.Join(links, " · "))
@@ -3,6 +3,7 @@ package main import ( "context" "maps"+ "os" "path/filepath" "slices" "strconv"@@ -31,12 +32,21 @@ type stardates struct { } // mainRefs are the refs the main line follows, in order of preference.-var mainRefs = []string{"refs/remotes/origin/main", "refs/remotes/origin/master", "refs/heads/main", "refs/heads/master"}+// "" stands for the branch origin's HEAD names, for a repo whose main+// branch has another name, eg origin/trunk. It comes after main and+// master, so naming origin's HEAD renumbers no repo that has them.+var mainRefs = []string{"refs/remotes/origin/main", "refs/remotes/origin/master", "", "refs/heads/main", "refs/heads/master"} // mainTip is the commit at the head of r's main line, and the ref it // came from. func (r repo) mainTip(ctx context.Context) (string, string, bool) { for _, ref := range mainRefs {+ if ref == "" {+ var ok bool+ if ref, ok = r.originHead(ctx); !ok {+ continue+ }+ } if h, ok := r.refTip(ctx, ref); ok { return h, ref, true }@@ -44,6 +54,26 @@ func (r repo) mainTip(ctx context.Context) (string, string, bool) { return "", "", false } +// originHead is the branch origin's HEAD points at, eg+// refs/remotes/origin/trunk, as git clone set it.+func (r repo) originHead(ctx context.Context) (string, bool) {+ var ref string+ if exists(filepath.Join(r.common, "reftable")) {+ out, err := git(ctx, r.gitDir, "symbolic-ref", "-q", "refs/remotes/origin/HEAD")+ if err != nil {+ return "", false+ }+ ref = strings.TrimSpace(string(out))+ } else {+ b, err := os.ReadFile(filepath.Join(r.common, "refs", "remotes", "origin", "HEAD"))+ if err != nil {+ return "", false+ }+ ref, _ = strings.CutPrefix(strings.TrimSpace(string(b)), "ref: ")+ }+ return ref, strings.HasPrefix(ref, "refs/remotes/origin/") && ref != "refs/remotes/origin/HEAD"+}+ // refTip is the commit ref points at. func (r repo) refTip(ctx context.Context, ref string) (string, bool) { if !exists(filepath.Join(r.common, "reftable")) {@@ -241,6 +271,16 @@ func (s *stardates) number(tip string, nodes map[string]starNode) { } } +// rowLabel is label for a row in a list of commits. A day and x, which+// several pending commits share, adds the short hash, eg 261026.x 8392cf3,+// so each row names its own commit.+func (s *stardates) rowLabel(c *commit) string {+ if s != nil && s.pending[c.hash] != "" {+ return s.pending[c.hash] + " " + c.short+ }+ return s.label(c)+}+ // label is what a reader sees for c: its stardate in s, its day and x // while it waits for a push, or its short hash when it has neither. func (s *stardates) label(c *commit) string {
@@ -65,6 +65,14 @@ func TestPendingCommits(t *testing.T) { if h != "a" && p.addr(c) != c.short { t.Errorf("%s addresses as %q, want its hash", h, p.addr(c)) }+ // A list's rows tell pending commits apart by their hash.+ row := want+ if h != "a" {+ row += " " + c.short+ }+ if got := p.rowLabel(c); got != row {+ t.Errorf("%s: row %q, want %q", h, got, row)+ } } if s.pending != nil || len(p.byDate) != 1 || p.perDay["261026"] != 0 { t.Errorf("pending commits were numbered: %v %v", p.byDate, p.perDay)@@ -91,12 +99,41 @@ func TestPendingVersion(t *testing.T) { if body := get(h, "/a/b.go").Body.String(); !strings.Contains(body, `title="`+star+` · `) { t.Errorf("top line names no %s:\n%s", star, body) }+ if body := get(h, "/a/b.go?log").Body.String(); !strings.Contains(body, ">"+star+" "+short+"</span>") {+ t.Errorf("history names no %s %s:\n%s", star, short, body)+ } rec := get(h, "/@"+short+"/a/") if rec.Code != 200 || !strings.Contains(rec.Body.String(), " @ "+star+"</a>") { t.Errorf("version @%s: %d\n%s", short, rec.Code, rec.Body.String()) } } +// A repo whose main branch is neither main nor master follows the branch+// origin's HEAD names.+func TestOriginHeadIsTheMainLine(t *testing.T) {+ _, dir := repoTree(t)+ full := head(t, dir, "HEAD")+ c := &commit{hash: full, short: full[:7]}+ if star := stardatesIn(context.Background(), dir).label(c); star == c.short {+ t.Fatalf("on the local line HEAD has no stardate")+ }+ for _, args := range [][]string{+ {"update-ref", "refs/remotes/origin/trunk", "HEAD~1"},+ {"symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk"},+ } {+ if out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput(); err != nil {+ t.Fatalf("git %v: %v\n%s", args, err, out)+ }+ }+ s := stardatesIn(context.Background(), dir)+ if got := s.label(c); got != c.short {+ t.Errorf("HEAD, past origin/trunk, is %q, want its hash", got)+ }+ if s.byHash[head(t, dir, "HEAD~1")] == "" {+ t.Error("origin/trunk's tip has no stardate")+ }+}+ func fresh() *stardates { return &stardates{byHash: map[string]string{}, byDate: map[string]string{}, perDay: map[string]int{}} }
@@ -29,6 +29,12 @@ var atSegment = regexp.MustCompile(`^@([0-9a-f]{4,40}|[0-9]{6}(?:\.[0-9]+)+)$`) // x, eg 261026.x, as the name of a commit. var stardateRev = regexp.MustCompile(`^[0-9]{6}\.(?:[0-9]+|x)(?:\.[0-9]+)*$`) +// isDay reports whether s is a date as a stardate writes it, eg 261024.+func isDay(s string) bool {+ _, err := time.Parse("060102", s)+ return len(s) == 6 && err == nil+}+ // findCommit is the commit rev names in the repo dir lies in, and that // repo's stardates. rev is a stardate, a day and x that one commit alone // holds while it waits for a push, or anything git rev-parse takes.@@ -36,6 +42,10 @@ func findCommit(ctx context.Context, dir, rev string) (commit, *stardates, error stars, spec := stardatesIn(ctx, dir), rev missing := &errno{syscall.ENOENT, "no commit " + rev + " in this repo"} switch {+ case isDay(rev):+ // Six digits are hex too, but a reader who types a date means+ // the day, which holds many commits.+ return commit{}, stars, &errno{syscall.EINVAL, rev + " is a day, not a commit; add its place, eg " + rev + ".1"} case strings.HasPrefix(rev, "-"): // git would read it as an option. return commit{}, stars, missing