Skip to content

Commit ab25e41

Browse files
committed
fix: git change detection works on any commit
1 parent f7be821 commit ab25e41

File tree

2 files changed

+344
-0
lines changed

2 files changed

+344
-0
lines changed

git/git.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,108 @@ func (git *Git) GetConfigValue(key string) (string, error) {
849849
return strings.TrimSpace(s), nil
850850
}
851851

852+
// IsFirstParentAncestor returns if a commit is on a first parent ancestor path of given rev.
853+
func (git *Git) IsFirstParentAncestor(rev, commit string) (bool, error) {
854+
ap := fmt.Sprintf("%s..%s", commit, rev)
855+
apLenStr, err := git.exec("rev-list", "--count", "--first-parent", "--ancestry-path", ap)
856+
if err != nil {
857+
return false, err
858+
}
859+
860+
apLen, err := strconv.ParseInt(apLenStr, 10, 64)
861+
if err != nil {
862+
return false, err
863+
}
864+
865+
// The above flags will also return a path for direct non-first parents.
866+
// Maybe this could be considered a bug in rev-list. Example:
867+
//
868+
// * 2d88e3e (HEAD -> main) Merge branch 'branch'
869+
// |\
870+
// | * 1222a5d (branch) branch change 2
871+
// | * 9ca168f branch change 1
872+
// * | dae608c main change 1
873+
// |/
874+
// * 67834ff initial
875+
//
876+
// `rev-list --first-parent --ancestry-path 1222a5d..main` => 2d88e3e
877+
//
878+
// That's why we get the length of the path (in this example case N=1)
879+
// and compare against main~N, which is the actual Nth first parent
880+
// (in this case 1222a5d != dae608c).
881+
882+
nthFirstParent, err := git.RevParse(fmt.Sprintf("%s~%d", rev, apLen))
883+
if err != nil {
884+
return false, err
885+
}
886+
887+
return commit == nthFirstParent, nil
888+
}
889+
890+
// RevList executes the git rev-list command, which typically returns a list of parents.
891+
// Note: rev-list can be called with many parameters.
892+
// We assume it's used so that a list of strings is returned.
893+
func (git *Git) RevList(args ...string) ([]string, error) {
894+
ret, err := git.exec("rev-list", args...)
895+
if err != nil {
896+
return nil, err
897+
}
898+
899+
revs := strings.Split(ret, "\n")
900+
fmt.Println(revs)
901+
902+
var cleanrevs []string
903+
for _, r := range revs {
904+
r = strings.TrimSpace(r)
905+
if r != "" {
906+
cleanrevs = append(cleanrevs, r)
907+
}
908+
}
909+
return cleanrevs, nil
910+
}
911+
912+
// FindForkPoint returns the parent commit at which the given commit forked from rev.
913+
func (git *Git) FindForkPoint(rev, commit string) (string, error) {
914+
fmt.Println(rev)
915+
fmt.Println(commit)
916+
917+
out, _ := git.exec("log", "--graph", "--oneline")
918+
fmt.Println(out)
919+
920+
revParents, err := git.RevList("--first-parent", rev)
921+
if err != nil {
922+
return "", err
923+
}
924+
925+
commitParents, err := git.RevList("--first-parent", commit)
926+
if err != nil {
927+
return "", err
928+
}
929+
930+
fmt.Println(revParents)
931+
fmt.Println(commitParents)
932+
933+
rlen := len(revParents)
934+
clen := len(commitParents)
935+
936+
if rlen == 0 || clen == 0 {
937+
return "", nil
938+
}
939+
940+
nearestCommonParent := ""
941+
942+
for negIdx := 1; negIdx <= rlen && negIdx <= clen; negIdx++ {
943+
curRevParent := revParents[rlen-negIdx]
944+
curCommitParent := commitParents[clen-negIdx]
945+
946+
if curRevParent == curCommitParent {
947+
nearestCommonParent = curRevParent
948+
}
949+
}
950+
951+
return nearestCommonParent, nil
952+
}
953+
852954
func (git *Git) exec(command string, args ...string) (string, error) {
853955
logger := log.With().
854956
Str("action", "Git.exec()").

git/git_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,248 @@ func TestGetConfigValue(t *testing.T) {
447447
assert.Error(t, err, "git config: non-existing key")
448448
}
449449

450+
func TestFindForkPoint(t *testing.T) {
451+
repodir := mkOneCommitRepo(t)
452+
453+
gw, err := git.WithConfig(git.Config{
454+
WorkingDir: repodir,
455+
Isolated: true,
456+
Env: []string{},
457+
AllowPorcelain: true,
458+
})
459+
assert.NoError(t, err, "new git wrapper")
460+
461+
hashToName := map[string]string{"": ""}
462+
nameToHash := map[string]string{"": ""}
463+
464+
setNamedCommit := func(name string) {
465+
hash, err := gw.RevParse("HEAD")
466+
assert.NoError(t, err)
467+
468+
hashToName[hash] = name
469+
nameToHash[name] = hash
470+
}
471+
472+
makeNamedCommit := func(name string) {
473+
test.WriteFile(t, repodir, name, "")
474+
assert.NoError(t, gw.Add(name))
475+
assert.NoError(t, gw.Commit(name))
476+
477+
setNamedCommit(name)
478+
}
479+
480+
type testcase struct {
481+
ForkedFrom string
482+
Commit string
483+
484+
WantForkPoint string
485+
}
486+
487+
var tests []testcase
488+
489+
setNamedCommit("main_commit_1")
490+
491+
tests = append(tests, []testcase{
492+
{
493+
ForkedFrom: "main",
494+
Commit: "main_commit_1",
495+
WantForkPoint: "main_commit_1",
496+
}}...,
497+
)
498+
499+
assert.NoError(t, gw.Checkout("branch_a", true))
500+
makeNamedCommit("branch_a_commit_1")
501+
makeNamedCommit("branch_a_commit_2")
502+
503+
tests = append(tests, []testcase{
504+
{
505+
ForkedFrom: "main",
506+
Commit: "branch_a_commit_1",
507+
WantForkPoint: "main_commit_1",
508+
},
509+
{
510+
ForkedFrom: "main",
511+
Commit: "branch_a_commit_2",
512+
WantForkPoint: "main_commit_1",
513+
}}...,
514+
)
515+
516+
assert.NoError(t, gw.Checkout("main", false))
517+
assert.NoError(t, gw.Merge("branch_a"))
518+
setNamedCommit("main_commit_2")
519+
520+
tests = append(tests, []testcase{
521+
{
522+
ForkedFrom: "main",
523+
Commit: "main_commit_2",
524+
WantForkPoint: "main_commit_2",
525+
}}...,
526+
)
527+
528+
assert.NoError(t, gw.Checkout("branch_b", true))
529+
makeNamedCommit("branch_b_commit_1")
530+
makeNamedCommit("branch_b_commit_2")
531+
532+
tests = append(tests, []testcase{
533+
{
534+
ForkedFrom: "main",
535+
Commit: "branch_b_commit_1",
536+
WantForkPoint: "main_commit_2",
537+
},
538+
{
539+
ForkedFrom: "main",
540+
Commit: "branch_b_commit_2",
541+
WantForkPoint: "main_commit_2",
542+
}}...,
543+
)
544+
545+
assert.NoError(t, gw.Checkout("main", false))
546+
assert.NoError(t, gw.Merge("branch_b"))
547+
setNamedCommit("main_commit_3")
548+
549+
tests = append(tests, []testcase{
550+
{
551+
ForkedFrom: "main",
552+
Commit: "main_commit_3",
553+
WantForkPoint: "main_commit_3",
554+
}}...,
555+
)
556+
557+
assert.NoError(t, gw.Checkout("branch_unmerged", true))
558+
makeNamedCommit("branch_unmerged_commit_1")
559+
makeNamedCommit("branch_unmerged_commit_2")
560+
makeNamedCommit("branch_unmerged_commit_3")
561+
562+
tests = append(tests, []testcase{
563+
{
564+
ForkedFrom: "main",
565+
Commit: "branch_unmerged_commit_1",
566+
WantForkPoint: "main_commit_3",
567+
},
568+
{
569+
ForkedFrom: "main",
570+
Commit: "branch_unmerged_commit_2",
571+
WantForkPoint: "main_commit_3",
572+
},
573+
{
574+
ForkedFrom: "main",
575+
Commit: "branch_unmerged_commit_3",
576+
WantForkPoint: "main_commit_3",
577+
}}...,
578+
)
579+
580+
assert.NoError(t, gw.Checkout("main", false))
581+
assert.NoError(t, gw.Checkout("branch_c", true))
582+
makeNamedCommit("branch_c_commit_1")
583+
584+
assert.NoError(t, gw.Checkout("branch_d", true))
585+
makeNamedCommit("branch_d_commit_1")
586+
587+
assert.NoError(t, gw.Checkout("branch_c", false))
588+
assert.NoError(t, gw.Merge("branch_d"))
589+
setNamedCommit("branch_c_commit_2")
590+
591+
makeNamedCommit("branch_c_commit_3")
592+
593+
assert.NoError(t, gw.Checkout("main", false))
594+
assert.NoError(t, gw.Merge("branch_c"))
595+
setNamedCommit("main_commit_4")
596+
597+
tests = append(tests, []testcase{
598+
{
599+
ForkedFrom: "main",
600+
Commit: "branch_c_commit_1",
601+
WantForkPoint: "main_commit_3",
602+
},
603+
{
604+
ForkedFrom: "main",
605+
Commit: "branch_d_commit_1",
606+
WantForkPoint: "main_commit_3",
607+
},
608+
{
609+
ForkedFrom: "branch_c",
610+
Commit: "branch_d_commit_1",
611+
WantForkPoint: "branch_c_commit_1",
612+
},
613+
{
614+
ForkedFrom: "main",
615+
Commit: "branch_c_commit_2",
616+
WantForkPoint: "main_commit_3",
617+
},
618+
{
619+
ForkedFrom: "main",
620+
Commit: "branch_c_commit_3",
621+
WantForkPoint: "main_commit_3",
622+
},
623+
{
624+
ForkedFrom: "main",
625+
Commit: "main_commit_4",
626+
WantForkPoint: "main_commit_4",
627+
}}...,
628+
)
629+
630+
assert.NoError(t, gw.Checkout("branch_wip", true))
631+
makeNamedCommit("branch_wip_commit_1")
632+
makeNamedCommit("branch_wip_commit_2")
633+
634+
tests = append(tests, []testcase{
635+
{
636+
ForkedFrom: "main",
637+
Commit: "branch_wip_commit_1",
638+
WantForkPoint: "main_commit_4",
639+
},
640+
{
641+
ForkedFrom: "main",
642+
Commit: "branch_wip_commit_2",
643+
WantForkPoint: "main_commit_4",
644+
}}...,
645+
)
646+
647+
/*
648+
* branch_wip_commit_2
649+
* branch_wip_commit_1
650+
/
651+
* main_commit_4
652+
|\
653+
| * branch_c_commit_3
654+
| * branch_c_commit_2
655+
| |\
656+
| | * branch_d_commit_1
657+
| |/
658+
| * branch_c_commit_1
659+
|/
660+
|
661+
| * branch_unmerged_commit_3
662+
| * branch_unmerged_commit_2
663+
| * branch_unmerged_commit_1
664+
|/
665+
* main_commit_3
666+
|\
667+
| * branch_b_commit_2
668+
| * branch_b_commit_1
669+
|/
670+
* main_commit_2
671+
|\
672+
| * branch_a_commit_2
673+
| * branch_a_commit_1
674+
|/
675+
* main_commit_1
676+
*/
677+
678+
for _, tc := range tests {
679+
wantName := tc.WantForkPoint
680+
wantHash := nameToHash[wantName]
681+
682+
gotHash, err := gw.FindForkPoint(tc.ForkedFrom, nameToHash[tc.Commit])
683+
assert.NoError(t, err)
684+
gotName := hashToName[gotHash]
685+
686+
assert.EqualStrings(t, wantHash, gotHash,
687+
"fork point, wantName=%v gotName=%v, wantHash=%v gotHash=%v",
688+
wantName, gotName, wantHash, gotHash)
689+
}
690+
}
691+
450692
const defaultBranch = "main"
451693

452694
func mkOneCommitRepo(t *testing.T) string {

0 commit comments

Comments
 (0)