current commit
261026.28 · A version sits on the tree's name
package main

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
)

// A version serves the tree as it was in one commit: files, listings and
// relative links stay in it, and its page says which commit it is. Its
// address names the commit by stardate.
func TestVersion(t *testing.T) {
	s, dir := repoTree(t)
	h := s.handler()
	first := head(t, dir, "HEAD~1")
	short := first[:7]
	stars := stardatesIn(context.Background(), dir)
	star, second, secondStar := stars.byHash[first], head(t, dir, "HEAD")[:7], stars.byHash[head(t, dir, "HEAD")]
	if !regexp.MustCompile(`^[0-9]{6}\.1$`).MatchString(star) || secondStar == "" {
		t.Fatalf("stardates %q and %q", star, secondStar)
	}
	at := "/@" + star + "/"

	if body := get(h, at+"a/b.go").Body.String(); strings.Contains(body, "package b") || !strings.Contains(body, "<a class=\"own now\" href=\"/a/b.go\">current</a>\n<a class=\"own now\" href=\"/a/b.go?commit="+first+"\">commit</a>\n<details class=\"own latest\" data-timeline=\"/a/b.go?timeline\" data-here=\""+at+"a/b.go\">") ||
		!strings.Contains(body, ">"+star+"</span> · <span class=\"subject\"") {
		t.Errorf("a/b.go as first committed:\n%s", body)
	}
	if body := get(h, "/a/b.go").Body.String(); !strings.Contains(body, `package</span> b`) {
		t.Errorf("a/b.go now:\n%s", body)
	}
	listing := get(h, at+"a/").Body.String()
	if !strings.Contains(listing, " @ "+star+"</a>") || !strings.Contains(listing, `href="b.go"`) || strings.Contains(listing, "Delete lock") || strings.Contains(listing, `<li class="log">`) {
		t.Errorf("listing of a/ as of %s:\n%s", short, listing)
	}
	if body := get(h, at+"untracked/x.txt"); body.Code != 404 {
		t.Errorf("untracked file found in a version: %d", body.Code)
	}
	for _, hash := range []string{first, short} {
		if rec := get(h, "/@"+hash+"/a/b.go"); rec.Header().Get("Location") != at+"a/b.go" {
			t.Errorf("hash %s redirects to %q", hash, rec.Header().Get("Location"))
		}
	}
	if rec := get(h, "/@"+star+"9/a/b.go"); rec.Code != 404 {
		t.Errorf("unknown stardate: %d", rec.Code)
	}
	if rec := get(h, "/@0000000/a/b.go"); rec.Code != 404 {
		t.Errorf("unknown commit: %d", rec.Code)
	}
	if body := get(h, "/a/b.go?log").Body.String(); !strings.Contains(body, `<a class="then" href="`+at+`a/b.go">browse</a>`) {
		t.Errorf("history links no version:\n%s", body)
	}
	// The menu lists every version of the page, then the history. The
	// newest leads to the current page while it matches the commit; once
	// it changes, a row of its own says so.
	body := get(h, "/a/b.go?timeline").Body.String()
	if !strings.Contains(body, `<li data-also="/@`+secondStar+`/a/b.go"><span class="date lead">`+secondStar+`</span> <a href="/a/b.go" title="`+second) || !strings.Contains(body, `</a> <span class="date">current</span>`) ||
		!strings.Contains(body, `</span> <a href="`+at+`a/b.go" title="`+short) || !strings.Contains(body, `<li class="log"><a href="?log">`) || strings.Contains(body, "uncommitted") {
		t.Errorf("versions of a/b.go:\n%s", body)
	}
	// A version through a commit that left the page alone names the
	// commit that last changed it, which the menu marks.
	if body := get(h, "/@"+secondStar+"/index.md").Body.String(); !strings.Contains(body, `data-here="`+at+`index.md"`) || !strings.Contains(body, `">Add the docs</span>`) {
		t.Errorf("index.md as of %s:\n%s", second, body)
	}
	os.WriteFile(filepath.Join(dir, "a", "b.go"), []byte("package c\n"), 0o644)
	body = get(h, "/a/b.go?timeline").Body.String()
	if !strings.Contains(body, `<li><span class="date lead"></span> <a href="/a/b.go">current</a> <span class="date">uncommitted changes</span></li>`) || !strings.Contains(body, `</span> <a href="/@`+secondStar+`/a/b.go"`) {
		t.Errorf("versions of a/b.go with changes:\n%s", body)
	}
	// A commit off the main line, eg on a branch, keeps its hash.
	exec.Command("git", "-C", dir, "checkout", "-qb", "side").Run()
	commitAll(t, dir, "On a side branch")
	side := head(t, dir, "HEAD")[:7]
	if body := get(h, "/a/b.go?timeline").Body.String(); !strings.Contains(body, `<li data-also="/@`+side+`/a/b.go">`) {
		t.Errorf("versions of a/b.go on a branch:\n%s", body)
	}
	// A version runs the page's script, for keys, search and line marks,
	// but holds no stream, since it never changes.
	s.live = newLive()
	if body := get(s.handler(), at+"a/b.go").Body.String(); !strings.Contains(body, `<script data-live="">`) {
		t.Errorf("version page without the script, or with a stream:\n%s", body)
	}
}

