internal/control/thread.go

9df917e73a67d15adecc3f45976690f6fcd4e47a
gitbay/internal/control/thread.go history · blame · raw

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