current commit
261029.2 · www --public reaches only the open internet and shows nothing of the machine
package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"net/url"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"syscall"
	"time"
)

// A version is a tree as it was in one commit, served at the tree's URL
// with @ and the commit's stardate after its name, eg
// /scratch/www@261024.4/doc/, or its hash when it has none. The version
// qualifies the tree, so it sits on the tree's name, not below it as an
// entry would; a tree at / has no name, so its versions sit below it, eg
// /@261024.4/. The version is in the path rather than a query, so
// relative links in its docs, listings and breadcrumbs stay in that
// version. A stardate always holds a dot, which no hash does.
var atSegment = regexp.MustCompile(`^@([0-9a-f]{4,40}|[0-9]{6}(?:\.[0-9]+)+)$`)

// stardateRev matches a stardate, eg 261024.4 or 261024.4.2, or a day and
// 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.
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
	case stardateRev.MatchString(rev) && strings.Contains(rev, ".x"):
		var held []string
		if stars != nil {
			for h, d := range stars.pending {
				if d == rev {
					held = append(held, h)
				}
			}
		}
		switch len(held) {
		case 0:
			return commit{}, stars, &errno{syscall.ENOENT, "no commit " + rev + " waits for a push in this repo"}
		case 1:
			spec = held[0]
		default:
			return commit{}, stars, &errno{syscall.EINVAL, fmt.Sprintf("%s names %d commits until they are pushed; give a hash", rev, len(held))}
		}
	case stardateRev.MatchString(rev):
		if stars == nil || stars.byDate[rev] == "" {
			return commit{}, stars, &errno{syscall.ENOENT, "no commit " + rev + " on this repo's main line"}
		}
		spec = stars.byDate[rev]
	}
	out, err := git(ctx, dir, "log", "-1", "--no-show-signature", "-z", "--format="+commitFormat, spec+"^{commit}", "--")
	if err != nil {
		return commit{}, stars, missing
	}
	cs := parseCommits(out)
	if len(cs) == 0 {
		return commit{}, stars, missing
	}
	return cs[0], stars, nil
}

// atVersion is the tree t as it was in the commit seg names, eg @261024.4
// or @5430ac3, and the segment that names it best: its stardate, else
// git's short hash. A commit never changes, so each is built once per
// tree and name.
func (t *tree) atVersion(ctx context.Context, seg string) (*tree, string, error) {
	m := atSegment.FindStringSubmatch(seg)
	if m == nil {
		return nil, "", fs.ErrNotExist
	}
	c, stars, err := findCommit(ctx, t.dir, m[1])
	if err != nil {
		return nil, "", err
	}
	if t.shared {
		c.body = dropTrailers(c.body)
	}
	best := "@" + stars.addr(&c)
	if v, ok := t.versions.Load(best); ok {
		return v.(*tree), best, nil
	}
	v := &tree{dir: t.dir, realDir: t.realDir, prefix: versionPrefix(t.prefix, best), label: t.label, wd: -1, at: &c, atLabel: stars.label(&c), base: t, public: t.public}
	v.fsys = commitFS{dir: t.dir, rev: c.hash, when: c.date}
	got, _ := t.versions.LoadOrStore(best, v)
	return got.(*tree), best, nil
}

// versionPrefix is the URL path of the version seg, eg @261024.4, of the
// tree at prefix: the version on the tree's name, or below / for a tree
// at the root.
func versionPrefix(prefix, seg string) string {
	if prefix == "/" {
		return "/" + seg + "/"
	}
	return strings.TrimSuffix(prefix, "/") + seg + "/"
}

// versionIn reads urlPath as a path in a version of t, eg
// /scratch/www@261024.4/doc/x.md: first is the version, @261024.4, and
// rest the path below it, doc/x.md. A tree at / reads its versions as
// entries, in serve.
func (t *tree) versionIn(urlPath string) (first, rest string, ok bool) {
	if t.prefix == "/" || !t.hasVersions() {
		return "", "", false
	}
	after, found := strings.CutPrefix(urlPath, strings.TrimSuffix(t.prefix, "/")+"@")
	if !found {
		return "", "", false
	}
	seg, rest, _ := strings.Cut(after, "/")
	if !atSegment.MatchString("@" + seg) {
		return "", "", false
	}
	return "@" + seg, rest, true
}

// serveVersion serves rest, a path below a version of t; first names the
// version, eg @261024.4. A hash of a commit with a stardate, or a longer
// or shorter hash than git's own, redirects to the best name. A share has
// no versions, as it shows no history.
func (t *tree) serveVersion(w http.ResponseWriter, r *http.Request, first, rest string) {
	v, seg, err := t.atVersion(r.Context(), first)
	if err != nil {
		name := strings.TrimSuffix(rest, "/")
		if name == "" {
			name = "."
		}
		t.fail(w, r, name, http.StatusNotFound, err.Error())
		return
	}
	if seg != first {
		u := (&url.URL{Path: versionPrefix(t.prefix, seg) + rest}).String()
		if r.URL.RawQuery != "" {
			u += "?" + r.URL.RawQuery
		}
		http.Redirect(w, r, u, http.StatusMovedPermanently)
		return
	}
	v.serve(w, r)
}

