Skip to content

Commit

Permalink
fix: git change detection works on any commit
Browse files Browse the repository at this point in the history
  • Loading branch information
snakster committed Nov 15, 2023
1 parent f7be821 commit 318124b
Show file tree
Hide file tree
Showing 2 changed files with 344 additions and 0 deletions.
102 changes: 102 additions & 0 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,108 @@ func (git *Git) GetConfigValue(key string) (string, error) {
return strings.TrimSpace(s), nil
}

// IsFirstParentAncestor returns if a commit is on a first parent ancestor path of given rev.
func (git *Git) IsFirstParentAncestor(rev, commit string) (bool, error) {
ap := fmt.Sprintf("%s..%s", commit, rev)
apLenStr, err := git.exec("rev-list", "--count", "--first-parent", "--ancestry-path", ap)
if err != nil {
return false, err
}

apLen, err := strconv.ParseInt(apLenStr, 10, 64)
if err != nil {
return false, err
}

// The above flags will also return a path for direct non-first parents.
// Maybe this could be considered a bug in rev-list. Example:
//
// * 2d88e3e (HEAD -> main) Merge branch 'branch'
// |\
// | * 1222a5d (branch) branch change 2
// | * 9ca168f branch change 1
// * | dae608c main change 1
// |/
// * 67834ff initial
//
// `rev-list --first-parent --ancestry-path 1222a5d..main` => 2d88e3e
//
// That's why we get the length of the path (in this example case N=1)
// and compare against main~N, which is the actual Nth first parent
// (in this case 1222a5d != dae608c).

nthFirstParent, err := git.RevParse(fmt.Sprintf("%s~%d", rev, apLen))
if err != nil {
return false, err
}

return commit == nthFirstParent, nil
}

// RevList executes the git rev-list command, which typically returns a list of parents.
// Note: rev-list can be called with many parameters.
// We assume it's used so that a list of strings is returned.
func (git *Git) RevList(args ...string) ([]string, error) {
ret, err := git.exec("rev-list", args...)
if err != nil {
return nil, err
}

revs := strings.Split(ret, "\n")
fmt.Println(revs)

var cleanrevs []string
for _, r := range revs {
r = strings.TrimSpace(r)
if r != "" {
cleanrevs = append(cleanrevs, r)
}
}
return cleanrevs, nil
}

// FindForkPoint returns the parent commit at which the given commit forked from rev.
func (git *Git) FindForkPoint(rev, commit string) (string, error) {
fmt.Println(rev)
fmt.Println(commit)

out, _ := git.exec("log", "--graph", "--oneline")
fmt.Println(out)

revParents, err := git.RevList("--first-parent", rev)
if err != nil {
return "", err
}

commitParents, err := git.RevList("--first-parent", commit)
if err != nil {
return "", err
}

fmt.Println(revParents)
fmt.Println(commitParents)

rlen := len(revParents)
clen := len(commitParents)

if rlen == 0 || clen == 0 {
return "", nil
}

nearestCommonParent := ""

for negIdx := 1; negIdx <= rlen && negIdx <= clen; negIdx++ {
curRevParent := revParents[rlen-negIdx]
curCommitParent := commitParents[clen-negIdx]

if curRevParent == curCommitParent {
nearestCommonParent = curRevParent
}
}

return nearestCommonParent, nil
}

