shithub: furgit

Download patch

ref: dfe20bce3551f4b53f97362b14bd55a7ba0e0de0
parent: 561c3b2cf5893430d4ff63883dab818d8d6f5c3f
author: Runxi Yu <me@runxiyu.org>
date: Thu Mar 5 12:01:31 EST 2026

testgit: Add pack object reader and many object maker

--- /dev/null
+++ b/internal/testgit/repo_make_many_objects_history.go
@@ -1,0 +1,83 @@
+package testgit
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/objectid"
+)
+
+const (
+	manyObjectsMainCommits = 640
+	manyObjectsDevCommits  = 220
+)
+
+// MakeManyObjectsHistory creates a large commit graph.
+func (testRepo *TestRepo) MakeManyObjectsHistory(tb testing.TB) {
+	tb.Helper()
+
+	var (
+		mainTip objectid.ObjectID
+		devTip  objectid.ObjectID
+		hasMain bool
+		hasDev  bool
+	)
+
+	for i := range manyObjectsMainCommits {
+		tree := testRepo.makeManyObjectsTree(tb, "main", i, 3)
+
+		var commit objectid.ObjectID
+		if hasMain {
+			commit = testRepo.CommitTree(tb, tree, fmt.Sprintf("main-%04d", i), mainTip)
+		} else {
+			commit = testRepo.CommitTree(tb, tree, fmt.Sprintf("main-%04d", i))
+			hasMain = true
+		}
+
+		mainTip = commit
+		if i%64 == 0 {
+			testRepo.TagAnnotated(tb, fmt.Sprintf("main-v%04d", i), mainTip, fmt.Sprintf("tag-main-%04d", i))
+		}
+	}
+
+	devTip = mainTip
+	hasDev = true
+
+	for i := range manyObjectsDevCommits {
+		tree := testRepo.makeManyObjectsTree(tb, "dev", i, 4)
+		commit := testRepo.CommitTree(tb, tree, fmt.Sprintf("dev-%04d", i), devTip)
+		devTip = commit
+
+		if i > 0 && i%55 == 0 {
+			mergeTree := testRepo.makeManyObjectsTree(tb, "merge", i, 2)
+
+			mainTip = testRepo.CommitTree(tb, mergeTree, fmt.Sprintf("merge-%04d", i), mainTip, devTip)
+			if i%110 == 0 {
+				testRepo.TagAnnotated(tb, fmt.Sprintf("merge-v%04d", i), mainTip, fmt.Sprintf("tag-merge-%04d", i))
+			}
+		}
+	}
+
+	if hasMain {
+		testRepo.UpdateRef(tb, "refs/heads/main", mainTip)
+	}
+
+	if hasDev {
+		testRepo.UpdateRef(tb, "refs/heads/dev", devTip)
+	}
+}
+
+// makeManyObjectsTree builds one synthetic tree with fanout blobs.
+func (testRepo *TestRepo) makeManyObjectsTree(tb testing.TB, prefix string, i int, files int) objectid.ObjectID {
+	tb.Helper()
+
+	lines := make([]string, 0, files)
+	for j := range files {
+		body := []byte(fmt.Sprintf("%s-%04d-%02d\n%s\n", prefix, i, j, strings.Repeat("x", 160+(i+j)%96)))
+		blobID := testRepo.HashObject(tb, "blob", body)
+		lines = append(lines, fmt.Sprintf("100644 blob %s\t%s_%04d_%02d.txt\n", blobID.String(), prefix, i, j))
+	}
+
+	return testRepo.Mktree(tb, strings.Join(lines, ""))
+}
--- /dev/null
+++ b/internal/testgit/repo_pack_objects_reader.go
@@ -1,0 +1,94 @@
+package testgit
+
+import (
+	"fmt"
+	"io"
+	"os/exec"
+	"strings"
+	"sync"
+	"testing"
+)
+
+// packObjectsReadCloser wraps a pipe reader and process wait fn.
+type packObjectsReadCloser struct {
+	reader io.ReadCloser
+	wait   func() error
+	once   sync.Once
+}
+
+// Read proxies reads to the wrapped reader.
+func (reader *packObjectsReadCloser) Read(dst []byte) (int, error) {
+	return reader.reader.Read(dst)
+}
+
+// Close closes the stream and waits for the underlying process.
+func (reader *packObjectsReadCloser) Close() error {
+	var out error
+
+	reader.once.Do(func() {
+		errClose := reader.reader.Close()
+		errWait := reader.wait()
+
+		if errClose != nil {
+			out = errClose
+
+			return
+		}
+
+		out = errWait
+	})
+
+	return out
+}
+
+// PackObjectsReader streams `git pack-objects --stdout --revs` output.
+func (testRepo *TestRepo) PackObjectsReader(tb testing.TB, revs []string, thin bool) io.ReadCloser {
+	tb.Helper()
+
+	args := []string{"pack-objects", "--stdout", "--revs"}
+	if thin {
+		args = append(args, "--thin")
+	}
+
+	//nolint:noctx
+	cmd := exec.Command("git", args...) //#nosec G204
+	cmd.Dir = testRepo.dir
+	cmd.Env = testRepo.env
+	cmd.Stdin = strings.NewReader(strings.Join(revs, "\n") + "\n")
+
+	pr, pw := io.Pipe()
+	cmd.Stdout = pw
+	stderr := &strings.Builder{}
+	cmd.Stderr = stderr
+
+	waitDone := make(chan error, 1)
+
+	go func() {
+		err := cmd.Start()
+		if err != nil {
+			_ = pw.CloseWithError(fmt.Errorf("git %v start failed: %w", args, err))
+
+			waitDone <- nil
+
+			return
+		}
+
+		err = cmd.Wait()
+		if err != nil {
+			_ = pw.CloseWithError(fmt.Errorf("git %v failed: %w\n%s", args, err, stderr.String()))
+		} else {
+			_ = pw.Close()
+		}
+
+		waitDone <- nil
+	}()
+
+	return &packObjectsReadCloser{
+		reader: pr,
+		wait: func() error {
+			<-waitDone
+
+			return nil
+		},
+	}
+}
--