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 · 24m ago 4b8c9e1d
history blame raw 5.3 KiB
1package agent
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// prepareWorktree has two jobs that must not be confused: keep a trusted cache for
12// the runner's release push, and give the workflow a disposable tree with its own
13// .git. The test plays the SSH transport with a script so the whole path runs
14// without a server, including a commit that is not reachable from any branch.
15func TestPrepareWorktreeSeparatesTrustedCacheFromJobTree(t *testing.T) {
16 tmp := t.TempDir()
17 src := filepath.Join(tmp, "src")
18 remote := filepath.Join(tmp, "demo.git")
19 cache := filepath.Join(tmp, "cache")
20
21 mustGit(t, "", "init", "-q", src)
22 mustGit(t, src, "config", "user.email", "a@example.com")
23 mustGit(t, src, "config", "user.name", "a")
24 if err := os.WriteFile(filepath.Join(src, "file"), []byte("one\n"), 0o644); err != nil {
25 t.Fatal(err)
26 }
27 mustGit(t, src, "add", "file")
28 mustGit(t, src, "commit", "-qm", "one")
29 mustGit(t, "", "clone", "-q", "--bare", src, remote)
30
31 // A second commit reaches the remote's object store but no ref. Fetching it by
32 // id is what the fallback in prepareWorktree has to handle.
33 if err := os.WriteFile(filepath.Join(src, "file"), []byte("two\n"), 0o644); err != nil {
34 t.Fatal(err)
35 }
36 mustGit(t, src, "commit", "-qam", "two")
37 mustGit(t, src, "push", "-q", remote, "HEAD:refs/heads/tmp")
38 sha := strings.TrimSpace(mustGit(t, src, "rev-parse", "HEAD"))
39 mustGit(t, remote, "update-ref", "-d", "refs/heads/tmp")
40 mustGit(t, remote, "config", "uploadpack.allowAnySHA1InWant", "true")
41
42 // Git talks to our script instead of ssh. The script serves the bare repository
43 // for the upload-pack request and fails the protocol probe so Git falls back to
44 // the ordinary transport.
45 ssh := filepath.Join(tmp, "ssh")
46 calls := filepath.Join(tmp, "ssh-calls")
47 script := "#!/bin/sh\n" +
48 "echo \"$*\" >> \"$VOLE_TEST_SSH_LOG\"\n" +
49 "for a in \"$@\"; do\n" +
50 " case \"$a\" in\n" +
51 " *git-upload-pack*) exec git-upload-pack \"$VOLE_TEST_REPO\" ;;\n" +
52 " esac\n" +
53 "done\n" +
54 "exit 1\n"
55 if err := os.WriteFile(ssh, []byte(script), 0o755); err != nil {
56 t.Fatal(err)
57 }
58 t.Setenv("GIT_SSH_COMMAND", ssh)
59 t.Setenv("GIT_SSH_VARIANT", "ssh")
60 t.Setenv("VOLE_TEST_REPO", remote)
61 t.Setenv("VOLE_TEST_SSH_LOG", calls)
62
63 co, err := prepareWorktree("ssh://git@example.invalid", cache, "demo", sha, "run1", 1)
64 if err != nil {
65 t.Fatalf("prepareWorktree: %v", err)
66 }
67 defer co.cleanup()
68
69 got := strings.TrimSpace(mustGit(t, co.Work, "rev-parse", "HEAD"))
70 if got != sha {
71 t.Fatalf("job tree is at %s, want %s", got, sha)
72 }
73 if _, err := os.Stat(filepath.Join(co.Work, ".git")); err != nil {
74 t.Fatalf("the job tree has no .git: %v", err)
75 }
76
77 // The job may edit its own Git directory. That must not reach the cache the
78 // runner later uses to push the record.
79 hook := filepath.Join(co.Work, ".git", "hooks", "pre-push")
80 if err := os.MkdirAll(filepath.Dir(hook), 0o755); err != nil {
81 t.Fatal(err)
82 }
83 if err := os.WriteFile(hook, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
84 t.Fatal(err)
85 }
86 cacheHook := filepath.Join(cache, "demo", ".git", "hooks", "pre-push")
87 if _, err := os.Stat(cacheHook); !os.IsNotExist(err) {
88 t.Fatalf("a job hook reached the trusted cache: %v", err)
89 }
90
91 // The temporary ref used to carry an unreachable commit must be gone too.
92 out := mustGit(t, filepath.Join(cache, "demo"), "for-each-ref", "--format=%(refname)",
93 "refs/vole/jobs")
94 if strings.TrimSpace(out) != "" {
95 t.Fatalf("temporary job refs were left in the cache:\n%s", out)
96 }
97
98 // Nothing reads the cache's working tree, so the first clone does not write one:
99 // that disk is spent on the job tree instead.
100 if _, err := os.Stat(filepath.Join(cache, "demo", "file")); !os.IsNotExist(err) {
101 t.Fatalf("the trusted cache has a working tree: %v", err)
102 }
103 if _, err := os.Stat(filepath.Join(cache, "demo", lastUsedStamp)); err != nil {
104 t.Fatalf("the cache does not record that a job used it: %v", err)
105 }
106
107 // A revision the cache already holds costs one fetch for the branches, and no
108 // download of history: that is what keeping the clone is for, and the branch
109 // fetch is what keeps the job's origin/* refs honest.
110 sentinel := filepath.Join(cache, "demo", ".vole-test-sentinel")
111 if err := os.WriteFile(sentinel, []byte("kept\n"), 0o644); err != nil {
112 t.Fatal(err)
113 }
114 before := sshCalls(t, calls)
115 again, err := prepareWorktree("ssh://git@example.invalid", cache, "demo", sha, "run2", 1)
116 if err != nil {
117 t.Fatalf("prepareWorktree for a cached revision: %v", err)
118 }
119 defer again.cleanup()
120 if got := sshCalls(t, calls); got != before+1 {
121 t.Fatalf("a cached revision made %d SSH call(s), want one branch fetch", got-before)
122 }
123 if _, err := os.Stat(sentinel); err != nil {
124 t.Fatalf("the cache was rebuilt instead of reused: %v", err)
125 }
126 if got := strings.TrimSpace(mustGit(t, again.Work, "rev-parse", "HEAD")); got != sha {
127 t.Fatalf("the second job tree is at %s, want %s", got, sha)
128 }
129}
130
131func sshCalls(t *testing.T, path string) int {
132 t.Helper()
133 b, err := os.ReadFile(path)
134 if err != nil && !os.IsNotExist(err) {
135 t.Fatal(err)
136 }
137 return strings.Count(string(b), "\n")
138}
139
140func mustGit(t *testing.T, dir string, args ...string) string {
141 t.Helper()
142 cmd := exec.Command("git", args...)
143 if dir != "" {
144 cmd.Dir = dir
145 }
146 out, err := cmd.CombinedOutput()
147 if err != nil {
148 t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out)
149 }
150 return string(out)
151}
152