func (git *Git) exec(command string, args ...string) (string, error) {
logger := log.With().
Str("action", "Git.exec()").
Expand Down
242 changes: 242 additions & 0 deletions git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,248 @@ func TestGetConfigValue(t *testing.T) {
assert.Error(t, err, "git config: non-existing key")
}

func TestFindForkPoint(t *testing.T) {
repodir := mkOneCommitRepo(t)

gw, err := git.WithConfig(git.Config{
WorkingDir: repodir,
Isolated: true,
Env: []string{},
AllowPorcelain: true,
})
assert.NoError(t, err, "new git wrapper")

hashToName := map[string]string{"": ""}
nameToHash := map[string]string{"": ""}

setNamedCommit := func(name string) {
hash, err := gw.RevParse("HEAD")
assert.NoError(t, err)

hashToName[hash] = name
nameToHash[name] = hash
}

makeNamedCommit := func(name string) {
test.WriteFile(t, repodir, name, "")
assert.NoError(t, gw.Add(name))
assert.NoError(t, gw.Commit(name))

setNamedCommit(name)
}

type testcase struct {
Commit string
ForkedFrom string

WantForkPoint string
}

var tests []testcase

setNamedCommit("main_commit_1")

tests = append(tests, []testcase{
{
Commit: "main_commit_1",
ForkedFrom: "main",
WantForkPoint: "main_commit_1",
}}...,
)

assert.NoError(t, gw.Checkout("branch_a", true))
makeNamedCommit("branch_a_commit_1")
makeNamedCommit("branch_a_commit_2")

tests = append(tests, []testcase{
{
Commit: "branch_a_commit_1",
ForkedFrom: "main",
WantForkPoint: "main_commit_1",
},
{
Commit: "branch_a_commit_2",
ForkedFrom: "main",
WantForkPoint: "main_commit_1",
}}...,
)

assert.NoError(t, gw.Checkout("main", false))
assert.NoError(t, gw.Merge("branch_a"))
setNamedCommit("main_commit_2")

tests = append(tests, []testcase{
{
Commit: "main_commit_2",
ForkedFrom: "main",
WantForkPoint: "main_commit_2",
}}...,
)

assert.NoError(t, gw.Checkout("branch_b", true))
makeNamedCommit("branch_b_commit_1")
makeNamedCommit("branch_b_commit_2")

tests = append(tests, []testcase{
{
ForkedFrom: "main",
Commit: "branch_b_commit_1",
WantForkPoint: "main_commit_2",
},
{
ForkedFrom: "main",
Commit: "branch_b_commit_2",
WantForkPoint: "main_commit_2",
}}...,
)

assert.NoError(t, gw.Checkout("main", false))
assert.NoError(t, gw.Merge("branch_b"))
setNamedCommit("main_commit_3")

tests = append(tests, []testcase{
{
Commit: "main_commit_3",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
}}...,
)

assert.NoError(t, gw.Checkout("branch_unmerged", true))
makeNamedCommit("branch_unmerged_commit_1")
makeNamedCommit("branch_unmerged_commit_2")
makeNamedCommit("branch_unmerged_commit_3")

tests = append(tests, []testcase{
{
Commit: "branch_unmerged_commit_1",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
},
{
Commit: "branch_unmerged_commit_2",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
},
{
Commit: "branch_unmerged_commit_3",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
}}...,
)

assert.NoError(t, gw.Checkout("main", false))
assert.NoError(t, gw.Checkout("branch_c", true))
makeNamedCommit("branch_c_commit_1")

assert.NoError(t, gw.Checkout("branch_d", true))
makeNamedCommit("branch_d_commit_1")

assert.NoError(t, gw.Checkout("branch_c", false))
assert.NoError(t, gw.Merge("branch_d"))
setNamedCommit("branch_c_commit_2")

makeNamedCommit("branch_c_commit_3")

assert.NoError(t, gw.Checkout("main", false))
assert.NoError(t, gw.Merge("branch_c"))
setNamedCommit("main_commit_4")

tests = append(tests, []testcase{
{
Commit: "branch_c_commit_1",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
},
{
Commit: "branch_d_commit_1",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
},
{
Commit: "branch_d_commit_1",
ForkedFrom: "branch_c",
WantForkPoint: "branch_c_commit_1",
},
{
Commit: "branch_c_commit_2",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
},
{
Commit: "branch_c_commit_3",
ForkedFrom: "main",
WantForkPoint: "main_commit_3",
},
{
Commit: "main_commit_4",
ForkedFrom: "main",
WantForkPoint: "main_commit_4",
}}...,
)

assert.NoError(t, gw.Checkout("branch_wip", true))
makeNamedCommit("branch_wip_commit_1")
makeNamedCommit("branch_wip_commit_2")

tests = append(tests, []testcase{
{
Commit: "branch_wip_commit_1",
ForkedFrom: "main",
WantForkPoint: "main_commit_4",
},
{
Commit: "branch_wip_commit_2",
ForkedFrom: "main",
WantForkPoint: "main_commit_4",
}}...,
)

/*
* branch_wip_commit_2
* branch_wip_commit_1
/
* main_commit_4
|\
| * branch_c_commit_3
| * branch_c_commit_2
| |\
| | * branch_d_commit_1
| |/
| * branch_c_commit_1
|/
|
| * branch_unmerged_commit_3
| * branch_unmerged_commit_2
| * branch_unmerged_commit_1
|/
* main_commit_3
|\
| * branch_b_commit_2
| * branch_b_commit_1
|/
* main_commit_2
|\
| * branch_a_commit_2
| * branch_a_commit_1
|/
* main_commit_1
*/

for _, tc := range tests {
wantName := tc.WantForkPoint
wantHash := nameToHash[wantName]

gotHash, err := gw.FindForkPoint(tc.ForkedFrom, nameToHash[tc.Commit])
assert.NoError(t, err)
gotName := hashToName[gotHash]

assert.EqualStrings(t, wantHash, gotHash,
"fork point, wantName=%v gotName=%v, wantHash=%v gotHash=%v",
wantName, gotName, wantHash, gotHash)
}
}

const defaultBranch = "main"

func mkOneCommitRepo(t *testing.T) string {
Expand Down

0 comments on commit 318124b

Please sign in to comment.