173 lines
6.9 KiB
Go
173 lines
6.9 KiB
Go
//go:build integration
|
|
|
|
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// The merge on real git repositories starts git processes, so it is an integration test (the unit level keeps its bound,
|
|
// issue 895); the unit tests of merge_test.go prove the commands and the refusals with a fake runner.
|
|
|
|
func realGit(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@example.invalid", "-c", "commit.gpgsign=false"}, args...)...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func writeFile(t *testing.T, dir, name, content string) {
|
|
t.Helper()
|
|
p := filepath.Join(dir, name)
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// mergeRig is a local bare repository standing in for the forge, a work repository that pushes to it, and an empty
|
|
// workspace: the fetch step's shape (FetchCommit of the head into the workspace, then MergeInto).
|
|
type mergeRig struct {
|
|
t *testing.T
|
|
bare, work, ws string
|
|
fork, trunk string // the commit both histories share, and the trunk's head
|
|
remote string
|
|
}
|
|
|
|
func newMergeRig(t *testing.T) *mergeRig {
|
|
root := t.TempDir()
|
|
r := &mergeRig{t: t, bare: filepath.Join(root, "r.git"), work: filepath.Join(root, "work"), ws: filepath.Join(root, "ws")}
|
|
for _, d := range []string{r.bare, r.work, r.ws} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
r.remote = "file://" + r.bare
|
|
realGit(t, r.bare, "init", "-q", "--bare")
|
|
realGit(t, r.work, "init", "-q", "-b", "main")
|
|
writeFile(t, r.work, "a.txt", "one\ntwo\nthree\n")
|
|
writeFile(t, r.work, "modules/m/module.json", "{}\n")
|
|
realGit(t, r.work, "add", ".")
|
|
realGit(t, r.work, "commit", "-q", "-m", "fork point")
|
|
r.fork = realGit(t, r.work, "rev-parse", "HEAD")
|
|
return r
|
|
}
|
|
|
|
// head makes a commit on a branch off the fork point, pushes it as the pull request's head ref and answers its sha.
|
|
func (r *mergeRig) head(files map[string]string) string {
|
|
realGit(r.t, r.work, "checkout", "-q", "-B", "pr", r.fork)
|
|
for n, c := range files {
|
|
writeFile(r.t, r.work, n, c)
|
|
}
|
|
realGit(r.t, r.work, "add", ".")
|
|
realGit(r.t, r.work, "commit", "-q", "-m", "head")
|
|
h := realGit(r.t, r.work, "rev-parse", "HEAD")
|
|
realGit(r.t, r.work, "push", "-q", r.bare, "HEAD:refs/pull/1/head")
|
|
return h
|
|
}
|
|
|
|
// advance moves the trunk past the fork point with a commit changing files, pushes it and answers its sha.
|
|
func (r *mergeRig) advance(files map[string]string) string {
|
|
realGit(r.t, r.work, "checkout", "-q", "-B", "main", r.fork)
|
|
for n, c := range files {
|
|
writeFile(r.t, r.work, n, c)
|
|
}
|
|
realGit(r.t, r.work, "add", ".")
|
|
realGit(r.t, r.work, "commit", "-q", "-m", "trunk moves")
|
|
r.trunk = realGit(r.t, r.work, "rev-parse", "HEAD")
|
|
realGit(r.t, r.work, "push", "-q", "-f", r.bare, "HEAD:refs/heads/main")
|
|
return r.trunk
|
|
}
|
|
|
|
func (r *mergeRig) fetch(head string) {
|
|
if err := FetchCommit(context.Background(), forgekey.ExecRunner, r.ws, r.remote, head, "refs/pull/1/head", AllowFileRemote()); err != nil {
|
|
r.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// 1674/AC1: a clean merge answers the merge commit and the changed files, and leaves the workspace at the merge, which
|
|
// holds both sides' changes.
|
|
func TestMergeIntoACleanMergeAnswersTheMergeCommitAndTheChangedFiles(t *testing.T) {
|
|
r := newMergeRig(t)
|
|
head := r.head(map[string]string{"b.txt": "head\n"})
|
|
base := r.advance(map[string]string{"a.txt": "one\ntwo\nthree\nfour\n"})
|
|
r.fetch(head)
|
|
res, err := MergeInto(context.Background(), forgekey.ExecRunner, r.ws, r.remote, base, head, AllowFileRemote())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Base != base || res.Head != head || len(res.Merge) != 40 || res.Merge == base || res.Merge == head || len(res.Conflicts) != 0 {
|
|
t.Fatalf("result = %+v", res)
|
|
}
|
|
if !reflect.DeepEqual(res.Changed, []string{"b.txt"}) {
|
|
t.Errorf("changed = %v, want only what the head adds to the trunk: b.txt", res.Changed)
|
|
}
|
|
if at := realGit(t, r.ws, "rev-parse", "HEAD"); at != res.Merge {
|
|
t.Errorf("the workspace is at %s, not at the merge %s", at, res.Merge)
|
|
}
|
|
for f, want := range map[string]string{"b.txt": "head\n", "a.txt": "one\ntwo\nthree\nfour\n"} {
|
|
if got, _ := os.ReadFile(filepath.Join(r.ws, f)); string(got) != want {
|
|
t.Errorf("%s = %q, want %q", f, got, want)
|
|
}
|
|
}
|
|
if parents := strings.Fields(realGit(t, r.ws, "rev-list", "--parents", "-n", "1", res.Merge)); len(parents) != 3 || parents[1] != base || parents[2] != head {
|
|
t.Errorf("the merge commit's parents = %v, want the trunk commit and the head", parents)
|
|
}
|
|
if author := realGit(t, r.ws, "log", "-1", "--format=%an <%ae>", res.Merge); author != "mesh merge check <merge-check@mesh.invalid>" {
|
|
t.Errorf("the merge is made as %q", author)
|
|
}
|
|
}
|
|
|
|
// 1674/AC2: a conflict is an answer: the files in conflict, no merge commit, and the workspace left at the trunk commit.
|
|
func TestMergeIntoAConflictAnswersTheFilesAndLeavesTheTreeAtTheTrunk(t *testing.T) {
|
|
r := newMergeRig(t)
|
|
head := r.head(map[string]string{"a.txt": "one\nHEAD\nthree\n", "c.txt": "c\n"})
|
|
base := r.advance(map[string]string{"a.txt": "one\nTRUNK\nthree\n"})
|
|
r.fetch(head)
|
|
res, err := MergeInto(context.Background(), forgekey.ExecRunner, r.ws, r.remote, base, head, AllowFileRemote())
|
|
if err != nil {
|
|
t.Fatalf("a conflict is an answer, not an error: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(res.Conflicts, []string{"a.txt"}) || res.Merge != "" || len(res.Changed) != 0 || res.Base != base || res.Head != head {
|
|
t.Fatalf("result = %+v", res)
|
|
}
|
|
if at := realGit(t, r.ws, "rev-parse", "HEAD"); at != base {
|
|
t.Errorf("the workspace is at %s, not at the trunk commit %s", at, base)
|
|
}
|
|
if got, _ := os.ReadFile(filepath.Join(r.ws, "a.txt")); string(got) != "one\nTRUNK\nthree\n" {
|
|
t.Errorf("a.txt = %q, want the trunk's", got)
|
|
}
|
|
if st := realGit(t, r.ws, "status", "--porcelain"); st != "" {
|
|
t.Errorf("the tree is not clean after the aborted merge: %q", st)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(r.ws, "c.txt")); err == nil {
|
|
t.Error("a file of the head is in a tree left at the trunk")
|
|
}
|
|
}
|
|
|
|
// A head already in the trunk merges into nothing: the merge is the trunk commit and no path changes.
|
|
func TestMergeIntoAHeadAlreadyInTheTrunkChangesNothing(t *testing.T) {
|
|
r := newMergeRig(t)
|
|
head := r.head(map[string]string{"b.txt": "head\n"})
|
|
r.fork = head // the trunk continues from the head
|
|
base := r.advance(map[string]string{"a.txt": "one\ntwo\nthree\nfour\n"})
|
|
r.fetch(head)
|
|
res, err := MergeInto(context.Background(), forgekey.ExecRunner, r.ws, r.remote, base, head, AllowFileRemote())
|
|
if err != nil || res.Merge != base || len(res.Changed) != 0 || len(res.Conflicts) != 0 {
|
|
t.Errorf("result = %+v, %v", res, err)
|
|
}
|
|
}
|