This repository has been archived by the owner on May 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
main.go
211 lines (180 loc) · 5.31 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package main
import (
"bytes"
"flag"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/bluele/slack"
"github.com/dustin/go-humanize"
)
const (
// BANNER is what is printed for help/info output
BANNER = "purr - %s\n"
// VERSION is the binary version.
VERSION = "v0.4.0"
)
var (
configFile string
debug bool
cliOutput bool
)
func main() {
flag.StringVar(&configFile, "config", "", "Read config from FILE")
flag.BoolVar(&debug, "d", false, "run in debug mode")
flag.BoolVar(&cliOutput, "o", false, "output to CLI rather than slack")
flag.Usage = func() {
fmt.Fprint(os.Stderr, fmt.Sprintf(BANNER, VERSION))
flag.PrintDefaults()
configHelp()
}
flag.Parse()
logger := NewStdOutLogger(debug)
conf, err := newConfig(configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
if errors := conf.validate(); len(errors) > 0 {
buf := &bytes.Buffer{}
for i := range errors {
fmt.Fprintln(buf, errors[i].Error())
}
usageAndExit(buf.String(), 1)
}
// these function will return channels that will emit a list of pull requests
// on channels and close the channel when they are done
gitHubPRs := trawlGitHub(conf, logger)
gitLabPRs := trawlGitLab(conf, logger)
// Merge the in channels into of channel and close it when the inputs are done
prs := merge(gitHubPRs, gitLabPRs)
// filter out pull requests that we don't want to send
filteredPRs := filter(conf.Filters, prs, logger)
// format takes a channel of pull requests and returns a message that groups
// pull request into repos and formats them into a slack friendly format
message := format(conf.Filters, filteredPRs)
if message.String() == "" {
logger.Debugf("No PRs found\n")
} else if cliOutput {
fmt.Print(message)
} else {
err := postToSlack(conf, message)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not send to slack: %v\n", err)
os.Exit(1)
}
}
}
// merge merges several channels into one output channel (fan-in)
func merge(channels ...<-chan *PullRequest) <-chan *PullRequest {
out := make(chan *PullRequest)
var wg sync.WaitGroup
wg.Add(len(channels))
// merges all in channels into an out channel
for _, c := range channels {
go func(prs <-chan *PullRequest) {
for pr := range prs {
out <- pr
}
wg.Done()
}(c)
}
// we have to wait until all input channels are closed before closing the out channel
go func() {
wg.Wait()
close(out)
}()
return out
}
// filter removes pull requests that should not show up in the final message, this could
// include PRs marked as Work in Progress or where users are not in the whitelist
func filter(filters *Filters, in <-chan *PullRequest, log Logger) chan *PullRequest {
out := make(chan *PullRequest)
go func() {
for pr := range in {
if filters.Filter(pr) {
out <- pr
} else {
log.Debugf("filtered PR '%s' (%s) \n", pr.Title, pr.WebLink)
}
}
close(out)
}()
return out
}
// format converts all pull requests into a message that is grouped by repo formatted for slack
func format(filters *Filters, prs <-chan *PullRequest) fmt.Stringer {
var numPRs int
var oldest *PullRequest
repositories := make(map[string][]*PullRequest)
lastUpdated := time.Now()
// loop through all PRs, will stop when the channel is closed
for pr := range prs {
// update the oldest pull request
if pr.Updated.Before(lastUpdated) {
oldest = pr
lastUpdated = pr.Updated
}
// update the total count of PRs
numPRs++
// group PRs with their repository
if _, ok := repositories[pr.Repository]; !ok {
repositories[pr.Repository] = make([]*PullRequest, 0)
}
repositories[pr.Repository] = append(repositories[pr.Repository], pr)
}
// create the slack message in "slack" format
buf := &bytes.Buffer{}
for repo, prs := range repositories {
fmt.Fprintf(buf, "*%s*\n", repo)
for i := range prs {
fmt.Fprintf(buf, "%s\n", prs[i])
}
fmt.Fprint(buf, "\n")
}
// summary
if numPRs > 0 {
fmt.Fprintf(buf, "\nThere are currently %d open pull requests", numPRs)
fmt.Fprintf(buf, " and the oldest (<%s|PR #%d>) was updated %s\n", oldest.WebLink, oldest.ID, humanize.Time(oldest.Updated))
}
fmt.Fprintf(buf, "%d pull request(s) filtered from these results\n", filters.NumFiltered())
return buf
}
// postToSlack will post the message to Slack. It will divide the message into smaller message if
// it's more than 30 lines long due to a max message size limitation enforced by the Slack API
func postToSlack(conf *Config, message fmt.Stringer) error {
const maxLines = 30
client := slack.New(conf.SlackToken)
opt := &slack.ChatPostMessageOpt{
AsUser: false,
Username: "purr",
IconEmoji: ":purr:",
}
// Don't send too large messages, send a new message per maxLines new lines
lines := strings.Split(message.String(), "\n")
lineBuffer := make([]string, maxLines)
for i := range lines {
lineBuffer = append(lineBuffer, lines[i])
if len(lineBuffer) == cap(lineBuffer) || i+1 == len(lines) {
msg := strings.Join(lineBuffer, "\n")
if msg != "" {
if err := client.ChatPostMessage(conf.SlackChannel, msg, opt); err != nil {
return err
}
}
lineBuffer = make([]string, cap(lineBuffer))
}
}
return nil
}
func usageAndExit(message string, exitCode int) {
if message != "" {
fmt.Fprintf(os.Stderr, message)
fmt.Fprintf(os.Stderr, "\n")
}
flag.Usage()
fmt.Fprintf(os.Stderr, "\n")
os.Exit(exitCode)
}