krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
main: e2e/import_test.go · raw
1package e2e
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func TestRepoImport(t *testing.T) {
12 inst := startInstance(t)
13 aliceKey := inst.newKey(t, "alice")
14 inst.admin(t, "admin", "user", "create", "alice",
15 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
16
17 // Source repo with a non-"main" default branch, a tag, and two commits.
18 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/src"); code != 0 {
19 t.Fatalf("repo create: %s", errOut)
20 }
21 work := t.TempDir()
22 env := inst.gitEnv(aliceKey)
23 mustGit(t, work, env, "clone", inst.sshURL("alice/src"), "w")
24 dir := filepath.Join(work, "w")
25 os.WriteFile(filepath.Join(dir, "code.txt"), []byte("v1\n"), 0o644)
26 mustGit(t, dir, env, "checkout", "-q", "-b", "trunk")
27 mustGit(t, dir, env, "add", ".")
28 mustGit(t, dir, env, "commit", "-q", "-m", "first")
29 mustGit(t, dir, env, "tag", "v1.0")
30 mustGit(t, dir, env, "commit", "-q", "--allow-empty", "-m", "second")
31 mustGit(t, dir, env, "push", "-q", "origin", "trunk", "v1.0")
32
33 // Point the source repo's HEAD at trunk so the remote advertises it.
34 srcBare := filepath.Join(inst.root, "repos", "alice", "src.git")
35 mustGit(t, srcBare, env, "symbolic-ref", "HEAD", "refs/heads/trunk")
36
37 // Import over HTTP from our own instance (public smart HTTP).
38 httpURL := fmt.Sprintf("http://127.0.0.1:%d/alice/src.git", inst.httpPort)
39 out, errOut, code := inst.ssh(t, aliceKey, "", "repo", "import", "alice/mirror", "--from", httpURL, "--private")
40 if code != 0 {
41 t.Fatalf("import: exit %d\n%s%s", code, out, errOut)
42 }
43 if !strings.Contains(out, "imported alice/mirror (private, default trunk)") {
44 t.Fatalf("import output: %s", out)
45 }
46 if !strings.Contains(out, "git data only") {
47 t.Fatalf("import note missing: %s", out)
48 }
49
50 // Everything came across: both commits, the tag, and the default branch.
51 logOut, _, code := inst.ssh(t, aliceKey, "", "repo", "log", "alice/mirror")
52 if code != 0 || !strings.Contains(logOut, "second") || !strings.Contains(logOut, "first") {
53 t.Fatalf("imported log: %d\n%s", code, logOut)
54 }
55 mirrorBare := filepath.Join(inst.root, "repos", "alice", "mirror.git")
56 tags := mustGit(t, mirrorBare, env, "tag", "--list")
57 if !strings.Contains(tags, "v1.0") {
58 t.Fatalf("tag not imported: %q", tags)
59 }
60 head := strings.TrimSpace(mustGit(t, mirrorBare, env, "symbolic-ref", "HEAD"))
61 if head != "refs/heads/trunk" {
62 t.Fatalf("imported HEAD = %s", head)
63 }
64 showOut, _, _ := inst.ssh(t, aliceKey, "", "repo", "show", "alice/mirror")
65 if !strings.Contains(showOut, "private") || !strings.Contains(showOut, "default: trunk") {
66 t.Fatalf("repo show after import: %s", showOut)
67 }
68
69 // The imported repo has working hooks (core.hooksPath was set): a
70 // protected-branch force-push is refused.
71 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "protect", "alice/mirror", "trunk"); code != 0 {
72 t.Fatalf("protect: %s", errOut)
73 }
74 mWork := t.TempDir()
75 mustGit(t, mWork, env, "clone", inst.sshURL("alice/mirror"), "m")
76 mDir := filepath.Join(mWork, "m")
77 mustGit(t, mDir, env, "commit", "-q", "--amend", "--allow-empty", "-m", "rewrite")
78 pushOut, pushCode := gitRun(t, mDir, env, "push", "--force", "origin", "trunk")
79 if pushCode == 0 || !strings.Contains(pushOut, "force-push refused") {
80 t.Fatalf("hooks not wired on imported repo:\n%s", pushOut)
81 }
82
83 // Import over git:// too.
84 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/src", "on"); code != 0 {
85 t.Fatalf("git-daemon on: %s", errOut)
86 }
87 gitURL := fmt.Sprintf("git://127.0.0.1:%d/alice/src.git", inst.gitPort)
88 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "import", "alice/mirror2", "--from", gitURL); code != 0 {
89 t.Fatalf("git:// import: %s", errOut)
90 }
91
92 // Org-owned imports: allowed for org admins, refused for non-members.
93 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 {
94 t.Fatalf("org create: %s", errOut)
95 }
96 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "import", "imports/mirror", "--from", httpURL); code != 0 {
97 t.Fatalf("org import: %s", errOut)
98 }
99 if out, _, code := inst.ssh(t, aliceKey, "", "repo", "log", "imports/mirror"); code != 0 || !strings.Contains(out, "first") {
100 t.Fatalf("org import log: %d\n%s", code, out)
101 }
102
103 // Refusals: bad scheme, credentials in URL, existing name, foreign owner.
104 cases := []struct {
105 args []string
106 want string
107 }{
108 {[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "https://, http://, and git://"},
109 {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"},
110 {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"},
111 {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"},
112 }
113 for _, tc := range cases {
114 _, errOut, code := inst.ssh(t, aliceKey, "", tc.args...)
115 if code == 0 || !strings.Contains(errOut, tc.want) {
116 t.Errorf("%v: exit %d, stderr %q (want %q)", tc.args, code, errOut, tc.want)
117 }
118 }
119
120 // A failed import leaves nothing behind.
121 _, _, code = inst.ssh(t, aliceKey, "", "repo", "import", "alice/gone", "--from",
122 fmt.Sprintf("http://127.0.0.1:%d/alice/nonexistent.git", inst.httpPort))
123 if code == 0 {
124 t.Fatal("import of nonexistent source succeeded")
125 }
126 if _, _, code = inst.ssh(t, aliceKey, "", "repo", "show", "alice/gone"); code != 3 {
127 t.Fatalf("failed import left a repo behind: exit %d, want 3", code)
128 }
129 if _, err := os.Stat(filepath.Join(inst.root, "repos", "alice", "gone.git")); !os.IsNotExist(err) {
130 t.Fatal("failed import left a directory behind")
131 }
132
133 // --token-stdin consumes a token from stdin without leaking it: the
134 // fetch works (token unused by our anonymous endpoint, but the askpass
135 // plumbing must not break it) and the token string appears nowhere in
136 // the repo config.
137 _, errOut, code = inst.ssh(t, aliceKey, "s3cr3t-token\n", "repo", "import", "alice/mirror3",
138 "--from", httpURL, "--token-stdin")
139 if code != 0 {
140 t.Fatalf("token-stdin import: %s", errOut)
141 }
142 cfgRaw, _ := os.ReadFile(filepath.Join(inst.root, "repos", "alice", "mirror3.git", "config"))
143 if strings.Contains(string(cfgRaw), "s3cr3t") {
144 t.Fatal("token leaked into repo config")
145 }
146}