vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 2h ago
4b8c9e1d
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "os/exec" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // A site is a branch, so these tests assert on the branch the far side ends up |
| 12 | // with — not on the intermediate commits. A bare repository on disk stands in for |
| 13 | // the server: `git push` is the same push either way. |
| 14 | |
| 15 | func bareRemote(t *testing.T) string { |
| 16 | t.Helper() |
| 17 | dir := filepath.Join(t.TempDir(), "site.git") |
| 18 | if out, err := exec.Command("git", "init", "--bare", "-q", "-b", "main", dir).CombinedOutput(); err != nil { |
| 19 | t.Fatalf("init bare: %v: %s", err, out) |
| 20 | } |
| 21 | return dir |
| 22 | } |
| 23 | |
| 24 | func cloneWithRemote(t *testing.T, remote string) string { |
| 25 | t.Helper() |
| 26 | dir := t.TempDir() |
| 27 | runGitDir(t, dir, "init", "-q", "-b", "main") |
| 28 | runGitDir(t, dir, "remote", "add", "origin", remote) |
| 29 | return dir |
| 30 | } |
| 31 | |
| 32 | func runGitDir(t *testing.T, dir string, args ...string) string { |
| 33 | t.Helper() |
| 34 | cmd := exec.Command("git", args...) |
| 35 | cmd.Dir = dir |
| 36 | cmd.Env = append(os.Environ(), |
| 37 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", |
| 38 | "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.com", |
| 39 | "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.com") |
| 40 | out, err := cmd.CombinedOutput() |
| 41 | if err != nil { |
| 42 | t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out) |
| 43 | } |
| 44 | return strings.TrimSpace(string(out)) |
| 45 | } |
| 46 | |
| 47 | func remoteRef(t *testing.T, remote, ref string) string { |
| 48 | t.Helper() |
| 49 | cmd := exec.Command("git", "-C", remote, "rev-parse", "--verify", "--quiet", ref) |
| 50 | out, _ := cmd.Output() |
| 51 | return strings.TrimSpace(string(out)) |
| 52 | } |
| 53 | |
| 54 | func remoteFile(t *testing.T, remote, ref, path string) string { |
| 55 | t.Helper() |
| 56 | out, err := exec.Command("git", "-C", remote, "cat-file", "-p", ref+":"+path).Output() |
| 57 | if err != nil { |
| 58 | return "" // not in the tree |
| 59 | } |
| 60 | return string(out) |
| 61 | } |
| 62 | |
| 63 | func writeFile(t *testing.T, path, content string) { |
| 64 | t.Helper() |
| 65 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if err := os.WriteFile(path, []byte(content), 0o644); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | // TestPagesDeployIsASnapshot is the whole point of the command: the branch's tree |
| 74 | // becomes the directory, so a file deleted from the build stops being served |
| 75 | // instead of living on until someone notices. |
| 76 | func TestPagesDeployIsASnapshot(t *testing.T) { |
| 77 | remote := bareRemote(t) |
| 78 | work := cloneWithRemote(t, remote) |
| 79 | t.Chdir(work) |
| 80 | |
| 81 | dist := filepath.Join(work, "dist") |
| 82 | writeFile(t, filepath.Join(dist, "index.html"), "<h1>v1</h1>\n") |
| 83 | writeFile(t, filepath.Join(dist, "assets", "app.css"), "body{color:#000}\n") |
| 84 | writeFile(t, filepath.Join(dist, ".nojekyll"), "") // dotfiles are part of a site |
| 85 | |
| 86 | if err := cmdPages([]string{"deploy", "dist"}); err != nil { |
| 87 | t.Fatalf("deploy: %v", err) |
| 88 | } |
| 89 | if got := remoteFile(t, remote, "refs/heads/pages", "index.html"); got != "<h1>v1</h1>\n" { |
| 90 | t.Fatalf("published index.html = %q", got) |
| 91 | } |
| 92 | if got := remoteFile(t, remote, "refs/heads/pages", "assets/app.css"); got == "" { |
| 93 | t.Fatal("nested file was not published") |
| 94 | } |
| 95 | if _, err := exec.Command("git", "-C", remote, "cat-file", "-e", "refs/heads/pages:.nojekyll").Output(); err != nil { |
| 96 | t.Fatal("dotfile was not published") |
| 97 | } |
| 98 | // The local repository is not switched to the site branch: deploying is not |
| 99 | // checking out, and a user mid-task should stay where they were. |
| 100 | if cur := runGitDir(t, work, "symbolic-ref", "--short", "HEAD"); cur != "main" { |
| 101 | t.Fatalf("deploy moved the checkout to %q", cur) |
| 102 | } |
| 103 | |
| 104 | // Second deploy: one file gone, one changed, one added. |
| 105 | if err := os.Remove(filepath.Join(dist, "assets", "app.css")); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | writeFile(t, filepath.Join(dist, "index.html"), "<h1>v2</h1>\n") |
| 109 | writeFile(t, filepath.Join(dist, "about.html"), "about\n") |
| 110 | if err := cmdPages([]string{"deploy", "dist"}); err != nil { |
| 111 | t.Fatalf("second deploy: %v", err) |
| 112 | } |
| 113 | if got := remoteFile(t, remote, "refs/heads/pages", "index.html"); got != "<h1>v2</h1>\n" { |
| 114 | t.Fatalf("index.html = %q after the second deploy", got) |
| 115 | } |
| 116 | if got := remoteFile(t, remote, "refs/heads/pages", "assets/app.css"); got != "" { |
| 117 | t.Fatalf("a file deleted from the build is still published: %q", got) |
| 118 | } |
| 119 | if got := remoteFile(t, remote, "refs/heads/pages", "about.html"); got != "about\n" { |
| 120 | t.Fatalf("added file = %q", got) |
| 121 | } |
| 122 | |
| 123 | // Deploying the same bytes again is not a push: a build that reruns on every |
| 124 | // commit must not add an empty commit per run. |
| 125 | before := remoteRef(t, remote, "refs/heads/pages") |
| 126 | if err := cmdPages([]string{"deploy", "dist", "--dry-run"}); err != nil { |
| 127 | t.Fatalf("dry run: %v", err) |
| 128 | } |
| 129 | if err := cmdPages([]string{"deploy", "dist"}); err != nil { |
| 130 | t.Fatalf("no-op deploy: %v", err) |
| 131 | } |
| 132 | if after := remoteRef(t, remote, "refs/heads/pages"); after != before { |
| 133 | t.Fatalf("a no-op deploy moved the branch from %.8s to %.8s", before, after) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | func TestPagesStatusAndUnpublish(t *testing.T) { |
| 138 | remote := bareRemote(t) |
| 139 | work := cloneWithRemote(t, remote) |
| 140 | t.Chdir(work) |
| 141 | |
| 142 | // Nothing published yet is a state, not an error. |
| 143 | out := captureStdout(t, func() error { return cmdPages([]string{"status"}) }) |
| 144 | if !strings.Contains(out, "no site published") { |
| 145 | t.Fatalf("status on an unpublished site = %q", out) |
| 146 | } |
| 147 | |
| 148 | dist := filepath.Join(work, "dist") |
| 149 | writeFile(t, filepath.Join(dist, "index.html"), "hello\n") |
| 150 | if err := cmdPages([]string{"deploy", "dist"}); err != nil { |
| 151 | t.Fatalf("deploy: %v", err) |
| 152 | } |
| 153 | out = captureStdout(t, func() error { return cmdPages([]string{"status"}) }) |
| 154 | for _, want := range []string{"pages", "1 file(s)", "site: publish"} { |
| 155 | if !strings.Contains(out, want) { |
| 156 | t.Fatalf("status does not mention %q:\n%s", want, out) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | out = captureStdout(t, func() error { return cmdPages([]string{"unpublish"}) }) |
| 161 | if !strings.Contains(out, "unpublished") { |
| 162 | t.Fatalf("unpublish said %q", out) |
| 163 | } |
| 164 | if ref := remoteRef(t, remote, "refs/heads/pages"); ref != "" { |
| 165 | t.Fatalf("pages still exists at %s after unpublish", ref) |
| 166 | } |
| 167 | if out := captureStdout(t, func() error { return cmdPages([]string{"status"}) }); !strings.Contains(out, "no site published") { |
| 168 | t.Fatalf("status after unpublish = %q", out) |
| 169 | } |
| 170 | // Unpublishing twice is not an error: the desired state is already true. |
| 171 | out = captureStdout(t, func() error { return cmdPages([]string{"unpublish"}) }) |
| 172 | if !strings.Contains(out, "nothing to unpublish") { |
| 173 | t.Fatalf("second unpublish said %q", out) |
| 174 | } |
| 175 | |
| 176 | // Publishing again after an unpublish must create the branch, not mistake the |
| 177 | // stale local ref for a live site: the local branch survives the delete. |
| 178 | if err := cmdPages([]string{"deploy", "dist"}); err != nil { |
| 179 | t.Fatalf("re-deploy after unpublish: %v", err) |
| 180 | } |
| 181 | if ref := remoteRef(t, remote, "refs/heads/pages"); ref == "" { |
| 182 | t.Fatal("re-deploy after unpublish did not publish anything") |
| 183 | } |
| 184 | if out := captureStdout(t, func() error { return cmdPages([]string{"status"}) }); strings.Contains(out, "no site published") { |
| 185 | t.Fatalf("status after re-deploy = %q", out) |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | func TestPagesDeployRefusesAnEmptyDirectory(t *testing.T) { |
| 190 | remote := bareRemote(t) |
| 191 | work := cloneWithRemote(t, remote) |
| 192 | t.Chdir(work) |
| 193 | empty := filepath.Join(work, "empty") |
| 194 | if err := os.MkdirAll(empty, 0o755); err != nil { |
| 195 | t.Fatal(err) |
| 196 | } |
| 197 | err := cmdPages([]string{"deploy", "empty"}) |
| 198 | if err == nil || !strings.Contains(err.Error(), "unpublish") { |
| 199 | t.Fatalf("deploying nothing = %v, want a refusal that names unpublish", err) |
| 200 | } |
| 201 | // The branch must not have been created by the refusal. |
| 202 | if ref := remoteRef(t, remote, "refs/heads/pages"); ref != "" { |
| 203 | t.Fatalf("a refused deploy created %s", ref) |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | // captureStdout runs fn with stdout redirected, so a command that reports rather |
| 208 | // than returns can be asserted on. |
| 209 | func captureStdout(t *testing.T, fn func() error) string { |
| 210 | t.Helper() |
| 211 | r, w, err := os.Pipe() |
| 212 | if err != nil { |
| 213 | t.Fatal(err) |
| 214 | } |
| 215 | old := os.Stdout |
| 216 | os.Stdout = w |
| 217 | done := make(chan string, 1) |
| 218 | go func() { |
| 219 | var b strings.Builder |
| 220 | buf := make([]byte, 4096) |
| 221 | for { |
| 222 | n, err := r.Read(buf) |
| 223 | if n > 0 { |
| 224 | b.Write(buf[:n]) |
| 225 | } |
| 226 | if err != nil { |
| 227 | break |
| 228 | } |
| 229 | } |
| 230 | done <- b.String() |
| 231 | }() |
| 232 | runErr := fn() |
| 233 | w.Close() |
| 234 | os.Stdout = old |
| 235 | out := <-done |
| 236 | if runErr != nil { |
| 237 | t.Fatalf("command failed: %v", runErr) |
| 238 | } |
| 239 | return out |
| 240 | } |
| 241 |