internal/control/thread.go

3c0c20da787d91bcad19aec3b5e1b5c4ac5a4745
gitbay/internal/control/thread.go history · blame · raw

119 lines · 4095 bytes

  1package control
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strconv"
  7	"strings"
  8
  9	"gitbay.org/gitbay/internal/policy"
 10	"gitbay.org/gitbay/internal/protocol"
 11	"gitbay.org/gitbay/internal/store"
 12)
 13
 14// Issues and merge requests share their shape: a numbered thread in a
 15// repository with an author, comments and participants. What used to be
 16// two copies of the same code, one per noun, lives here once (#110).
 17
 18// refArgs resolves "<owner/name> <n>" to the repository and the number.
 19func refArgs(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool, noun string) (store.Repo, int64, int) {
 20	if len(args) < 2 {
 21		return store.Repo{}, 0, c.fail(protocol.ExitUsage, "expected <owner/name> <number>")
 22	}
 23	repo, code := resolveRepo(c, args[0], perm)
 24	if code >= 0 {
 25		return repo, 0, code
 26	}
 27	n, err := strconv.ParseInt(args[1], 10, 64)
 28	if err != nil {
 29		return repo, 0, c.fail(protocol.ExitUsage, "bad %s number %q", noun, args[1])
 30	}
 31	return repo, n, -1
 32}
 33
 34// authorOrWrite admits the thread's author and anyone with write access,
 35// which is who may change what a thread says or whether it is open.
 36// -1 means proceed; what names the action in the refusal.
 37func authorOrWrite(c *Ctx, repo store.Repo, author, what string) int {
 38	if author == c.User.Username {
 39		return -1
 40	}
 41	grant, err := c.Store.AccessRole(repo.ID, c.User.ID)
 42	if err != nil {
 43		return c.fail(protocol.ExitFailure, "%v", err)
 44	}
 45	if !policy.CanWrite(c.User, repo, grant) {
 46		return c.fail(protocol.ExitDenied, "only the author or users with write access can %s", what)
 47	}
 48	return -1
 49}
 50
 51// commentOut is one comment as show emits it, for both nouns.
 52type commentOut struct {
 53	Author     string `json:"author"`
 54	Body       string `json:"body"`
 55	BodyFormat string `json:"body_format,omitempty"`
 56	CreatedAt  string `json:"created_at"`
 57}
 58
 59// thread is what a comment command needs to know about its noun.
 60type thread struct {
 61	symbol  string // "#" or "!"
 62	segment string // "issues" or "mrs"
 63	event   string // issue.commented or mr.commented
 64	kind    string // the noun an inbox row is filed under
 65}
 66
 67var (
 68	issueThread = thread{"#", "issues", "issue.commented", "issue"}
 69	mrThread    = thread{"!", "mrs", "mr.commented", "mr"}
 70)
 71
 72// runComment is issue comment and mr comment: the noun's resolver hands
 73// back the thread's id, number and title, and the rest is the same.
 74func runComment(c *Ctx, args []string, t thread, noun string,
 75	resolve func(rest []string) (repo store.Repo, id, number int64, title string, code int),
 76	add func(id, userID int64, body, format string) error,
 77	participants func(id int64) ([]int64, error),
 78) int {
 79	f, err := parseFlags(args, flagSpec{Values: []string{"--message", "--file", "--format"}, MaxPos: -1,
 80		Usage: noun + " comment <owner/name> <n> [--message <m> | --file -] [--format md|org]"})
 81	if err != nil {
 82		return c.fail(protocol.ExitUsage, "%v", err)
 83	}
 84	fmtName, err := markupFormat(f.Value("--format"))
 85	if err != nil {
 86		return c.failErr(err)
 87	}
 88	if fmtName == "" {
 89		fmtName = "md"
 90	}
 91	repo, id, number, title, code := resolve(f.Pos)
 92	if code >= 0 {
 93		return code
 94	}
 95	if code := refuseArchived(c, repo); code >= 0 {
 96		return code
 97	}
 98	body, err := bodyFrom(c, f.Value("--message"), f.Value("--file"))
 99	if err != nil {
100		return c.failErr(err)
101	}
102	if strings.TrimSpace(body) == "" {
103		return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -")
104	}
105	if err := add(id, c.User.ID, body, fmtName); err != nil {
106		return c.fail(protocol.ExitFailure, "%v", err)
107	}
108	c.Store.RecordEvent(repo.ID, c.User.ID, t.event, fmt.Sprintf(`{"number":%d}`, number))
109	if parts, err := participants(id); err == nil {
110		notify(c, parts, notice{repo: repo, kind: t.kind,
111			subject: fmt.Sprintf("[%s] %s%d: %s", repo.Path(), t.symbol, number, title),
112			action:  fmt.Sprintf("commented on %s%d", t.symbol, number),
113			excerpt: body, path: fmt.Sprintf("%s/%s/%d", repo.Path(), t.segment, number)})
114	}
115	notifyMentions(c, repo, t, id, number, title, body)
116	return c.emit(Created{Number: number}, func(w io.Writer) {
117		fmt.Fprintf(w, "commented on %s%s%d\n", repo.Path(), t.symbol, number)
118	})
119}