package main
import (
"encoding/hex"
"fmt"
mathRand "math/rand/v2"
"os"
"os/exec"
osPath "path"
"runtime"
"strings"
"testing"
"time"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
gitObject "github.com/go-git/go-git/v6/plumbing/object"
"github.com/splitsh/lite/splitter"
"github.com/stretchr/testify/assert"
)
func makeTestRepo(t *testing.T, repoDir string) {
// just some random case for reproducing, you may use random like below and for few try
_seed, err := hex.DecodeString("450cdfbaf0237162954a8ea55a6f549d4eab90c3992d49f3ecf36ade8d256482")
if !assert.NoError(t, err) {
t.FailNow()
}
seed := [32]byte{}
copy(seed[:], _seed)
// _, err := rand.Read(seed[:])
// if !assert.NoError(t, err) {
// t.FailNow()
// }
// fmt.Printf("seed: %x\n", seed)
mathRng := mathRand.NewChaCha8(seed)
repo, err := git.PlainInit(
repoDir,
false,
git.WithDefaultBranch(plumbing.ReferenceName("refs/heads/test")),
)
if !assert.NoError(t, err) {
t.FailNow()
}
defer repo.Close()
worktree, err := repo.Worktree()
if !assert.NoError(t, err) {
t.FailNow()
}
fakeCommitter := &gitObject.Signature{
Name: "Test User",
Email: "splitsh-test@example.org",
When: time.Now(),
}
// make fake empty Readme.md
err = os.WriteFile(osPath.Join(repoDir, "Readme.md"), []byte("# stub for test\n"), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
_, err = worktree.Add("Readme.md")
if !assert.NoError(t, err) {
t.FailNow()
}
_, err = worktree.Commit("Initial commit", &git.CommitOptions{
Author: fakeCommitter,
Committer: fakeCommitter,
})
if !assert.NoError(t, err) {
t.FailNow()
}
// create fake commits
for range 64 {
subdir := fmt.Sprintf("project%d", mathRng.Uint64()%4)
fileNames := []string{
"project.go",
"init.py",
"Readme.md",
"util.c",
"42.php",
}
file := fileNames[mathRng.Uint64()%uint64(len(fileNames))]
action := "modify"
if mathRng.Uint64()%2 == 0 {
action = "delete"
}
var content string
switch {
case strings.HasSuffix(file, ".go"):
content = fmt.Sprintf("package %s\n// test file for subtree test\nconst someFakeVar = %d\n", subdir, mathRng.Uint64())
case strings.HasSuffix(file, ".py"):
content = fmt.Sprintf("# test file for subtree test\nSOME_FAKE_VAR = %d\n", mathRng.Uint64())
case strings.HasSuffix(file, ".md"):
content = fmt.Sprintf("# test subdir %s\n\nfor subtree test\n\n```\n%d\n```", subdir, mathRng.Uint64())
case strings.HasSuffix(file, ".c"):
content = fmt.Sprintf("//for subtree test\n\n#include <stdint.h>\n\nconst uint64_t someFakeVar = %d\n```", mathRng.Uint64())
case strings.HasSuffix(file, ".php"):
content = fmt.Sprintf("<?php\n//for subtree test\n\nconst $someFakeVar = %d;\n```", mathRng.Uint64())
default:
// wtf?
t.Fatalf("wtf")
}
err = os.MkdirAll(osPath.Join(repoDir, subdir), 0755)
if !assert.NoError(t, err) {
t.FailNow()
}
switch action {
case "modify":
err = os.WriteFile(osPath.Join(repoDir, subdir, file), []byte(content), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
case "delete":
if _, err = os.Stat(osPath.Join(repoDir, subdir, file)); err != nil {
err = os.WriteFile(osPath.Join(repoDir, subdir, file), []byte(content), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
} else {
err = os.Remove(osPath.Join(repoDir, subdir, file))
if !assert.NoError(t, err) {
t.FailNow()
}
}
}
_, err = worktree.Add(osPath.Join(subdir, file))
if !assert.NoError(t, err) {
t.FailNow()
}
_, err = worktree.Commit(fmt.Sprintf("%s/chore: fake commit", subdir), &git.CommitOptions{
Author: fakeCommitter,
Committer: fakeCommitter,
})
if !assert.NoError(t, err) {
t.FailNow()
}
}
}
func TestSubtree(t *testing.T) {
var err error
if runtime.GOOS != "linux" {
t.Skipf("this test is only designed for linux")
}
tempWorktreeDir, err := os.MkdirTemp("/tmp", "splitsh-main-")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.RemoveAll(tempWorktreeDir)
makeTestRepo(t, tempWorktreeDir)
codeDir := tempWorktreeDir
// since it's random generated, sometimes there's no project0 generated
// so check it
entries, err := os.ReadDir(tempWorktreeDir)
if !assert.NoError(t, err) {
t.FailNow()
}
prefix := "project0"
for _, dir := range entries {
if strings.HasPrefix(dir.Name(), "project") {
prefix = dir.Name()
break
}
}
mainRepoBranch := "test"
// prepare dest dir
tempDestDir, err := os.MkdirTemp("/tmp", "splitsh-dest-")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.RemoveAll(tempDestDir)
testSpliter := func(t *testing.T, split func() error) {
// reinit dest git dir
err = os.RemoveAll(osPath.Join(tempDestDir, ".git"))
if !assert.NoError(t, err) {
t.FailNow()
}
err = split()
if !assert.NoError(t, err) {
t.Fatalf("failed to split subtree: %v", err)
}
cmd := exec.Command(
"git",
"log",
"--oneline",
"splited",
)
cmd.Dir = codeDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if !assert.NoError(t, err) {
t.Fatalf("failed to show log: %v", err)
}
}
t.Run("GitCmd", func(t *testing.T) {
testSpliter(t, func() error {
cmd := exec.Command(
"git",
// "-c", "diff.renameLimit=8192",
"subtree", "split",
"--prefix", prefix,
"--branch", "splited",
mainRepoBranch,
)
cmd.Dir = codeDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
return err
})
})
t.Run("SplitshLite", func(t *testing.T) {
testSpliter(t, func() error {
var prefixes prefixesFlag
err := prefixes.Set(prefix)
if err != nil {
return err
}
config := &splitter.Config{
Path: codeDir,
Origin: "refs/heads/test",
Prefixes: prefixes,
Target: "refs/heads/splited",
Commit: "",
Debug: false,
Scratch: false,
GitVersion: "latest",
}
result := &splitter.Result{}
if err := splitter.Split(config, result); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "%s\n", result.Head().String())
return nil
})
})
}
=== RUN TestSubtree
=== RUN TestSubtree/GitCmd
Created branch 'splited'
3f10b857842ed9b7a98ea72b9c02b8c705416d46
3f10b85 project0/chore: fake commit
eb00515 project0/chore: fake commit
084d236 project0/chore: fake commit
fbd1c1c project0/chore: fake commit
525a56f project0/chore: fake commit
b767aa1 project0/chore: fake commit
5e5e3ed project0/chore: fake commit
9e883c7 project0/chore: fake commit
f6db248 project0/chore: fake commit
6400806 project0/chore: fake commit
030601a project0/chore: fake commit
8177ab4 project0/chore: fake commit
b1b1ce4 project0/chore: fake commit
5b476cb project3/chore: fake commit
c1f6223 project2/chore: fake commit
bc6edea project2/chore: fake commit
ae6198a project0/chore: fake commit
ac9df46 project2/chore: fake commit
e55fd4e project0/chore: fake commit
6188851 Initial commit
=== RUN TestSubtree/SplitshLite
22cac676ac562a28ecabf10a7c9460b9725c9c86
22cac67 project0/chore: fake commit
918fb82 project0/chore: fake commit
67b37f0 project0/chore: fake commit
5ba88f3 project0/chore: fake commit
513195a project0/chore: fake commit
a0ad97b project0/chore: fake commit
1605327 project0/chore: fake commit
4281a75 project0/chore: fake commit
2f1e9ff project0/chore: fake commit
32d1764 project0/chore: fake commit
0bacaed project0/chore: fake commit
0cd10dc project0/chore: fake commit
9f320eb project0/chore: fake commit
--- PASS: TestSubtree (0.68s)
--- PASS: TestSubtree/GitCmd (0.58s)
--- PASS: TestSubtree/SplitshLite (0.01s)
PASS
ok github.com/splitsh/lite 0.691s
Result shows HEAD commit, history is different, official git generates a history with initial commit, and lite do not.
I've tried git 2.9.4 and 2.54.0, all them generated the same (with-initial) history, so this is not some git behavior change after 2.8.0+
For some not so special git trees, splitsh-lite have different behavior:
save this to sometest_test.go on the top of this project
with test command:
the result:
Result shows HEAD commit, history is different, official git generates a history with initial commit, and lite do not.
I've tried git 2.9.4 and 2.54.0, all them generated the same (with-initial) history, so this is not some git behavior change after 2.8.0+