123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 | // Package tools_test runs git-selfhost against a stand-in server: ssh and
// doas run the commands here, in a scratch directory, with the real
// repo-render, new-repo and post-receive hook.
package tools_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
// realRenderer is repo-render, built for the tests, in a directory with
// new-repo beside it, where repo-render add finds it.
var realRenderer string
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "selfhost-test")
if err != nil {
panic(err)
}
realRenderer = filepath.Join(dir, "repo-render")
if out, err := exec.Command("go", "build", "-o", realRenderer, "../cmd/repo-render").CombinedOutput(); err != nil {
panic(fmt.Sprintf("building repo-render: %v\n%s", err, out))
}
newRepo, err := filepath.Abs("../contrib/new-repo")
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(dir, "repo-render-new-repo"), []byte("#!/bin/sh\nexec "+newRepo+" \"$@\"\n"), 0o755); err != nil {
panic(err)
}
code := m.Run()
os.RemoveAll(dir)
os.Exit(code)
}
type env struct {
t *testing.T
root string
path string
}
const serverConfig = `host: code.example.com
git_dir: GIT_DIR
htdocs: ROOT/htdocs
state_dir: ROOT/state
sections:
- name: tools
title: Tools
- name: libs
title: Libraries
`
func newEnv(t *testing.T) *env {
e := &env{t: t, root: t.TempDir()}
bin := e.p("bin")
// ssh HOST CMD runs CMD in the git user's home, which is git_dir; so
// does git's ssh HOST git-receive-pack 'NAME.git'.
e.script(filepath.Join(bin, "ssh"), `while [ $# -gt 0 ]; do case $1 in -o|-p|-i|-l|-F|-O) shift 2 ;; -*) shift ;; *) break ;; esac; done
shift
cd `+e.p("git")+` && exec sh -c "$*"`)
e.script(filepath.Join(bin, "doas"), `exec "$@"`)
e.script(filepath.Join(bin, "logger"), `cat >> `+e.p("syslog"))
// The hook's enqueue and kick are only noted; the rest is the real
// repo-render.
e.script(filepath.Join(bin, "repo-render"), `case "$1" in enqueue|kick) echo "$*" >> `+e.p("calls")+`; exit ;; esac
exec `+realRenderer+` "$@"`)
e.path = bin + string(os.PathListSeparator) + os.Getenv("PATH")
if err := os.MkdirAll(e.p("git"), 0o755); err != nil {
t.Fatal(err)
}
e.write("etc/repo-render.yaml", strings.NewReplacer("GIT_DIR", e.p("git"), "ROOT", e.root).Replace(serverConfig))
e.write("gitconfig", `[selfhost]
server = me@server
config = `+e.p("etc/repo-render.yaml")+`
mirror = good
[selfhost "good"]
url = `+e.p("{repo}-mirror.git")+`
keys = https://example.com/{repo}/settings/keys
`)
e.git("init", "-q", "--bare", e.p("proj-mirror.git"))
work := e.p("proj")
e.git("init", "-q", "-b", "trunk", work)
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "first")
e.git("-C", work, "tag", "v1")
return e
}
func (e *env) p(name string) string { return filepath.Join(e.root, name) }
func (e *env) write(name, body string) {
e.t.Helper()
if err := os.MkdirAll(filepath.Dir(e.p(name)), 0o755); err != nil {
e.t.Fatal(err)
}
if err := os.WriteFile(e.p(name), []byte(body), 0o644); err != nil {
e.t.Fatal(err)
}
}
func (e *env) script(p, body string) {
e.t.Helper()
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
e.t.Fatal(err)
}
if err := os.WriteFile(p, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
e.t.Fatal(err)
}
}
func (e *env) run(dir, name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "PATH="+e.path, "HOME="+e.root,
"GIT_CONFIG_GLOBAL="+e.p("gitconfig"), "GIT_CONFIG_NOSYSTEM=1",
"GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com",
"GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com")
out, err := cmd.CombinedOutput()
return string(out), err
}
func (e *env) git(args ...string) string {
e.t.Helper()
out, err := e.run(e.root, "git", args...)
if err != nil {
e.t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
return strings.TrimSpace(out)
}
func (e *env) selfhost(args ...string) (string, error) {
self, err := filepath.Abs("git-selfhost")
if err != nil {
e.t.Fatal(err)
}
return e.run(e.p("proj"), self, args...)
}
func (e *env) read(name string) string {
b, _ := os.ReadFile(e.p(name))
return string(b)
}
func TestSelfhost(t *testing.T) {
e := newEnv(t)
work := e.p("proj")
// The working copy's origin is the mirror, so it is renamed after it.
e.git("-C", work, "remote", "add", "origin", e.p("proj-mirror.git"))
out, err := e.selfhost("--yes", "-d", "It's my project", "--section", "Tools", "--tag", "go", "--tag", "cli",
"--mirror", "good", "--mirror-url", "/nonexistent/broken.git")
if err != nil {
t.Fatalf("git-selfhost: %v\n%s", err, out)
}
dir := e.p("git/proj.git")
// --section took the title; the repository has the section's name.
if got := e.git("-C", dir, "config", "--get-regexp", `^repo-render\.(section|tag)$`); got != "repo-render.section tools\nrepo-render.tag go\nrepo-render.tag cli" {
t.Errorf("placement:\n%s", got)
}
if cfg := e.read("etc/repo-render.yaml"); cfg != strings.NewReplacer("GIT_DIR", e.p("git"), "ROOT", e.root).Replace(serverConfig) {
t.Errorf("the config changed:\n%s", cfg)
}
if got := e.read("git/proj.git/description"); got != "It's my project\n" {
t.Errorf("description = %q", got)
}
if got := e.git("-C", dir, "symbolic-ref", "HEAD"); got != "refs/heads/trunk" {
t.Errorf("HEAD = %s", got)
}
// A mirror with no repository is left out.
if !strings.Contains(out, "there's no repository at /nonexistent/broken.git") {
t.Errorf("the missing mirror was not reported:\n%s", out)
}
if got := e.git("-C", dir, "config", "--get-all", "repo-render.mirror"); got != e.p("proj-mirror.git") {
t.Errorf("mirrors = %q", got)
}
key := e.p("etc/keys/proj")
if got := e.git("-C", dir, "config", "core.sshCommand"); !strings.HasSuffix(got, " -i "+key) {
t.Errorf("core.sshCommand = %q", got)
}
if pub := e.read("etc/keys/proj.pub"); !strings.HasPrefix(pub, "ssh-ed25519 ") || !strings.Contains(out, strings.TrimSpace(pub)) {
t.Errorf("the key %q is not shown:\n%s", pub, out)
}
if !strings.Contains(out, "good: https://example.com/proj/settings/keys") {
t.Errorf("no link to the mirror's key settings:\n%s", out)
}
// The check ran the hook's mirror push.
if !strings.Contains(out, "proj: mirrored to "+e.p("proj-mirror.git")) {
t.Errorf("the mirror was not checked:\n%s", out)
}
if got := e.git("-C", work, "remote", "get-url", "origin"); got != "git@server:"+e.p("git/proj.git") {
t.Errorf("origin = %s", got)
}
if got := e.git("-C", work, "remote", "get-url", "good"); got != e.p("proj-mirror.git") {
t.Errorf("the old origin, now good = %s", got)
}
head := e.git("-C", work, "rev-parse", "HEAD")
if got := e.git("-C", dir, "rev-parse", "trunk", "v1"); got != head+"\n"+head {
t.Errorf("the server has %s, want %s twice", got, head)
}
for deadline := time.Now().Add(10 * time.Second); ; time.Sleep(20 * time.Millisecond) {
if out, err := e.run(e.root, "git", "-C", e.p("proj-mirror.git"), "rev-parse", "trunk"); err == nil && strings.TrimSpace(out) == head {
break
}
if time.Now().After(deadline) {
t.Fatal("the mirror never got trunk")
}
}
}
func TestSelfhostRefuses(t *testing.T) {
e := newEnv(t)
work := e.p("proj")
empty := e.p("empty")
e.git("init", "-q", "-b", "main", empty)
self, _ := filepath.Abs("git-selfhost")
if out, err := e.run(empty, self, "--yes"); err == nil || !strings.Contains(out, "no commits yet") {
t.Errorf("a working copy with no commits: %v\n%s", err, out)
}
if out, err := e.selfhost("--yes", "--section", "Nope"); err == nil || !strings.Contains(out, "there's no section Nope on me@server: its sections are tools libs") {
t.Errorf("an unknown section: %v\n%s", err, out)
}
e.git("-C", work, "remote", "add", "origin", "git@elsewhere:proj.git")
if out, err := e.selfhost("--yes"); err == nil || !strings.Contains(out, "isn't one of the mirrors") {
t.Errorf("an origin that is no mirror: %v\n%s", err, out)
}
e.git("-C", work, "remote", "remove", "origin")
if out, err := e.selfhost("--yes", "--dry-run", "--no-mirrors"); err != nil || !strings.Contains(out, "(dry run: not run)") {
t.Errorf("--dry-run: %v\n%s", err, out)
}
if _, err := os.Stat(e.p("git/proj.git")); err == nil {
t.Error("a refused or dry run made the repository")
}
if _, err := os.Stat(e.p("git/empty.git")); err == nil {
t.Error("a working copy with no commits made its repository")
}
if got := e.git("-C", work, "remote"); got != "" {
t.Errorf("remotes: %s", got)
}
}
// The default branch is the mirror's, whichever is checked out, and is
// the one pushed.
func TestSelfhostBranch(t *testing.T) {
e := newEnv(t)
work, mirror := e.p("proj"), e.p("proj-mirror.git")
e.git("-C", work, "push", "-q", mirror, "trunk")
e.git("-C", mirror, "symbolic-ref", "HEAD", "refs/heads/trunk")
e.git("-C", work, "checkout", "-q", "-b", "feature")
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "half done")
out, err := e.selfhost("--yes")
if err != nil {
t.Fatalf("git-selfhost: %v\n%s", err, out)
}
if !strings.Contains(out, "default branch: trunk (good's default branch)") {
t.Errorf("output:\n%s", out)
}
dir := e.p("git/proj.git")
if got := e.git("-C", dir, "symbolic-ref", "HEAD"); got != "refs/heads/trunk" {
t.Errorf("HEAD = %s", got)
}
if got := e.git("-C", dir, "for-each-ref", "--format=%(refname)", "refs/heads/"); got != "refs/heads/trunk" {
t.Errorf("the server has %s", got)
}
}
// Each refusal changes nothing, here or on the server.
func TestSelfhostBranchRefuses(t *testing.T) {
for _, c := range []struct {
name, want string
setup func(e *env, work, mirror string)
args []string
}{{
name: "the mirror is ahead",
want: "good's trunk has commits that your trunk doesn't",
setup: func(e *env, work, mirror string) {
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "theirs")
e.git("-C", work, "push", "-q", mirror, "trunk")
e.git("-C", mirror, "symbolic-ref", "HEAD", "refs/heads/trunk")
e.git("-C", work, "reset", "-q", "--hard", "HEAD^")
},
}, {
name: "the mirror has diverged",
want: "good's trunk has commits that your trunk doesn't",
setup: func(e *env, work, mirror string) {
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "theirs")
e.git("-C", work, "push", "-q", mirror, "trunk")
e.git("-C", mirror, "symbolic-ref", "HEAD", "refs/heads/trunk")
e.git("-C", work, "commit", "-q", "--amend", "--allow-empty", "-m", "ours")
},
}, {
name: "the mirror's default is not here",
want: "good's default branch is main, and there's no main here",
setup: func(e *env, work, mirror string) {
e.git("-C", work, "push", "-q", mirror, "trunk:main")
e.git("-C", mirror, "symbolic-ref", "HEAD", "refs/heads/main")
},
}, {
name: "no mirror and two branches",
want: "which branch is the default?",
setup: func(e *env, work, mirror string) {
e.git("-C", work, "branch", "feature")
},
args: []string{"--no-mirrors"},
}} {
t.Run(c.name, func(t *testing.T) {
e := newEnv(t)
work, mirror := e.p("proj"), e.p("proj-mirror.git")
c.setup(e, work, mirror)
before := e.git("-C", mirror, "for-each-ref")
out, err := e.selfhost(append([]string{"--yes"}, c.args...)...)
if err == nil || !strings.Contains(out, c.want) {
t.Errorf("want a refusal with %q: %v\n%s", c.want, err, out)
}
if _, err := os.Stat(e.p("git/proj.git")); err == nil {
t.Error("it made the repository")
}
if got := e.git("-C", work, "remote"); got != "" {
t.Errorf("remotes: %s", got)
}
if got := e.git("-C", mirror, "for-each-ref"); got != before {
t.Errorf("the mirror changed:\n%s", got)
}
})
}
}
// Run again, it picks up where it was, and changes only what it's told.
func TestSelfhostAgain(t *testing.T) {
e := newEnv(t)
work := e.p("proj")
if out, err := e.selfhost("--yes", "--section", "Tools", "-d", "First"); err != nil {
t.Fatalf("git-selfhost: %v\n%s", err, out)
}
key := e.read("etc/keys/proj")
// As if it had stopped before the remote was added
e.git("-C", work, "remote", "remove", "origin")
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "more")
out, err := e.selfhost("--yes", "--section", "Libraries")
if err != nil {
t.Fatalf("git-selfhost again: %v\n%s", err, out)
}
if !strings.Contains(out, "picking up where it was") {
t.Errorf("output:\n%s", out)
}
if got := e.git("-C", e.p("git/proj.git"), "config", "repo-render.section"); got != "libs" {
t.Errorf("proj wasn't moved to Libraries: %q", got)
}
if got := e.read("git/proj.git/description"); got != "First\n" {
t.Errorf("description = %q", got)
}
if e.read("etc/keys/proj") != key {
t.Error("the key changed")
}
if got, want := e.git("-C", e.p("git/proj.git"), "rev-parse", "trunk"), e.git("-C", work, "rev-parse", "HEAD"); got != want {
t.Errorf("the server's trunk = %s, want %s", got, want)
}
// And again, with nothing to do: nothing is placed.
out, err = e.selfhost("--yes")
if err != nil || strings.Contains(out, "put it in") {
t.Errorf("git-selfhost a third time: %v\n%s", err, out)
}
}
func TestSelfhostGuards(t *testing.T) {
for _, c := range []struct {
name, want string
setup func(e *env, work, mirror string)
args []string
}{{
name: "another repository of the name",
want: "with commits your trunk doesn't have",
setup: func(e *env, work, mirror string) {
other := e.p("unrelated")
e.git("init", "-q", "-b", "trunk", other)
e.git("-C", other, "commit", "-q", "--allow-empty", "-m", "someone else's")
e.git("init", "-q", "--bare", "-b", "trunk", e.p("git/proj.git"))
e.git("-C", other, "push", "-q", e.p("git/proj.git"), "trunk")
},
}, {
name: "a shallow clone",
want: "shallow clone",
setup: func(e *env, work, mirror string) {
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "second")
e.git("clone", "-q", "--depth", "1", "file://"+work, e.p("shallow"))
if err := os.RemoveAll(work); err != nil {
e.t.Fatal(err)
}
if err := os.Rename(e.p("shallow"), work); err != nil {
e.t.Fatal(err)
}
e.git("-C", work, "remote", "remove", "origin")
},
}, {
name: "a tag that differs on the mirror",
want: "tag v1 is different here and on good",
setup: func(e *env, work, mirror string) {
e.git("-C", work, "commit", "-q", "--allow-empty", "-m", "elsewhere")
e.git("-C", work, "push", "-q", mirror, "trunk", "HEAD:refs/tags/v1")
},
}, {
name: "an https mirror",
want: "isn't an ssh URL",
args: []string{"--mirror-url", "https://example.com/me/proj"},
}} {
t.Run(c.name, func(t *testing.T) {
e := newEnv(t)
work, mirror := e.p("proj"), e.p("proj-mirror.git")
if c.setup != nil {
c.setup(e, work, mirror)
}
before := e.read("etc/repo-render.yaml")
out, err := e.selfhost(append([]string{"--yes"}, c.args...)...)
if err == nil || !strings.Contains(out, c.want) {
t.Errorf("want a refusal with %q: %v\n%s", c.want, err, out)
}
if got := e.read("etc/repo-render.yaml"); got != before {
t.Errorf("the config changed:\n%s", got)
}
if got := e.git("-C", work, "remote"); got != "" {
t.Errorf("remotes: %s", got)
}
if _, err := os.Stat(e.p("etc/keys/proj")); err == nil {
t.Error("it made a key")
}
})
}
}
// In a worktree, the name is the repository's.
func TestSelfhostWorktree(t *testing.T) {
e := newEnv(t)
work := e.p("proj")
e.git("-C", work, "worktree", "add", "-q", e.p("proj-feature"), "-b", "feature")
self, _ := filepath.Abs("git-selfhost")
out, err := e.run(e.p("proj-feature"), self, "--yes", "--no-section", "--branch", "trunk")
if err != nil {
t.Fatalf("git-selfhost: %v\n%s", err, out)
}
if _, err := os.Stat(e.p("git/proj.git")); err != nil {
t.Errorf("no proj.git:\n%s", out)
}
}
|