// versionLine is the top line of a page in a version: the page's last
// commit as of the version, a link to that commit's page, and the menu of
// the page's versions, whose first row leads to the page as it is now. A version reached through a commit that left
// the page alone, eg by browsing a directory's history, names the commit
// that last changed the page, so the menu can mark it.
func (t *tree) versionLine(ctx context.Context, name string, dir bool) string {
	rest := below(name, dir)
	c := t.base.lastAt(ctx, name, t.at)
	now := (&url.URL{Path: t.base.prefix + rest}).String()
	show := (&url.URL{Path: t.base.prefix + rest, RawQuery: "commit=" + c.hash}).String()
	stars := stardatesIn(ctx, t.dir)
	return topLine(c, stars.label(c), "", now, t.base.versionURL(stars, c, rest), now, show)
}

// lastAt is the newest commit up to at that touched name, a path below
// t, or at itself if git cannot say. A commit never changes, so the
// answer is kept under at's hash.
func (t *tree) lastAt(ctx context.Context, name string, at *commit) *commit {
	spec := "./"
	if name != "." {
		spec = "./" + name
	}
	key := "lastAt\x00" + t.dir + "\x00" + at.hash + "\x00" + spec
	if v, ok := gitCache.get(key); ok {
		return v.(*commit)
	}
	out, err := git(ctx, t.dir, "log", "--no-show-signature", "-z", "--format="+commitFormat, "-1", at.hash, "--", spec)
	if err != nil {
		return at
	}
	c := at
	if cs := t.commits(out); len(cs) > 0 {
		c = &cs[0]
	}
	gitCache.put(key, c, int64(len(c.hash)+len(c.short)+len(c.subject)+len(c.body))+64)
	return c
}

// below is the page at name as a path below its tree's URL: "" for the
// root, and a directory's ends in a slash.
func below(name string, dir bool) string {
	switch {
	case name == ".":
		return ""
	case dir:
		return name + "/"
	}
	return name
}

// hasVersions reports whether t serves its versions at @: a tree as it is
// now, on loopback. A repo on the web address has them too: its clone
// holds every commit and tree, and git fetches the old files a version
// shows from the forge as a page needs them.
func (t *tree) hasVersions() bool {
	return t.at == nil && !t.shared
}

// historyInVersions reports whether the history of the page at name is
// the one t's versions show. Versions read the repo t lies in, at name's
// own path, while a history follows links and runs git where the page's
// file lies, which may be another repo: one nested in t, or one of many
// in a tree that lies in none.
func (t *tree) historyInVersions(ctx context.Context, name string) bool {
	if !t.hasVersions() {
		return false
	}
	dir, spec, err := t.gitPlace(name)
	if err != nil || filepath.Join(dir, spec) != filepath.Join(t.realDir, filepath.FromSlash(name)) {
		return false
	}
	here, there := repoAt(ctx, t.dir), repoAt(ctx, dir)
	return here.complaint == "" && there.complaint == "" && here.gitDir == there.gitDir
}

// versionURL is the URL of rest, a path below t, in commit c's version,
// named by its stardate in s or else its short hash.
func (t *tree) versionURL(s *stardates, c *commit, rest string) string {
	return (&url.URL{Path: versionPrefix(t.prefix, "@"+s.addr(c)) + rest}).String()
}

// commitFS reads a directory as it was in one commit: listings from git
// ls-tree, files from git cat-file. dir is where git runs, so paths are
// relative to it, as the tree's are.
type commitFS struct {
	dir, rev string
	when     time.Time
}

// gitEntry is one name in a listing from git ls-tree.
type gitEntry struct {
	name, typ, obj string
	mode           uint32
	size           int64
}

// list is the directory name as it was, by git ls-tree, kept in the git
// cache under the commit, which never changes.
func (c commitFS) list(name string) ([]gitEntry, error) {
	key := "ls\x00" + c.dir + "\x00" + c.rev + "\x00" + name
	if v, ok := gitCache.get(key); ok {
		return v.([]gitEntry), nil
	}
	spec, treeRev := "./", c.rev+"^{tree}"
	if name != "." {
		spec, treeRev = "./"+name+"/", c.rev+":"+name
	}
	if inCache(c.dir) {
		if err := fetchMissing(context.Background(), c.dir, treeRev, name, c.rev); err != nil {
			return nil, err
		}
	}
	out, err := git(context.Background(), c.dir, "ls-tree", "-l", "-z", c.rev, "--", spec)
	if err != nil {
		return nil, err
	}
	var entries []gitEntry
	size := int64(0)
	for _, rec := range strings.Split(string(out), "\x00") {
		meta, p, ok := strings.Cut(rec, "\t")
		if !ok {
			continue
		}
		f := strings.Fields(meta)
		if len(f) != 4 {
			continue
		}
		mode, _ := strconv.ParseUint(f[0], 8, 32)
		n, _ := strconv.ParseInt(f[3], 10, 64)
		e := gitEntry{name: path.Base(p), typ: f[1], obj: f[2], mode: uint32(mode), size: n}
		entries = append(entries, e)
		size += int64(len(e.name)+len(e.obj)) + 48
	}
	gitCache.put(key, entries, size)
	return entries, nil
}

