internal/control/thread.go
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}