@ 261101.7
current
package main

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

// The main line numbers each day from one; a merge names what it brought
// in after itself, and so does a merge inside that. The day is the one the
// commit records, whatever the reader's zone.
func TestNumberCommits(t *testing.T) {
	// Newest first, as git log prints: hash, parents, committer date.
	log := strings.Join([]string{
		"c m 2026-10-25T09:00:00+02:00",
		"m a x2 2026-10-24T18:00:00+02:00",
		"x2 x1 z 2026-10-24T17:00:00+02:00",
		"z a 2026-10-24T16:30:00+02:00",
		"x1 a 2026-10-24T16:00:00+02:00",
		"a 2026-10-24T23:30:00-07:00",
	}, "\x00")
	s := fresh().extend("c", []byte(log))
	for h, want := range map[string]string{"a": "261024.1", "m": "261024.2", "x1": "261024.2.1", "x2": "261024.2.2", "z": "261024.2.2.1", "c": "261025.1"} {
		if got := s.byHash[h]; got != want {
			t.Errorf("%s: %q, want %q", h, got, want)
		}
		if got := s.byDate[want]; got != h {
			t.Errorf("%s names %q, want %q", want, got, h)
		}
	}
	if got := s.label(&commit{hash: "off", short: "0ff"}); got != "0ff" {
		t.Errorf("a commit off the line is %q, want its hash", got)
	}
	// Numbering up to m, then the commits after it, gives the same names;
	// a line that does not run through the old tip numbers nothing.
	old := fresh().extend("m", []byte(strings.Join(strings.Split(log, "\x00")[1:], "\x00")))
	if got := old.extend("c", []byte("c m 2026-10-25T09:00:00+02:00")); got == nil || got.byHash["c"] != "261025.1" || got.byHash["z"] != "261024.2.2.1" {
		t.Errorf("extended past m: %v", got)
	}
	if old.byHash["c"] != "" {
		t.Error("extend changed the set it extended")
	}
	if got := old.extend("d", []byte("d x1 2026-10-25T09:00:00+02:00")); got != nil {
		t.Errorf("a line off the old tip extended: %v", got.byHash)
	}
}

// A commit on the local branch that origin's line has not reached reads as
// its day and x, and a merge it made names what it brought in after
// itself; none of them gets a place or an address.
func TestPendingCommits(t *testing.T) {
	s := fresh().extend("a", []byte("a 2026-10-24T16:00:00+02:00"))
	p := s.withPending("m", []byte(strings.Join([]string{
		"m b y 2026-10-26T10:00:00+02:00",
		"y a 2026-10-26T09:30:00+02:00",
		"b a 2026-10-26T09:00:00+02:00",
	}, "\x00")))
	for h, want := range map[string]string{"a": "261024.1", "b": "261026.x", "m": "261026.x", "y": "261026.x.1"} {
		c := &commit{hash: h, short: h + "0"}
		if got := p.label(c); got != want {
			t.Errorf("%s: %q, want %q", h, got, want)
		}
		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)
	}
}

// An unpushed commit shows its day and x on the page and in a version's
// crumb, and its version keeps the hash in its address.
func TestPendingVersion(t *testing.T) {
	s, dir := repoTree(t)
	h := s.handler()
	branch, err := exec.Command("git", "-C", dir, "symbolic-ref", "--short", "HEAD").Output()
	if err != nil {
		t.Fatal(err)
	}
	if out, err := exec.Command("git", "-C", dir, "update-ref", "refs/remotes/origin/"+strings.TrimSpace(string(branch)), "HEAD~1").CombinedOutput(); err != nil {
		t.Fatalf("update-ref: %v\n%s", err, out)
	}
	short := head(t, dir, "HEAD")[:7]
	star := stardatesIn(context.Background(), dir).label(&commit{hash: head(t, dir, "HEAD"), short: short})
	if !regexp.MustCompile(`^[0-9]{6}\.x$`).MatchString(star) {
		t.Fatalf("unpushed commit is %q, want its day and x", star)
	}
	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{}}
}