// A page whose history comes from another repo, or through a link, links
// no versions from its menu or history, since the tree's versions would
// not hold it.
func TestVersionLinksOnlyForOwnHistory(t *testing.T) {
	s, dir := repoTree(t)
	h := s.handler()
	sub := filepath.Join(dir, "sub")
	os.Mkdir(sub, 0o755)
	if out, err := exec.Command("git", "init", "-q", sub).CombinedOutput(); err != nil {
		t.Fatalf("git init: %v\n%s", err, out)
	}
	for _, text := range []string{"one\n", "two\n"} {
		os.WriteFile(filepath.Join(sub, "x.md"), []byte(text), 0o644)
		commitAll(t, sub, "Write "+text)
	}
	os.Symlink("index.md", filepath.Join(dir, "link.md"))
	commitAll(t, dir, "Link the index")
	for _, p := range []string{"/sub/x.md", "/link.md"} {
		if body := get(h, p+"?timeline").Body.String(); !strings.Contains(body, "?commit=") || strings.Contains(body, "/@") {
			t.Errorf("%s?timeline: want commit links and no versions:\n%s", p, body)
		}
		if body := get(h, p+"?log").Body.String(); strings.Contains(body, "browse</a>") {
			t.Errorf("%s?log links versions:\n%s", p, body)
		}
	}
	if body := get(h, "/index.md?timeline").Body.String(); !strings.Contains(body, "/@") {
		t.Errorf("index.md lost its versions:\n%s", body)
	}
}

// A named tree's versions sit on its name, eg /notes@261024.1/, which
// reads as the tree at that commit; the old address below the tree, eg
// /notes/@261024.1/, redirects there for good. A tree around it never
// claims the version, and a name that is no version is no version.
func TestVersionOnTheName(t *testing.T) {
	s, dir := repoTree(t)
	tr := s.list()[0]
	tr.prefix, tr.label = "/notes/", "~/notes"
	addTree(t, s, t.TempDir(), "/", "~")
	h := s.handler()
	first := head(t, dir, "HEAD~1")
	star := stardatesIn(context.Background(), dir).byHash[first]
	at := "/notes@" + star + "/"
	rec := get(h, at+"a/b.go")
	if body := rec.Body.String(); rec.Code != 200 || !strings.Contains(body, `<a class="own now" href="/notes/a/b.go">current</a>`) || !strings.Contains(body, `data-here="`+at+`a/b.go"`) {
		t.Errorf("version on the name: %d\n%s", rec.Code, body)
	}
	if rec := get(h, at); rec.Code != 200 || !strings.Contains(rec.Body.String(), `<a href="a/">a/</a>`) {
		t.Errorf("version's root: %d", rec.Code)
	}
	for p, want := range map[string]string{
		"/notes/@" + star + "/a/b.go":   at + "a/b.go",
		"/notes/@" + star + "/":         at,
		"/notes/@" + first[:7] + "/a/":  "/notes@" + first[:7] + "/a/",
		"/notes@" + first[:7] + "/a/":   at + "a/",
		"/notes/@" + star + "/?log":     at + "?log",
	} {
		if rec := get(h, p); rec.Code != 301 || rec.Header().Get("Location") != want {
			t.Errorf("%s: %d to %q, want 301 to %q", p, rec.Code, rec.Header().Get("Location"), want)
		}
	}
	if rec := get(h, "/notes@nothing/"); rec.Code != 404 || strings.Contains(rec.Body.String(), "~/notes") && !strings.Contains(rec.Body.String(), "not found") {
		t.Errorf("a name that is no version: %d", rec.Code)
	}
}