/
commitmsg.py
executable file
·335 lines (293 loc) · 9.72 KB
/
commitmsg.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
#!/usr/bin/env python
#
# This is a simple script to generate commit messages formatted in the way
# the PostgreSQL projects prefer them. Anybody else is of course also
# free to use it..
#
# Note: the script (not surprisingly) uses the git commands in pipes, so git
# needs to be available in the path.
#
# Copyright (C) 2010 PostgreSQL Global Development Group
# Author: Magnus Hagander <magnus@hagander.net>
#
# Released under the PostgreSQL license
#
#
# The script is configured by having a file named commitmsg.ini in the same
# directory as the script, with the following format:
#
# [commitmsg]
# destination = somewhere@somewhere.com
# fallbacksender = somewhere@somewhere.com
# subject = pgsql: $shortmsg
# gitweb = http://git.postgresql.org/gitweb?p=postgresql.git;a=$action;h=$commit
# debug = 0
# commitmsg = 1
# tagmsg = 1
# branchmsg = 1
#
# Expansion variables are available for the following fields:
# subject - shortmsg
# gitweb - action, commit
#
# destination is the address to send commit messages to.
# fallbacksender is the sender address to use for activities which don't have an
# author, such as creation/removal of a branch.
# subject is the subject of the email
# gitweb is a template URL for a gitweb link
# debug set to 1 to output data on console instead of sending email
# commitmsg set to 0 to disable generation of commit mails
# tagmsg set to 0 to disable generation of tag creation mails
# branchmsg set to 0 to disable generation of branch creation mails
#
import sys
import os.path
from subprocess import Popen, PIPE
from email.mime.text import MIMEText
from ConfigParser import ConfigParser
cfgname = "%s/commitmsg.ini" % os.path.dirname(sys.argv[0])
if not os.path.isfile(cfgname):
raise Exception("Config file '%s' is missing!" % cfgname)
c = ConfigParser()
c.read(cfgname)
# Figure out if we should do debugging
try:
debug = int(c.get('commitmsg', 'debug'))
except Exception, e:
print "Except: %s" % e
debug = 1
def should_send_message(msgtype):
"""
Determine if a specific message type should be sent, by looking
in the ini file. Unless specifically disabled, all types are
sent.
"""
try:
sendit = int(c.get('commitmsg', '%smsg' % msgtype))
if sendit == 0:
return False
return True
except Exception, e:
return True
allmail = []
def sendmail(msg):
allmail.append(msg)
def flush_mail():
"""
Send off an email using the local sendmail binary.
"""
# Need to reverse list to make sure we send the emails out in the same
# order as the commits appeared. They'll usually go out at the same time
# anyway, but if they do get different timestamps, we want them in the
# correct order.
for msg in reversed(allmail):
if debug == 1:
print msg
else:
env = os.environ
# Add /usr/bin to the path, needed on debian
env["PATH"] += ":/usr/sbin"
pipe = Popen("sendmail -t", shell=True, env=env, stdin=PIPE).stdin
pipe.write(msg.as_string())
pipe.close()
def create_message(text, sender, subject):
# Don't specify utf8 when doing a charset, because that will encode the output
# as base64 which is completely useless on the console...
if debug == 1:
msg = MIMEText(text)
else:
msg = MIMEText(text, _charset='utf-8')
msg['To'] = c.get('commitmsg', 'destination')
msg['From'] = sender
msg['Subject'] = subject
return msg
def parse_commit_log(lines):
"""
Parse a single commit off the commitlog, which should be in an array
of lines, generate a commit message, and send it.
The order of the array must be reversed.
The contents of the array will be destroyed.
"""
if len(lines) == 0:
return False
# Reset our parsing data
commitinfo = ""
authorinfo = ""
committerinfo = ""
mergeinfo = ""
while True:
l = lines.pop().strip()
if l == "":
break
elif l.startswith("commit "):
commitinfo = l
elif l.startswith("Author: "):
authorinfo = l
elif l.startswith("Commit: "):
committerinfo = l
elif l.startswith("Merge: "):
mergeinfo = l
else:
raise Exception("Unknown header line: %s" % l)
if not (commitinfo or authorinfo):
# If none of these existed, we must've hit the end of the log
return False
# Check for any individual piece that is missing
if not commitinfo:
raise Exception("Could not find commit hash!")
if not authorinfo:
raise Exception("Could not find author!")
if not committerinfo:
raise Exception("Could not find committer!")
commitmsg = []
# We are in the commit message until we hit one line that doesn't start
# with four spaces (commit message).
while len(lines):
l = lines.pop()
if l.startswith(" "):
# Remove the 4 leading spaces and any trailing spaces
commitmsg.append(l[4:].rstrip())
else:
break # something else means start of stats section
diffstat = []
while len(lines):
l = lines.pop()
if l.strip() == "": break
if not l.startswith(" "):
# If there is no starting space, it means there were no stats rows,
# and we're already looking at the next commit. Put this line back
# on the list and move on
lines.append(l)
break
diffstat.append(l.strip())
# Figure out affected branches
p = Popen("git branch --contains %s" % commitinfo[7:], shell=True, stdout=PIPE)
branches = p.stdout.readlines()
p.stdout.close()
# Everything is parsed, put together an email
mail = []
mail.extend(commitmsg)
mail.append("")
if len(branches) > 1:
mail.append("Branches")
mail.append("--------")
else:
mail.append("Branch")
mail.append("------")
mail.append("\n".join([branch.strip(" *\r\n") for branch in branches]))
mail.append("")
mail.append("Details")
mail.append("-------")
mail.append(
c.get('commitmsg', 'gitweb').replace('$action','commitdiff').replace('$commit', commitinfo[7:]))
if committerinfo[7:] != authorinfo[7:]:
mail.append(authorinfo) # already includes Author: part
mail.append("")
mail.append("Modified Files")
mail.append("--------------")
mail.extend(diffstat)
mail.append("")
mail.append("")
msg = create_message("\n".join(mail),
committerinfo[7:],
c.get('commitmsg','subject').replace("$shortmsg",
commitmsg[0][:80-len(c.get('commitmsg','subject'))]))
sendmail(msg)
return True
def parse_annotated_tag(lines):
"""
Parse a single commit off the commitlog, which should be in an array
of lines, generate a commit message, and send it.
The contents of the array will be destroyed.
"""
if len(lines) == 0:
return
if not lines[0].startswith('tag'):
raise Exception("Tag message does not start with tag!")
tagname = lines[0][4:].strip()
if not lines[1].startswith('Tagger: '):
raise Exception("Tag message does not contain tagger!")
author = lines[1][8:].strip()
if not lines[2].startswith('Date: '):
raise Exception("Tag message does not contain date!")
if not lines[3].strip() == "":
raise Exception("Tag message does not contian message separator!")
mail = []
mail.append('Tag %s has been created.' % tagname)
mail.append("View: %s" % (
c.get('commitmsg','gitweb').replace('$action','tag').replace('$commit', 'refs/tags/%s' % tagname)))
mail.append("")
mail.append("Log Message")
mail.append("-----------")
for i in range(4, len(lines)):
if lines[i].strip() == '':
break
mail.append(lines[i].rstrip())
# Ignore the commit it's referring to here
sendmail(
create_message("\n".join(mail),
author,
c.get('commitmsg','subject').replace('$shortmsg', 'Tag %s has been created.' % tagname)))
return
if __name__ == "__main__":
# Get a list of refs on stdin, do something smart with it
while True:
l = sys.stdin.readline()
if not l: break
(oldobj, newobj, ref) = l.split()
# Build the email
if oldobj == "".zfill(40):
# old object being all zeroes means a new branch or tag was created
if ref.startswith("refs/heads/"):
# It's a branch!
if not should_send_message('branch'): continue
sendmail(
create_message("Branch %s was created.\n\nView: %s" % (
ref,
c.get('commitmsg', 'gitweb').replace('$action','shortlog').replace('$commit', ref),
),
c.get('commitmsg', 'fallbacksender'),
c.get('commitmsg','subject').replace("$shortmsg",
"Branch %s was created" % ref)))
elif ref.startswith("refs/tags/"):
# It's a tag!
# It can be either an annotated tag or a lightweight one.
if not should_send_message('tag'): continue
p = Popen("git cat-file -t %s" % ref, shell=True, stdout=PIPE)
t = p.stdout.read().strip()
p.stdout.close()
if t == "commit":
# Lightweight tag with no further information
sendmail(
create_message("Tag %s was created.\n" % ref,
c.get('commitmsg', 'fallbacksender'),
c.get('commitmsg','subject').replace("$shortmsg",
"Tag %s was created" % ref)))
elif t == "tag":
# Annotated tag! Get the description!
p = Popen("git show %s" % ref, shell=True, stdout=PIPE)
lines = p.stdout.readlines()
p.stdout.close()
parse_annotated_tag(lines)
else:
raise Exception("Unknown tag type '%s'" % t)
else:
raise Exception("Unknown branch/tag type %s" % ref)
elif newobj == "".zfill(40):
# new object being all zeroes means a branch was removed
if not should_send_message('branch'): continue
sendmail(
create_message("Branch %s was removed." % ref,
c.get('commitmsg', 'fallbacksender'),
c.get('commitmsg','subject').replace("$shortmsg",
"Branch %s was removed" % ref)))
else:
# If both are real object ids, we can call git log on them
if not should_send_message('commit'): continue
cmd = "git log %s..%s --stat --pretty=full" % (oldobj, newobj)
p = Popen(cmd, shell=True, stdout=PIPE)
lines = p.stdout.readlines()
lines.reverse()
while parse_commit_log(lines):
pass
flush_mail()