aboutsummaryrefslogtreecommitdiff
path: root/internal/testgit
diff options
context:
space:
mode:
Diffstat (limited to 'internal/testgit')
-rw-r--r--internal/testgit/command.go2
-rw-r--r--internal/testgit/packobjects.go145
-rw-r--r--internal/testgit/seed.go206
-rw-r--r--internal/testgit/tree.go2
-rw-r--r--internal/testgit/verifypack.go13
5 files changed, 366 insertions, 2 deletions
diff --git a/internal/testgit/command.go b/internal/testgit/command.go
index db874bd1..4e696ece 100644
--- a/internal/testgit/command.go
+++ b/internal/testgit/command.go
@@ -15,7 +15,7 @@ func (repo *Repo) command(
) *exec.Cmd {
tb.Helper()
- cmd := exec.CommandContext(tb.Context(), command, args...) //nolint:gosec // Test helper runs caller-selected commands.
+ cmd := exec.CommandContext(tb.Context(), command, args...) //nolint:gosec
cmd.Dir = repo.path
cmd.Env = repo.env
diff --git a/internal/testgit/packobjects.go b/internal/testgit/packobjects.go
new file mode 100644
index 00000000..62f86963
--- /dev/null
+++ b/internal/testgit/packobjects.go
@@ -0,0 +1,145 @@
+package testgit
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "lindenii.org/go/furgit/object/id"
+)
+
+// ErrInvalidPackObjectsOptions reports an inconsistent
+// combination of pack-objects options.
+var ErrInvalidPackObjectsOptions = errors.New("internal/testgit: invalid pack-objects options")
+
+// PackObjectsOptions controls one on-disk pack-objects invocation.
+type PackObjectsOptions struct {
+ // RevIndex requests writing a .rev reverse index alongside the pack.
+ RevIndex bool
+
+ // Revs changes how the input is interpreted.
+ //
+ // When false, each include is one object name,
+ // and exactly those objects are packed.
+ //
+ // When true, each include is a revision argument,
+ // and pack-objects packs the closure of objects
+ // reachable from the includes but not from the excludes,
+ // the same walk git rev-list --objects performs.
+ Revs bool
+
+ // Exclude lists objects to omit by reachability,
+ // fed as "^<oid>" revision arguments.
+ // A non-nil Exclude requires Revs.
+ Exclude []id.ObjectID
+}
+
+// PackObjects packs the include objects with git pack-objects
+// into a temporary directory,
+// and returns the artifact path prefix "<dir>/pack-<hash>",
+// to which ".pack", ".idx", and ".rev" suffixes apply.
+func (repo *Repo) PackObjects(tb testing.TB, include []id.ObjectID, opts PackObjectsOptions) (string, error) {
+ tb.Helper()
+
+ if opts.Exclude != nil && !opts.Revs {
+ return "", fmt.Errorf("%w: Exclude requires Revs", ErrInvalidPackObjectsOptions)
+ }
+
+ dir := tb.TempDir()
+
+ revIndex := "false"
+ if opts.RevIndex {
+ revIndex = "true"
+ }
+
+ args := []string{"-c", "pack.writeReverseIndex=" + revIndex, "pack-objects"}
+ if opts.Revs {
+ args = append(args, "--revs")
+ }
+
+ args = append(args, "--end-of-options", filepath.Join(dir, "pack"))
+
+ out, err := repo.run(tb, packObjectsStdin(include, opts.Exclude), "git", args...)
+ if err != nil {
+ return "", err
+ }
+
+ return filepath.Join(dir, "pack-"+strings.TrimSpace(string(out))), nil
+}
+
+// PackObjectsStdoutOptions controls one streamed pack-objects invocation.
+type PackObjectsStdoutOptions struct {
+ // Revs changes how the input is interpreted.
+ //
+ // When false, each include is one object name,
+ // and exactly those objects are packed.
+ //
+ // When true, each include is a revision argument,
+ // and pack-objects packs the closure of objects
+ // reachable from the includes but not from the excludes,
+ // the same walk git rev-list --objects performs.
+ Revs bool
+
+ // Thin omits excluded base objects from the pack,
+ // producing a thin pack that must be completed before use.
+ // Thin requires Revs.
+ Thin bool
+
+ // Exclude lists objects to omit by reachability,
+ // fed as "^<oid>" revision arguments.
+ // A non-nil Exclude requires Revs.
+ Exclude []id.ObjectID
+}
+
+// PackObjectsStdout packs the include objects with git pack-objects
+// and returns the pack stream written to standard output.
+func (repo *Repo) PackObjectsStdout(tb testing.TB, include []id.ObjectID, opts PackObjectsStdoutOptions) ([]byte, error) {
+ tb.Helper()
+
+ if opts.Exclude != nil && !opts.Revs {
+ return nil, fmt.Errorf("%w: Exclude requires Revs", ErrInvalidPackObjectsOptions)
+ }
+
+ if opts.Thin && !opts.Revs {
+ return nil, fmt.Errorf("%w: Thin requires Revs", ErrInvalidPackObjectsOptions)
+ }
+
+ args := []string{"pack-objects", "--stdout"}
+ if opts.Revs {
+ args = append(args, "--revs")
+ }
+
+ if opts.Thin {
+ args = append(args, "--thin")
+ }
+
+ out, err := repo.run(tb, packObjectsStdin(include, opts.Exclude), "git", args...)
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+// packObjectsStdin builds the pack-objects standard input:
+// the include object IDs,
+// followed by the exclude object IDs as "^<oid>" lines.
+func packObjectsStdin(include, exclude []id.ObjectID) *bytes.Buffer {
+ var stdin bytes.Buffer
+
+ for _, oid := range include {
+ stdin.WriteString(oid.String())
+ stdin.WriteByte('\n')
+ }
+
+ for _, oid := range exclude {
+ stdin.WriteByte('^')
+ stdin.WriteString(oid.String())
+ stdin.WriteByte('\n')
+ }
+
+ return &stdin
+}
diff --git a/internal/testgit/seed.go b/internal/testgit/seed.go
new file mode 100644
index 00000000..9f60b23e
--- /dev/null
+++ b/internal/testgit/seed.go
@@ -0,0 +1,206 @@
+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
+}
diff --git a/internal/testgit/tree.go b/internal/testgit/tree.go
index 501c7949..ff9b1918 100644
--- a/internal/testgit/tree.go
+++ b/internal/testgit/tree.go
@@ -50,7 +50,7 @@ func (repo *Repo) LsTree(tb testing.TB, oid id.ObjectID) ([]TreeEntry, error) {
return nil, fmt.Errorf("ls-tree: %w", err)
}
- var entries []TreeEntry
+ entries := make([]TreeEntry, 0, bytes.Count(stdout, []byte{0}))
for record := range bytes.SplitSeq(stdout, []byte{0}) {
if len(record) == 0 {
diff --git a/internal/testgit/verifypack.go b/internal/testgit/verifypack.go
new file mode 100644
index 00000000..ec59572a
--- /dev/null
+++ b/internal/testgit/verifypack.go
@@ -0,0 +1,13 @@
+package testgit
+
+import (
+ "testing"
+)
+
+// VerifyPack runs git verify-pack -v on one pack index path
+// and returns its raw verbose output.
+func (repo *Repo) VerifyPack(tb testing.TB, idxPath string) ([]byte, error) {
+ tb.Helper()
+
+ return repo.run(tb, nil, "git", "verify-pack", "-v", "--end-of-options", idxPath)
+}