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) + }) +}