261017.12 · 7d3aa432cb8e6ec51aa21d24e3344d38f6d68c48
Git answers are cached within a memory budget
Most pages ran two git subprocesses, about 3 ms of a 3.3 ms doc page. The server now keeps which repo each directory lies in and the histories it shows, and reads HEAD from the repo's files. A history is kept under the commit HEAD names, so a new commit misses the cache. A doc page renders in about 0.6 ms on a repeat view.
--cache sets the memory for it, default 32M; 0 caches nothing.
reading declarations
Changes to githead.go. All files
@@ -0,0 +1,147 @@+package main++import (+ "bufio"+ "context"+ "os"+ "path/filepath"+ "strings"+)++// repo is the git repo a directory lies in, as git found it, or git's+// complaint when it lies in none.+type repo struct {+ gitDir, common, top string+ complaint string+}++// repoAt asks git once which repo dir lies in, so git's own checks, such as+// safe.directory, decide. The answer is kept until a .git appears or goes+// where it would change: between dir and the repo's top, or above dir when+// dir lies in no repo.+func repoAt(ctx context.Context, dir string) repo {+ key := "repo\x00" + dir+ if v, ok := gitCache.get(key); ok {+ if r := v.(repo); r.holds(dir) {+ return r+ }+ gitCache.drop(key)+ }+ var r repo+ out, err := git(ctx, dir, "rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir", "--show-toplevel")+ if err != nil {+ // Inside a git directory there is no work tree to name.+ out, err = git(ctx, dir, "rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir")+ }+ if err != nil {+ r.complaint = string(out) + err.Error()+ } else {+ f := strings.Split(strings.TrimSpace(string(out)), "\n")+ r.gitDir, r.common = f[0], f[1]+ if len(f) > 2 {+ r.top = f[2]+ }+ }+ gitCache.put(key, r, int64(len(r.gitDir)+len(r.common)+len(r.top)+len(r.complaint)))+ return r+}++// holds reports whether r still answers for dir: its git directory is+// there, and no .git has appeared or gone in the levels that decide.+func (r repo) holds(dir string) bool {+ if r.complaint != "" {+ for d := dir; ; d = filepath.Dir(d) {+ if exists(filepath.Join(d, ".git")) {+ return false+ }+ if d == filepath.Dir(d) {+ return true+ }+ }+ }+ if !exists(filepath.Join(r.gitDir, "HEAD")) {+ return false+ }+ if r.top == "" {+ return true+ }+ for d := dir; d != r.top; d = filepath.Dir(d) {+ if exists(filepath.Join(d, ".git")) || d == filepath.Dir(d) {+ return false+ }+ }+ return exists(filepath.Join(r.top, ".git"))+}++func exists(p string) bool {+ _, err := os.Lstat(p)+ return err == nil+}++// head names the commit HEAD points at. It reads the repo's files, which+// takes microseconds where git takes a millisecond, and asks git only for+// what it cannot read: a reftable store or a ref it does not find.+func (r repo) head(ctx context.Context) string {+ if h, ok := r.readHead(); ok {+ return h+ }+ out, err := git(ctx, r.gitDir, "rev-parse", "HEAD")+ if err != nil {+ return "no HEAD: " + err.Error()+ }+ return strings.TrimSpace(string(out))+}++func (r repo) readHead() (string, bool) {+ if exists(filepath.Join(r.common, "reftable")) {+ return "", false+ }+ ref := "HEAD"+ for range 5 {+ val, ok := r.readRef(ref)+ if !ok {+ return "", false+ }+ target, symbolic := strings.CutPrefix(val, "ref: ")+ if !symbolic {+ return val, isHash(val)+ }+ ref = target+ }+ return "", false+}++// readRef reads a ref from the worktree's git directory, then the common+// one, then packed-refs.+func (r repo) readRef(ref string) (string, bool) {+ for _, d := range []string{r.gitDir, r.common} {+ if b, err := os.ReadFile(filepath.Join(d, filepath.FromSlash(ref))); err == nil {+ return strings.TrimSpace(string(b)), true+ }+ }+ f, err := os.Open(filepath.Join(r.common, "packed-refs"))+ if err != nil {+ return "", false+ }+ defer f.Close()+ sc := bufio.NewScanner(f)+ for sc.Scan() {+ hash, name, ok := strings.Cut(sc.Text(), " ")+ if ok && name == ref {+ return hash, true+ }+ }+ return "", false+}++func isHash(s string) bool {+ if len(s) != 40 && len(s) != 64 {+ return false+ }+ for _, c := range s {+ if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {+ return false+ }+ }+ return true+}