1 |
#! /usr/bin/python |
2 |
# -*- Python -*- |
3 |
|
4 |
"""Complicated notification for CVS checkins. |
5 |
|
6 |
This script is used to provide email notifications of changes to the CVS |
7 |
repository. These email changes will include context diffs of the changes. |
8 |
Really big diffs will be trimmed. |
9 |
|
10 |
This script is run from a CVS loginfo file (see $CVSROOT/CVSROOT/loginfo). To |
11 |
set this up, create a loginfo entry that looks something like this: |
12 |
|
13 |
mymodule /path/to/this/script %%s some-email-addr@your.domain |
14 |
|
15 |
In this example, whenever a checkin that matches `mymodule' is made, this |
16 |
script is invoked, which will generate the diff containing email, and send it |
17 |
to some-email-addr@your.domain. |
18 |
|
19 |
Note: This module used to also do repository synchronizations via |
20 |
rsync-over-ssh, but since the repository has been moved to SourceForge, |
21 |
this is no longer necessary. The syncing functionality has been ripped |
22 |
out in the 3.0, which simplifies it considerably. Access the 2.x versions |
23 |
to refer to this functionality. Because of this, the script is misnamed. |
24 |
|
25 |
It no longer makes sense to run this script from the command line. Doing so |
26 |
will only print out this usage information. |
27 |
|
28 |
Usage: |
29 |
|
30 |
%(PROGRAM)s [options] <%%S> email-addr [email-addr ...] |
31 |
|
32 |
Where options is: |
33 |
|
34 |
--cvsroot=<path> |
35 |
Use <path> as the environment variable CVSROOT. Otherwise this |
36 |
variable must exist in the environment. |
37 |
|
38 |
--help |
39 |
-h |
40 |
Print this text. |
41 |
|
42 |
--context=# |
43 |
-C # |
44 |
Include # lines of context around lines that differ (default: 2). |
45 |
|
46 |
-c |
47 |
Produce a context diff (default). |
48 |
|
49 |
-u |
50 |
Produce a unified diff (smaller, but harder to read). |
51 |
|
52 |
<%%S> |
53 |
CVS %%s loginfo expansion. When invoked by CVS, this will be a single |
54 |
string containing the directory the checkin is being made in, relative |
55 |
to $CVSROOT, followed by the list of files that are changing. If the |
56 |
%%s in the loginfo file is %%{sVv}, context diffs for each of the |
57 |
modified files are included in any email messages that are generated. |
58 |
|
59 |
email-addrs |
60 |
At least one email address. |
61 |
|
62 |
""" |
63 |
|
64 |
import os |
65 |
import sys |
66 |
import string |
67 |
import time |
68 |
import getopt |
69 |
|
70 |
# Notification command |
71 |
MAILCMD = '/bin/mail -s "CVS: %(SUBJECT)s" %(PEOPLE)s 2>&1 > /dev/null' |
72 |
|
73 |
# Diff trimming stuff |
74 |
DIFF_HEAD_LINES = 20 |
75 |
DIFF_TAIL_LINES = 20 |
76 |
DIFF_TRUNCATE_IF_LARGER = 1000 |
77 |
|
78 |
PROGRAM = sys.argv[0] |
79 |
|
80 |
BINARY_EXPLANATION_LINES = [ |
81 |
"(This appears to be a binary file; contents omitted.)\n" |
82 |
] |
83 |
|
84 |
|
85 |
def usage(code, msg=''): |
86 |
print __doc__ % globals() |
87 |
if msg: |
88 |
print msg |
89 |
sys.exit(code) |
90 |
|
91 |
|
92 |
|
93 |
def calculate_diff(filespec, contextlines): |
94 |
try: |
95 |
file, oldrev, newrev = string.split(filespec, ',') |
96 |
except ValueError: |
97 |
# No diff to report |
98 |
return '***** Bogus filespec: %s' % filespec |
99 |
if oldrev == 'NONE': |
100 |
try: |
101 |
if os.path.exists(file): |
102 |
fp = open(file) |
103 |
else: |
104 |
update_cmd = 'cvs -fn update -r %s -p %s' % (newrev, file) |
105 |
fp = os.popen(update_cmd) |
106 |
lines = fp.readlines() |
107 |
fp.close() |
108 |
# Is this a binary file? Let's look at the first few |
109 |
# lines to figure it out: |
110 |
for line in lines[:5]: |
111 |
for c in string.rstrip(line): |
112 |
if c < ' ' or c > chr(127): |
113 |
lines = BINARY_EXPLANATION_LINES[:] |
114 |
break |
115 |
lines.insert(0, '--- NEW FILE: %s ---\n' % file) |
116 |
except IOError, e: |
117 |
lines = ['***** Error reading new file: ', |
118 |
str(e), '\n***** file: ', file, ' cwd: ', os.getcwd()] |
119 |
elif newrev == 'NONE': |
120 |
lines = ['--- %s DELETED ---\n' % file] |
121 |
else: |
122 |
# This /has/ to happen in the background, otherwise we'll run into CVS |
123 |
# lock contention. What a crock. |
124 |
if contextlines > 0: |
125 |
difftype = "-C " + str(contextlines) |
126 |
else: |
127 |
difftype = "-u" |
128 |
diffcmd = "/usr/bin/cvs -f diff -kk %s --minimal -r %s -r %s '%s'" % ( |
129 |
difftype, oldrev, newrev, file) |
130 |
fp = os.popen(diffcmd) |
131 |
lines = fp.readlines() |
132 |
sts = fp.close() |
133 |
# ignore the error code, it always seems to be 1 :( |
134 |
## if sts: |
135 |
## return 'Error code %d occurred during diff\n' % (sts >> 8) |
136 |
if len(lines) > DIFF_TRUNCATE_IF_LARGER: |
137 |
removedlines = len(lines) - DIFF_HEAD_LINES - DIFF_TAIL_LINES |
138 |
del lines[DIFF_HEAD_LINES:-DIFF_TAIL_LINES] |
139 |
lines.insert(DIFF_HEAD_LINES, |
140 |
'[...%d lines suppressed...]\n' % removedlines) |
141 |
return string.join(lines, '') |
142 |
|
143 |
|
144 |
|
145 |
def blast_mail(mailcmd, filestodiff, contextlines): |
146 |
# cannot wait for child process or that will cause parent to retain cvs |
147 |
# lock for too long. Urg! |
148 |
if not os.fork(): |
149 |
# in the child |
150 |
# give up the lock you cvs thang! |
151 |
time.sleep(2) |
152 |
fp = os.popen(mailcmd, 'w') |
153 |
fp.write(sys.stdin.read()) |
154 |
fp.write('\n') |
155 |
# append the diffs if available |
156 |
for file in filestodiff: |
157 |
fp.write(calculate_diff(file, contextlines)) |
158 |
fp.write('\n') |
159 |
fp.close() |
160 |
# doesn't matter what code we return, it isn't waited on |
161 |
os._exit(0) |
162 |
|
163 |
|
164 |
|
165 |
# scan args for options |
166 |
def main(): |
167 |
contextlines = 2 |
168 |
try: |
169 |
opts, args = getopt.getopt(sys.argv[1:], 'hC:cu', |
170 |
['context=', 'cvsroot=', 'help']) |
171 |
except getopt.error, msg: |
172 |
usage(1, msg) |
173 |
|
174 |
# parse the options |
175 |
for opt, arg in opts: |
176 |
if opt in ('-h', '--help'): |
177 |
usage(0) |
178 |
elif opt == '--cvsroot': |
179 |
os.environ['CVSROOT'] = arg |
180 |
elif opt in ('-C', '--context'): |
181 |
contextlines = int(arg) |
182 |
elif opt == '-c': |
183 |
if contextlines <= 0: |
184 |
contextlines = 2 |
185 |
elif opt == '-u': |
186 |
contextlines = 0 |
187 |
|
188 |
# What follows is the specification containing the files that were |
189 |
# modified. The argument actually must be split, with the first component |
190 |
# containing the directory the checkin is being made in, relative to |
191 |
# $CVSROOT, followed by the list of files that are changing. |
192 |
if not args: |
193 |
usage(1, 'No CVS module specified') |
194 |
SUBJECT = args[0] |
195 |
specs = string.split(args[0]) |
196 |
del args[0] |
197 |
|
198 |
# The remaining args should be the email addresses |
199 |
if not args: |
200 |
usage(1, 'No recipients specified') |
201 |
|
202 |
# Now do the mail command |
203 |
PEOPLE = string.join(args) |
204 |
mailcmd = MAILCMD % vars() |
205 |
|
206 |
print 'Mailing %s...' % PEOPLE |
207 |
if specs == ['-', 'Imported', 'sources']: |
208 |
return |
209 |
if specs[-3:] == ['-', 'New', 'directory']: |
210 |
del specs[-3:] |
211 |
elif len(specs) > 2: |
212 |
L = specs[:2] |
213 |
for s in specs[2:]: |
214 |
prev = L[-1] |
215 |
if string.count(prev, ",") < 2: |
216 |
L[-1] = "%s %s" % (prev, s) |
217 |
else: |
218 |
L.append(s) |
219 |
specs = L |
220 |
print 'Generating notification message...' |
221 |
blast_mail(mailcmd, specs[1:], contextlines) |
222 |
print 'Generating notification message... done.' |
223 |
|
224 |
|
225 |
|
226 |
if __name__ == '__main__': |
227 |
main() |
228 |
sys.exit(0) |