From 8c062af20bb6e22cd7276b8d73a868324425e682 Mon Sep 17 00:00:00 2001 From: Yun Dou Date: Fri, 13 Mar 2026 16:20:56 +0800 Subject: [PATCH 1/2] Add some unit tests --- go.mod | 6 +- go.sum | 9 ++ run-tests.sh | 6 +- splitter/cache_test.go | 200 ++++++++++++++++++++++++++++++++++++++++ splitter/config_test.go | 40 ++++++++ splitter/utils.go | 3 + splitter/utils_test.go | 195 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 455 insertions(+), 4 deletions(-) create mode 100644 splitter/cache_test.go create mode 100644 splitter/config_test.go create mode 100644 splitter/utils_test.go diff --git a/go.mod b/go.mod index 9424c21..6052595 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,17 @@ module github.com/splitsh/lite -go 1.22 +go 1.23 require ( github.com/libgit2/git2go/v34 v34.0.0 + github.com/stretchr/testify v1.8.1 go.etcd.io/bbolt v1.3.9 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.20.0 // indirect golang.org/x/sys v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9e563e5..95d2311 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -6,6 +7,11 @@ github.com/libgit2/git2go/v34 v34.0.0 h1:UKoUaKLmiCRbOCD3PtUi2hD6hESSXzME/9OUZrG github.com/libgit2/git2go/v34 v34.0.0/go.mod h1:blVco2jDAw6YTXkErMMqzHLcAjKkwF0aWIRHBqiJkZ0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= @@ -26,5 +32,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/run-tests.sh b/run-tests.sh index fc7570a..df2fb81 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -29,7 +29,7 @@ simpleTest() { rm -rf simple mkdir simple cd simple - git init > /dev/null + git init --initial-branch=main > /dev/null switchAsSammy "Sat, 24 Nov 1973 19:01:02 +0200" "Sat, 24 Nov 1973 19:11:22 +0200" echo "a" > a @@ -83,7 +83,7 @@ mergeTest() { rm -rf merge mkdir -p merge/src cd merge - git init > /dev/null + git init --initial-branch=main > /dev/null switchAsSammy "Sat, 24 Nov 1973 19:01:01 +0200" "Sat, 24 Nov 1973 19:01:01 +0200" echo -e "a\n\nb\n\nc\n\n" > src/foo @@ -159,7 +159,7 @@ filemodeTest() { rm -rf filemode mkdir filemode cd filemode - git init > /dev/null + git init --initial-branch=main > /dev/null switchAsSammy "Sat, 24 Nov 1973 19:01:02 +0200" "Sat, 24 Nov 1973 19:11:22 +0200" echo "a" > a diff --git a/splitter/cache_test.go b/splitter/cache_test.go new file mode 100644 index 0000000..d098cb9 --- /dev/null +++ b/splitter/cache_test.go @@ -0,0 +1,200 @@ +package splitter + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + mathRand "math/rand/v2" + "os" + "path/filepath" + "testing" + + git "github.com/libgit2/git2go/v34" + "github.com/stretchr/testify/assert" +) + +func TestCache(t *testing.T) { + + var err error + pwd, err := os.Getwd() + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Chdir(pwd) + + tempDir := t.TempDir() + defer os.RemoveAll(tempDir) + os.Chdir(tempDir) + + _, bCommitHashes, headCommitHash := makeTestRepo(t, tempDir) + + // test config + config := &Config{ + Path: filepath.Join(tempDir, "test-repo"), + GitVersion: "latest", + Git: 3, + Origin: "HEAD", + Target: "HEAD", + Prefixes: []*Prefix{ + { + From: "dira", + To: "/path/to/dira", + }, + }, + } + + // test cache + cache, err := newCache("b", config) + if !assert.NoError(t, err) { + t.FailNow() + } + defer cache.close() + + headCommitHashBytes, err := hex.DecodeString(headCommitHash) + if !assert.NoError(t, err) { + t.FailNow() + } + testOidHead := git.NewOidFromBytes(headCommitHashBytes) + fmt.Println(testOidHead.String()) + oid2CommitHashBytes, err := hex.DecodeString(bCommitHashes[0]) + if !assert.NoError(t, err) { + t.FailNow() + } + testOid2 := git.NewOidFromBytes(oid2CommitHashBytes) + + rng := mathRand.ChaCha8{} + seed := [32]byte{} + rand.Read(seed[:]) + rng.Seed(seed) + + // test set/get head + t.Run("GetSetHead", func(t *testing.T) { + oidGot := cache.getHead() + assert.Nil(t, oidGot) + + cache.setHead(testOid2) + oidGot = cache.getHead() + if !assert.NotNil(t, oidGot) { + t.FailNow() + } + assert.Equal(t, testOid2.String(), oidGot.String()) + }) + + t.Run("GetSet", func(t *testing.T) { + oidGot := cache.get(testOid2) + assert.Nil(t, oidGot) + + cache.set(testOid2, testOidHead) + oidGot = cache.get(testOid2) + if !assert.NotNil(t, oidGot) { + t.FailNow() + } + assert.Equal(t, testOidHead.String(), oidGot.String()) + + const testOidCount = 10 + testOidKeys := make([][]byte, testOidCount*20) + testOidValues := make([][]byte, testOidCount*20) + for i := 0; i < testOidCount; i++ { + buf := make([]byte, 40) + rng.Read(buf) + testOidKeys[i] = buf[0:20] + testOidValues[i] = buf[20:40] + + oidKey := git.NewOidFromBytes(testOidKeys[i]) + oidValue := git.NewOidFromBytes(testOidValues[i]) + cache.set(oidKey, oidValue) + oidGot := cache.get(oidKey) + assert.Equal(t, oidValue.String(), oidGot.String()) + } + + testGets := make([]*git.Oid, testOidCount) + for i := 0; i < testOidCount; i++ { + testGets[i] = git.NewOidFromBytes(testOidKeys[i]) + } + oidsGot := cache.gets(testGets) + for i := 0; i < testOidCount; i++ { + assert.Equal(t, git.NewOidFromBytes(testOidValues[i]).String(), oidsGot[i].String()) + } + }) +} +func TestCacheFlush(t *testing.T) { + var err error + pwd, err := os.Getwd() + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Chdir(pwd) + + tempDir := t.TempDir() + defer os.RemoveAll(tempDir) + os.Chdir(tempDir) + + _, _, headCommitHash := makeTestRepo(t, tempDir) + + config := &Config{ + Path: filepath.Join(tempDir, "test-repo"), + GitVersion: "latest", + Git: 3, + Origin: "HEAD", + Target: "HEAD", + } + cache, err := newCache("b", config) + if !assert.NoError(t, err) { + t.FailNow() + } + defer cache.close() + + headCommitHashBytes, err := hex.DecodeString(headCommitHash) + if !assert.NoError(t, err) { + t.FailNow() + } + testOidHead := git.NewOidFromBytes(headCommitHashBytes) + + rng := mathRand.ChaCha8{} + seed := [32]byte{} + rand.Read(seed[:]) + rng.Seed(seed) + + // set head + cache.setHead(testOidHead) + // set some commits + const testOidCount = 10 + testOidKeys := make([][]byte, testOidCount) + testOidValues := make([][]byte, testOidCount) + for i := 0; i < testOidCount; i++ { + buf := make([]byte, 40) + rng.Read(buf) + testOidKeys[i] = buf[0:20] + testOidValues[i] = buf[20:40] + + oidKey := git.NewOidFromBytes(testOidKeys[i]) + oidValue := git.NewOidFromBytes(testOidValues[i]) + cache.set(oidKey, oidValue) + } + + // flush it + err = cache.flush() + if !assert.NoError(t, err) { + t.FailNow() + } + cache.close() + + // create new cache + newCache, err := newCache("b", config) + if !assert.NoError(t, err) { + t.FailNow() + } + defer newCache.close() + + // check if the head is set + headGot := newCache.getHead() + assert.Equal(t, testOidHead.String(), headGot.String()) + + // check if the commits are set + for i := 0; i < testOidCount; i++ { + oidKey := git.NewOidFromBytes(testOidKeys[i]) + oidValue := git.NewOidFromBytes(testOidValues[i]) + oidGot := newCache.get(oidKey) + assert.Equal(t, oidValue.String(), oidGot.String()) + } +} diff --git a/splitter/config_test.go b/splitter/config_test.go new file mode 100644 index 0000000..af0018a --- /dev/null +++ b/splitter/config_test.go @@ -0,0 +1,40 @@ +package splitter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_Validate(t *testing.T) { + testCases := []struct { + name string + ref string + errStr string + }{ + {name: "Empty", ref: "", errStr: "the origin is not a valid Git reference"}, + {name: "HEAD", ref: "HEAD", errStr: ""}, + {name: "HEAD^", ref: "HEAD^", errStr: "the origin is not a valid Git reference"}, + {name: "HEAD^2", ref: "HEAD^2", errStr: "the origin is not a valid Git reference"}, + {name: "HEAD-1", ref: "HEAD-1", errStr: "the origin is not a valid Git reference"}, + {name: "RandomString1", ref: "main", errStr: "the origin is not a valid Git reference"}, + {name: "RandomString2", ref: "master", errStr: "the origin is not a valid Git reference"}, + {name: "RefHeads", ref: "refs/heads/somebranch", errStr: ""}, + {name: "RefTags", ref: "refs/tags/sometag", errStr: ""}, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + config := &Config{ + GitVersion: "latest", + Origin: testCase.ref, + Target: testCase.ref, + } + err := config.Validate() + if testCase.errStr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, testCase.errStr) + } + }) + } +} diff --git a/splitter/utils.go b/splitter/utils.go index 0e19bbf..c59fb99 100644 --- a/splitter/utils.go +++ b/splitter/utils.go @@ -68,6 +68,9 @@ func normalizeOrigin(repo *git.Repository, origin string) (string, error) { if obj != nil { obj.Free() } + if ref == nil { + return "", fmt.Errorf("bad revision for origin: ref is nil") + } defer ref.Free() return ref.Name(), nil diff --git a/splitter/utils_test.go b/splitter/utils_test.go new file mode 100644 index 0000000..160708d --- /dev/null +++ b/splitter/utils_test.go @@ -0,0 +1,195 @@ +package splitter + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + git "github.com/libgit2/git2go/v34" + "github.com/stretchr/testify/assert" +) + +func TestGitDirectory(t *testing.T) { + const worktreePath = "worktree" + const bareRepoPath = "bare-repo" + + var err error + pwd, err := os.Getwd() + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Chdir(pwd) + + tempDir := t.TempDir() + defer os.RemoveAll(tempDir) + os.Chdir(tempDir) + + // create a bare repo using command + createRepoCmd := exec.Command("git", "init", "--bare", bareRepoPath) + createRepoCmd.Dir = tempDir + err = createRepoCmd.Run() + if !assert.NoError(t, err) { + t.FailNow() + } + + // create a worktree repo using command + err = os.Mkdir(filepath.Join(tempDir, worktreePath), 0755) + if !assert.NoError(t, err) { + t.FailNow() + } + createWorktreeCmd := exec.Command("git", "init", worktreePath) + createWorktreeCmd.Dir = tempDir + err = createWorktreeCmd.Run() + if !assert.NoError(t, err) { + t.FailNow() + } + + t.Run("BareRepo", func(t *testing.T) { + os.Chdir(filepath.Join(tempDir, bareRepoPath)) + assert.Equal(t, GitDirectory(bareRepoPath), bareRepoPath) + }) + t.Run("WorktreeRepo", func(t *testing.T) { + os.Chdir(filepath.Join(tempDir, worktreePath)) + assert.Equal(t, GitDirectory(worktreePath), worktreePath) + }) +} + +func TestSplitMessage(t *testing.T) { + t.Skip("TODO") +} + +func execCmd(t *testing.T, args ...string) { + cmd := exec.Command(args[0], args[1:]...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + err := cmd.Run() + if !assert.NoError(t, err) { + t.Fatalf("err when executing command %s: %s", cmd.String(), err) + t.FailNow() + } +} +func execCmdOutput(t *testing.T, args ...string) string { + cmd := exec.Command(args[0], args[1:]...) + output, err := cmd.Output() + if !assert.NoError(t, err) { + t.Fatalf("err when executing command %s: %s", cmd.String(), err) + t.FailNow() + } + return string(output) +} + +func makeTestRepo(t *testing.T, tempDir string) ([]string, []string, string) { + var err error + // create a repo using command + err = os.Mkdir(filepath.Join(tempDir, "test-repo"), 0755) + if !assert.NoError(t, err) { + t.FailNow() + } + err = os.Chdir(filepath.Join(tempDir, "test-repo")) + if !assert.NoError(t, err) { + t.FailNow() + } + execCmd(t, "git", "init", "--initial-branch", "a") + + // create some commits on test branch + const aCommits = 10 + aCommitHashes := make([]string, aCommits) + for i := 0; i < aCommits; i++ { + os.WriteFile("somea.code", []byte(fmt.Sprintf("test %d", i)), 0644) + execCmd(t, "git", "add", "somea.code") + execCmd(t, "git", "commit", "--message", fmt.Sprintf("test %d", i)) + aCommitHashes[i] = strings.TrimSpace(execCmdOutput(t, "git", "rev-parse", "HEAD")) + } + + // create a new branch + execCmd(t, "git", "checkout", "-b", "b") + execCmd(t, "git", "reset", "--hard", aCommitHashes[5]) + + // create some commits on b branch + const bCommits = 10 + bCommitHashes := make([]string, bCommits) + for i := 0; i < bCommits; i++ { + os.WriteFile("someb.code", []byte(fmt.Sprintf("test %d", i)), 0644) + execCmd(t, "git", "add", "someb.code") + execCmd(t, "git", "commit", "--message", fmt.Sprintf("test %d", i)) + bCommitHashes[i] = strings.TrimSpace(execCmdOutput(t, "git", "rev-parse", "HEAD")) + } + + // merge b branch into a + execCmd(t, "git", "merge", "b") + + // get HEAD commit hash + headCommitHash := strings.TrimSpace(execCmdOutput(t, "git", "rev-parse", "HEAD")) + + // make some tags + execCmd(t, "git", "tag", "lightweight") + execCmd(t, "git", "tag", "annotated", "-m", "annotated tag") + + return aCommitHashes, bCommitHashes, headCommitHash +} + +func TestNormalizeOrigin(t *testing.T) { + + var err error + pwd, err := os.Getwd() + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Chdir(pwd) + + tempDir := t.TempDir() + defer os.RemoveAll(tempDir) + os.Chdir(tempDir) + + _, _, headCommitHash := makeTestRepo(t, tempDir) + + repo, err := git.OpenRepository(filepath.Join(tempDir, "test-repo")) + if !assert.NoError(t, err) { + t.FailNow() + } + defer repo.Free() + + t.Run("HEAD", func(t *testing.T) { + ref, err := normalizeOrigin(repo, "HEAD") + assert.NoError(t, err) + assert.Equal(t, "refs/heads/b", ref) + + // not on a branch, this will faild + ref, err = normalizeOrigin(repo, "HEAD^") + assert.Error(t, err) + + ref, err = normalizeOrigin(repo, "HEAD^2") + assert.Error(t, err) + + ref, err = normalizeOrigin(repo, "HEAD-1") + assert.Error(t, err) + }) + + t.Run("Tags", func(t *testing.T) { + ref, err := normalizeOrigin(repo, "lightweight") + assert.NoError(t, err) + assert.Equal(t, "refs/tags/lightweight", ref) + + ref, err = normalizeOrigin(repo, "annotated") + assert.NoError(t, err) + assert.Equal(t, "refs/tags/annotated", ref) + }) + + t.Run("Commits", func(t *testing.T) { + _, err := normalizeOrigin(repo, headCommitHash) + assert.Error(t, err) + }) + + t.Run("Ref", func(t *testing.T) { + ref, err := normalizeOrigin(repo, "refs/heads/b") + assert.NoError(t, err) + assert.Equal(t, "refs/heads/b", ref) + + ref, err = normalizeOrigin(repo, "refs/tags/lightweight") + assert.NoError(t, err) + assert.Equal(t, "refs/tags/lightweight", ref) + }) +} From 668a6b8d45d6639445d525a2e9434c5cfe06af22 Mon Sep 17 00:00:00 2001 From: Yun Dou Date: Fri, 13 Mar 2026 16:25:22 +0800 Subject: [PATCH 2/2] Use go-git to reimplement I've got tired of building static libgit2 and fighting with git2go and cgo. state.go is rewrote by cursor, tested with run-tests.sh and unit tests Co-authored-by: Cursor --- go.mod | 23 +- go.sum | 82 ++++-- run-tests.sh | 2 +- splitter/cache.go | 56 +++-- splitter/cache_test.go | 71 +++--- splitter/config.go | 19 +- splitter/result.go | 8 +- splitter/state.go | 553 +++++++++++++++++++++++------------------ splitter/utils.go | 35 ++- splitter/utils_test.go | 5 +- 10 files changed, 479 insertions(+), 375 deletions(-) diff --git a/go.mod b/go.mod index 6052595..89d79f0 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,30 @@ module github.com/splitsh/lite -go 1.23 +go 1.24.0 require ( - github.com/libgit2/git2go/v34 v34.0.0 - github.com/stretchr/testify v1.8.1 + github.com/go-git/go-git/v6 v6.0.0-20260312103649-3b3581068cee + github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.3.9 ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg/v2 v2.0.2 // indirect + github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/kevinburke/ssh_config v1.5.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.20.0 // indirect - golang.org/x/sys v0.17.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 95d2311..cce7a50 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,69 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/libgit2/git2go/v34 v34.0.0 h1:UKoUaKLmiCRbOCD3PtUi2hD6hESSXzME/9OUZrGcgu8= -github.com/libgit2/git2go/v34 v34.0.0/go.mod h1:blVco2jDAw6YTXkErMMqzHLcAjKkwF0aWIRHBqiJkZ0= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= +github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f h1:Uvbx7nITO3Sd1GdXarX0TbyYmOaSNIJP0mm4LocEyyA= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= +github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= +github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= +github.com/go-git/go-git/v6 v6.0.0-20260312103649-3b3581068cee h1:drZI7wohqpPBTcMnl2QcW6IUiPBmErlu12kPf8zz3Eo= +github.com/go-git/go-git/v6 v6.0.0-20260312103649-3b3581068cee/go.mod h1:V/qoTD4qCYizR+fKFA9++d2APoE8Yheci7dXALaSeuI= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA= +github.com/kevinburke/ssh_config v1.5.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/run-tests.sh b/run-tests.sh index df2fb81..c7afe89 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -116,7 +116,7 @@ mergeTest() { git merge branch2 --no-edit -s ours > /dev/null GIT_SUBTREE_SPLIT_SHA1_2="a2c4245703f8dac149ab666242a12e1d4b2510d9" - GIT_SUBTREE_SPLIT_SHA1_3="ba0dab2c4e99d68d11088f2c556af92851e93b14" + GIT_SUBTREE_SPLIT_SHA1_3=`git subtree split --prefix=src/ -q` GIT_SPLITSH_SHA1_2=`$LITE_PATH --git="<2.8.0" --prefix=src/ 2>/dev/null` GIT_SPLITSH_SHA1_3=`$LITE_PATH --prefix=src/ 2>/dev/null` diff --git a/splitter/cache.go b/splitter/cache.go index fca8953..5da4182 100644 --- a/splitter/cache.go +++ b/splitter/cache.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6/plumbing" bolt "go.etcd.io/bbolt" ) @@ -85,60 +85,70 @@ func key(config *Config) []byte { return h.Sum(nil) } -func (c *cache) setHead(head *git.Oid) { - c.data["head/"+c.branch] = head[0:20] +func (c *cache) setHead(head *plumbing.ObjectID) { + c.data["head/"+c.branch] = head.Bytes() } -func (c *cache) getHead() *git.Oid { - if head, ok := c.data["head"+c.branch]; ok { - return git.NewOidFromBytes(head) +func (c *cache) getHead() *plumbing.ObjectID { + if head, ok := c.data["head/"+c.branch]; ok { + // since we set the hash generated by ourself + // this will always be successful + hash, _ := plumbing.FromBytes(head) + return &hash } - var oid *git.Oid + var oid *plumbing.ObjectID = nil c.db.View(func(tx *bolt.Tx) error { result := tx.Bucket(c.key).Get([]byte("head/" + c.branch)) if result != nil { c.data["head/"+c.branch] = result - oid = git.NewOidFromBytes(result) + hash, _ := plumbing.FromBytes(result) + oid = &hash } return nil }) return oid } -func (c *cache) get(rev *git.Oid) *git.Oid { - if v, ok := c.data[string(rev[0:20])]; ok { - return git.NewOidFromBytes(v) +func (c *cache) get(rev *plumbing.ObjectID) *plumbing.ObjectID { + if v, ok := c.data[string(rev.Bytes())]; ok { + // since we set the hash generated by ourself + // this will always be successful + hash, _ := plumbing.FromBytes(v) + return &hash } - var oid *git.Oid + var oid *plumbing.ObjectID = nil c.db.View(func(tx *bolt.Tx) error { - result := tx.Bucket(c.key).Get(rev[0:20]) + result := tx.Bucket(c.key).Get(rev.Bytes()) if result != nil { - c.data[string(rev[0:20])] = result - oid = git.NewOidFromBytes(result) + c.data[string(rev.Bytes())] = result + hash, _ := plumbing.FromBytes(result) + oid = &hash } return nil }) return oid } -func (c *cache) set(rev, newrev *git.Oid) { - c.data[string(rev[0:20])] = newrev[0:20] +func (c *cache) set(rev, newrev *plumbing.ObjectID) { + c.data[string(rev.Bytes())] = newrev.Bytes() } -func (c *cache) gets(commits []*git.Oid) []*git.Oid { - var oids []*git.Oid +func (c *cache) gets(commits []*plumbing.ObjectID) []*plumbing.ObjectID { + var oids []*plumbing.ObjectID = nil c.db.View(func(tx *bolt.Tx) error { b := tx.Bucket(c.key) for _, commit := range commits { - result := c.data[string(commit[0:20])] + result := c.data[string(commit.Bytes())] if result != nil { - oids = append(oids, git.NewOidFromBytes(result)) + hash, _ := plumbing.FromBytes(result) + oids = append(oids, &hash) } else { - result := b.Get(commit[0:20]) + result := b.Get(commit.Bytes()) if result != nil { - oids = append(oids, git.NewOidFromBytes(result)) + hash, _ := plumbing.FromBytes(result) + oids = append(oids, &hash) } } } diff --git a/splitter/cache_test.go b/splitter/cache_test.go index d098cb9..538322f 100644 --- a/splitter/cache_test.go +++ b/splitter/cache_test.go @@ -3,13 +3,12 @@ package splitter import ( "crypto/rand" "encoding/hex" - "fmt" mathRand "math/rand/v2" "os" "path/filepath" "testing" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6/plumbing" "github.com/stretchr/testify/assert" ) @@ -50,17 +49,8 @@ func TestCache(t *testing.T) { } defer cache.close() - headCommitHashBytes, err := hex.DecodeString(headCommitHash) - if !assert.NoError(t, err) { - t.FailNow() - } - testOidHead := git.NewOidFromBytes(headCommitHashBytes) - fmt.Println(testOidHead.String()) - oid2CommitHashBytes, err := hex.DecodeString(bCommitHashes[0]) - if !assert.NoError(t, err) { - t.FailNow() - } - testOid2 := git.NewOidFromBytes(oid2CommitHashBytes) + testOidHead := plumbing.NewHash(headCommitHash) + testOid2 := plumbing.NewHash(bCommitHashes[0]) rng := mathRand.ChaCha8{} seed := [32]byte{} @@ -72,7 +62,7 @@ func TestCache(t *testing.T) { oidGot := cache.getHead() assert.Nil(t, oidGot) - cache.setHead(testOid2) + cache.setHead(&testOid2) oidGot = cache.getHead() if !assert.NotNil(t, oidGot) { t.FailNow() @@ -81,39 +71,40 @@ func TestCache(t *testing.T) { }) t.Run("GetSet", func(t *testing.T) { - oidGot := cache.get(testOid2) + oidGot := cache.get(&testOid2) assert.Nil(t, oidGot) - cache.set(testOid2, testOidHead) - oidGot = cache.get(testOid2) + cache.set(&testOid2, &testOidHead) + oidGot = cache.get(&testOid2) if !assert.NotNil(t, oidGot) { t.FailNow() } assert.Equal(t, testOidHead.String(), oidGot.String()) const testOidCount = 10 - testOidKeys := make([][]byte, testOidCount*20) - testOidValues := make([][]byte, testOidCount*20) + testOidKeys := make([]string, testOidCount) + testOidValues := make([]string, testOidCount) for i := 0; i < testOidCount; i++ { buf := make([]byte, 40) rng.Read(buf) - testOidKeys[i] = buf[0:20] - testOidValues[i] = buf[20:40] + testOidKeys[i] = hex.EncodeToString(buf[0:20]) + testOidValues[i] = hex.EncodeToString(buf[20:40]) - oidKey := git.NewOidFromBytes(testOidKeys[i]) - oidValue := git.NewOidFromBytes(testOidValues[i]) - cache.set(oidKey, oidValue) - oidGot := cache.get(oidKey) + oidKey := plumbing.NewHash(testOidKeys[i]) + oidValue := plumbing.NewHash(testOidValues[i]) + cache.set(&oidKey, &oidValue) + oidGot := cache.get(&oidKey) assert.Equal(t, oidValue.String(), oidGot.String()) } - testGets := make([]*git.Oid, testOidCount) + testGets := make([]*plumbing.ObjectID, testOidCount) for i := 0; i < testOidCount; i++ { - testGets[i] = git.NewOidFromBytes(testOidKeys[i]) + hash := plumbing.NewHash(testOidKeys[i]) + testGets[i] = &hash } oidsGot := cache.gets(testGets) for i := 0; i < testOidCount; i++ { - assert.Equal(t, git.NewOidFromBytes(testOidValues[i]).String(), oidsGot[i].String()) + assert.Equal(t, plumbing.NewHash(testOidValues[i]).String(), oidsGot[i].String()) } }) } @@ -144,11 +135,7 @@ func TestCacheFlush(t *testing.T) { } defer cache.close() - headCommitHashBytes, err := hex.DecodeString(headCommitHash) - if !assert.NoError(t, err) { - t.FailNow() - } - testOidHead := git.NewOidFromBytes(headCommitHashBytes) + testOidHead := plumbing.NewHash(headCommitHash) rng := mathRand.ChaCha8{} seed := [32]byte{} @@ -156,20 +143,20 @@ func TestCacheFlush(t *testing.T) { rng.Seed(seed) // set head - cache.setHead(testOidHead) + cache.setHead(&testOidHead) // set some commits const testOidCount = 10 - testOidKeys := make([][]byte, testOidCount) - testOidValues := make([][]byte, testOidCount) + testOidKeys := make([][]byte, testOidCount*20) + testOidValues := make([][]byte, testOidCount*20) for i := 0; i < testOidCount; i++ { buf := make([]byte, 40) rng.Read(buf) testOidKeys[i] = buf[0:20] testOidValues[i] = buf[20:40] - oidKey := git.NewOidFromBytes(testOidKeys[i]) - oidValue := git.NewOidFromBytes(testOidValues[i]) - cache.set(oidKey, oidValue) + oidKey := plumbing.NewHash(hex.EncodeToString(testOidKeys[i])) + oidValue := plumbing.NewHash(hex.EncodeToString(testOidValues[i])) + cache.set(&oidKey, &oidValue) } // flush it @@ -192,9 +179,9 @@ func TestCacheFlush(t *testing.T) { // check if the commits are set for i := 0; i < testOidCount; i++ { - oidKey := git.NewOidFromBytes(testOidKeys[i]) - oidValue := git.NewOidFromBytes(testOidValues[i]) - oidGot := newCache.get(oidKey) + oidKey := plumbing.NewHash(hex.EncodeToString(testOidKeys[i])) + oidValue := plumbing.NewHash(hex.EncodeToString(testOidValues[i])) + oidGot := newCache.get(&oidKey) assert.Equal(t, oidValue.String(), oidGot.String()) } } diff --git a/splitter/config.go b/splitter/config.go index b5aefdb..23aa327 100644 --- a/splitter/config.go +++ b/splitter/config.go @@ -6,7 +6,8 @@ import ( "strings" "sync" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" bolt "go.etcd.io/bbolt" ) @@ -73,20 +74,14 @@ func Split(config *Config, result *Result) error { // Validate validates the configuration func (config *Config) Validate() error { - ok, err := git.ReferenceNameIsValid(config.Origin) + err := plumbing.ReferenceName(config.Origin).Validate() if err != nil { - return err - } - if !ok { - return fmt.Errorf("the origin is not a valid Git reference") + return fmt.Errorf("the origin is not a valid Git reference: %w", err) } - ok, err = git.ReferenceNameIsValid(config.Target) - if err != nil { - return err - } - if config.Target != "" && !ok { - return fmt.Errorf("the target is not a valid Git reference") + err = plumbing.ReferenceName(config.Target).Validate() + if config.Target != "" && err != nil { + return fmt.Errorf("the target is not a valid Git reference: %w", err) } git, ok := supportedGitVersions[config.GitVersion] diff --git a/splitter/result.go b/splitter/result.go index 7dbcabc..3db40c2 100644 --- a/splitter/result.go +++ b/splitter/result.go @@ -4,7 +4,7 @@ import ( "sync" "time" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6/plumbing" ) // Result represents the outcome of a split @@ -12,7 +12,7 @@ type Result struct { mu sync.RWMutex traversed int created int - head *git.Oid + head *plumbing.ObjectID duration time.Duration } @@ -47,13 +47,13 @@ func (r *Result) Duration(precision time.Duration) time.Duration { } // Head returns the latest split sha1 -func (r *Result) Head() *git.Oid { +func (r *Result) Head() *plumbing.ObjectID { r.mu.RLock() defer r.mu.RUnlock() return r.head } -func (r *Result) moveHead(oid *git.Oid) { +func (r *Result) moveHead(oid *plumbing.ObjectID) { r.mu.Lock() r.head = oid r.mu.Unlock() diff --git a/splitter/state.go b/splitter/state.go index a5da90a..d284a46 100644 --- a/splitter/state.go +++ b/splitter/state.go @@ -4,17 +4,20 @@ import ( "fmt" "log" "os" + "sort" "strings" - "sync" "time" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/filemode" + "github.com/go-git/go-git/v6/plumbing/object" + "github.com/go-git/go-git/v6/plumbing/storer" ) type state struct { config *Config origin string - repoMu *sync.Mutex repo *git.Repository cache *cache logger *log.Logger @@ -33,21 +36,16 @@ func newState(config *Config, result *Result) (*state, error) { state := &state{ config: config, result: result, - repoMu: config.RepoMu, repo: config.Repo, logger: config.Logger, } if state.repo == nil { - if state.repo, err = git.OpenRepository(config.Path); err != nil { + if state.repo, err = git.PlainOpen(config.Path); err != nil { return nil, err } } - if state.repoMu == nil { - state.repoMu = &sync.Mutex{} - } - if state.logger == nil { state.logger = log.New(os.Stderr, "", log.LstdFlags) } @@ -95,7 +93,6 @@ func (s *state) close() error { if err != nil { return err } - s.repo.Free() return nil } @@ -105,85 +102,206 @@ func (s *state) flush() error { } if s.config.Target != "" { - branch, err := s.repo.LookupBranch(s.config.Target, git.BranchLocal) - if err == nil { - branch.Delete() - branch.Free() + targetRef := plumbing.ReferenceName(s.config.Target) + err := s.repo.Storer.RemoveReference(targetRef) + if err != nil && err != plumbing.ErrReferenceNotFound { + return err + } + + if targetRef.IsBranch() { + _ = s.repo.DeleteBranch(targetRef.Short()) } } return nil } +type gitHashMap[T any] map[string]T + +// splitRange determines the commit range to process, equivalent to +// git rev-list's "A..B" range semantics. +// +// Returns (from, excludeSet, error) where: +// - from: the origin commit hash to start walking from +// - excludeSet: commits reachable from a prior split point to skip, +// nil means walk all ancestors of from +// +// Three cases depending on prior state: +// 1. Cache has a head → incremental split (head..origin) +// 2. Config specifies a commit → resume from that point (commit^..origin) +// 3. Neither → full split from origin +func (s *state) splitRange() (*plumbing.Hash, gitHashMap[bool], error) { + originRef, err := s.repo.Reference(plumbing.ReferenceName(s.origin), true) + if err != nil { + return nil, nil, fmt.Errorf("cannot resolve origin %s: %s", s.origin, err) + } + originHash, err := peelTag(s.repo, originRef.Hash()) + if err != nil { + return nil, nil, err + } + + // Case 1: incremental — only process commits since last cached head + if head := s.cache.getHead(); head != nil { + s.result.moveHead(s.cache.get(head)) + excludeSet, err := s.reachableFrom(head) + return originHash, excludeSet, err + } + + // Case 2: resume from a specific commit — exclude its first parent and ancestors + if s.config.Commit != "" { + commitHash := plumbing.NewHash(s.config.Commit) + s.result.moveHead(s.cache.get(&commitHash)) + return &commitHash, nil, nil + } + + // Case 3: full split — no exclusions + return originHash, nil, nil +} + +// collectCommits walks commit history from the given hash and returns +// commits in topological order (parents before children) for replay. +// +// go-git's LogOrderDFSPost is not a true topological sort for DAGs +// with merge commits, so we do our own BFS + Kahn's algorithm. +func (s *state) collectCommits(from *plumbing.Hash, excludeSet gitHashMap[bool]) ([]*object.Commit, error) { + commitMap := make(map[string]*object.Commit) + queue := []plumbing.Hash{*from} + for len(queue) > 0 { + hash := queue[0] + queue = queue[1:] + key := hash.String() + if commitMap[key] != nil { + continue + } + if excludeSet != nil && excludeSet[key] { + continue + } + c, err := s.repo.CommitObject(hash) + if err != nil { + return nil, fmt.Errorf("impossible to read commit %s: %s", hash, err) + } + commitMap[key] = c + for _, ph := range c.ParentHashes { + queue = append(queue, ph) + } + } + if len(commitMap) == 0 { + return nil, nil + } + children := make(map[string][]string) + inDegree := make(map[string]int) + for key, c := range commitMap { + deg := 0 + for _, ph := range c.ParentHashes { + pk := ph.String() + if commitMap[pk] != nil { + deg++ + children[pk] = append(children[pk], key) + } + } + inDegree[key] = deg + } + var ready []string + for key, deg := range inDegree { + if deg == 0 { + ready = append(ready, key) + } + } + var result []*object.Commit + for len(ready) > 0 { + key := ready[len(ready)-1] + ready = ready[:len(ready)-1] + result = append(result, commitMap[key]) + for _, childKey := range children[key] { + inDegree[childKey]-- + if inDegree[childKey] == 0 { + ready = append(ready, childKey) + } + } + } + return result, nil +} + func (s *state) split() error { startTime := time.Now() defer func() { s.result.end(startTime) }() - - revWalk, err := s.walker() + from, excludeSet, err := s.splitRange() if err != nil { - return fmt.Errorf("impossible to walk the repository: %s", err) + return err } - defer revWalk.Free() - - var iterationErr error - var lastRev *git.Oid - err = revWalk.Iterate(func(rev *git.Commit) bool { - defer rev.Free() - lastRev = rev.Id() - + commits, err := s.collectCommits(from, excludeSet) + if err != nil { + return err + } + var lastHash plumbing.Hash + for _, rev := range commits { + lastHash = rev.Hash if s.config.Debug { - s.logger.Printf("Processing commit: %s\n", rev.Id().String()) + s.logger.Printf("Processing commit: %s\n", rev.Hash) } - - var newrev *git.Oid - newrev, err = s.splitRev(rev) + newrev, err := s.splitRev(rev) if err != nil { - iterationErr = err - return false + return err } - if newrev != nil { s.result.moveHead(newrev) } - - return true - }) - if err != nil { - return err } - if iterationErr != nil { - return iterationErr + // Record the last processed original commit as cache head + // so future runs can skip already-split commits. + if lastHash != plumbing.ZeroHash { + s.cache.setHead(&lastHash) } - - if lastRev != nil { - s.cache.setHead(lastRev) - } - return s.updateTarget() } -func (s *state) walker() (*git.RevWalk, error) { - revWalk, err := s.repo.Walk() +func (s *state) reachableFrom(hash *plumbing.Hash) (gitHashMap[bool], error) { + iter, err := s.repo.Log(&git.LogOptions{From: *hash, Order: git.LogOrderDFS}) + if err != nil { + return nil, err + } + defer iter.Close() + set := make(gitHashMap[bool]) + err = iter.ForEach(func(c *object.Commit) error { + set[c.Hash.String()] = true + return nil + }) if err != nil { - return nil, fmt.Errorf("impossible to walk the repository: %s", err) + return nil, fmt.Errorf("failed to collect reachable commits: %s", err) } + return set, nil +} - err = s.pushRevs(revWalk) +func (s *state) isReachableFrom(target, source *plumbing.Hash) (bool, error) { + if target.String() == source.String() { + return true, nil + } + iter, err := s.repo.Log(&git.LogOptions{From: *source, Order: git.LogOrderDFS}) if err != nil { - return nil, fmt.Errorf("impossible to determine split range: %s", err) + return false, err } - - revWalk.Sorting(git.SortTopological | git.SortReverse) - - return revWalk, nil + defer iter.Close() + targetStr := target.String() + found := false + err = iter.ForEach(func(c *object.Commit) error { + if c.Hash.String() == targetStr { + found = true + return storer.ErrStop + } + return nil + }) + if err != nil { + return false, err + } + return found, nil } -func (s *state) splitRev(rev *git.Commit) (*git.Oid, error) { +func (s *state) splitRev(rev *object.Commit) (*plumbing.Hash, error) { s.result.incTraversed() - v := s.cache.get(rev.Id()) + v := s.cache.get(&rev.Hash) if v != nil { if s.config.Debug { s.logger.Printf(" prior: %s\n", v.String()) @@ -191,10 +309,9 @@ func (s *state) splitRev(rev *git.Commit) (*git.Oid, error) { return v, nil } - var parents []*git.Oid - var n uint - for n = 0; n < rev.ParentCount(); n++ { - parents = append(parents, rev.ParentId(n)) + var parents []*plumbing.Hash + for i := range rev.ParentHashes { + parents = append(parents, &rev.ParentHashes[i]) } if s.config.Debug { @@ -220,14 +337,12 @@ func (s *state) splitRev(rev *git.Commit) (*git.Oid, error) { return nil, err } - if nil == tree { - // should never happen + if tree == nil { return nil, nil } - defer tree.Free() if s.config.Debug { - s.logger.Printf(" tree is: %s\n", tree.Id().String()) + s.logger.Printf(" tree is: %s\n", tree.Hash.String()) } newrev, created, err := s.copyOrSkip(rev, tree, newParents) @@ -243,17 +358,16 @@ func (s *state) splitRev(rev *git.Commit) (*git.Oid, error) { s.result.incCreated() } - s.cache.set(rev.Id(), newrev) + s.cache.set(&rev.Hash, newrev) return newrev, nil } -func (s *state) subtreeForCommit(commit *git.Commit) (*git.Tree, error) { +func (s *state) subtreeForCommit(commit *object.Commit) (*object.Tree, error) { tree, err := commit.Tree() if err != nil { return nil, err } - defer tree.Free() if s.simplePrefix != "" { return s.treeByPath(tree, s.simplePrefix) @@ -262,24 +376,22 @@ func (s *state) subtreeForCommit(commit *git.Commit) (*git.Tree, error) { return s.treeByPaths(tree) } -func (s *state) treeByPath(tree *git.Tree, prefix string) (*git.Tree, error) { - treeEntry, err := tree.EntryByPath(prefix) +func (s *state) treeByPath(tree *object.Tree, prefix string) (*object.Tree, error) { + entry, err := tree.FindEntry(prefix) if err != nil { return nil, nil } - if treeEntry.Type != git.ObjectTree { - // tree is not a tree (a directory for a gitmodule for instance), skip + if entry.Mode != filemode.Dir { return nil, nil } - return s.repo.LookupTree(treeEntry.Id) + return s.repo.TreeObject(entry.Hash) } -func (s *state) treeByPaths(tree *git.Tree) (*git.Tree, error) { - var currentTree, prefixedTree, mergedTree *git.Tree +func (s *state) treeByPaths(tree *object.Tree) (*object.Tree, error) { + var currentTree *object.Tree for _, prefix := range s.config.Prefixes { - // splitting splitTree, err := s.treeByPath(tree, prefix.From) if err != nil { return nil, err @@ -296,7 +408,7 @@ func (s *state) treeByPaths(tree *git.Tree) (*git.Tree, error) { splitTree = prunedTree } - // adding the prefix + var prefixedTree *object.Tree if prefix.To != "" { prefixedTree, err = s.addPrefixToTree(splitTree, prefix.To) if err != nil { @@ -306,134 +418,124 @@ func (s *state) treeByPaths(tree *git.Tree) (*git.Tree, error) { prefixedTree = splitTree } - // merging with the current tree if currentTree != nil { - mergedTree, err = s.mergeTrees(currentTree, prefixedTree) - currentTree.Free() - prefixedTree.Free() + mergedTree, err := s.mergeTrees(currentTree, prefixedTree) if err != nil { return nil, err } + currentTree = mergedTree } else { - mergedTree = prefixedTree + currentTree = prefixedTree } - - currentTree = mergedTree } return currentTree, nil } -func (s *state) mergeTrees(t1, t2 *git.Tree) (*git.Tree, error) { - index, err := s.repo.MergeTrees(nil, t1, t2, nil) - if err != nil { - return nil, err +func (s *state) mergeTrees(t1, t2 *object.Tree) (*object.Tree, error) { + entryMap := make(map[string]object.TreeEntry) + for _, e := range t1.Entries { + entryMap[e.Name] = e } - defer index.Free() - if index.HasConflicts() { - return nil, fmt.Errorf("cannot split as there is a merge conflict between two paths") + for _, e := range t2.Entries { + if existing, ok := entryMap[e.Name]; ok { + if existing.Hash.Equal(e.Hash) && existing.Mode == e.Mode { + continue + } + if existing.Mode == filemode.Dir && e.Mode == filemode.Dir { + existingTree, err := s.repo.TreeObject(existing.Hash) + if err != nil { + return nil, err + } + newTree, err := s.repo.TreeObject(e.Hash) + if err != nil { + return nil, err + } + merged, err := s.mergeTrees(existingTree, newTree) + if err != nil { + return nil, err + } + entryMap[e.Name] = object.TreeEntry{ + Name: e.Name, + Mode: filemode.Dir, + Hash: merged.Hash, + } + continue + } + return nil, fmt.Errorf("cannot split as there is a merge conflict between two paths") + } + entryMap[e.Name] = e } - oid, err := index.WriteTreeTo(s.repo) - if err != nil { - return nil, err + entries := make([]object.TreeEntry, 0, len(entryMap)) + for _, e := range entryMap { + entries = append(entries, e) } - return s.repo.LookupTree(oid) + return s.storeTree(entries) } -func (s *state) addPrefixToTree(tree *git.Tree, prefix string) (*git.Tree, error) { - treeOid := tree.Id() +func (s *state) addPrefixToTree(tree *object.Tree, prefix string) (*object.Tree, error) { + treeHash := tree.Hash parts := strings.Split(prefix, "/") for i := len(parts) - 1; i >= 0; i-- { - treeBuilder, err := s.repo.TreeBuilder() - if err != nil { - return nil, err - } - defer treeBuilder.Free() - - err = treeBuilder.Insert(parts[i], treeOid, git.FilemodeTree) - if err != nil { - return nil, err - } - - treeOid, err = treeBuilder.Write() + entries := []object.TreeEntry{{ + Name: parts[i], + Mode: filemode.Dir, + Hash: treeHash, + }} + newTree, err := s.storeTree(entries) if err != nil { return nil, err } + treeHash = newTree.Hash } - prefixedTree, err := s.repo.LookupTree(treeOid) - if err != nil { - return nil, err - } - - return prefixedTree, nil + return s.repo.TreeObject(treeHash) } -func (s *state) pruneTree(tree *git.Tree, excludes []string) (*git.Tree, error) { - var err error - treeBuilder, err := s.repo.TreeBuilder() - if err != nil { - return nil, err - } - defer treeBuilder.Free() - - err = tree.Walk(func(path string, entry *git.TreeEntry) error { - // always add files at the root directory - if entry.Type == git.ObjectBlob { - if err := treeBuilder.Insert(entry.Name, entry.Id, entry.Filemode); err != nil { - return err - } - return nil +func (s *state) pruneTree(tree *object.Tree, excludes []string) (*object.Tree, error) { + var entries []object.TreeEntry + for _, entry := range tree.Entries { + if entry.Mode.IsFile() { + entries = append(entries, entry) + continue } - if entry.Type != git.ObjectTree { - // should never happen - return fmt.Errorf("Unexpected entry %s/%s (type %s)", path, entry.Name, entry.Type) + if entry.Mode != filemode.Dir { + return nil, fmt.Errorf("Unexpected entry %s (type %s)", entry.Name, entry.Mode) } - // exclude directory in excludes + excluded := false for _, exclude := range excludes { if entry.Name == exclude { - return git.TreeWalkSkip + excluded = true + break } } - if err := treeBuilder.Insert(entry.Name, entry.Id, git.FilemodeTree); err != nil { - return err + if !excluded { + entries = append(entries, entry) } - return git.TreeWalkSkip - }) - - if err != nil { - return nil, err - } - - treeOid, err := treeBuilder.Write() - if err != nil { - return nil, err } - return s.repo.LookupTree(treeOid) + return s.storeTree(entries) } -func (s *state) copyOrSkip(rev *git.Commit, tree *git.Tree, newParents []*git.Oid) (*git.Oid, bool, error) { - var identical, nonIdentical *git.Oid - var gotParents []*git.Oid - var p []*git.Commit +func (s *state) copyOrSkip(rev *object.Commit, tree *object.Tree, newParents []*plumbing.Hash) (*plumbing.Hash, bool, error) { + var identical, nonIdentical *plumbing.Hash + var parentHashes []plumbing.Hash for _, parent := range newParents { ptree, err := s.topTreeForCommit(parent) if err != nil { return nil, false, err } - if nil == ptree { + if ptree == nil { continue } - if ptree.Cmp(tree.Id()) == 0 { - // an identical parent could be used in place of this rev. + if ptree.Equal(tree.Hash) { identical = parent } else { nonIdentical = parent @@ -442,109 +544,94 @@ func (s *state) copyOrSkip(rev *git.Commit, tree *git.Tree, newParents []*git.Oi // sometimes both old parents map to the same newparent // eliminate duplicates isNew := true - for _, gp := range gotParents { - if gp.Cmp(parent) == 0 { + for _, gp := range parentHashes { + if gp.Equal(*parent) { isNew = false break } } if isNew { - gotParents = append(gotParents, parent) - commit, err := s.repo.LookupCommit(parent) - if err != nil { - return nil, false, err - } - defer commit.Free() - p = append(p, commit) + parentHashes = append(parentHashes, *parent) } } copyCommit := false - if s.config.Git > 2 && nil != identical && nil != nonIdentical { - revWalk, err := s.repo.Walk() + if s.config.Git > 2 && identical != nil && nonIdentical != nil { + reachable, err := s.isReachableFrom(nonIdentical, identical) if err != nil { return nil, false, fmt.Errorf("impossible to walk the repository: %s", err) } - - s.repoMu.Lock() - defer s.repoMu.Unlock() - - err = revWalk.PushRange(fmt.Sprintf("%s..%s", identical, nonIdentical)) - if err != nil { - return nil, false, fmt.Errorf("impossible to determine split range: %s", err) - } - - err = revWalk.Iterate(func(rev *git.Commit) bool { - // we need to preserve history along the other branch + if !reachable { copyCommit = true - return false - }) - if err != nil { - return nil, false, err } - - revWalk.Free() } - if nil != identical && !copyCommit { + if identical != nil && !copyCommit { return identical, false, nil } - commit, err := s.copyCommit(rev, tree, p) + hash, err := s.copyCommit(rev, tree, parentHashes) if err != nil { return nil, false, err } - return commit, true, nil + return hash, true, nil } -func (s *state) topTreeForCommit(sha *git.Oid) (*git.Oid, error) { - commit, err := s.repo.LookupCommit(sha) - if err != nil { - return nil, err - } - defer commit.Free() - - tree, err := commit.Tree() +func (s *state) topTreeForCommit(sha *plumbing.Hash) (*plumbing.Hash, error) { + commit, err := s.repo.CommitObject(*sha) if err != nil { return nil, err } - defer tree.Free() - return tree.Id(), nil + treeHash := commit.TreeHash + return &treeHash, nil } -func (s *state) copyCommit(rev *git.Commit, tree *git.Tree, parents []*git.Commit) (*git.Oid, error) { +func (s *state) copyCommit(rev *object.Commit, tree *object.Tree, parents []plumbing.Hash) (*plumbing.Hash, error) { + // fmt.Printf("copyCommit: %s %s\n", rev.Hash.String(), tree.Hash.String()) if s.config.Debug { parentStrs := make([]string, len(parents)) for i, parent := range parents { - parentStrs[i] = parent.Id().String() + parentStrs[i] = parent.String() } - s.logger.Printf(" copy commit \"%s\" \"%s\" \"%s\"\n", rev.Id().String(), tree.Id().String(), strings.Join(parentStrs, " ")) + s.logger.Printf(" copy commit \"%s\" \"%s\" \"%s\"\n", rev.Hash.String(), tree.Hash.String(), strings.Join(parentStrs, " ")) } - message := rev.RawMessage() + message := rev.Message if s.config.Git == 1 { message = s.legacyMessage(rev) } - author := rev.Author() + author := rev.Author if author.Email == "" { author.Email = "nobody@example.com" } - committer := rev.Committer() + committer := rev.Committer if committer.Email == "" { committer.Email = "nobody@example.com" } - oid, err := s.repo.CreateCommit("", author, committer, message, tree, parents...) + newCommit := &object.Commit{ + Author: author, + Committer: committer, + Message: message, + TreeHash: tree.Hash, + ParentHashes: parents, + } + + obj := s.repo.Storer.NewEncodedObject() + if err := newCommit.Encode(obj); err != nil { + return nil, err + } + hash, err := s.repo.Storer.SetEncodedObject(obj) if err != nil { return nil, err } - return oid, nil + return &hash, nil } func (s *state) updateTarget() error { @@ -552,61 +639,31 @@ func (s *state) updateTarget() error { return nil } - if nil == s.result.Head() { + head := s.result.Head() + if head == nil { return fmt.Errorf("unable to create branch %s as it is empty (no commits were split)", s.config.Target) } - obj, ref, err := s.repo.RevparseExt(s.config.Target) - if obj != nil { - obj.Free() - } - if err != nil { - ref, err = s.repo.References.Create(s.config.Target, s.result.Head(), false, "subtree split") - if err != nil { - return err - } - ref.Free() - } else { - defer ref.Free() - ref.SetTarget(s.result.Head(), "subtree split") - } - - return nil + ref := plumbing.NewHashReference(plumbing.ReferenceName(s.config.Target), *head) + return s.repo.Storer.SetReference(ref) } -func (s *state) legacyMessage(rev *git.Commit) string { - subject, body := SplitMessage(rev.Message()) +func (s *state) legacyMessage(rev *object.Commit) string { + subject, body := SplitMessage(rev.Message) return subject + "\n\n" + body } -// pushRevs sets the range to split -func (s *state) pushRevs(revWalk *git.RevWalk) error { - s.repoMu.Lock() - defer s.repoMu.Unlock() - - var start *git.Oid - start = s.cache.getHead() - if start != nil { - s.result.moveHead(s.cache.get(start)) - // FIXME: CHECK that this is an ancestor of the branch? - return revWalk.PushRange(fmt.Sprintf("%s..%s", start, s.origin)) - } - - // find the latest split sha1 if any on origin - var err error - if s.config.Commit != "" { - start, err = git.NewOid(s.config.Commit) - if err != nil { - return err - } - s.result.moveHead(s.cache.get(start)) - return revWalk.PushRange(fmt.Sprintf("%s^..%s", start, s.origin)) +func (s *state) storeTree(entries []object.TreeEntry) (*object.Tree, error) { + sort.Sort(object.TreeEntrySorter(entries)) + tree := &object.Tree{Entries: entries} + obj := s.repo.Storer.NewEncodedObject() + if err := tree.Encode(obj); err != nil { + return nil, err } - - branch, err := s.repo.RevparseSingle(s.origin) + hash, err := s.repo.Storer.SetEncodedObject(obj) if err != nil { - return err + return nil, err } - - return revWalk.Push(branch.Id()) + tree.Hash = hash + return tree, nil } diff --git a/splitter/utils.go b/splitter/utils.go index c59fb99..01cf4ef 100644 --- a/splitter/utils.go +++ b/splitter/utils.go @@ -7,7 +7,8 @@ import ( "regexp" "strings" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" ) var messageNormalizer = regexp.MustCompile(`\s*\r?\n`) @@ -61,17 +62,29 @@ func normalizeOrigin(repo *git.Repository, origin string) (string, error) { origin = "HEAD" } - obj, ref, err := repo.RevparseExt(origin) - if err != nil { - return "", fmt.Errorf("bad revision for origin: %s", err) + // try as a tagReference + if tagRef, err := repo.Tag(origin); err == nil { + return tagRef.Name().String(), nil } - if obj != nil { - obj.Free() - } - if ref == nil { - return "", fmt.Errorf("bad revision for origin: ref is nil") + + if ref, err := repo.Reference(plumbing.ReferenceName(origin), true); err == nil { + return ref.Name().String(), nil } - defer ref.Free() - return ref.Name(), nil + return "", fmt.Errorf("bad revision for origin") +} + +func peelTag(repo *git.Repository, maybeTagHash plumbing.Hash) (*plumbing.Hash, error) { + tagObj, err := repo.TagObject(maybeTagHash) + switch err { + case nil: + // annotated tag + return &tagObj.Target, nil + case plumbing.ErrObjectNotFound: + // lightweight tag + return &maybeTagHash, nil + default: + // real error + return nil, err + } } diff --git a/splitter/utils_test.go b/splitter/utils_test.go index 160708d..02d1299 100644 --- a/splitter/utils_test.go +++ b/splitter/utils_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - git "github.com/libgit2/git2go/v34" + "github.com/go-git/go-git/v6" "github.com/stretchr/testify/assert" ) @@ -146,11 +146,10 @@ func TestNormalizeOrigin(t *testing.T) { _, _, headCommitHash := makeTestRepo(t, tempDir) - repo, err := git.OpenRepository(filepath.Join(tempDir, "test-repo")) + repo, err := git.PlainOpen(".") if !assert.NoError(t, err) { t.FailNow() } - defer repo.Free() t.Run("HEAD", func(t *testing.T) { ref, err := normalizeOrigin(repo, "HEAD")