package gitx
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/go-git/go-git/v5/plumbing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
func TestCreateMakesBareSpaceWithInitialCommit(t *testing.T) {
repo, root := newSpace(t)
ctx := context.Background()
if got, want := repo.Dir(), spaceDir(root); got != want {
t.Fatalf("Dir = %q, want %q", got, want)
}
if got := repo.ApprovedBranch(); got != DefaultApprovedBranch {
t.Fatalf("ApprovedBranch = %q, want %q", got, DefaultApprovedBranch)
}
// Bare: objects and HEAD at the top level, no worktree.
for _, name := range []string{"HEAD", "objects", "refs"} {
if _, err := os.Stat(filepath.Join(repo.Dir(), name)); err != nil {
t.Fatalf("bare repo missing %s: %v", name, err)
}
}
if _, err := os.Stat(filepath.Join(repo.Dir(), ".git")); !os.IsNotExist(err) {
t.Fatalf("repo at %q is not bare", repo.Dir())
}
head, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatalf("ApprovedHead: %v", err)
}
c, err := repo.repo.CommitObject(head)
if err != nil {
t.Fatalf("CommitObject: %v", err)
}
if c.NumParents() != 0 {
t.Fatalf("initial commit has %d parents, want 0", c.NumParents())
}
if !strings.Contains(c.Message, fxSpace.String()) {
t.Fatalf("initial commit message %q does not name the space", c.Message)
}
if c.Author.Email != "bigbes@gmail.com" {
t.Fatalf("initial commit author = %q, want the supplied owner", c.Author.Email)
}
tree, err := c.Tree()
if err != nil {
t.Fatalf("Tree: %v", err)
}
if len(tree.Entries) != 0 {
t.Fatalf("initial commit tree has %d entries, want an empty tree", len(tree.Entries))
}
if docs := docPaths(t, repo, DefaultApprovedBranch); len(docs) != 0 {
t.Fatalf("fresh space lists documents: %v", docs)
}
}
func TestCreateRefusesToClobberAndCleansUpOwner(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
if _, err := Create(ctx, root, fxSpace, CreateOptions{Owner: owner(0)}); err != nil {
t.Fatalf("Create: %v", err)
}
_, err := Create(ctx, root, fxSpace, CreateOptions{Owner: owner(0)})
if !errors.Is(err, ErrExists) {
t.Fatalf("second Create error = %v, want ErrExists", err)
}
// A missing owner identity is refused rather than invented, and the
// half-made directory is removed.
other := core.SpaceRef{Owner: "bigbes", Name: "notes"}
if _, err := Create(ctx, root, other, CreateOptions{}); err == nil {
t.Fatal("Create with no owner identity succeeded")
}
if _, err := os.Stat(DiskPath(root, other)); !os.IsNotExist(err) {
t.Fatalf("failed Create left %q behind", DiskPath(root, other))
}
}
func TestCreateAndOpenRejectBadInput(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
if _, err := Create(ctx, "relative/root", fxSpace, CreateOptions{Owner: owner(0)}); err == nil {
t.Fatal("Create accepted a relative repos root")
}
if _, err := Open("relative/root", fxSpace); err == nil {
t.Fatal("Open accepted a relative repos root")
}
bad := []core.SpaceRef{
{Owner: "..", Name: "rfcs"},
{Owner: "bigbes", Name: ".."},
{Owner: "bigbes", Name: "a/b"},
{Owner: "", Name: "rfcs"},
{Owner: "bigbes", Name: "-rf"},
}
for _, sr := range bad {
if _, err := Create(ctx, root, sr, CreateOptions{Owner: owner(0)}); err == nil {
t.Fatalf("Create accepted %+v", sr)
}
// Open reports every rejection as ErrNotFound so probing cannot tell a
// malformed name from an absent space.
if _, err := Open(root, sr); !errors.Is(err, ErrNotFound) {
t.Fatalf("Open(%+v) error = %v, want ErrNotFound", sr, err)
}
}
}
func TestOpenReadsApprovedBranchFromHead(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
sr := core.SpaceRef{Owner: "bigbes", Name: "ops"}
if _, err := Create(ctx, root, sr, CreateOptions{Owner: owner(0), ApprovedBranch: "approved"}); err != nil {
t.Fatalf("Create: %v", err)
}
repo, err := Open(root, sr)
if err != nil {
t.Fatalf("Open: %v", err)
}
if got := repo.ApprovedBranch(); got != "approved" {
t.Fatalf("ApprovedBranch = %q, want %q", got, "approved")
}
if _, err := repo.ApprovedHead(ctx); err != nil {
t.Fatalf("ApprovedHead: %v", err)
}
}
func TestOpenMissingSpaceIsNotFound(t *testing.T) {
root := t.TempDir()
if _, err := Open(root, fxSpace); !errors.Is(err, ErrNotFound) {
t.Fatalf("Open of an absent space = %v, want ErrNotFound", err)
}
// A directory that exists but is not a repository is equally not found.
if err := os.MkdirAll(DiskPath(root, fxSpace), 0o755); err != nil {
t.Fatal(err)
}
if _, err := Open(root, fxSpace); !errors.Is(err, ErrNotFound) {
t.Fatalf("Open of a non-repository = %v, want ErrNotFound", err)
}
}
func TestSignatureValidation(t *testing.T) {
now := time.Now()
cases := []struct {
name string
sig Signature
ok bool
}{
{"complete", Signature{Name: "bigbes", Email: "b@example.com", When: now}, true},
{"no name", Signature{Email: "b@example.com", When: now}, false},
{"blank name", Signature{Name: " ", Email: "b@example.com", When: now}, false},
{"no email", Signature{Name: "bigbes", When: now}, false},
{"zero time", Signature{Name: "bigbes", Email: "b@example.com"}, false},
{"angle bracket", Signature{Name: "a <b>", Email: "b@example.com", When: now}, false},
{"newline", Signature{Name: "a\nb", Email: "b@example.com", When: now}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.sig.validate("author")
if tc.ok && err != nil {
t.Fatalf("validate = %v, want nil", err)
}
if !tc.ok && err == nil {
t.Fatal("validate = nil, want an error")
}
})
}
}
func TestCommitMetaRendersTrailerBlock(t *testing.T) {
m := CommitMeta{
Message: "Add storage model section\n\nRationale goes here.\n\n\n",
Trailers: []Trailer{
{Key: "X-Agent-Session", Value: "8fb9c9a4-b078-4af1-89eb-d97c522f9921"},
{Key: "X-Agent-Base", Value: "deadbeef"},
},
Author: agent(1),
Committer: owner(1),
}
want := "Add storage model section\n\nRationale goes here.\n\n" +
"X-Agent-Session: 8fb9c9a4-b078-4af1-89eb-d97c522f9921\n" +
"X-Agent-Base: deadbeef\n"
if got := m.text(); got != want {
t.Fatalf("text() =\n%q\nwant\n%q", got, want)
}
if err := m.validate(); err != nil {
t.Fatalf("validate: %v", err)
}
}
func TestCommitMetaRejectsForgedTrailers(t *testing.T) {
base := CommitMeta{Message: "subject", Author: agent(1), Committer: owner(1)}
bad := base
bad.Trailers = []Trailer{{Key: "X-Agent-Session", Value: "a\nX-Agent-Base: forged"}}
if err := bad.validate(); err == nil {
t.Fatal("a trailer value carrying a newline was accepted")
}
bad = base
bad.Trailers = []Trailer{{Key: "X Agent: Session", Value: "v"}}
if err := bad.validate(); err == nil {
t.Fatal("a malformed trailer key was accepted")
}
bad = base
bad.Message = "\n\nbody only"
if err := bad.validate(); err == nil {
t.Fatal("a message with no subject line was accepted")
}
bad = base
bad.Message = " "
if err := bad.validate(); err == nil {
t.Fatal("a blank message was accepted")
}
}
func TestWithLockSerializesAndHonoursContext(t *testing.T) {
repo, _ := newSpace(t)
held := make(chan struct{})
release := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- repo.WithLock(context.Background(), func(context.Context) error {
close(held)
<-release
return nil
})
}()
<-held
// A second holder cannot get in while the first is running, and gives up
// when its context expires rather than blocking forever.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := repo.WithLock(ctx, func(context.Context) error {
t.Error("second WithLock ran while the lock was held")
return nil
})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("contended WithLock error = %v, want context.DeadlineExceeded", err)
}
close(release)
if err := <-done; err != nil {
t.Fatalf("first WithLock: %v", err)
}
// A second handle over the same directory shares the lock: it is keyed by
// the repository directory, not by the Repo value.
other, err := Open(filepath.Dir(filepath.Dir(repo.Dir())), fxSpace)
if err != nil {
t.Fatalf("Open: %v", err)
}
if spaceLock(other.Dir()) != spaceLock(repo.Dir()) {
t.Fatal("two handles over one space have different locks")
}
}
func TestProposalBranchNaming(t *testing.T) {
b, err := ProposalBranch(42)
if err != nil {
t.Fatalf("ProposalBranch: %v", err)
}
if b != "proposals/42" {
t.Fatalf("ProposalBranch(42) = %q", b)
}
if _, err := ProposalBranch(0); err == nil {
t.Fatal("ProposalBranch(0) succeeded")
}
if id, ok := ParseProposalBranch("proposals/42"); !ok || id != 42 {
t.Fatalf("ParseProposalBranch = %d, %v", id, ok)
}
for _, name := range []string{"proposals", "proposals/", "main", "proposals/draft", "proposals/-1"} {
if _, ok := ParseProposalBranch(name); ok {
t.Fatalf("ParseProposalBranch(%q) reported an id", name)
}
}
if IsProposalBranch("proposals") {
t.Fatal("the bare namespace is not a proposal branch")
}
if !IsProposalBranch("proposals/draft") {
t.Fatal("a non-numeric proposal branch is still a proposal branch")
}
}
func TestListProposalBranches(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
head, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatal(err)
}
for _, b := range []string{"proposals/2", "proposals/1"} {
if _, err := repo.CreateProposalBranch(ctx, b, head.String()); err != nil {
t.Fatalf("CreateProposalBranch(%q): %v", b, err)
}
}
got, err := repo.ListProposalBranches(ctx)
if err != nil {
t.Fatalf("ListProposalBranches: %v", err)
}
if len(got) != 2 || got[0].Name != "proposals/1" || got[1].Name != "proposals/2" {
t.Fatalf("ListProposalBranches = %+v", got)
}
if got[0].Head != head {
t.Fatalf("proposals/1 head = %s, want %s", got[0].Head, head)
}
}
func TestResolveRevAndValidation(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
head, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatal(err)
}
for _, rev := range []string{DefaultApprovedBranch, head.String(), head.String()[:10]} {
got, err := repo.ResolveRev(ctx, rev)
if err != nil {
t.Fatalf("ResolveRev(%q): %v", rev, err)
}
if got != head {
t.Fatalf("ResolveRev(%q) = %s, want %s", rev, got, head)
}
}
// Revision arithmetic is not part of the read contract.
for _, rev := range []string{"main^", "main~1", "main@{0}", "", "-main", "main..other", "ma in"} {
if _, err := repo.ResolveRev(ctx, rev); !errors.Is(err, ErrBadRev) {
t.Fatalf("ResolveRev(%q) error = %v, want ErrBadRev", rev, err)
}
}
if _, err := repo.ResolveRev(ctx, "nosuchbranch"); !errors.Is(err, ErrNotFound) {
t.Fatalf("ResolveRev of an unknown branch = %v, want ErrNotFound", err)
}
// A tree sha is a valid object but not a commit.
tree, err := repo.treeOf(head)
if err != nil {
t.Fatal(err)
}
if _, err := repo.ResolveRev(ctx, tree.Hash.String()); !errors.Is(err, ErrNotFound) {
t.Fatalf("ResolveRev of a tree sha = %v, want ErrNotFound", err)
}
}
func TestIsAncestor(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
base, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatal(err)
}
next := pushApproved(t, repo, ownerMeta("add a doc", 1),
Write{Path: "specs/0001.md", Content: doc("SPEC-0001", "One", "body")})
for _, tc := range []struct {
a, b plumbing.Hash
want bool
}{
{base, next, true},
{next, base, false},
{base, base, true},
} {
got, err := repo.IsAncestor(ctx, tc.a, tc.b)
if err != nil {
t.Fatalf("IsAncestor(%s, %s): %v", tc.a, tc.b, err)
}
if got != tc.want {
t.Fatalf("IsAncestor(%s, %s) = %v, want %v", tc.a, tc.b, got, tc.want)
}
}
if _, err := repo.IsAncestor(ctx, plumbing.NewHash(strings.Repeat("0", 39)+"1"), next); !errors.Is(err, ErrNotFound) {
t.Fatalf("IsAncestor of an unknown commit = %v, want ErrNotFound", err)
}
}