package testgit
import (
"fmt"
"strings"
"testing"
"lindenii.org/go/furgit/object/id"
"lindenii.org/go/furgit/object/typ"
)
// Seeded lists the objects created by one [Repo.SeedHistory], by type.
// Each object ID appears exactly once.
type Seeded struct {
Blobs []id.ObjectID
Trees []id.ObjectID
Commits []id.ObjectID
Tags []id.ObjectID
}
// All returns all seeded object IDs.
func (seeded *Seeded) All() []id.ObjectID {
all := make([]id.ObjectID, 0, len(seeded.Blobs)+len(seeded.Trees)+len(seeded.Commits)+len(seeded.Tags))
all = append(all, seeded.Blobs...)
all = append(all, seeded.Trees...)
all = append(all, seeded.Commits...)
all = append(all, seeded.Tags...)
return all
}
// SeedHistory creates one deterministic synthetic history
// of 64 commits on refs/heads/main,
// covering all four object types and various situations,
// such as stable, growing, unique, and empty blobs,
// nested subtrees shared across several commits,
// executable and symlink tree entries,
// and an annotated tag every sixteenth commit.
func (repo *Repo) SeedHistory(tb testing.TB) (Seeded, error) {
tb.Helper()
seeder := &historySeeder{
repo: repo,
tb: tb,
seen: make(map[id.ObjectID]struct{}),
seeded: Seeded{
Blobs: nil,
Trees: nil,
Commits: nil,
Tags: nil,
},
}
err := seeder.run()
if err != nil {
return Seeded{}, err
}
return seeder.seeded, nil
}
type historySeeder struct {
repo *Repo
tb testing.TB
seen map[id.ObjectID]struct{}
seeded Seeded
}
func (seeder *historySeeder) run() error {
readme, err := seeder.blob("stable readme\n")
if err != nil {
return err
}
empty, err := seeder.blob("")
if err != nil {
return err
}
link, err := seeder.blob("README")
if err != nil {
return err
}
var parents []id.ObjectID
for i := range 64 {
commitID, err := seeder.commit(i, readme, empty, link, parents)
if err != nil {
return fmt.Errorf("seed commit %d: %w", i, err)
}
seeder.seeded.Commits = append(seeder.seeded.Commits, commitID)
parents = []id.ObjectID{commitID}
if (i+1)%16 == 0 {
err = seeder.tag(fmt.Sprintf("v%d", (i+1)/16), commitID)
if err != nil {
return fmt.Errorf("seed tag at commit %d: %w", i, err)
}
}
}
return seeder.repo.UpdateRef(seeder.tb, "refs/heads/main", parents[0])
}
func (seeder *historySeeder) commit(i int, readme, empty, link id.ObjectID, parents []id.ObjectID) (id.ObjectID, error) {
var growingContent strings.Builder
for j := range i + 1 {
fmt.Fprintf(&growingContent, "entry %d\n", j)
}
growing, err := seeder.blob(growingContent.String())
if err != nil {
return id.ObjectID{}, err
}
tool, err := seeder.blob(fmt.Sprintf("tool %d\n", i))
if err != nil {
return id.ObjectID{}, err
}
inner, err := seeder.blob(fmt.Sprintf("inner %d\n", i/4))
if err != nil {
return id.ObjectID{}, err
}
subtree, err := seeder.tree([]TreeEntry{
{Mode: "100644", Type: typ.Blob, OID: inner, Name: "inner"},
{Mode: "120000", Type: typ.Blob, OID: link, Name: "link"},
})
if err != nil {
return id.ObjectID{}, err
}
root, err := seeder.tree([]TreeEntry{
{Mode: "100644", Type: typ.Blob, OID: readme, Name: "README"},
{Mode: "100644", Type: typ.Blob, OID: empty, Name: "empty"},
{Mode: "100644", Type: typ.Blob, OID: growing, Name: "log"},
{Mode: "040000", Type: typ.Tree, OID: subtree, Name: "sub"},
{Mode: "100755", Type: typ.Blob, OID: tool, Name: "tool"},
})
if err != nil {
return id.ObjectID{}, err
}
return seeder.repo.CommitTree(
seeder.tb,
root,
CommitTreeOptions{Message: fmt.Sprintf("commit %d", i)}, //nolint:exhaustruct
parents...,
)
}
func (seeder *historySeeder) blob(content string) (id.ObjectID, error) {
oid, err := seeder.repo.HashObject(seeder.tb, typ.Blob, strings.NewReader(content))
if err != nil {
return id.ObjectID{}, err
}
if seeder.record(oid) {
seeder.seeded.Blobs = append(seeder.seeded.Blobs, oid)
}
return oid, nil
}
func (seeder *historySeeder) tree(entries []TreeEntry) (id.ObjectID, error) {
oid, err := seeder.repo.MkTree(seeder.tb, entries)
if err != nil {
return id.ObjectID{}, err
}
if seeder.record(oid) {
seeder.seeded.Trees = append(seeder.seeded.Trees, oid)
}
return oid, nil
}
func (seeder *historySeeder) tag(name string, target id.ObjectID) error {
oid, err := seeder.repo.TagAnnotated(
seeder.tb,
name,
target,
TagAnnotatedOptions{Message: "tag " + name}, //nolint:exhaustruct
)
if err != nil {
return err
}
seeder.seeded.Tags = append(seeder.seeded.Tags, oid)
return nil
}
func (seeder *historySeeder) record(oid id.ObjectID) bool {
if _, ok := seeder.seen[oid]; ok {
return false
}
seeder.seen[oid] = struct{}{}
return true
}