Commit b46c9682ce
Verified · cmc
Layout: unified · split
internal/control/build.go +10 −1
| @@ -564,12 +564,21 @@ func runSecretList(c *Ctx, args []string) int { | |||
| 564 | if err != nil { | 564 | if err != nil { |
| 565 | return c.fail(protocol.ExitFailure, "%v", err) | 565 | return c.fail(protocol.ExitFailure, "%v", err) |
| 566 | } | 566 | } |
| 567 | return c.emit(names, func(w io.Writer) { | 567 | return c.emitView(names, func(w io.Writer) { |
| 568 | tb := c.table(w, "NAME") | 568 | tb := c.table(w, "NAME") |
| 569 | for _, n := range names { | 569 | for _, n := range names { |
| 570 | tb.row(cRef(n)) | 570 | tb.row(cRef(n)) |
| 571 | } | 571 | } |
| 572 | tb.flush() | 572 | tb.flush() |
| 573 | }, func() screen { | ||
| 574 | rows := make([]row, len(names)) | ||
| 575 | for i, n := range names { | ||
| 576 | rows[i] = rowOf(cRef(n)) | ||
| 577 | } | ||
| 578 | return listScreen("Build secrets", rows, | ||
| 579 | action{"Secrets", []string{"repo", "secret", "set", repo.Path(), "<NAME>"}}, | ||
| 580 | action{"Secrets", []string{"repo", "secret", "remove", repo.Path(), "<NAME>"}}, | ||
| 581 | ) | ||
| 573 | }) | 582 | }) |
| 574 | } | 583 | } |
| 575 | 584 | ||
internal/control/deploykey.go +11 −1
| @@ -119,13 +119,23 @@ func runDeployKeyList(c *Ctx, args []string) int { | |||
| 119 | ds = append(ds, out{k.Fingerprint, k.Algo, mode, k.Label, k.LastUsedAt, k.ExpiresAt}) | 119 | ds = append(ds, out{k.Fingerprint, k.Algo, mode, k.Label, k.LastUsedAt, k.ExpiresAt}) |
| 120 | } | 120 | } |
| 121 | now := time.Now() | 121 | now := time.Now() |
| 122 | return c.emit(ds, func(w io.Writer) { | 122 | return c.emitView(ds, func(w io.Writer) { |
| 123 | tb := c.table(w, "FINGERPRINT", "ALGO", "MODE", "LABEL", "USED", "EXPIRES") | 123 | tb := c.table(w, "FINGERPRINT", "ALGO", "MODE", "LABEL", "USED", "EXPIRES") |
| 124 | for _, d := range ds { | 124 | for _, d := range ds { |
| 125 | tb.row(cFlex(d.Fingerprint), cText(d.Algo), cState(d.Mode), cText(d.Label), | 125 | tb.row(cFlex(d.Fingerprint), cText(d.Algo), cState(d.Mode), cText(d.Label), |
| 126 | cText(c.usedText(d.LastUsedAt)), cText(c.expiresText(d.ExpiresAt, now))) | 126 | cText(c.usedText(d.LastUsedAt)), cText(c.expiresText(d.ExpiresAt, now))) |
| 127 | } | 127 | } |
| 128 | tb.flush() | 128 | tb.flush() |
| 129 | }, func() screen { | ||
| 130 | rows := make([]row, len(ds)) | ||
| 131 | for i, d := range ds { | ||
| 132 | rows[i] = rowOf(cRef(d.Fingerprint), cState(d.Mode), cText(d.Label), | ||
| 133 | cMeta(d.Algo, "used "+c.usedText(d.LastUsedAt), c.expiresText(d.ExpiresAt, now))) | ||
| 134 | } | ||
| 135 | return listScreen("Deploy keys", rows, | ||
| 136 | action{"Keys", []string{"repo", "deploy-key", "add", repo.Path()}}, | ||
| 137 | action{"Keys", []string{"repo", "deploy-key", "remove", repo.Path(), "<fingerprint>"}}, | ||
| 138 | ) | ||
| 129 | }) | 139 | }) |
| 130 | } | 140 | } |
| 131 | 141 | ||
internal/control/mirrorcmd.go +21 −1
| @@ -122,7 +122,7 @@ func runMirrorList(c *Ctx, args []string) int { | |||
| 122 | // The token never leaves the server, in any encoding. | 122 | // The token never leaves the server, in any encoding. |
| 123 | ds = append(ds, out{m.ID, m.Direction, m.URL, m.Username, m.Dirty, m.LastSync, m.LastError}) | 123 | ds = append(ds, out{m.ID, m.Direction, m.URL, m.Username, m.Dirty, m.LastSync, m.LastError}) |
| 124 | } | 124 | } |
| 125 | return c.emit(ds, func(w io.Writer) { | 125 | return c.emitView(ds, func(w io.Writer) { |
| 126 | tb := c.table(w, "ID", "DIRECTION", "URL", "LAST", "STATUS") | 126 | tb := c.table(w, "ID", "DIRECTION", "URL", "LAST", "STATUS") |
| 127 | for _, d := range ds { | 127 | for _, d := range ds { |
| 128 | status := "ok" | 128 | status := "ok" |
| @@ -135,6 +135,26 @@ func runMirrorList(c *Ctx, args []string) int { | |||
| 135 | tb.row(cRef(fmt.Sprintf("%d", d.ID)), cText(d.Direction), cText(d.URL), cText("last "+orDash(d.LastSync)), cState(status)) | 135 | tb.row(cRef(fmt.Sprintf("%d", d.ID)), cText(d.Direction), cText(d.URL), cText("last "+orDash(d.LastSync)), cState(status)) |
| 136 | } | 136 | } |
| 137 | tb.flush() | 137 | tb.flush() |
| 138 | }, func() screen { | ||
| 139 | rows := make([]row, len(ds)) | ||
| 140 | for i, d := range ds { | ||
| 141 | state := "ok" | ||
| 142 | if d.Pending { | ||
| 143 | state = "pending" | ||
| 144 | } | ||
| 145 | if d.LastError != "" { | ||
| 146 | state = "error" | ||
| 147 | } | ||
| 148 | synced := "never synced" | ||
| 149 | if d.LastSync != "" { | ||
| 150 | synced = "synced " + relAge(d.LastSync, termNow()) | ||
| 151 | } | ||
| 152 | rows[i] = rowOf(cRef(strconv.FormatInt(d.ID, 10)), cGlyph(state), cFlex(d.URL), cMeta(d.Direction, synced), cMark(d.LastError, sgrRed)) | ||
| 153 | } | ||
| 154 | return listScreen("Mirrors", rows, | ||
| 155 | action{"Mirrors", []string{"repo", "mirror", "sync", repo.Path()}}, | ||
| 156 | action{"Mirrors", []string{"repo", "mirror", "remove", repo.Path(), "<id>"}}, | ||
| 157 | ) | ||
| 138 | }) | 158 | }) |
| 139 | } | 159 | } |
| 140 | 160 | ||
internal/control/pagescmd.go +14 −1
| @@ -220,11 +220,24 @@ func runDomainList(c *Ctx, args []string) int { | |||
| 220 | } | 220 | } |
| 221 | list = append(list, out{d.Domain, state, d.VerifiedAt}) | 221 | list = append(list, out{d.Domain, state, d.VerifiedAt}) |
| 222 | } | 222 | } |
| 223 | return c.emit(list, func(w io.Writer) { | 223 | return c.emitView(list, func(w io.Writer) { |
| 224 | tb := c.table(w, "DOMAIN", "STATE") | 224 | tb := c.table(w, "DOMAIN", "STATE") |
| 225 | for _, d := range list { | 225 | for _, d := range list { |
| 226 | tb.row(cRef(d.Domain), cState(d.State)) | 226 | tb.row(cRef(d.Domain), cState(d.State)) |
| 227 | } | 227 | } |
| 228 | tb.flush() | 228 | tb.flush() |
| 229 | }, func() screen { | ||
| 230 | rows := make([]row, len(list)) | ||
| 231 | for i, d := range list { | ||
| 232 | state := d.State | ||
| 233 | if state == "expired" { | ||
| 234 | state = "failed" | ||
| 235 | } | ||
| 236 | rows[i] = rowOf(cGlyph(state), cRef(d.Domain), cState(d.State)) | ||
| 237 | } | ||
| 238 | return listScreen("Pages domains", rows, | ||
| 239 | action{"Domains", []string{"repo", "domain", "add", repo.Path(), "<domain>"}}, | ||
| 240 | action{"Domains", []string{"repo", "domain", "verify", repo.Path(), "<domain>"}}, | ||
| 241 | ) | ||
| 229 | }) | 242 | }) |
| 230 | } | 243 | } |
internal/control/repo.go +70 −6
| @@ -360,7 +360,7 @@ func runRepoList(c *Ctx, args []string) int { | |||
| 360 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, r.OwnerName, r.Name)) | 360 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, r.OwnerName, r.Name)) |
| 361 | ds = append(ds, out{r.Path(), r.Visibility, desc, r.Settings.Archived}) | 361 | ds = append(ds, out{r.Path(), r.Visibility, desc, r.Settings.Archived}) |
| 362 | } | 362 | } |
| 363 | return c.emitPage(p, ds, next, func(w io.Writer) { | 363 | return c.emitPageView(p, ds, next, func(w io.Writer) { |
| 364 | tb := c.table(w, "PATH", "VISIBILITY", "DESCRIPTION") | 364 | tb := c.table(w, "PATH", "VISIBILITY", "DESCRIPTION") |
| 365 | for _, d := range ds { | 365 | for _, d := range ds { |
| 366 | cells := []cell{cLink(d.Path, c.siteURL(d.Path)), cState(d.Visibility), cFlex(d.Description)} | 366 | cells := []cell{cLink(d.Path, c.siteURL(d.Path)), cState(d.Visibility), cFlex(d.Description)} |
| @@ -370,6 +370,20 @@ func runRepoList(c *Ctx, args []string) int { | |||
| 370 | tb.row(cells...) | 370 | tb.row(cells...) |
| 371 | } | 371 | } |
| 372 | tb.flush() | 372 | tb.flush() |
| 373 | }, func() screen { | ||
| 374 | rows := make([]row, len(ds)) | ||
| 375 | for i, d := range ds { | ||
| 376 | state := d.Visibility | ||
| 377 | if d.Archived { | ||
| 378 | state += ", archived" | ||
| 379 | } | ||
| 380 | rows[i] = rowOf(cLink(d.Path, c.siteURL(d.Path)), cState(state), cFlex(d.Description)) | ||
| 381 | } | ||
| 382 | s := listScreen("Repositories", rows) | ||
| 383 | if len(ds) > 0 { | ||
| 384 | s.actions = []action{{"Read", []string{"repo", "show", ds[0].Path}}} | ||
| 385 | } | ||
| 386 | return s | ||
| 373 | }) | 387 | }) |
| 374 | } | 388 | } |
| 375 | 389 | ||
| @@ -771,12 +785,21 @@ func runAccessList(c *Ctx, args []string) int { | |||
| 771 | for _, e := range entries { | 785 | for _, e := range entries { |
| 772 | ds = append(ds, out{e.Username, e.Role, e.Source}) | 786 | ds = append(ds, out{e.Username, e.Role, e.Source}) |
| 773 | } | 787 | } |
| 774 | return c.emit(ds, func(w io.Writer) { | 788 | return c.emitView(ds, func(w io.Writer) { |
| 775 | tb := c.table(w, "USER", "ROLE", "SOURCE") | 789 | tb := c.table(w, "USER", "ROLE", "SOURCE") |
| 776 | for _, d := range ds { | 790 | for _, d := range ds { |
| 777 | tb.row(cRef(d.User), cState(d.Role), cText("via "+d.Source)) | 791 | tb.row(cRef(d.User), cState(d.Role), cText("via "+d.Source)) |
| 778 | } | 792 | } |
| 779 | tb.flush() | 793 | tb.flush() |
| 794 | }, func() screen { | ||
| 795 | rows := make([]row, len(ds)) | ||
| 796 | for i, d := range ds { | ||
| 797 | rows[i] = rowOf(cRef(d.User), cState(d.Role), cMeta("via "+d.Source)) | ||
| 798 | } | ||
| 799 | return listScreen("Access", rows, | ||
| 800 | action{"Access", []string{"repo", "access", "grant", repo.Path(), "<user>", "write"}}, | ||
| 801 | action{"Access", []string{"repo", "access", "revoke", repo.Path(), "<user>"}}, | ||
| 802 | ) | ||
| 780 | }) | 803 | }) |
| 781 | } | 804 | } |
| 782 | 805 | ||
| @@ -960,6 +983,19 @@ func archiveRepo(c *Ctx, repo store.Repo, archived bool) int { | |||
| 960 | return c.emit(s, func(w io.Writer) { fmt.Fprintf(w, "%sd %s\n", verb, repo.Path()) }) | 983 | return c.emit(s, func(w io.Writer) { fmt.Fprintf(w, "%sd %s\n", verb, repo.Path()) }) |
| 961 | } | 984 | } |
| 962 | 985 | ||
| 986 | // topicsScreen is a repository's topics at a terminal, after a read or | ||
| 987 | // an edit. | ||
| 988 | func topicsScreen(repo store.Repo, topics []string) screen { | ||
| 989 | rows := make([]row, len(topics)) | ||
| 990 | for i, t := range topics { | ||
| 991 | rows[i] = rowOf(cRef(t)) | ||
| 992 | } | ||
| 993 | return listScreen("Topics", rows, | ||
| 994 | action{"Edit", []string{"repo", "topics", "add", repo.Path(), "<topic>"}}, | ||
| 995 | action{"Edit", []string{"repo", "topics", "remove", repo.Path(), "<topic>"}}, | ||
| 996 | ) | ||
| 997 | } | ||
| 998 | |||
| 963 | func runTopicsList(c *Ctx, args []string) int { | 999 | func runTopicsList(c *Ctx, args []string) int { |
| 964 | if len(args) != 1 { | 1000 | if len(args) != 1 { |
| 965 | return c.usage() | 1001 | return c.usage() |
| @@ -972,12 +1008,14 @@ func runTopicsList(c *Ctx, args []string) int { | |||
| 972 | if err != nil { | 1008 | if err != nil { |
| 973 | return c.fail(protocol.ExitFailure, "%v", err) | 1009 | return c.fail(protocol.ExitFailure, "%v", err) |
| 974 | } | 1010 | } |
| 975 | return c.emit(topics, func(w io.Writer) { | 1011 | return c.emitView(topics, func(w io.Writer) { |
| 976 | tb := c.table(w, "TOPIC") | 1012 | tb := c.table(w, "TOPIC") |
| 977 | for _, t := range topics { | 1013 | for _, t := range topics { |
| 978 | tb.row(cRef(t)) | 1014 | tb.row(cRef(t)) |
| 979 | } | 1015 | } |
| 980 | tb.flush() | 1016 | tb.flush() |
| 1017 | }, func() screen { | ||
| 1018 | return topicsScreen(repo, topics) | ||
| 981 | }) | 1019 | }) |
| 982 | } | 1020 | } |
| 983 | 1021 | ||
| @@ -1031,12 +1069,14 @@ func editTopics(c *Ctx, args []string, add bool) int { | |||
| 1031 | if err != nil { | 1069 | if err != nil { |
| 1032 | return c.fail(protocol.ExitFailure, "%v", err) | 1070 | return c.fail(protocol.ExitFailure, "%v", err) |
| 1033 | } | 1071 | } |
| 1034 | return c.emit(now, func(w io.Writer) { | 1072 | return c.emitView(now, func(w io.Writer) { |
| 1035 | tb := c.table(w, "TOPIC") | 1073 | tb := c.table(w, "TOPIC") |
| 1036 | for _, t := range now { | 1074 | for _, t := range now { |
| 1037 | tb.row(cRef(t)) | 1075 | tb.row(cRef(t)) |
| 1038 | } | 1076 | } |
| 1039 | tb.flush() | 1077 | tb.flush() |
| 1078 | }, func() screen { | ||
| 1079 | return topicsScreen(repo, now) | ||
| 1040 | }) | 1080 | }) |
| 1041 | } | 1081 | } |
| 1042 | 1082 | ||
| @@ -1079,12 +1119,22 @@ func runRepoSearch(c *Ctx, args []string) int { | |||
| 1079 | } | 1119 | } |
| 1080 | ds = append(ds, out{r.Path(), r.Visibility, desc, topics}) | 1120 | ds = append(ds, out{r.Path(), r.Visibility, desc, topics}) |
| 1081 | } | 1121 | } |
| 1082 | return c.emit(ds, func(w io.Writer) { | 1122 | return c.emitView(ds, func(w io.Writer) { |
| 1083 | tb := c.table(w, "PATH", "VISIBILITY", "DESCRIPTION") | 1123 | tb := c.table(w, "PATH", "VISIBILITY", "DESCRIPTION") |
| 1084 | for _, d := range ds { | 1124 | for _, d := range ds { |
| 1085 | tb.row(cLink(d.Path, c.siteURL(d.Path)), cState(d.Visibility), cFlex(d.Description)) | 1125 | tb.row(cLink(d.Path, c.siteURL(d.Path)), cState(d.Visibility), cFlex(d.Description)) |
| 1086 | } | 1126 | } |
| 1087 | tb.flush() | 1127 | tb.flush() |
| 1128 | }, func() screen { | ||
| 1129 | rows := make([]row, len(ds)) | ||
| 1130 | for i, d := range ds { | ||
| 1131 | rows[i] = rowOf(cLink(d.Path, c.siteURL(d.Path)), cState(d.Visibility), cFlex(d.Description), cMeta(strings.Join(d.Topics, ", "))) | ||
| 1132 | } | ||
| 1133 | s := listScreen(fmt.Sprintf("Repositories matching %q", args[0]), rows) | ||
| 1134 | if len(ds) > 0 { | ||
| 1135 | s.actions = []action{{"Read", []string{"repo", "show", ds[0].Path}}} | ||
| 1136 | } | ||
| 1137 | return s | ||
| 1088 | }) | 1138 | }) |
| 1089 | } | 1139 | } |
| 1090 | 1140 | ||
| @@ -1249,12 +1299,26 @@ func runRepoBookmarks(c *Ctx, args []string) int { | |||
| 1249 | Bookmarks: c.Store.BookmarkCount(r.ID), | 1299 | Bookmarks: c.Store.BookmarkCount(r.ID), |
| 1250 | }) | 1300 | }) |
| 1251 | } | 1301 | } |
| 1252 | return c.emit(out, func(w io.Writer) { | 1302 | return c.emitView(out, func(w io.Writer) { |
| 1253 | tb := c.table(w, "PATH", "COUNT", "DESCRIPTION") | 1303 | tb := c.table(w, "PATH", "COUNT", "DESCRIPTION") |
| 1254 | for _, b := range out { | 1304 | for _, b := range out { |
| 1255 | tb.row(cRef(b.Path), cNum(int64(b.Bookmarks)), cFlex(b.Description)) | 1305 | tb.row(cRef(b.Path), cNum(int64(b.Bookmarks)), cFlex(b.Description)) |
| 1256 | } | 1306 | } |
| 1257 | tb.flush() | 1307 | tb.flush() |
| 1308 | }, func() screen { | ||
| 1309 | rows := make([]row, len(out)) | ||
| 1310 | for i, b := range out { | ||
| 1311 | n := fmt.Sprintf("%d bookmarks", b.Bookmarks) | ||
| 1312 | if b.Bookmarks == 1 { | ||
| 1313 | n = "1 bookmark" | ||
| 1314 | } | ||
| 1315 | rows[i] = rowOf(cLink(b.Path, c.siteURL(b.Path)), cState(b.Visibility), cFlex(b.Description), cMeta(n)) | ||
| 1316 | } | ||
| 1317 | s := listScreen("Bookmarks", rows) | ||
| 1318 | if len(out) > 0 { | ||
| 1319 | s.actions = []action{{"Read", []string{"repo", "show", out[0].Path}}} | ||
| 1320 | } | ||
| 1321 | return s | ||
| 1258 | }) | 1322 | }) |
| 1259 | } | 1323 | } |
| 1260 | 1324 | ||
internal/control/runnerrepo.go +17 −1
| @@ -114,7 +114,7 @@ func runRepoRunnerList(c *Ctx, args []string) int { | |||
| 114 | if runners == nil { | 114 | if runners == nil { |
| 115 | runners = []store.RepoRunner{} | 115 | runners = []store.RepoRunner{} |
| 116 | } | 116 | } |
| 117 | return c.emit(runners, func(w io.Writer) { | 117 | return c.emitView(runners, func(w io.Writer) { |
| 118 | tb := c.table(w, "FINGERPRINT", "ALGO", "USER", "SEEN", "HELD") | 118 | tb := c.table(w, "FINGERPRINT", "ALGO", "USER", "SEEN", "HELD") |
| 119 | for _, r := range runners { | 119 | for _, r := range runners { |
| 120 | seen := r.LastSeen | 120 | seen := r.LastSeen |
| @@ -128,6 +128,22 @@ func runRepoRunnerList(c *Ctx, args []string) int { | |||
| 128 | tb.row(cRef(r.Fingerprint), cText(r.Algo), cText(r.Username), cAge(seen), cText(held)) | 128 | tb.row(cRef(r.Fingerprint), cText(r.Algo), cText(r.Username), cAge(seen), cText(held)) |
| 129 | } | 129 | } |
| 130 | tb.flush() | 130 | tb.flush() |
| 131 | }, func() screen { | ||
| 132 | rows := make([]row, len(runners)) | ||
| 133 | for i, r := range runners { | ||
| 134 | lead, held := cGlyph(""), "" | ||
| 135 | if r.BuildNumber != 0 { | ||
| 136 | lead, held = cGlyph("running"), fmt.Sprintf("building %s #%d %s", r.BuildRepo, r.BuildNumber, r.BuildJob) | ||
| 137 | } | ||
| 138 | seen := "never seen" | ||
| 139 | if r.LastSeen != "" { | ||
| 140 | seen = "seen " + relAge(r.LastSeen, termNow()) | ||
| 141 | } | ||
| 142 | rows[i] = rowOf(cRef(r.Fingerprint), lead, cFlex(r.Username), cMeta(r.Algo, seen, held)) | ||
| 143 | } | ||
| 144 | return listScreen("Runners", rows, | ||
| 145 | action{"Runners", []string{"repo", "runner", "remove", repo.Path(), "<fingerprint>"}}, | ||
| 146 | ) | ||
| 131 | }) | 147 | }) |
| 132 | } | 148 | } |
| 133 | 149 | ||
internal/control/screen.go +6
| @@ -57,6 +57,12 @@ type action struct { | |||
| 57 | argv []string | 57 | argv []string |
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | // listScreen is a list command's terminal screen: one section of rows | ||
| 61 | // and the commands that apply. | ||
| 62 | func listScreen(title string, rows []row, actions ...action) screen { | ||
| 63 | return screen{sections: []section{{title: title, n: len(rows), rows: rows}}, actions: actions} | ||
| 64 | } | ||
| 65 | |||
| 60 | // render draws s: the parts in order, one blank line between parts and | 66 | // render draws s: the parts in order, one blank line between parts and |
| 61 | // between sections. | 67 | // between sections. |
| 62 | func (c *Ctx) render(w io.Writer, s screen) { | 68 | func (c *Ctx) render(w io.Writer, s screen) { |
internal/control/stage3repo_test.go added +149
| @@ -0,0 +1,149 @@ | |||
| 1 | package control | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bytes" | ||
| 5 | "crypto/ed25519" | ||
| 6 | "strings" | ||
| 7 | "testing" | ||
| 8 | |||
| 9 | "gitbay.org/gitbay/internal/protocol" | ||
| 10 | "gitbay.org/gitbay/internal/store" | ||
| 11 | "golang.org/x/crypto/ssh" | ||
| 12 | ) | ||
| 13 | |||
| 14 | // dispatchAs runs argv as u with stdin, piped, and returns stdout; a | ||
| 15 | // non-zero exit fails t. | ||
| 16 | func dispatchAs(t *testing.T, st *store.Store, u store.User, stdin string, argv ...string) string { | ||
| 17 | t.Helper() | ||
| 18 | out, errOut := &bytes.Buffer{}, &bytes.Buffer{} | ||
| 19 | c := &Ctx{User: u, Scope: "full", Store: st, Stdout: out, Stderr: errOut, Stdin: strings.NewReader(stdin)} | ||
| 20 | if code := Dispatch(c, argv); code != protocol.ExitOK { | ||
| 21 | t.Fatalf("%v: exit %d: %s", argv, code, errOut) | ||
| 22 | } | ||
| 23 | return out.String() | ||
| 24 | } | ||
| 25 | |||
| 26 | // atTerminal runs argv as u at a 100-column terminal without colour | ||
| 27 | // and returns stdout; a non-zero exit fails t. | ||
| 28 | func atTerminal(t *testing.T, st *store.Store, u store.User, argv ...string) string { | ||
| 29 | t.Helper() | ||
| 30 | out, errOut := &bytes.Buffer{}, &bytes.Buffer{} | ||
| 31 | c := &Ctx{User: u, Scope: "full", Store: st, Stdout: out, Stderr: errOut, Stdin: strings.NewReader(""), Term: Term{Cols: 100}} | ||
| 32 | if code := Dispatch(c, argv); code != protocol.ExitOK { | ||
| 33 | t.Fatalf("%v: exit %d: %s", argv, code, errOut) | ||
| 34 | } | ||
| 35 | return out.String() | ||
| 36 | } | ||
| 37 | |||
| 38 | // checkLegend fails t for any suggested command in a rendered screen | ||
| 39 | // (a line starting "gitbay ", or after "+n more ") that the registry | ||
| 40 | // would not dispatch with the flags it names. | ||
| 41 | func checkLegend(t *testing.T, out string) { | ||
| 42 | t.Helper() | ||
| 43 | var s screen | ||
| 44 | for _, line := range strings.Split(out, "\n") { | ||
| 45 | if i := strings.Index(line, "more gitbay "); i >= 0 { | ||
| 46 | line = line[i+len("more "):] | ||
| 47 | } | ||
| 48 | for _, cmd := range strings.Split(line, " ") { | ||
| 49 | cmd = strings.TrimSpace(cmd) | ||
| 50 | if !strings.HasPrefix(cmd, "gitbay ") { | ||
| 51 | continue | ||
| 52 | } | ||
| 53 | argv, err := protocol.Tokenize(strings.TrimPrefix(cmd, "gitbay ")) | ||
| 54 | if err != nil { | ||
| 55 | t.Errorf("%q: %v", cmd, err) | ||
| 56 | continue | ||
| 57 | } | ||
| 58 | s.actions = append(s.actions, action{"", argv}) | ||
| 59 | } | ||
| 60 | } | ||
| 61 | if len(s.actions) == 0 { | ||
| 62 | t.Errorf("no suggested commands in:\n%s", out) | ||
| 63 | } | ||
| 64 | checkActions(t, s) | ||
| 65 | } | ||
| 66 | |||
| 67 | // newPubKey is an ed25519 public key in authorized_keys form, the same | ||
| 68 | // on every run so its fingerprint can be pinned. | ||
| 69 | func newPubKey(t *testing.T) string { | ||
| 70 | t.Helper() | ||
| 71 | pub := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{7}, ed25519.SeedSize)).Public().(ed25519.PublicKey) | ||
| 72 | sp, err := ssh.NewPublicKey(pub) | ||
| 73 | if err != nil { | ||
| 74 | t.Fatal(err) | ||
| 75 | } | ||
| 76 | return string(ssh.MarshalAuthorizedKey(sp)) | ||
| 77 | } | ||
| 78 | |||
| 79 | // repoListsFixture is a repository with something in every list task 1 | ||
| 80 | // migrates. | ||
| 81 | func repoListsFixture(t *testing.T) (*store.Store, store.Repo, store.User) { | ||
| 82 | t.Helper() | ||
| 83 | st, repo, uid := newQueueTestRepo(t) | ||
| 84 | owner := store.User{ID: uid, Username: "alice", IsAdmin: true} | ||
| 85 | p := repo.Path() | ||
| 86 | dispatchAs(t, st, owner, "", "repo", "topics", "add", p, "cli", "forge") | ||
| 87 | dispatchAs(t, st, owner, "s3cret\n", "repo", "secret", "set", p, "DEPLOY_TOKEN") | ||
| 88 | dispatchAs(t, st, owner, testRunnerPub, "repo", "deploy-key", "add", p) | ||
| 89 | dispatchAs(t, st, owner, "", "repo", "domain", "add", p, "docs.example.test") | ||
| 90 | dispatchAs(t, st, owner, "", "repo", "bookmark", p) | ||
| 91 | // mirror add resolves the host, so the fixture goes to the store. | ||
| 92 | if _, err := st.AddMirror(repo.ID, "push", "https://mirror.example.test/app.git", "", "tok"); err != nil { | ||
| 93 | t.Fatal(err) | ||
| 94 | } | ||
| 95 | dispatchAs(t, st, owner, newPubKey(t), "repo", "runner", "add", p) | ||
| 96 | return st, repo, owner | ||
| 97 | } | ||
| 98 | |||
| 99 | func TestRepoListsPlainPinned(t *testing.T) { | ||
| 100 | st, repo, owner := repoListsFixture(t) | ||
| 101 | p := repo.Path() | ||
| 102 | for name, argv := range map[string][]string{ | ||
| 103 | "repo-topics": {"repo", "topics", p}, | ||
| 104 | "repo-secret-list": {"repo", "secret", "list", p}, | ||
| 105 | "repo-deploy-key-list": {"repo", "deploy-key", "list", p}, | ||
| 106 | "repo-domain-list": {"repo", "domain", "list", p}, | ||
| 107 | "repo-access-list": {"repo", "access", "list", p}, | ||
| 108 | "repo-bookmarks": {"repo", "bookmarks"}, | ||
| 109 | "repo-search": {"repo", "search", "app"}, | ||
| 110 | "repo-list": {"repo", "list"}, | ||
| 111 | "repo-mirror-list": {"repo", "mirror", "list", p}, | ||
| 112 | "repo-runner-list": {"repo", "runner", "list", p}, | ||
| 113 | } { | ||
| 114 | pinPlain(t, name, dispatchAs(t, st, owner, "", argv...)) | ||
| 115 | } | ||
| 116 | pinPlain(t, "repo-topics-add", dispatchAs(t, st, owner, "", "repo", "topics", "add", p, "git")) | ||
| 117 | } | ||
| 118 | |||
| 119 | func TestRepoListScreens(t *testing.T) { | ||
| 120 | st, repo, owner := repoListsFixture(t) | ||
| 121 | p := repo.Path() | ||
| 122 | for _, tc := range []struct { | ||
| 123 | argv []string | ||
| 124 | want []string | ||
| 125 | }{ | ||
| 126 | {[]string{"repo", "topics", p}, []string{"Topics (2)\ncli\nforge\n"}}, | ||
| 127 | {[]string{"repo", "topics", "add", p, "git"}, []string{"Topics (3)\n"}}, | ||
| 128 | {[]string{"repo", "secret", "list", p}, []string{"Build secrets (1)\nDEPLOY_TOKEN\n"}}, | ||
| 129 | {[]string{"repo", "deploy-key", "list", p}, []string{"Deploy keys (1)\n", " ro runner@test ssh-ed25519 · used never"}}, | ||
| 130 | {[]string{"repo", "domain", "list", p}, []string{"Pages domains (1)\n", "◐ docs.example.test pending"}}, | ||
| 131 | {[]string{"repo", "access", "list", p}, []string{"Access (1)\nalice admin via owner\n"}}, | ||
| 132 | {[]string{"repo", "mirror", "list", p}, []string{"Mirrors (1)\n1 ◐ https://mirror.example.test/app.git push · never synced"}}, | ||
| 133 | {[]string{"repo", "runner", "list", p}, []string{"Runners (1)\n", " alice ssh-ed25519 · never seen"}}, | ||
| 134 | {[]string{"repo", "bookmarks"}, []string{"Bookmarks (1)\nalice/app public 1 bookmark\n"}}, | ||
| 135 | {[]string{"repo", "search", "app"}, []string{"Repositories matching \"app\" (1)\nalice/app public cli, forge"}}, | ||
| 136 | {[]string{"repo", "list"}, []string{"Repositories (1)\nalice/app public\n"}}, | ||
| 137 | } { | ||
| 138 | out := atTerminal(t, st, owner, tc.argv...) | ||
| 139 | for _, w := range tc.want { | ||
| 140 | if !strings.Contains(out, w) { | ||
| 141 | t.Errorf("%v: missing %q in:\n%s", tc.argv, w, out) | ||
| 142 | } | ||
| 143 | } | ||
| 144 | if strings.Contains(out, "s3cret") || strings.Contains(out, "tok\n") { | ||
| 145 | t.Errorf("%v: secret on screen:\n%s", tc.argv, out) | ||
| 146 | } | ||
| 147 | checkLegend(t, out) | ||
| 148 | } | ||
| 149 | } | ||
internal/control/testdata/plain/repo-access-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | alice admin via owner | ||
internal/control/testdata/plain/repo-bookmarks.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | alice/app 1 | ||
internal/control/testdata/plain/repo-deploy-key-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | SHA256:re738AN1/nwapcQTqXzvkuuJNj+U6StCED66ZUJNCdE ssh-ed25519 ro runner@test never never | ||
internal/control/testdata/plain/repo-domain-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | docs.example.test pending | ||
internal/control/testdata/plain/repo-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | alice/app public | ||
internal/control/testdata/plain/repo-mirror-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | 1 push https://mirror.example.test/app.git last - pending | ||
internal/control/testdata/plain/repo-runner-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | SHA256:z/fSv0Z0RZS+Lccbc6ZoOWwt/fbj1VFJGSBKQbo4icE ssh-ed25519 alice never idle | ||
internal/control/testdata/plain/repo-search.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | alice/app public | ||
internal/control/testdata/plain/repo-secret-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | DEPLOY_TOKEN | ||
internal/control/testdata/plain/repo-topics-add.txt added +3
| @@ -0,0 +1,3 @@ | |||
| 1 | cli | ||
| 2 | forge | ||
| 3 | git | ||
internal/control/testdata/plain/repo-topics.txt added +2
| @@ -0,0 +1,2 @@ | |||
| 1 | cli | ||
| 2 | forge | ||