internal/control/status.go
139 lines · 4308 bytes
1package control
2
3import (
4 "fmt"
5 "io"
6 "strings"
7
8 "gitbay.org/gitbay/internal/gitutil"
9 "gitbay.org/gitbay/internal/policy"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14func init() {
15 register(Command{Path: []string{"status", "set"},
16 Summary: "report a commit status (CI)",
17 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]",
18 Run: runStatusSet})
19 register(Command{Path: []string{"status", "list"},
20 Summary: "statuses on a commit",
21 Usage: "status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList})
22}
23
24var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true}
25
26func runStatusSet(c *Ctx, args []string) int {
27 var path, sha, context, state, description, url string
28 rest := args
29 for i := 0; i < len(rest); i++ {
30 switch rest[i] {
31 case "--context", "--state", "--description", "--url":
32 if i+1 >= len(rest) {
33 return c.fail(protocol.ExitUsage, "%s requires a value", rest[i])
34 }
35 v := rest[i+1]
36 switch rest[i] {
37 case "--context":
38 context = v
39 case "--state":
40 state = v
41 case "--description":
42 description = v
43 case "--url":
44 url = v
45 }
46 i++
47 default:
48 if path == "" {
49 path = rest[i]
50 } else if sha == "" {
51 sha = rest[i]
52 } else {
53 return c.fail(protocol.ExitUsage, "unexpected argument %q", rest[i])
54 }
55 }
56 }
57 if path == "" || sha == "" || context == "" || !validStatusState[state] {
58 return c.fail(protocol.ExitUsage, "usage: status set <owner/name> <sha> --context <c> --state pending|success|failure|error")
59 }
60 if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
61 return c.fail(protocol.ExitUsage, "--url must be http(s)")
62 }
63 // Reporting a status is a write: CI identities need write access (an
64 // API token with full scope, or an account grant).
65 repo, code := resolveRepo(c, path, policy.CanWrite)
66 if code >= 0 {
67 return code
68 }
69 if code := refuseArchived(c, repo); code >= 0 {
70 return code
71 }
72 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
73 full, err := gitutil.ResolveRef(dir, sha)
74 if err != nil {
75 return c.fail(protocol.ExitNotFound, "no commit %s in %s", sha, repo.Path())
76 }
77 if err := c.Store.SetCommitStatus(repo.ID, full, context, state, description, url, c.User.ID); err != nil {
78 return c.fail(protocol.ExitFailure, "%v", err)
79 }
80 c.Store.RecordEvent(repo.ID, c.User.ID, "status",
81 fmt.Sprintf(`{"sha":%q,"context":%q,"state":%q}`, full, context, state))
82 return c.emit(map[string]string{"sha": full, "context": context, "state": state}, func(w io.Writer) {
83 fmt.Fprintf(w, "%s on %.10s: %s\n", context, full, state)
84 })
85}
86
87func runStatusList(c *Ctx, args []string) int {
88 if len(args) != 2 {
89 return c.fail(protocol.ExitUsage, "usage: status list <owner/name> <sha>")
90 }
91 repo, code := resolveRepo(c, args[0], policy.CanRead)
92 if code >= 0 {
93 return code
94 }
95 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
96 full, err := gitutil.ResolveRef(dir, args[1])
97 if err != nil {
98 return c.fail(protocol.ExitNotFound, "no commit %s in %s", args[1], repo.Path())
99 }
100 statuses, err := c.Store.ListCommitStatuses(repo.ID, full)
101 if err != nil {
102 return c.fail(protocol.ExitFailure, "%v", err)
103 }
104 type out struct {
105 Context string `json:"context"`
106 State string `json:"state"`
107 Description string `json:"description,omitempty"`
108 URL string `json:"url,omitempty"`
109 Creator string `json:"creator,omitempty"`
110 }
111 var ds []out
112 for _, s := range statuses {
113 ds = append(ds, out{s.Context, s.State, s.Description, s.TargetURL, s.Creator})
114 }
115 d := struct {
116 SHA string `json:"sha"`
117 Combined string `json:"combined"`
118 Statuses []out `json:"statuses"`
119 }{full, combinedOf(statuses), ds}
120 return c.emit(d, func(w io.Writer) {
121 fmt.Fprintf(w, "%.10s: %s\n", d.SHA, orNone(d.Combined))
122 for _, x := range ds {
123 extra := ""
124 if x.Description != "" {
125 extra = "\t" + x.Description
126 }
127 fmt.Fprintf(w, " %s\t%s%s\n", x.Context, x.State, extra)
128 }
129 })
130}
131
132func combinedOf(statuses []store.CommitStatus) string { return store.CombinedStatus(statuses) }
133
134func orNone(s string) string {
135 if s == "" {
136 return "no statuses"
137 }
138 return s
139}