internal/control/thread.go

122 lines · 4312 bytes

7 symbols in this file
  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.usageWith("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	ID         int64         `json:"id"`
 54	Author     string        `json:"author"`
 55	Body       string        `json:"body"`
 56	BodyFormat string        `json:"body_format,omitempty"`
 57	CreatedAt  string        `json:"created_at"`
 58	Kind       string        `json:"-"` // "comment" or "system"; not part of the wire shape
 59	Reactions  []ReactionOut `json:"reactions,omitempty"`
 60}
 61
 62// thread is what a comment command needs to know about its noun.
 63type thread struct {
 64	symbol  string // "#" or "!"
 65	segment string // "issues" or "mrs"
 66	event   string // issue.commented or mr.commented
 67	kind    string // the noun an inbox row is filed under
 68}
 69
 70var (
 71	issueThread = thread{"#", "issues", "issue.commented", "issue"}
 72	mrThread    = thread{"!", "mrs", "mr.commented", "mr"}
 73)
 74
 75// runComment is issue comment and mr comment: the noun's resolver hands
 76// back the thread's id, number and title, and the rest is the same.
 77func runComment(c *Ctx, args []string, t thread, noun string,
 78	resolve func(rest []string) (repo store.Repo, id, number int64, title string, code int),
 79	add func(id, userID int64, body, format string) error,
 80	participants func(id int64) ([]int64, error),
 81) int {
 82	f, err := c.parseArgs(args, flagSpec{Values: []string{"--message", "--file", "--format"}, MaxPos: -1,
 83		Usage: noun + " comment <owner/name> <n> [--message <m> | --file -] [--format md|org]"})
 84	if err != nil {
 85		return c.fail(protocol.ExitUsage, "%v", err)
 86	}
 87	fmtName, err := markupFormat(f.Value("--format"))
 88	if err != nil {
 89		return c.failInput(err)
 90	}
 91	if fmtName == "" {
 92		fmtName = "md"
 93	}
 94	repo, id, number, title, code := resolve(f.Pos)
 95	if code >= 0 {
 96		return code
 97	}
 98	if code := refuseArchived(c, repo); code >= 0 {
 99		return code
100	}
101	body, err := bodyFrom(c, f.Value("--message"), f.Value("--file"))
102	if err != nil {
103		return c.failInput(err)
104	}
105	if strings.TrimSpace(body) == "" {
106		return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -")
107	}
108	if err := add(id, c.User.ID, body, fmtName); err != nil {
109		return c.fail(protocol.ExitFailure, "%v", err)
110	}
111	c.Store.RecordEvent(repo.ID, c.User.ID, t.event, fmt.Sprintf(`{"number":%d}`, number))
112	if parts, err := participants(id); err == nil {
113		notify(c, parts, notice{repo: repo, kind: t.kind, number: number,
114			subject: fmt.Sprintf("[%s] %s%d: %s", repo.Path(), t.symbol, number, title),
115			action:  fmt.Sprintf("commented on %s%d", t.symbol, number),
116			excerpt: body, path: fmt.Sprintf("%s/%s/%d", repo.Path(), t.segment, number)})
117	}
118	notifyMentions(c, repo, t, id, number, title, body)
119	return c.emit(Created{Number: number}, func(w io.Writer) {
120		fmt.Fprintf(w, "commented on %s%s%d\n", repo.Path(), t.symbol, number)
121	})
122}