package index
import "testing"
// A branch NAME is not the same thing as a check-in's branch FIELD.
//
// fossil lets a check-in carry a propagating `sym-NAME` tag for a name
// other than its own branch=, which is how a branch is renamed without
// orphaning the old name: the tip says branch=main and keeps a
// propagating sym-trunk, so both names resolve to it. fossil and zit
// both answer that way.
//
// quarry answered from checkin.branch alone — one column, one name —
// so the old name resolved to the last check-in whose branch FIELD
// still said it, years back. On the tcl mirror that is why `trunk`
// served a 2020 tree while the repository's actual trunk was current.
// Reported by jva, who reproduced it on a two-check-in repository
// rather than arguing from the big one (6833076f).
func TestARenamedBranchKeepsResolvingUnderItsOldName(t *testing.T) {
ix := openIndex(t)
// The original trunk.
ci(t, ix, "c1", "trunk", "2026-01-01T10:00:00", "first")
// The rename: branch= becomes main, and a PROPAGATING sym-trunk
// keeps the old name pointing here.
ci(t, ix, "c2", "main", "2026-01-02T10:00:00", "renamed to main", "c1")
tag(t, ix, "c2", "sym-trunk", "*", "2026-01-02T10:00:00")
foldBranches(t, ix)
if got, err := ix.BranchTip("main"); err != nil || got != "c2" {
t.Errorf("main resolved to %q (err %v), want c2", got, err)
}
// The one that was wrong: trunk must follow the propagating tag to
// c2, not stop at the last check-in whose branch field said trunk.
got, err := ix.BranchTip("trunk")
if err != nil {
t.Fatal(err)
}
if got == "c1" {
t.Error("trunk still resolves to the pre-rename check-in: " +
"the resolver read checkin.branch and ignored the propagating sym-trunk tag")
}
if got != "c2" {
t.Errorf("trunk resolved to %q, want c2", got)
}
}
// A cancelled sym tag stops counting. Otherwise removing a name would
// be impossible and a branch could never really be renamed away.
func TestACancelledSymTagStopsResolving(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "c1", "trunk", "2026-01-01T10:00:00", "first")
ci(t, ix, "c2", "main", "2026-01-02T10:00:00", "renamed", "c1")
tag(t, ix, "c2", "sym-trunk", "*", "2026-01-02T10:00:00")
// Later artifact cancels the old name on this check-in.
tag(t, ix, "c2", "sym-trunk", "-", "2026-01-03T10:00:00")
foldBranches(t, ix)
if got, _ := ix.BranchTip("trunk"); got == "c2" {
t.Error("a cancelled sym-trunk still resolved trunk to the check-in that dropped it")
}
}
// The ordinary case must not move: a branch with no sym games resolves
// to its newest leaf exactly as before. This is the assertion that
// protects landing tip and target resolution, which is what branchTip
// is really for.
func TestAnOrdinaryBranchStillResolvesToItsNewestLeaf(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "a1", "feature", "2026-01-01T10:00:00", "one")
ci(t, ix, "a2", "feature", "2026-01-02T10:00:00", "two", "a1")
ci(t, ix, "a3", "feature", "2026-01-03T10:00:00", "three", "a2")
foldBranches(t, ix)
if got, err := ix.BranchTip("feature"); err != nil || got != "a3" {
t.Errorf("feature resolved to %q (err %v), want a3", got, err)
}
if got, err := ix.BranchTip("nonesuch"); err != nil || got != "" {
t.Errorf("an unknown branch resolved to %q (err %v), want empty", got, err)
}
}