checkout_test.go
1package checkout
2
3import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "git.kilimanjaro.io/ci/internal/lex"
12)
13
14func sh(t *testing.T, dir string, args ...string) string {
15 t.Helper()
16 cmd := exec.Command(args[0], args[1:]...)
17 cmd.Dir = dir
18 cmd.Env = append(os.Environ(),
19 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
20 "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
21 out, err := cmd.CombinedOutput()
22 if err != nil {
23 t.Fatalf("%v: %v\n%s", args, err, out)
24 }
25 return strings.TrimSpace(string(out))
26}
27
28// forge makes a bare repo <forge>/<name>.git with commits writing file=v1
29// then file=v2 on main, and a tag v1 at the first. It returns the shas.
30func forge(t *testing.T, dir, name string) (v1, v2 string) {
31 t.Helper()
32 work := filepath.Join(t.TempDir(), name)
33 if err := os.MkdirAll(work, 0o755); err != nil {
34 t.Fatal(err)
35 }
36 sh(t, work, "git", "init", "-q", "-b", "main")
37 if err := os.WriteFile(filepath.Join(work, "file"), []byte("v1"), 0o644); err != nil {
38 t.Fatal(err)
39 }
40 sh(t, work, "git", "add", "file")
41 sh(t, work, "git", "commit", "-qm", "v1")
42 v1 = sh(t, work, "git", "rev-parse", "HEAD")
43 sh(t, work, "git", "tag", "v1")
44 if err := os.WriteFile(filepath.Join(work, "file"), []byte("v2"), 0o644); err != nil {
45 t.Fatal(err)
46 }
47 sh(t, work, "git", "commit", "-qam", "v2")
48 v2 = sh(t, work, "git", "rev-parse", "HEAD")
49 bare := filepath.Join(dir, name+".git")
50 sh(t, dir, "git", "clone", "-q", "--bare", work, bare)
51 return v1, v2
52}
53
54func read(t *testing.T, p string) string {
55 t.Helper()
56 b, err := os.ReadFile(p)
57 if err != nil {
58 t.Fatal(err)
59 }
60 return string(b)
61}
62
63func TestLayout(t *testing.T) {
64 t.Setenv("GIT_CONFIG_GLOBAL", "/dev/null")
65 ctx := context.Background()
66 fdir := t.TempDir()
67 m1, m2 := forge(t, fdir, "main")
68 _, _ = forge(t, fdir, "servers")
69 d1, _ := forge(t, fdir, "data")
70 src := &Source{MirrorsDir: filepath.Join(t.TempDir(), "mirrors"), URL: URLFunc(filepath.Join(fdir, "{repo}.git"), "")}
71
72 run := filepath.Join(t.TempDir(), "run")
73 mainMirror, err := src.Mirror(ctx, "main")
74 if err != nil {
75 t.Fatal(err)
76 }
77 local := t.TempDir()
78 got, err := Layout(ctx, src, run, Main{Repo: "main", From: mainMirror, Sha: m1, Origin: "git@forge:main"},
79 []lex.Repo{
80 {Repo: "servers", Path: "../servers"},
81 {Repo: "data", Path: "../data", Rev: "v1", Env: "SECRETS_DIR"},
82 {Repo: "tools", Path: "../tools"},
83 }, map[string]string{"tools": local})
84 if err != nil {
85 t.Fatal(err)
86 }
87 if len(got) != 4 {
88 t.Fatalf("%+v", got)
89 }
90 if read(t, filepath.Join(run, "main", "file")) != "v1" || got[0].Rev != m1 {
91 t.Errorf("main checkout not at v1: %+v", got[0])
92 }
93 if read(t, filepath.Join(run, "servers", "file")) != "v2" {
94 t.Error("servers not at the default branch")
95 }
96 if got[2].Rev != d1 || got[2].Env != "SECRETS_DIR" || read(t, filepath.Join(run, "data", "file")) != "v1" {
97 t.Errorf("data: %+v", got[2])
98 }
99 if p, _ := os.Readlink(filepath.Join(run, "tools")); p != local || !got[3].Link {
100 t.Errorf("tools override: %q %+v", p, got[3])
101 }
102 if u := sh(t, filepath.Join(run, "main"), "git", "remote", "get-url", "origin"); u != "git@forge:main" {
103 t.Errorf("origin %q", u)
104 }
105 if u := sh(t, filepath.Join(run, "servers"), "git", "remote", "get-url", "origin"); u != filepath.Join(fdir, "servers.git") {
106 t.Errorf("servers origin %q", u)
107 }
108 // the checkout borrows objects; nothing was registered in the mirror
109 if _, err := os.Stat(filepath.Join(mainMirror, "worktrees")); err == nil {
110 t.Error("mirror has worktree metadata")
111 }
112 _ = m2
113}
114
115func TestMirrorFetchesNewCommits(t *testing.T) {
116 t.Setenv("GIT_CONFIG_GLOBAL", "/dev/null")
117 ctx := context.Background()
118 fdir := t.TempDir()
119 _, v2 := forge(t, fdir, "r")
120 src := &Source{MirrorsDir: t.TempDir(), URL: URLFunc(filepath.Join(fdir, "{repo}.git"), "")}
121 m, err := src.Mirror(ctx, "r")
122 if err != nil {
123 t.Fatal(err)
124 }
125 if sha, _ := Resolve(ctx, m, ""); sha != v2 {
126 t.Fatalf("HEAD %s want %s", sha, v2)
127 }
128 // push a new commit to the forge; the next Mirror fetches it
129 work := filepath.Join(t.TempDir(), "w")
130 sh(t, "", "git", "clone", "-q", filepath.Join(fdir, "r.git"), work)
131 if err := os.WriteFile(filepath.Join(work, "file"), []byte("v3"), 0o644); err != nil {
132 t.Fatal(err)
133 }
134 sh(t, work, "git", "commit", "-qam", "v3")
135 sh(t, work, "git", "push", "-q", "origin", "main")
136 v3 := sh(t, work, "git", "rev-parse", "HEAD")
137 if _, err := src.Mirror(ctx, "r"); err != nil {
138 t.Fatal(err)
139 }
140 if sha, _ := Resolve(ctx, m, ""); sha != v3 {
141 t.Errorf("after fetch HEAD %s want %s", sha, v3)
142 }
143 if _, err := Resolve(ctx, m, "nope"); err == nil {
144 t.Error("resolved a missing rev")
145 }
146}
147
148func TestLayoutRejectsBadPaths(t *testing.T) {
149 ctx := context.Background()
150 src := &Source{MirrorsDir: t.TempDir(), URL: URLFunc("/nowhere/{repo}.git", "")}
151 wc := t.TempDir()
152 for _, p := range []string{"../../escape", "sub", "../main", "."} {
153 run := filepath.Join(t.TempDir(), "run")
154 _, err := Layout(ctx, src, run, Main{Repo: "main", WorkingCopy: wc}, []lex.Repo{{Repo: "x", Path: p}}, nil)
155 if err == nil || !strings.Contains(err.Error(), "sibling") {
156 t.Errorf("path %q: %v", p, err)
157 }
158 }
159 run := filepath.Join(t.TempDir(), "run")
160 if _, err := Layout(ctx, src, run, Main{Repo: "main", WorkingCopy: wc}, nil, map[string]string{"x": wc}); err == nil {
161 t.Error("override for an unknown repo accepted")
162 }
163}
164
165func TestURLs(t *testing.T) {
166 cases := map[string]string{
167 "[email protected]:sovrn": "[email protected]:servers",
168 "ssh://git@host/u/sovrn": "ssh://git@host/u/servers",
169 "https://host/u/sovrn.git": "https://host/u/servers.git",
170 "git+ssh://[email protected]/sovrn": "git+ssh://[email protected]/servers",
171 }
172 for of, want := range cases {
173 if got, err := SiblingURL(of, "servers"); err != nil || got != want {
174 t.Errorf("SiblingURL(%q) = %q, %v", of, got, err)
175 }
176 }
177 for in, want := range map[string]string{"git@h:sovrn.git": "sovrn", "ssh://h/u/moods/": "moods", "https://h/x.git": "x", "": ""} {
178 if got := RepoName(in); got != want {
179 t.Errorf("RepoName(%q) = %q", in, got)
180 }
181 }
182 if u, _ := URLFunc("git@h:{repo}", "x")("data"); u != "git@h:data" {
183 t.Errorf("template: %s", u)
184 }
185 if _, err := URLFunc("", "")("data"); err == nil {
186 t.Error("no origin and no template accepted")
187 }
188 if _, err := URLFunc("git@h:fixed", "")("data"); err == nil {
189 t.Error("template without {repo} accepted")
190 }
191}