internal/control/thread.go
122 lines · 4312 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 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}