Thanks to visit codestin.com
Credit goes to github.com

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: "[email protected]",
		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+

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions