Skip to content

Mismatch splited history versus git subtree split #88

Description

@douyun-dtyq

For some not so special git trees, splitsh-lite have different behavior:

save this to sometest_test.go on the top of this project

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

with test command:

# with my go-git branch:
go test . -v --count=1 --run="TestSubtree"
# with master branch:
#  add libs for repro and test
go get -u github.com/go-git/go-git/v6
go get -u github.com/stretchr/testify
#  magic for build lite
git clone https://github.com/libgit2/git2go.git --recursive ../git2go
pushd ../git2go
  make build-libgit2-static
popd
go mod edit -replace github.com/libgit2/git2go/v34=../git2go
go mod tidy
go test . -v --count=1 --run="TestSubtree" --tags "static"

the result:

=== 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+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions