forked from davglass/github-trac
-
Notifications
You must be signed in to change notification settings - Fork 1
/
github.py
277 lines (222 loc) · 10.2 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
from trac.core import *
from trac.resource import ResourceNotFound
from trac.config import Option, IntOption, ListOption, BoolOption
from trac.web.api import IRequestFilter, IRequestHandler, Href
from trac.env import IEnvironmentSetupParticipant
from trac.util.translation import _
from trac.util.text import shorten_line
from trac.db import Table, Column, Index
from trac.wiki import IWikiSyntaxProvider
from genshi.builder import tag
from hook import CommitHook
import simplejson
import re
from git import Git
class GithubPlugin(Component):
implements(IRequestHandler, IRequestFilter, IEnvironmentSetupParticipant,
IWikiSyntaxProvider)
key = Option('github', 'apitoken', '', doc = """Your GitHub API Token found here: https://github.com/account, """)
closestatus = Option('github', 'closestatus', '', doc = """This is the status used to close a ticket. It defaults to closed.""")
browser = Option('github', 'browser', '', doc = """Place your GitHub Source Browser URL here to have the /browser entry point redirect to GitHub.""")
autofetch = Option('github', 'autofetch', '', doc = """Should we auto fetch the repo when we get a commit hook from GitHub.""")
repo = Option('trac', 'repository_dir' '', doc = """This is your repository dir""")
revmap = Option('github', 'svn_revmap', '', doc = """a plaintext file mapping svn revisions to git hashes""")
enable_revmap = Option('github', 'enable_revmap', 0, doc = """use the svn->git map when a request looks like a svn changeset """)
SCHEMA = Table('svn_revmap', key = ('svn_rev', 'git_hash'))[
Column('svn_rev'),
Column('git_hash'),
Column('commit_msg'),
Index(['svn_rev', 'git_hash']),]
def __init__(self):
self.hook = CommitHook(self.env)
self.env.log.debug("API Token: %s" % self.key)
self.env.log.debug("Browser: %s" % self.browser)
self.processHook = False
# IEnvironmentSetupParticpant methods
def environment_created(self):
if self.enable_revmap:
self._upgrade_db(self.env.get_db_cnx())
#return true if the db table doesn't exist or needs to be updated
def environment_needs_upgrade(self, db):
if self.enable_revmap == 0:
return False
cursor = db.cursor()
try:
cursor.execute("SELECT COUNT(*) FROM svn_revmap")
row = cursor.fetchone()
#if there's one or more rows, assume everything's ok
if row[0] > 0:
return False
return True
except:
return True
def upgrade_environment(self, db):
if self.enable_revmap:
self._upgrade_db(db)
def _upgrade_db(self, db):
#open the revision map
try:
revmap_fd = open(self.revmap, 'rb')
except IOError:
raise ResourceNotFound(_("revision map '%(revmap)s' not found", revmap=self.revmap))
cursor = db.cursor()
try:
cursor.execute("DROP TABLE svn_revmap;")
except:
pass
try:
from trac.db import DatabaseManager
db_backend, ignored = DatabaseManager(self.env)._get_connector()
except ImportError:
db_backend = self.env.get_db_cnx()
for stmt in db_backend.to_sql(self.SCHEMA):
self.env.log.debug(stmt)
cursor.execute(stmt)
insert_count = 0
git_hash = revmap_fd.readline()[0:-1]
while 1:
#make sure this line is the hash
if not re.match(r'[0-9a-f]{40}', git_hash):
raise Exception("expecting hash, found '%s'" % git_hash)
line = revmap_fd.readline()[0:-1]
if line.startswith('git-svn-id:'):
commit_msg = '<no commit message>'
else:
#slurp lines into the commit messsages until there's a blank line, a line starting with git-svn-id or a hash
commit_msg = ''
while not re.match(r'[0-9a-f]{40}', line) and not line.startswith('git-svn-id:'):
if len(line) > 0:
commit_msg = commit_msg + ' ' + line
line = revmap_fd.readline()[0:-1]
if not line.startswith('git-svn-id:'):
raise Exception("expected git-svn-id, got '%s'" % line)
svn_rev_match = re.match(r'^git-svn-id:.*@(\d+) ', line)
svn_rev = svn_rev_match.group(1)
insert_query = "INSERT INTO svn_revmap (svn_rev, git_hash, commit_msg) VALUES (%s, %s, %s);"
self.env.log.debug(insert_query % (svn_rev, git_hash, commit_msg))
cursor.execute(insert_query, (svn_rev, git_hash, commit_msg.decode('utf-8')))
insert_count += 1
if svn_rev == '1':
break
git_hash = revmap_fd.readline()[0:-1]
while len(git_hash) == 0:
git_hash = revmap_fd.readline()[0:-1]
self.env.log.debug("inserted %d mappings into svn_revmap" % insert_count)
# IWikiSyntaxProvider methods
def get_wiki_syntax(self):
yield (r"r\d+", #svn revision links ("r1432")
lambda formatter, ns, match:
self._format_changeset_link(formatter, 'svn', ns, match))
yield (r"[0-9a-fA-F]{5,40}", #git hashes ("eb390eca04394")
lambda formatter, ns, match:
self._format_changeset_link(formatter, 'git', ns, match))
#pre_process_request deals with link resolution
def get_link_resolvers(self):
return []
def _format_changeset_link(self, formatter, rev_type, ns, match):
git_hash = match.group(0)
if rev_type == 'svn':
svn_rev = match.group(0).replace('r','',1)
git_hash = self._get_git_hash(svn_rev)
if self._git_hash_is_valid(git_hash):
title = shorten_line(self._get_git_title(git_hash))
return tag.a(match.group(0), href="%s/%s" % (formatter.href.changeset(), git_hash),
title=title, class_="changeset")
return match.group(0)
# IRequestHandler methods
def match_request(self, req):
self.env.log.debug("Match Request")
serve = req.path_info.rstrip('/') == ('/github/%s' % self.key) and req.method == 'POST'
if serve:
self.processHook = True
#This is hacky but it's the only way I found to let Trac post to this request
# without a valid form_token
req.form_token = None
self.env.log.debug("Handle Request: %s" % serve)
return serve
def process_request(self, req):
if self.processHook:
self.processCommitHook(req)
# This has to be done via the pre_process_request handler
# Seems that the /browser request doesn't get routed to match_request :(
def pre_process_request(self, req, handler):
if self.browser:
serve = req.path_info.startswith('/browser')
self.env.log.debug("Handle Pre-Request /browser: %s" % serve)
if serve:
self.processBrowserURL(req)
serve2 = req.path_info.startswith('/changeset')
self.env.log.debug("Handle Pre-Request /changeset: %s" % serve2)
if serve2:
self.processChangesetURL(req)
return handler
def post_process_request(self, req, template, data, content_type):
return (template, data, content_type)
def _get_git_hash(self, svn_rev):
cursor = self.env.get_db_cnx().cursor()
row = cursor.execute("SELECT git_hash FROM svn_revmap WHERE svn_rev = %s;" % svn_rev).fetchone()
if row:
return row[0]
return None
def _git_hash_is_valid(self, git_hash):
cursor = self.env.get_db_cnx().cursor()
row = cursor.execute("SELECT 1 FROM svn_revmap WHERE git_hash LIKE '%s%%';" % git_hash).fetchone()
if row:
return True
return False
def _get_git_title(self, git_hash):
cursor = self.env.get_db_cnx().cursor()
row = cursor.execute("SELECT commit_msg FROM svn_revmap WHERE git_hash LIKE '%s%%';" % git_hash).fetchone()
if row:
return row[0]
return "<no commit message>"
def processChangesetURL(self, req):
self.env.log.debug("processChangesetURL")
browser = self.browser.replace('/tree/master', '/commit/')
url = req.path_info.replace('/changeset/', '')
self.env.log.debug("url is %s" % url)
svn_rev_match = re.match( '^([0-9]{1,6})([^0-9a-fA-F]|$)', url)
if svn_rev_match and self.enable_revmap:
svn_rev = svn_rev_match.group(1)
git_hash = self._get_git_hash(svn_rev)
if git_hash:
url = git_hash
self.env.log.debug("mapping svn revision %s to github hash %s" % (svn_rev, git_hash));
else:
self.env.log.debug("couldn't map svn revision %s", svn_rev);
req.redirect(self.browser)
#XXX: fail gracefully if it doesn't exist
if not url:
browser = self.browser
url = ''
redirect = '%s%s' % (browser, url)
self.env.log.debug("Redirect URL: %s" % redirect)
out = 'Going to GitHub: %s' % redirect
req.redirect(redirect)
def processBrowserURL(self, req):
self.env.log.debug("processBrowserURL")
browser = self.browser.replace('/master', '/')
rev = req.args.get('rev')
url = req.path_info.replace('/browser', '')
if not rev:
rev = ''
redirect = '%s%s%s' % (browser, rev, url)
self.env.log.debug("Redirect URL: %s" % redirect)
out = 'Going to GitHub: %s' % redirect
req.redirect(redirect)
def processCommitHook(self, req):
self.env.log.debug("processCommitHook")
status = self.closestatus
if not status:
status = 'closed'
data = req.args.get('payload')
if data:
jsondata = simplejson.loads(data)
for i in jsondata['commits']:
self.hook.process(i, status)
if self.autofetch:
repo = Git(self.repo)
try:
repo.execute(['git', 'fetch'])
except:
self.env.log.debug("git fetch failed!")