internal/control/status.go

70dc0648f931f6f7112c6b71b0be485eeae4077f
gitbay/internal/control/status.go history · blame · raw

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}