Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 2h ago 4b8c9e1d
history blame raw 7.9 KiB
1package main
2
3import (
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
15func 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
24func 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
32func 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
47func 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
54func 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
63func 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.
76func 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
137func 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
189func 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.
209func 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