// fetchMissing fetches the blobs of the directory tree names that a
// partial clone lacks, in one request. ls-tree -l reads each blob's size,
// and git would fetch each missing blob for it in a request of its own,
// about half a second apiece on GitHub: 12 s for fzf's src/. rev-list
// with --missing=print never fetches, and tree:1 keeps it to the
// directory's own entries. name and rev only label the log line.
func fetchMissing(ctx context.Context, dir, tree, name, rev string) error {
	out, err := git(ctx, dir, "rev-list", "--objects", "--missing=print", "--filter=tree:1", tree)
	if err != nil {
		return err
	}
	var missing []string
	for _, l := range strings.Split(string(out), "\n") {
		if obj, ok := strings.CutPrefix(l, "?"); ok {
			missing = append(missing, obj)
		}
	}
	if len(missing) == 0 {
		return nil
	}
	start := time.Now()
	_, err = gitIn(ctx, dir, strings.NewReader(strings.Join(missing, "\n")+"\n"),
		"-c", "fetch.negotiationAlgorithm=noop", "fetch", "--no-tags", "--no-write-fetch-head",
		"--recurse-submodules=no", "--filter=blob:none", "--stdin", "origin")
	logInfo("fetched %d files of %s at %s in %s in %d ms", len(missing), name, rev[:12], tildePath(dir), time.Since(start).Milliseconds())
	return err
}

// entry finds name in its directory's listing.
func (c commitFS) entry(name string) (gitEntry, error) {
	entries, err := c.list(path.Dir(name))
	if err != nil {
		return gitEntry{}, err
	}
	base := path.Base(name)
	for _, e := range entries {
		if e.name == base {
			return e, nil
		}
	}
	return gitEntry{}, fs.ErrNotExist
}

// blob reads a file's content by its object, kept in the git cache.
func (c commitFS) blob(obj string) ([]byte, error) {
	key := "blob\x00" + c.dir + "\x00" + obj
	if v, ok := gitCache.get(key); ok {
		return v.([]byte), nil
	}
	b, err := git(context.Background(), c.dir, "cat-file", "blob", obj)
	if err != nil {
		return nil, err
	}
	gitCache.put(key, b, int64(len(b)))
	return b, nil
}

func (c commitFS) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
	}
	if name == "." {
		return &commitDir{fs: c, info: commitInfo{name: ".", dir: true, when: c.when}}, nil
	}
	e, err := c.entry(name)
	if err != nil {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
	}
	info := c.info(e)
	if info.dir {
		return &commitDir{fs: c, path: name, info: info}, nil
	}
	b, err := c.blob(e.obj)
	if err != nil {
		return nil, &fs.PathError{Op: "open", Path: name, Err: err}
	}
	return &commitFile{Reader: bytes.NewReader(b), info: info}, nil
}

// info describes an entry; a submodule shows as an empty directory.
func (c commitFS) info(e gitEntry) commitInfo {
	return commitInfo{name: e.name, size: e.size, dir: e.typ == "tree" || e.typ == "commit", when: c.when}
}

type commitInfo struct {
	name string
	size int64
	dir  bool
	when time.Time
}

func (i commitInfo) Name() string       { return i.name }
func (i commitInfo) Size() int64        { return i.size }
func (i commitInfo) ModTime() time.Time { return i.when }
func (i commitInfo) IsDir() bool        { return i.dir }
func (i commitInfo) Sys() any           { return nil }
func (i commitInfo) Mode() fs.FileMode {
	if i.dir {
		return fs.ModeDir | 0o555
	}
	return 0o444
}
func (i commitInfo) Type() fs.FileMode          { return i.Mode().Type() }
func (i commitInfo) Info() (fs.FileInfo, error) { return i, nil }

// commitFile is a file's content, seekable, as http.ServeContent needs.
type commitFile struct {
	*bytes.Reader
	info commitInfo
}

func (f *commitFile) Stat() (fs.FileInfo, error) { return f.info, nil }
func (f *commitFile) Close() error               { return nil }

type commitDir struct {
	fs   commitFS
	path string
	info commitInfo
	read bool
}

func (d *commitDir) Stat() (fs.FileInfo, error) { return d.info, nil }
func (d *commitDir) Close() error               { return nil }
func (d *commitDir) Read([]byte) (int, error) {
	return 0, &fs.PathError{Op: "read", Path: d.path, Err: errors.New("is a directory")}
}

func (d *commitDir) ReadDir(n int) ([]fs.DirEntry, error) {
	if d.read {
		if n > 0 {
			return nil, io.EOF
		}
		return nil, nil
	}
	d.read = true
	name := d.path
	if name == "" {
		name = "."
	}
	entries, err := d.fs.list(name)
	if err != nil {
		return nil, err
	}
	out := make([]fs.DirEntry, len(entries))
	for i, e := range entries {
		out[i] = d.fs.info(e)
	}
	return out, nil
}