| package repograph |
| |
| import ( |
| "context" |
| "fmt" |
| "io/ioutil" |
| "path" |
| "testing" |
| |
| assert "github.com/stretchr/testify/require" |
| "go.skia.org/infra/go/deepequal" |
| git_testutils "go.skia.org/infra/go/git/testutils" |
| "go.skia.org/infra/go/testutils" |
| "go.skia.org/infra/go/util" |
| ) |
| |
| // gitSetup initializes a Git repo in a temporary directory with some commits. |
| // Returns the path of the temporary directory, the Graph object associated with |
| // the repo, and a slice of the commits which were added. |
| // |
| // The repo layout looks like this: |
| // |
| // c1--c2------c4--c5-- |
| // \-c3-----/ |
| func gitSetup(t *testing.T) (context.Context, *git_testutils.GitBuilder, *Graph, []*Commit, func()) { |
| ctx := context.Background() |
| g := git_testutils.GitInit(t, ctx) |
| g.CommitGen(ctx, "myfile.txt") |
| |
| tmp, err := ioutil.TempDir("", "") |
| assert.NoError(t, err) |
| |
| repo, err := NewGraph(ctx, g.Dir(), tmp) |
| assert.NoError(t, err) |
| assert.NoError(t, repo.Update(ctx)) |
| |
| c1 := repo.Get("master") |
| assert.NotNil(t, c1) |
| assert.Equal(t, 0, len(c1.GetParents())) |
| assert.False(t, util.TimeIsZero(c1.Timestamp)) |
| |
| g.CommitGen(ctx, "myfile.txt") |
| assert.NoError(t, repo.Update(ctx)) |
| c2 := repo.Get("master") |
| assert.NotNil(t, c2) |
| assert.Equal(t, 1, len(c2.GetParents())) |
| assert.Equal(t, c1, c2.GetParents()[0]) |
| assert.Equal(t, []string{"master"}, repo.Branches()) |
| assert.False(t, util.TimeIsZero(c2.Timestamp)) |
| |
| // Create a second branch. |
| g.CreateBranchTrackBranch(ctx, "branch2", "origin/master") |
| g.CommitGen(ctx, "anotherfile.txt") |
| assert.NoError(t, repo.Update(ctx)) |
| c3 := repo.Get("branch2") |
| assert.NotNil(t, c3) |
| assert.Equal(t, c2, repo.Get("master")) |
| assert.Equal(t, []string{"branch2", "master"}, repo.Branches()) |
| assert.False(t, util.TimeIsZero(c3.Timestamp)) |
| |
| // Commit again to master. |
| g.CheckoutBranch(ctx, "master") |
| g.CommitGen(ctx, "myfile.txt") |
| assert.NoError(t, repo.Update(ctx)) |
| assert.Equal(t, c3, repo.Get("branch2")) |
| c4 := repo.Get("master") |
| assert.NotNil(t, c4) |
| assert.False(t, util.TimeIsZero(c4.Timestamp)) |
| |
| // Merge branch1 into master. |
| g.MergeBranch(ctx, "branch2") |
| assert.NoError(t, repo.Update(ctx)) |
| assert.Equal(t, []string{"branch2", "master"}, repo.Branches()) |
| c5 := repo.Get("master") |
| assert.NotNil(t, c5) |
| assert.Equal(t, c3, repo.Get("branch2")) |
| assert.False(t, util.TimeIsZero(c5.Timestamp)) |
| |
| return ctx, g, repo, []*Commit{c1, c2, c3, c4, c5}, func() { |
| g.Cleanup() |
| testutils.RemoveAll(t, tmp) |
| } |
| } |
| |
| func TestGraph(t *testing.T) { |
| testutils.MediumTest(t) |
| ctx, g, repo, commits, cleanup := gitSetup(t) |
| defer cleanup() |
| |
| c1 := commits[0] |
| c2 := commits[1] |
| c3 := commits[2] |
| c4 := commits[3] |
| c5 := commits[4] |
| |
| // Trace commits back to the beginning of time. |
| assert.Equal(t, []*Commit{c4, c3}, c5.GetParents()) |
| assert.Equal(t, []*Commit{c2}, c4.GetParents()) |
| assert.Equal(t, []*Commit{c1}, c2.GetParents()) |
| assert.Equal(t, []*Commit{}, c1.GetParents()) |
| assert.Equal(t, []*Commit{c2}, c3.GetParents()) |
| |
| // Ensure that we can start in an empty dir and check out from scratch properly. |
| tmp2, err := ioutil.TempDir("", "") |
| assert.NoError(t, err) |
| defer testutils.RemoveAll(t, tmp2) |
| repo2, err := NewGraph(ctx, g.Dir(), tmp2) |
| assert.NoError(t, err) |
| assert.NoError(t, repo2.Update(ctx)) |
| deepequal.AssertDeepEqual(t, repo.Branches(), repo2.Branches()) |
| m1 := repo.Get("master") |
| m2 := repo2.Get("master") |
| // These will confuse AssertDeepEqual. |
| m1.repo = nil |
| m2.repo = nil |
| deepequal.AssertDeepEqual(t, m1, m2) |
| } |
| |
| func TestSerialize(t *testing.T) { |
| testutils.MediumTest(t) |
| ctx, g, repo, _, cleanup := gitSetup(t) |
| defer cleanup() |
| |
| repo2, err := NewGraph(ctx, g.Dir(), path.Dir(repo.repo.Dir())) |
| assert.NoError(t, err) |
| assert.NoError(t, repo2.Update(ctx)) |
| |
| deepequal.AssertDeepEqual(t, repo, repo2) |
| } |
| |
| func TestRecurse(t *testing.T) { |
| testutils.LargeTest(t) |
| _, _, repo, commits, cleanup := gitSetup(t) |
| defer cleanup() |
| |
| c1 := commits[0] |
| c2 := commits[1] |
| c3 := commits[2] |
| c4 := commits[3] |
| c5 := commits[4] |
| |
| // Get the list of commits using head.Recurse(). Ensure that we get all |
| // of the commits but don't get any duplicates. |
| head := repo.Get("master") |
| assert.NotNil(t, head) |
| gotCommits := map[*Commit]bool{} |
| assert.NoError(t, head.Recurse(func(c *Commit) (bool, error) { |
| assert.False(t, gotCommits[c]) |
| gotCommits[c] = true |
| return true, nil |
| })) |
| assert.Equal(t, len(commits), len(gotCommits)) |
| for _, c := range commits { |
| assert.True(t, gotCommits[c]) |
| } |
| |
| // Verify that we properly return early when the passed-in function |
| // return false. |
| gotCommits = map[*Commit]bool{} |
| assert.NoError(t, head.Recurse(func(c *Commit) (bool, error) { |
| gotCommits[c] = true |
| if c == c3 || c == c4 { |
| return false, nil |
| } |
| return true, nil |
| })) |
| assert.False(t, gotCommits[c1]) |
| assert.False(t, gotCommits[c2]) |
| |
| // Verify that we properly exit immediately when the passed-in function |
| // returns an error. |
| gotCommits = map[*Commit]bool{} |
| assert.Error(t, head.Recurse(func(c *Commit) (bool, error) { |
| gotCommits[c] = true |
| if c == c4 { |
| return false, fmt.Errorf("STOP!") |
| } |
| return true, nil |
| })) |
| assert.False(t, gotCommits[c1]) |
| assert.False(t, gotCommits[c2]) |
| assert.False(t, gotCommits[c3]) |
| assert.True(t, gotCommits[c4]) |
| assert.True(t, gotCommits[c5]) |
| } |
| |
| func TestRecurseAllBranches(t *testing.T) { |
| testutils.LargeTest(t) |
| ctx, g, repo, commits, cleanup := gitSetup(t) |
| defer cleanup() |
| |
| c1 := commits[0] |
| c2 := commits[1] |
| c3 := commits[2] |
| c4 := commits[3] |
| |
| test := func() { |
| gotCommits := map[*Commit]bool{} |
| assert.NoError(t, repo.RecurseAllBranches(func(c *Commit) (bool, error) { |
| assert.False(t, gotCommits[c]) |
| gotCommits[c] = true |
| return true, nil |
| })) |
| assert.Equal(t, len(commits), len(gotCommits)) |
| for _, c := range commits { |
| assert.True(t, gotCommits[c]) |
| } |
| } |
| |
| // Get the list of commits using head.RecurseAllBranches(). Ensure that |
| // we get all of the commits but don't get any duplicates. |
| test() |
| |
| // The above used only one branch. Add a branch and ensure that we see |
| // its commits too. |
| g.CreateBranchTrackBranch(ctx, "mybranch", "origin/master") |
| g.CommitGen(ctx, "anotherfile.txt") |
| assert.NoError(t, repo.Update(ctx)) |
| c := repo.Get("mybranch") |
| assert.NotNil(t, c) |
| commits = append(commits, c) |
| test() |
| |
| // Verify that we don't revisit a branch whose HEAD is an ancestor of |
| // a different branch HEAD. |
| g.CreateBranchAtCommit(ctx, "ancestorbranch", c3.Hash) |
| assert.NoError(t, repo.Update(ctx)) |
| test() |
| |
| // Verify that we still stop recursion when requested. |
| gotCommits := map[*Commit]bool{} |
| assert.NoError(t, repo.RecurseAllBranches(func(c *Commit) (bool, error) { |
| gotCommits[c] = true |
| if c == c3 || c == c4 { |
| return false, nil |
| } |
| return true, nil |
| })) |
| assert.False(t, gotCommits[c1]) |
| assert.False(t, gotCommits[c2]) |
| |
| // Verify that we error out properly. |
| gotCommits = map[*Commit]bool{} |
| assert.Error(t, repo.RecurseAllBranches(func(c *Commit) (bool, error) { |
| gotCommits[c] = true |
| // Because of nondeterministic map iteration and the added |
| // branches, we have to halt way back at c2 in order to have |
| // a sane, deterministic test case. |
| if c == c2 { |
| return false, fmt.Errorf("STOP!") |
| } |
| return true, nil |
| })) |
| assert.False(t, gotCommits[c1]) |
| assert.True(t, gotCommits[c2]) |
| } |
| |
| func TestFindCommit(t *testing.T) { |
| testutils.LargeTest(t) |
| _, g1, repo1, commits1, cleanup1 := gitSetup(t) |
| defer cleanup1() |
| _, g2, repo2, commits2, cleanup2 := gitSetup(t) |
| defer cleanup2() |
| |
| m := Map{ |
| g1.Dir(): repo1, |
| g2.Dir(): repo2, |
| } |
| |
| tc := []struct { |
| hash string |
| url string |
| repo *Graph |
| err bool |
| }{ |
| { |
| hash: commits1[0].Hash, |
| url: g1.Dir(), |
| repo: repo1, |
| err: false, |
| }, |
| { |
| hash: commits1[len(commits1)-1].Hash, |
| url: g1.Dir(), |
| repo: repo1, |
| err: false, |
| }, |
| { |
| hash: commits2[0].Hash, |
| url: g2.Dir(), |
| repo: repo2, |
| err: false, |
| }, |
| { |
| hash: commits2[len(commits2)-1].Hash, |
| url: g2.Dir(), |
| repo: repo2, |
| err: false, |
| }, |
| { |
| hash: "", |
| err: true, |
| }, |
| { |
| hash: "abcdef", |
| err: true, |
| }, |
| } |
| for _, c := range tc { |
| commit, url, repo, err := m.FindCommit(c.hash) |
| if c.err { |
| assert.Error(t, err) |
| } else { |
| assert.Nil(t, err) |
| assert.NotNil(t, commit) |
| assert.Equal(t, c.hash, commit.Hash) |
| assert.Equal(t, c.url, url) |
| assert.Equal(t, c.repo, repo) |
| } |
| } |
| } |
| |
| func TestUpdateHistoryChanged(t *testing.T) { |
| testutils.LargeTest(t) |
| ctx, g, repo, commits, cleanup := gitSetup(t) |
| defer cleanup() |
| |
| // c3 is the one commit on branch2. |
| c3 := repo.Get("branch2") |
| assert.NotNil(t, c3) |
| assert.Equal(t, c3, commits[2]) // c3 from setup() |
| |
| // Change branch 2 to be based at c4 with one commit, c6. |
| g.CheckoutBranch(ctx, "branch2") |
| g.Reset(ctx, "--hard", commits[3].Hash) // c4 from setup() |
| f := "myfile" |
| c6hash := g.CommitGen(ctx, f) |
| |
| assert.NoError(t, repo.Update(ctx)) |
| c6 := repo.Get("branch2") |
| assert.NotNil(t, c6) |
| assert.Equal(t, c6hash, c6.Hash) |
| |
| // Ensure that c3 is not reachable from c6. |
| anc, err := repo.repo.IsAncestor(ctx, c3.Hash, c6.Hash) |
| assert.NoError(t, err) |
| assert.False(t, anc) |
| |
| assert.NoError(t, c6.Recurse(func(c *Commit) (bool, error) { |
| assert.NotEqual(t, c, c3) |
| return true, nil |
| })) |
| } |
| |
| func TestUpdateAndReturnNewCommits(t *testing.T) { |
| testutils.LargeTest(t) |
| ctx, g, repo, _, cleanup := gitSetup(t) |
| defer cleanup() |
| |
| // The repo has commits, but gitSetup has already run Update(), so |
| // there's nothing new. |
| newCommits, err := repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, len(newCommits), 0) |
| |
| // No new commits. |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 0, len(newCommits)) |
| |
| // Add a few commits, ensure that they get picked up. |
| g.CheckoutBranch(ctx, "master") |
| f := "myfile" |
| new1 := g.CommitGen(ctx, f) |
| new2 := g.CommitGen(ctx, f) |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 2, len(newCommits)) |
| if newCommits[0].Hash == new1 { |
| assert.Equal(t, new2, newCommits[1].Hash) |
| } else { |
| assert.Equal(t, new1, newCommits[1].Hash) |
| assert.Equal(t, new2, newCommits[0].Hash) |
| } |
| |
| // Add commits on both branches, ensure that they get picked up. |
| new1 = g.CommitGen(ctx, f) |
| g.CheckoutBranch(ctx, "branch2") |
| new2 = g.CommitGen(ctx, "file2") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 2, len(newCommits)) |
| if newCommits[0].Hash == new1 { |
| assert.Equal(t, new2, newCommits[1].Hash) |
| } else { |
| assert.Equal(t, new1, newCommits[1].Hash) |
| assert.Equal(t, new2, newCommits[0].Hash) |
| } |
| |
| // Add a new branch. Make sure that we don't get duplicate commits. |
| g.CheckoutBranch(ctx, "master") |
| g.CreateBranchTrackBranch(ctx, "branch3", "master") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 0, len(newCommits)) |
| assert.Equal(t, 3, len(repo.BranchHeads())) |
| |
| // Make sure we get no duplicates if the branch heads aren't the same. |
| g.Reset(ctx, "--hard", "master^") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 0, len(newCommits)) |
| |
| // Create a new branch. |
| g.CheckoutBranch(ctx, "master") |
| g.CreateBranchTrackBranch(ctx, "branch4", "master") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 0, len(newCommits)) |
| |
| // Add a commit on the new branch. |
| new1 = g.CommitGen(ctx, f) |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 1, len(newCommits)) |
| assert.Equal(t, new1, newCommits[0].Hash) |
| |
| // Add a merge commit. Because there were no commits on master in |
| // between, the master branch head moves and now has the same hash as |
| // "branch4", which means that there aren't any new commits even though |
| // the branch head changed. |
| g.CheckoutBranch(ctx, "master") |
| mergeCommit := g.MergeBranch(ctx, "branch4") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 0, len(newCommits)) |
| assert.Equal(t, mergeCommit, new1) |
| |
| // Create a new branch. |
| g.CheckoutBranch(ctx, "master") |
| g.CreateBranchTrackBranch(ctx, "branch5", "master") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 0, len(newCommits)) |
| |
| // Add a commit on the new branch. |
| new1 = g.CommitGen(ctx, f) |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 1, len(newCommits)) |
| assert.Equal(t, new1, newCommits[0].Hash) |
| |
| // Add a commit on the master branch. |
| g.CheckoutBranch(ctx, "master") |
| new1 = g.CommitGen(ctx, "file2") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 1, len(newCommits)) |
| assert.Equal(t, new1, newCommits[0].Hash) |
| |
| // Merge "branch5" into master. This should result in a new commit. |
| mergeCommit = g.MergeBranch(ctx, "branch5") |
| newCommits, err = repo.UpdateAndReturnNewCommits(ctx) |
| assert.NoError(t, err) |
| assert.Equal(t, 1, len(newCommits)) |
| assert.Equal(t, mergeCommit, newCommits[0].Hash) |
| } |