280 lines
7.2 KiB
Go
280 lines
7.2 KiB
Go
package gitcmd
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
aliceName = "Alice"
|
|
aliceEmail = "alice@example.com"
|
|
bobName = "Bob"
|
|
bobEmail = "bob@example.com"
|
|
|
|
readmeV1 = "line1\nline2\n"
|
|
readmeV2 = "line1\nline3\n"
|
|
mainTxt = "main line\n"
|
|
deletedTxt = "to be deleted\n"
|
|
)
|
|
|
|
var binaryContent = []byte{'B', 0x00, 'i', 'n', 0x00, '!', '*', '*'}
|
|
|
|
const (
|
|
d1 = "2024-01-01T10:00:00+00:00"
|
|
d2 = "2024-01-02T10:00:00+00:00"
|
|
d3 = "2024-01-03T10:00:00+00:00"
|
|
d4 = "2024-01-04T10:00:00+00:00"
|
|
d5 = "2024-01-05T10:00:00+00:00"
|
|
)
|
|
|
|
func mustParseTime(t *testing.T, s string) time.Time {
|
|
t.Helper()
|
|
tm, err := time.Parse(time.RFC3339, s)
|
|
if err != nil {
|
|
t.Fatalf("parse time %q: %v", s, err)
|
|
}
|
|
return tm
|
|
}
|
|
|
|
type fixture struct {
|
|
repo *Repository
|
|
c1 string
|
|
c2 string
|
|
c3 string
|
|
c4 string
|
|
c5 string
|
|
}
|
|
|
|
func requireGit(t *testing.T) string {
|
|
t.Helper()
|
|
p, err := exec.LookPath("git")
|
|
if err != nil {
|
|
t.Skipf("git not found on PATH: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
var isolateEnv = []string{"GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null"}
|
|
|
|
func gitRun(t *testing.T, dir string, env []string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
if dir != "" {
|
|
cmd.Dir = dir
|
|
}
|
|
cmd.Env = append(os.Environ(), isolateEnv...)
|
|
cmd.Env = append(cmd.Env, env...)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v in %q: %v\n%s", args, dir, err, out)
|
|
}
|
|
}
|
|
|
|
func headSHA(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
out, err := exec.Command("git", "-C", dir, "rev-parse", "HEAD").Output()
|
|
if err != nil {
|
|
t.Fatalf("rev-parse HEAD in %q: %v", dir, err)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func writeFileBytes(t *testing.T, path string, content []byte) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, content, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func commitEnv(name, email, dateRFC3339 string) []string {
|
|
return []string{
|
|
"GIT_AUTHOR_NAME=" + name,
|
|
"GIT_AUTHOR_EMAIL=" + email,
|
|
"GIT_AUTHOR_DATE=" + dateRFC3339,
|
|
"GIT_COMMITTER_NAME=" + name,
|
|
"GIT_COMMITTER_EMAIL=" + email,
|
|
"GIT_COMMITTER_DATE=" + dateRFC3339,
|
|
}
|
|
}
|
|
|
|
func commitAs(t *testing.T, dir, name, email, date, msg string) {
|
|
t.Helper()
|
|
gitRun(t, dir, nil, "add", "-A")
|
|
gitRun(t, dir, commitEnv(name, email, date), "commit", "-q", "-m", msg)
|
|
}
|
|
|
|
func newFixture(t *testing.T) *fixture {
|
|
t.Helper()
|
|
requireGit(t)
|
|
dir := t.TempDir()
|
|
work := filepath.Join(dir, "work")
|
|
bare := filepath.Join(dir, "bare.git")
|
|
|
|
gitRun(t, "", nil, "init", "-q", "-b", "main", work)
|
|
|
|
writeFile(t, filepath.Join(work, "README.md"), readmeV1)
|
|
commitAs(t, work, aliceName, aliceEmail, d1, "add readme")
|
|
c1 := headSHA(t, work)
|
|
|
|
writeFile(t, filepath.Join(work, "src", "main.txt"), mainTxt)
|
|
writeFile(t, filepath.Join(work, "deleted.txt"), deletedTxt)
|
|
commitAs(t, work, bobName, bobEmail, d2, "add main and temp")
|
|
c2 := headSHA(t, work)
|
|
|
|
writeFile(t, filepath.Join(work, "README.md"), readmeV2)
|
|
commitAs(t, work, aliceName, aliceEmail, d3, "expand readme")
|
|
c3 := headSHA(t, work)
|
|
|
|
gitRun(t, work, nil, "branch", "feature")
|
|
|
|
writeFileBytes(t, filepath.Join(work, "binary.bin"), binaryContent)
|
|
commitAs(t, work, aliceName, aliceEmail, d4, "add binary")
|
|
c4 := headSHA(t, work)
|
|
|
|
if err := os.Remove(filepath.Join(work, "deleted.txt")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commitAs(t, work, bobName, bobEmail, d5, "remove temp")
|
|
c5 := headSHA(t, work)
|
|
|
|
gitRun(t, work, commitEnv(aliceName, aliceEmail, d1), "tag", "-a", "v1.0", c1, "-m", "release v1.0")
|
|
gitRun(t, work, nil, "tag", "v0.9", c2)
|
|
|
|
gitRun(t, "", nil, "clone", "--bare", "-q", work, bare)
|
|
repo, err := OpenRepository(context.Background(), bare)
|
|
if err != nil {
|
|
t.Fatalf("OpenRepository: %v", err)
|
|
}
|
|
return &fixture{repo: repo, c1: c1, c2: c2, c3: c3, c4: c4, c5: c5}
|
|
}
|
|
|
|
func newMergeFixture(t *testing.T) *fixture {
|
|
t.Helper()
|
|
requireGit(t)
|
|
dir := t.TempDir()
|
|
work := filepath.Join(dir, "work")
|
|
bare := filepath.Join(dir, "bare.git")
|
|
|
|
gitRun(t, "", nil, "init", "-q", "-b", "main", work)
|
|
|
|
writeFile(t, filepath.Join(work, "a.txt"), "a\n")
|
|
commitAs(t, work, "Merger", "m@x", "2024-02-01T10:00:00+00:00", "c1 a")
|
|
c1 := headSHA(t, work)
|
|
|
|
gitRun(t, work, nil, "branch", "feature")
|
|
gitRun(t, work, nil, "checkout", "-q", "feature")
|
|
writeFile(t, filepath.Join(work, "b.txt"), "b\n")
|
|
commitAs(t, work, "Merger", "m@x", "2024-02-02T10:00:00+00:00", "c2 b")
|
|
c2 := headSHA(t, work)
|
|
|
|
gitRun(t, work, nil, "checkout", "-q", "main")
|
|
writeFile(t, filepath.Join(work, "c.txt"), "c\n")
|
|
commitAs(t, work, "Merger", "m@x", "2024-02-03T10:00:00+00:00", "c3 c")
|
|
c3 := headSHA(t, work)
|
|
|
|
gitRun(t, work, commitEnv("Merger", "m@x", "2024-02-04T10:00:00+00:00"),
|
|
"merge", "--no-ff", "feature", "-m", "merge feature")
|
|
c4 := headSHA(t, work)
|
|
|
|
gitRun(t, "", nil, "clone", "--bare", "-q", work, bare)
|
|
repo, err := OpenRepository(context.Background(), bare)
|
|
if err != nil {
|
|
t.Fatalf("OpenRepository: %v", err)
|
|
}
|
|
return &fixture{repo: repo, c1: c1, c2: c2, c3: c3, c4: c4}
|
|
}
|
|
|
|
func newDetachedHeadRepo(t *testing.T) *Repository {
|
|
t.Helper()
|
|
requireGit(t)
|
|
dir := t.TempDir()
|
|
bare := filepath.Join(dir, "detached.git")
|
|
gitRun(t, "", nil, "init", "-q", "--bare", bare)
|
|
if err := os.WriteFile(filepath.Join(bare, "HEAD"),
|
|
[]byte("0123456789abcdef0123456789abcdef01234567\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repo, err := OpenRepository(context.Background(), bare)
|
|
if err != nil {
|
|
t.Fatalf("OpenRepository: %v", err)
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func TestFixtureSmoke(t *testing.T) {
|
|
f := newFixture(t)
|
|
|
|
cs, err := f.repo.ListCommits("HEAD", 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(cs) != 5 {
|
|
t.Fatalf("commits = %d, want 5", len(cs))
|
|
}
|
|
if cs[0].ID != f.c5 || cs[4].ID != f.c1 {
|
|
t.Errorf("commit order: first=%s (want %s), last=%s (want %s)",
|
|
cs[0].ID, f.c5, cs[4].ID, f.c1)
|
|
}
|
|
|
|
bs, err := f.repo.ListBranches()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(bs) != 2 {
|
|
t.Fatalf("branches = %d, want 2", len(bs))
|
|
}
|
|
branchByName := map[string]Branch{}
|
|
for _, b := range bs {
|
|
branchByName[b.Name] = b
|
|
}
|
|
if main, ok := branchByName["main"]; !ok || !main.IsDefault || main.SHA != f.c5 {
|
|
t.Errorf("main branch = %+v", branchByName["main"])
|
|
}
|
|
if feat, ok := branchByName["feature"]; !ok || feat.SHA != f.c3 {
|
|
t.Errorf("feature branch = %+v", branchByName["feature"])
|
|
}
|
|
|
|
ts, err := f.repo.ListTags()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ts) != 2 {
|
|
t.Fatalf("tags = %d, want 2", len(ts))
|
|
}
|
|
|
|
contribs, err := f.repo.GetContributors("HEAD")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(contribs) != 2 {
|
|
t.Fatalf("contributors = %d, want 2", len(contribs))
|
|
}
|
|
if contribs[0].Author != aliceName || contribs[0].Commits != 3 {
|
|
t.Errorf("contribs[0] = %+v, want Alice/3", contribs[0])
|
|
}
|
|
if contribs[1].Author != bobName || contribs[1].Commits != 2 {
|
|
t.Errorf("contribs[1] = %+v, want Bob/2", contribs[1])
|
|
}
|
|
|
|
if db, err := f.repo.DefaultBranch(); err != nil || db != "main" {
|
|
t.Errorf("DefaultBranch = %q, %v", db, err)
|
|
}
|
|
}
|