-
Notifications
You must be signed in to change notification settings - Fork 162
/
github.py
338 lines (270 loc) · 9.69 KB
/
github.py
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
"""!hub [-r <repository>] <command> <arguments> is a command line interface to github
For all commands, hub will assume that you are using the channel's default
repository unless you specify it explicitly with -r.
To set a channel's default repository, use `!hub setdefault <repo>`
To view a channel's default repository, use `!hub getdefault`
Commands:
* `issues`: display the 5 most recent open issues in the repository
ex: `!hub issues`
* `issue`: display a particular issue
ex: `!hub issue 6`
* `create`: create an issue with the title given by <arguments>
ex: `!hub create Title of a bug I found`
* `search`: search the issues for a repository
ex: `!hub search bot` will return the first 5 issues containing "bot" in
the default repository
* `pulls`: display the 5 most recent open pull requests
ex: `!hub pulls`
* `pull`: display a particular pull request
ex: `!hub pull 55`
"""
import argparse
import json
import os
import re
import requests
HUB_URL = 'https://api.github.com/{0}'
class Github(object):
def __init__(self, username, password):
self.auth = username, password
def _get(self, url_fragment, **params):
return requests.get(
HUB_URL.format(url_fragment),
auth=self.auth,
params=params
)
def _post(self, url_fragment, data={}, **params):
return requests.post(
HUB_URL.format(url_fragment),
auth=self.auth,
data=data,
params=params
)
def issues(self, repo):
# defaults to only open issues
res = self._get('repos/{0}/issues'.format(repo))
if res.status_code == 200:
return res.json()
def issue(self, repo, n):
res = self._get('repos/{0}/issues/{1}'.format(repo, n))
if res.status_code == 200:
return res.json()
def create_issue(self, repo, title, body=''):
res = self._post(
'repos/{0}/issues'.format(repo),
data=json.dumps({
"title": title,
"body": body}))
if res.status_code == 201:
return res.json()
def search_issue_in_repo(self, repo, query):
return self._get(
'search/issues',
q="{0} repo:{1}".format(query, repo)).json()
def pull_requests(self, repo):
res = self._get('repos/{0}/pulls'.format(repo))
if res.status_code == 200:
return res.json()
def pull(self, repo, n):
res = self._get('repos/{0}/pulls/{1}'.format(repo, n))
if res.status_code == 200:
return res.json()
def get_all_repos(self):
repos = self._get('user/repos', per_page=100)
repo_names = [repo["full_name"] for repo in repos.json()]
last = re.findall(r'rel="last"', repos.headers['link'])
page = 2
while last:
repos = self._get('user/repos', per_page=100, page=page)
repo_names += [repo["full_name"] for repo in repos.json()]
last = re.findall(r'rel="last"', repos.headers['link'])
page += 1
return repo_names
# create an authed github object
HUB = Github(os.environ.get("GITHUB_USER"), os.environ.get("GITHUB_PASS"))
# Gather all repo names available to the authed user. Eventually this will
# need to be refreshed; but for now just assume this is good enough.
# ALL_REPOS = HUB.get_all_repos()
def format_issue(issue_json, verbose=False):
d = {
"author_icon": issue_json["user"]["avatar_url"],
"author_name": issue_json["user"]["login"],
"author_link": issue_json["user"]["html_url"],
"fallback": issue_json["title"],
"title": "[{0}] {1}".format(issue_json["number"], issue_json["title"]),
"title_link": issue_json["html_url"],
"color": "good"
}
if verbose:
d["text"] = issue_json["body"]
return d
def format_pull(pull_json):
pull_json["s"] = "s" if pull_json["commits"] > 1 else ""
text = "{body}\n> _*{commits}* commit{s} *{additions}* ++ " \
"*{deletions}* -- *{changed_files}* changed_".format(
**pull_json)
d = {
"author_icon": pull_json["user"]["avatar_url"],
"author_name": pull_json["user"]["login"],
"author_link": pull_json["user"]["html_url"],
"fallback": pull_json["title"],
"title": "[{0}] {1}".format(pull_json["number"], pull_json["title"]),
"title_link": pull_json["html_url"],
"color": "good",
"mrkdwn_in": ["text"],
"text": text,
}
return d
def get_default_repo(server, room):
rows = server.query('''
SELECT repo FROM github_room_repo_defaults WHERE room=?''', room)
if rows:
return rows[0][0]
return None
def set_default_repo(server, room, repo):
server.query('''
INSERT INTO github_room_repo_defaults(room, repo)
VALUES (?, ?)''', room, repo)
def issues(repo, _):
issues = HUB.issues(repo)
if issues is None:
return "Unable to find repository {0}".format(repo)
l = len(issues)
if l == 0:
return "0 open issues on repository {0}".format(repo)
elif l > 5:
text = "{0} open issues, showing the 5 most recent".format(l)
else:
text = "{0} open issues".format(l)
attachments = json.dumps([format_issue(i) for i in issues[:5]])
return {
"attachments": attachments,
"text": text,
}
def pulls(repo, _):
pulls = HUB.pull_requests(repo)
if pulls is None:
return "Unable to find repository {0}".format(repo)
l = len(pulls)
if l == 0:
return "0 open pull requests on repository {0}".format(repo)
elif l > 5:
text = "{0} open pull requests, showing the 5 most recent".format(l)
else:
text = "{0} open pull requests".format(l)
attachments = json.dumps([format_issue(p) for p in pulls[:5]])
return {
"attachments": attachments,
"text": text,
}
def issue(repo, body):
n = body[0]
issue = HUB.issue(repo, n)
if not issue:
return "Unable to find issue #{0} in repo {1}".format(n, repo)
return {
"attachments": json.dumps([format_issue(issue, verbose=True)]),
"text": "",
}
def pull_request(repo, body):
n = body[0]
pull = HUB.pull(repo, n)
if not issue:
return "Unable to find pull request #{0} in repo {1}".format(n, repo)
return {
"attachments": json.dumps([format_pull(pull)]),
"text": "",
}
def create_issue(repo, body):
title = ' '.join(body)
issue = HUB.create_issue(repo, title)
if not issue:
return "Unable to create issue in repo {0}".format(repo)
attachment = json.dumps([format_issue(issue)])
return {
"attachments": attachment,
"text": "",
}
def search(repo, body):
query = ' '.join(body)
response = HUB.search_issue_in_repo(repo, query)
if response["total_count"] == 0:
return {
"text": "sorry, no issues found"
}
issues = response["items"]
attachments = json.dumps([format_issue(i) for i in issues[:5]])
text = "Found {0} items".format(response["total_count"])
return {
"attachments": attachments,
"text": text
}
def getdefault(repo, _):
return "Default repo for this room is `{0}`. " \
"To change it, run `!hub setdefault <repo_name>`".format(repo)
COMMANDS = {
"issues": issues,
"pulls": pulls,
"issue": issue,
"pull": pull_request,
"create": create_issue,
"search": search,
"getdefault": getdefault
}
def github(server, room, cmd, body, repo):
# If repo wasn't passed in explicitly, grab it from the database
if not repo:
repo = get_default_repo(server, room)
# If we still couldn't find one in the database, either it's a command to
# set it or we can instrcut the user how to do so
if not repo:
if cmd == "setdefault":
set_default_repo(server, room, body[0])
return "Default repo for this room set to `{0}`".format(body[0])
else:
return "Unable to find default repo for this channel. "\
"Run `!hub setdefault <repo_name>`"
try:
command_func = COMMANDS[cmd]
return command_func(repo, body)
except KeyError:
return
# Only run create_database on this module's first execution
FIRST=True
def create_database(server):
server.query('''
CREATE TABLE IF NOT EXISTS github_room_repo_defaults
(room text, repo text)''')
FIRST=False
ARGPARSE = argparse.ArgumentParser()
ARGPARSE.add_argument('-r', dest="repo")
ARGPARSE.add_argument('command', nargs=1)
ARGPARSE.add_argument('body', nargs='*')
def on_message(msg, server):
if FIRST:
create_database(server)
text = msg.get("text", "")
match = re.findall(r"!hub\s*(.*)", text)
if not match:
return
# If given -h or -v, argparse will try to quit. Don't let it.
try:
ns = ARGPARSE.parse_args(match[0].split(' '))
except SystemExit:
return __doc__
command = ns.command[0]
# if the user calls !hub with no arguments, print help
if not len(command):
return __doc__
kwargs = github(server, msg["channel"], command, ns.body, ns.repo)
# if github() didn't return anything, or returned any non-dict arg,
# just return it
if not kwargs or not isinstance(kwargs, dict):
return kwargs
# otherwise, post the message via the slack API; this lets us use fancy
# formatting rather than the plain formatting the RTM API allows
server.slack.post_message(
msg['channel'],
'',
as_user=server.slack.server.username,
**kwargs)