261101.5 · 4c2d0371693dc2f3c3f3293bd77a9e2267d527d3
git.go splits off the history pages
git.go held both running git and the pages built from it. The timeline, log and commit pages and their diffs move to historypage.go with their tests. git.go keeps running git, reading commits and the cache warm.
Declarations moved unchanged, as a sorted comparison of every line outside the import blocks confirms. git blame -C -C traces them.
reading declarations
Changes to git.go. All files
@@ -8,17 +8,12 @@ import ( "encoding/hex" "errors" "fmt"- "html/template" "io" "io/fs"- "net/http"- "net/url" "os" "os/exec"- "path" "path/filepath" "regexp"- "strconv" "strings" "time" )@@ -404,84 +399,6 @@ func (t *tree) gitHead(name string) (gitDir, state string) { return r.gitDir, r.gitDir + "\n" + r.head(ctx) } -// 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-// its tree's own repo.-func (t *tree) starsFor(ctx context.Context, name string) *stardates {- if t.at != nil {- return stardatesIn(ctx, t.dir)- }- dir, _, err := t.gitPlace(name)- if err != nil {- return nil- }- return stardatesIn(ctx, dir)-}--// logLine is the line at the top of a doc or code page in a repo, or ""-// when no commit touched the page. b is the page's file as read.-func (t *tree) logLine(ctx context.Context, name string, b []byte) string {- if t.at != nil {- return t.versionLine(ctx, name, false)- }- c := t.lastCommit(ctx, name)- if c == nil {- return ""- }- state := "current"- if t.pageChanged(ctx, name, b) {- state = "changed"- }- return topLine(c, t.starsFor(ctx, name).label(c), state, "", "", "", "")-}--// topLine is the line at the top of a page in a repo, and the whole line-// is one target: it opens a menu of the page's versions, which loads from-// base?timeline. On the page as it is, the line says only state, current-// or changed, since where the reader is matters more than which commit;-// the commit, by star, its stardate or hash, and subject, is on hover.-// On a version, the line names the commit by star and subject, and now-// and show, links outside the line, lead to the page as it is and to the-// commit's page, which names its full hash. here, when not "", is the URL-// of the version shown, which the menu marks. Without the script the-// menu holds a link to the history.-func topLine(c *commit, star, state, base, here, now, show string) string {- esc := template.HTMLEscapeString- what := star + " · " + subjectOf(c)- // The stardate leads, as it does in histories and the version menu.- line := fmt.Sprintf("<span class=\"date\" title=\"%s %s\">%s</span> · <span class=\"subject\" title=\"%s %s\">%s</span>",- esc(c.short), exact(c.date), esc(star), esc(c.short), exact(c.date), esc(subjectOf(c)))- if state != "" {- line = fmt.Sprintf("<span class=\"state\" title=\"%s · %s %s\">%s</span>", esc(what), esc(c.short), exact(c.date), state)- }- if here != "" {- here = " data-here=\"" + esc(here) + "\""- }- // The links to the page as it is and to the commit float right,- // before the line, so they stay out of the line's one target.- out := ""- if now != "" {- out = fmt.Sprintf("<a class=\"own now\" href=\"%s\">current</a>\n", esc(now))- }- if show != "" {- out += fmt.Sprintf("<a class=\"own now\" href=\"%s\">commit</a>\n", esc(show))- }- // The whole line opens the timeline, so no word labels it; the name- // shows on hover and is read out first.- label := "timeline, " + state + ", " + what- if state == "" {- label = "timeline, " + what- }- return out + fmt.Sprintf("<details class=\"own latest\" data-timeline=\"%s?timeline\"%s>\n<summary title=\"timeline\" aria-label=\"%s\">%s</summary>\n"+- "<div class=\"versions\"><a href=\"%s?log\">%s</a></div>\n</details>\n",- esc(base), here, esc(label), line, esc(base), esc(logLabel))-}- // pageChanged reports whether b, the file of the page at name, differs // from its last commit. Git's id for b, against the id HEAD keeps for // the path, answers in microseconds; git status, which took 16 ms on@@ -540,442 +457,3 @@ func (t *tree) changed(ctx context.Context, name string) bool { out, err := git(ctx, dir, "status", "--porcelain", "-z", "--", spec) return err == nil && len(out) > 0 }--// subjectOf is what a link to a commit says: its subject, or its hash-// when the message is empty, so the link stays visible.-func subjectOf(c *commit) string {- if c.subject == "" {- return c.short- }- return c.subject-}--// exact is a commit's time to the minute, as hover shows it.-func exact(t time.Time) string {- return t.Local().Format("2006-01-02 15:04")-}--// serveTimeline lists every version of name, newest first, for the menu-// on its top line: each commit that touched it, by subject and stardate, then-// its history. The newest commit's row leads to the current page and says-// so, unless the page has changes not yet committed; then a row of its-// own leads there and says that. A page whose history the tree's-// versions do not hold, eg in a share or a nested repo, links each-// commit's page instead.-func (t *tree) serveTimeline(w http.ResponseWriter, r *http.Request, name string, dir bool) {- crumbs := append(t.pageCrumbs(name, dir), crumb{Label: "timeline"})- title := "timeline " + t.title(name)- log, err := t.gitLog(r.Context(), name, maxLog+1)- if err != nil {- t.render(w, r, http.StatusNotFound, page{Title: title, Crumbs: crumbs, Kind: "error",- Body: template.HTML("<pre class=\"error\">" + template.HTMLEscapeString(err.Error()) + "</pre>")})- return- }- if len(log) > maxLog {- log = log[:maxLog]- }- rest := below(name, dir)- versions := t.historyInVersions(r.Context(), name)- stars := t.starsFor(r.Context(), name)- esc := template.HTMLEscapeString- var body strings.Builder- // 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.rowLabel(&log[i])- }- body.WriteString("<ol class=\"versions\"" + leadStyle(labels) + ">\n")- current := (&url.URL{Path: t.prefix + rest}).String()- clean := versions && !t.changed(r.Context(), name)- if versions && !clean {- fmt.Fprintf(&body, "<li><span class=\"date lead\"></span> <a href=\"%s\">current</a> <span class=\"date\">uncommitted changes</span></li>\n", esc(current))- }- for i := range log {- c := &log[i]- href, also, note := "?commit="+c.hash, "", ""- if versions {- href = t.versionURL(stars, c, rest)- }- // The current page is the newest commit's version, so its row- // leads there and is marked on either page.- if clean && i == 0 {- href, also, note = current, fmt.Sprintf(" data-also=\"%s\"", esc(href)), " · current"- }- if note != "" {- note = " <span class=\"date\">current</span>"- }- fmt.Fprintf(&body, "<li%s><span class=\"date lead\">%s</span> <a href=\"%s\" title=\"%s %s\">%s</a>%s</li>\n",- also, esc(labels[i]), esc(href), esc(c.short), exact(c.date), esc(subjectOf(c)), note)- }- fmt.Fprintf(&body, "<li class=\"log\"><a href=\"?log\">%s</a></li>\n</ol>\n", esc(logLabel))- t.render(w, r, http.StatusOK, page{Title: title, Crumbs: crumbs, Kind: "timeline", Body: template.HTML(body.String())})-}--// serveLog lists the commits that touched name, newest first, each with-// its hash, date, subject and body.-func (t *tree) serveLog(w http.ResponseWriter, r *http.Request, name string, dir bool) {- crumbs := append(t.pageCrumbs(name, dir), crumb{Label: logLabel})- title := "git log " + t.title(name)- start := time.Now()- log, err := t.gitLog(r.Context(), name, maxLog+1)- took := time.Since(start)- if err != nil {- t.render(w, r, http.StatusNotFound, page{Title: title, Crumbs: crumbs, Kind: "error",- Body: template.HTML("<pre class=\"error\">" + template.HTMLEscapeString(err.Error()) + "</pre>")})- return- }- stars := t.starsFor(r.Context(), name)- esc := template.HTMLEscapeString- var body strings.Builder- if len(log) == 0 {- shown := t.title(name)- if dir && name != "." {- shown += "/"- }- body.WriteString("<p class=\"own\">No commit touches " + esc(shown) + ".</p>\n")- }- if len(log) > maxLog {- log = log[:maxLog]- fmt.Fprintf(&body, "<p class=\"own\">The %d newest commits. <code>git log</code> lists the rest.</p>\n", maxLog)- }- // git walks every commit back to the ones that touched name, unless- // the repo's commit-graph tells it which paths each commit changed.- // www leaves the repo alone and names the command instead. A share- // says nothing, since its viewer cannot run it.- if took > slowGit && !t.shared && t.lacksPathFilters(r.Context(), name) {- fmt.Fprintf(&body, "<p class=\"own\">git took %d ms for this history. "+- "<code>git commit-graph write --reachable --changed-paths</code> in the repo makes it several times faster.</p>\n",- took.Milliseconds())- }- // Each commit folds out to its message and its diff. The diff comes- // from the commit page when the fold opens, through the one script;- // without it, the link in its place opens that page. A row is one- // target, the fold, so the links to the commit and its version wait- // inside it. A local tree links each commit's version of the page:- // the file or directory as it was then.- browse := t.historyInVersions(r.Context(), name)- // 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.rowLabel(&log[i])- }- body.WriteString("<ol class=\"log\"" + leadStyle(labels) + ">\n")- for i := range log {- c := &log[i]- show := esc("?commit=" + c.hash)- then := ""- if browse {- then = fmt.Sprintf(" · <a class=\"then\" href=\"%s\">browse</a>", esc(t.versionURL(stars, c, below(name, 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(labels[i], c), esc(c.subject), show, esc(c.hash), then)- for _, para := range paragraphs(c.body) {- body.WriteString("<p>" + para + "</p>\n")- }- fmt.Fprintf(&body, "<div class=\"diffs\"><p class=\"own\"><a href=\"%s\">diff</a></p></div>\n</details></li>\n", show)- }- body.WriteString("</ol>\n")- t.render(w, r, http.StatusOK, page{Title: title, Crumbs: crumbs, Kind: "log", Body: template.HTML(body.String())})-}--// pageCrumbs are the crumbs of the page at name, the last a link back to-// it, for a page that hangs below it.-func (t *tree) pageCrumbs(name string, dir bool) []crumb {- crumbs := t.crumbs(name)- href := name- switch {- case name == ".":- href = ""- case dir:- href += "/"- }- crumbs[len(crumbs)-1].Href = (&url.URL{Path: t.prefix + href}).String()- return crumbs-}--// maxDiff caps the bytes of diff a commit page shows.-const maxDiff = 1 << 20--var hexHash = regexp.MustCompile(`^[0-9a-f]{4,64}$`)--// fileDiff is one file's part of a commit's diff: its name in the tree-// and the diff's lines below git's header. A file whose type changed has-// two.-type fileDiff struct {- name string // relative to the tree- rel string // relative to where git runs- lines []string-}--// gitShow reads the commit that hash names and its diff, limited to name.-// A share leaves out files whose names begin with a dot. cut reports a-// diff cut at maxDiff.-func (t *tree) gitShow(ctx context.Context, name, hash string) (c *commit, diffs []fileDiff, cut bool, err error) {- dir, spec, err := t.gitPlace(name)- if err != nil {- return nil, nil, false, t.gitErr(err)- }- if t.shared && !openRepo(ctx, dir) {- return nil, nil, false, errNoHistory- }- // A commit never changes, so its diff is kept under its full hash.- var key string- if r := repoAt(ctx, dir); r.complaint == "" && fullHash(hash) {- key = fmt.Sprintf("show\x00%s\x00%s\x00%s\x00%s\x00%s\x00%t", r.gitDir, dir, spec, name, hash, t.shared)- if v, ok := gitCache.get(key); ok {- s := v.(shown)- return &s.c, s.diffs, s.cut, nil- }- }- // ^{commit} refuses a blob or a tree, which would show a file's text.- rev := hash + "^{commit}"- show := func(args ...string) ([]byte, error) {- args = append([]string{"show", "--no-ext-diff", "--no-textconv", "--no-show-signature", "--no-color",- "--no-renames", "--diff-merges=first-parent", "--relative", "--src-prefix=a/", "--dst-prefix=b/"}, args...)- return git(ctx, dir, args...)- }- out, err := show("-s", "-z", "--format="+commitFormat, rev, "--")- if err != nil {- return nil, nil, false, t.gitErr(err)- }- log := t.commits(out)- if len(log) == 0 {- return nil, nil, false, t.gitErr(errors.New("no commit " + hash))- }- out, err = show("--format=", "--patch", rev, "--", spec)- if err != nil {- return nil, nil, false, t.gitErr(err)- }- if len(out) > maxDiff {- out, cut = out[:bytes.LastIndexByte(out[:maxDiff], '\n')+1], true- }- for _, b := range strings.Split("\n"+string(out), "\ndiff --git ")[1:] {- head, _, _ := strings.Cut(b, "\n")- f, ok := diffName(head)- if !ok || t.shared && hasDotName(f) {- continue- }- rel := f- switch {- case spec != ".":- f = name- case name != ".":- f = path.Join(name, f)- }- diffs = append(diffs, fileDiff{name: f, rel: rel, lines: diffLines(b)})- }- if key != "" {- c := log[0]- size := int64(len(c.hash)+len(c.short)+len(c.subject)+len(c.body)) + 64- for _, d := range diffs {- size += int64(len(d.name)) + 16- for _, l := range d.lines {- size += int64(len(l)) + 16- }- }- gitCache.put(key, shown{c, diffs, cut}, size)- }- return &log[0], diffs, cut, nil-}--// shown is a commit and its diff as gitShow keeps them.-type shown struct {- c commit- diffs []fileDiff- cut bool-}--// fullHash reports whether hash is a whole SHA-1 or SHA-256 name in hex,-// which no later commit can make ambiguous.-func fullHash(hash string) bool {- if len(hash) != 40 && len(hash) != 64 {- return false- }- return !strings.ContainsFunc(hash, func(r rune) bool { return !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f') })-}--// diffName reads the file's name from the header of its diff, a/name-// b/name, which git quotes as C does when the name holds a quote, a-// backslash or a control character. Without renames both names are one.-func diffName(head string) (string, bool) {- if !strings.HasPrefix(head, `"`) {- n := (len(head) - len("a/ b/")) / 2- if n < 1 || !strings.HasPrefix(head, "a/") {- return "", false- }- return head[2 : 2+n], true- }- for i := 1; i < len(head); i++ {- switch head[i] {- case '\\':- i++- case '"':- f, err := strconv.Unquote(head[:i+1])- return strings.TrimPrefix(f, "a/"), err == nil && strings.HasPrefix(f, "a/")- }- }- return "", false-}--// diffLines drops the header git puts above a file's diff, which names-// the file again, and keeps the lines that say what kind of change it is.-func diffLines(block string) []string {- var out []string- lines := strings.Split(strings.TrimSuffix(block, "\n"), "\n")[1:]- for i, line := range lines {- if strings.HasPrefix(line, "@@") {- return append(out, lines[i:]...)- }- switch {- case strings.HasPrefix(line, "index "), strings.HasPrefix(line, "--- "), strings.HasPrefix(line, "+++ "):- case strings.HasPrefix(line, "Binary files "):- out = append(out, "Binary file changed")- default:- out = append(out, line)- }- }- return out-}--// serveCommit shows the commit that hash names: its message and its diff-// limited to name, with a link to the diff across the whole tree. A-// history's fold takes the diff from div.diffs.-func (t *tree) serveCommit(w http.ResponseWriter, r *http.Request, name string, dir bool, hash string) {- crumbs := t.pageCrumbs(name, dir)- crumbs = append(crumbs, crumb{Label: logLabel, Href: crumbs[len(crumbs)-1].Href + "?log"}, crumb{Label: hash})- title := "git show " + hash + " " + t.title(name)- fail := func(err error) {- t.render(w, r, http.StatusNotFound, page{Title: title, Crumbs: crumbs, Kind: "error",- Body: template.HTML("<pre class=\"error\">" + template.HTMLEscapeString(err.Error()) + "</pre>")})- }- if !hexHash.MatchString(hash) {- fail(errors.New("Not a commit hash: " + hash))- return- }- c, diffs, cut, err := t.gitShow(r.Context(), name, hash)- if err != nil {- fail(err)- return- }- crumbs[len(crumbs)-1].Label = c.short- esc := template.HTMLEscapeString- 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).label(c), c), esc(c.hash), esc(c.subject))- for _, para := range paragraphs(c.body) {- body.WriteString("<p>" + para + "</p>\n")- }- body.WriteString("</div>\n")- // The semantic diff comes inline when it is kept or asked for with- // &semantic; otherwise the page asks for it once it shows the plain- // diff.- var sem *semantic- _, asked := r.URL.Query()["semantic"]- if key := t.semanticKey(name, c.hash); key != "" {- if v, ok := semanticCache.get(key); ok {- sem = v.(*semantic)- }- }- if sem == nil && asked {- sem = t.semanticDiff(r.Context(), name, c.hash, diffs)- }- switch {- case sem != nil:- body.WriteString(t.semanticHTML(r.URL.Path, c, sem))- case len(diffs) > 0:- q := r.URL.Query()- q.Set("commit", c.hash)- q.Set("semantic", "")- fmt.Fprintf(&body, "<div class=\"semantic\" data-src=\"%s\"><p class=\"own\">reading declarations</p></div>\n", esc((&url.URL{Path: r.URL.Path, RawQuery: q.Encode()}).String()))- }- body.WriteString("<div class=\"diffs\">\n")- all := (&url.URL{Path: t.prefix, RawQuery: "commit=" + c.hash}).String()- shown := name- if dir {- shown += "/"- }- switch {- case name != "." && len(diffs) == 0:- fmt.Fprintf(&body, "<p class=\"own\">It does not change %s. <a href=\"%s\">All files</a></p>\n", esc(shown), esc(all))- case name != ".":- fmt.Fprintf(&body, "<p class=\"own\">Changes to %s. <a href=\"%s\">All files</a></p>\n", esc(shown), esc(all))- case len(diffs) == 0:- body.WriteString("<p class=\"own\">It changes no file here.</p>\n")- }- for _, d := range diffs {- label := esc(d.name)- if _, err := fs.Stat(t.fsys, d.name); err == nil {- label = fmt.Sprintf("<a href=\"%s\">%s</a>", esc((&url.URL{Path: t.prefix + d.name}).String()), label)- }- var marks map[int][][2]int- if sem != nil {- marks = sem.marks[d.name]- }- fmt.Fprintf(&body, "<p class=\"own file\">%s</p>\n%s", label, diffPre(d.lines, marks))- }- if cut {- fmt.Fprintf(&body, "<p class=\"own\">The diff stops at %d KiB. <code>git show %s</code> shows the rest.</p>\n", maxDiff>>10, esc(c.short))- }- body.WriteString("</div>\n")- t.render(w, r, http.StatusOK, page{Title: title, Crumbs: crumbs, Kind: "commit", Body: template.HTML(body.String())})-}--// diffPre renders a diff's lines, each a block tinted by its kind, with-// the changed byte ranges marks gives by line index.-func diffPre(lines []string, marks map[int][][2]int) string {- var b strings.Builder- b.WriteString("<pre class=\"diff\">")- for i, line := range lines {- class := "meta"- switch {- case strings.HasPrefix(line, "@@"):- class = "hunk"- case strings.HasPrefix(line, "+"):- class = "add"- case strings.HasPrefix(line, "-"):- class = "del"- case strings.HasPrefix(line, " "):- class = "same"- }- fmt.Fprintf(&b, "<span class=\"%s\">%s</span>", class, markLine(line, marks[i]))- }- b.WriteString("</pre>\n")- return b.String()-}--// paragraphs splits a commit body at blank lines and joins the lines of-// each, since most bodies are wrapped by hand to 72 columns. A line that-// starts a list item or is indented keeps its own line.-func paragraphs(body string) []string {- var out []string- for _, para := range strings.Split(body, "\n\n") {- var b strings.Builder- for i, line := range strings.Split(strings.Trim(para, "\n"), "\n") {- if i > 0 {- if listLine(line) {- b.WriteString("<br>")- } else {- b.WriteString(" ")- }- }- b.WriteString(template.HTMLEscapeString(strings.TrimRight(line, " \t")))- }- if b.Len() > 0 {- out = append(out, b.String())- }- }- return out-}--func listLine(line string) bool {- if line == "" || line[0] == ' ' || line[0] == '\t' {- return true- }- if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") || strings.HasPrefix(line, "+ ") {- return true- }- num := strings.TrimLeft(line, "0123456789")- return len(num) < len(line) && (strings.HasPrefix(num, ". ") || strings.HasPrefix(num, ") "))-}