diff options
Diffstat (limited to 'internal/testgit')
| -rw-r--r-- | internal/testgit/command.go | 2 | ||||
| -rw-r--r-- | internal/testgit/packobjects.go | 145 | ||||
| -rw-r--r-- | internal/testgit/seed.go | 206 | ||||
| -rw-r--r-- | internal/testgit/tree.go | 2 | ||||
| -rw-r--r-- | internal/testgit/verifypack.go | 13 |
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) +} |
