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 ", 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) } }