internal/control/thread.go
119 lines · 4099 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.failInput(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.failInput(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}