internal/control/thread.go
116 lines · 3939 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}
65
66var (
67 issueThread = thread{"#", "issues", "issue.commented"}
68 mrThread = thread{"!", "mrs", "mr.commented"}
69)
70
71// runComment is issue comment and mr comment: the noun's resolver hands
72// back the thread's id, number and title, and the rest is the same.
73func runComment(c *Ctx, args []string, t thread, noun string,
74 resolve func(rest []string) (repo store.Repo, id, number int64, title string, code int),
75 add func(id, userID int64, body, format string) error,
76 participants func(id int64) ([]int64, error),
77) int {
78 f, err := parseFlags(args, flagSpec{Values: []string{"--message", "--file", "--format"}, MaxPos: -1,
79 Usage: noun + " comment <owner/name> <n> [--message <m> | --file -] [--format md|org]"})
80 if err != nil {
81 return c.fail(protocol.ExitUsage, "%v", err)
82 }
83 fmtName, err := markupFormat(f.Value("--format"))
84 if err != nil {
85 return c.failErr(err)
86 }
87 if fmtName == "" {
88 fmtName = "md"
89 }
90 repo, id, number, title, code := resolve(f.Pos)
91 if code >= 0 {
92 return code
93 }
94 if code := refuseArchived(c, repo); code >= 0 {
95 return code
96 }
97 body, err := bodyFrom(c, f.Value("--message"), f.Value("--file"))
98 if err != nil {
99 return c.failErr(err)
100 }
101 if strings.TrimSpace(body) == "" {
102 return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -")
103 }
104 if err := add(id, c.User.ID, body, fmtName); err != nil {
105 return c.fail(protocol.ExitFailure, "%v", err)
106 }
107 c.Store.RecordEvent(repo.ID, c.User.ID, t.event, fmt.Sprintf(`{"number":%d}`, number))
108 if parts, err := participants(id); err == nil {
109 subject := fmt.Sprintf("[%s] %s%d: %s", repo.Path(), t.symbol, number, title)
110 notifyUsers(c, parts, subject,
111 notifyBody(c, fmt.Sprintf("commented on %s%d", t.symbol, number), body, fmt.Sprintf("%s/%s/%d", repo.Path(), t.segment, number)))
112 }
113 return c.emit(Created{Number: number}, func(w io.Writer) {
114 fmt.Fprintf(w, "commented on %s%s%d\n", repo.Path(), t.symbol, number)
115 })
116}