3 # NOTE: Until SourceForge installs a modern version of Python on the cvs
4 # servers, this script MUST be compatible with Python 1.5.2.
6 """Complicated notification for CVS checkins.
8 This script is used to provide email notifications of changes to the CVS
9 repository. These email changes will include context diffs of the changes.
10 Really big diffs will be trimmed.
12 This script is run from a CVS loginfo file (see $CVSROOT/CVSROOT/loginfo). To
13 set this up, create a loginfo entry that looks something like this:
15 mymodule /path/to/this/script %%s some-email-addr@your.domain
17 In this example, whenever a checkin that matches `mymodule' is made, this
18 script is invoked, which will generate the diff containing email, and send it
19 to some-email-addr@your.domain.
21 Note: This module used to also do repository synchronizations via
22 rsync-over-ssh, but since the repository has been moved to SourceForge,
23 this is no longer necessary. The syncing functionality has been ripped
24 out in the 3.0, which simplifies it considerably. Access the 2.x versions
25 to refer to this functionality. Because of this, the script is misnamed.
27 It no longer makes sense to run this script from the command line. Doing so
28 will only print out this usage information.
32 %(PROGRAM)s [options] <%%S> email-addr [email-addr ...]
37 Use <path> as the environment variable CVSROOT. Otherwise this
38 variable must exist in the environment.
42 Include # lines of context around lines that differ (default: 2).
45 Produce a context diff (default).
48 Produce a unified diff (smaller).
51 Don't print as much status to stdout.
55 The hostname that email messages appear to be coming from. The From:
56 header will of the outgoing message will look like user@hostname. By
57 default, hostname is the machine's fully qualified domain name.
62 The rest of the command line arguments are:
65 CVS %%s loginfo expansion. When invoked by CVS, this will be a single
66 string containing the directory the checkin is being made in, relative
67 to $CVSROOT, followed by the list of files that are changing. If the
68 %%s in the loginfo file is %%{sVv}, context diffs for each of the
69 modified files are included in any email messages that are generated.
72 At least one email address.
85 from socket import getfqdn
89 hostname = socket.gethostname()
90 byaddr = socket.gethostbyaddr(socket.gethostbyname(hostname))
92 aliases.insert(0, byaddr[0])
93 aliases.insert(0, hostname)
98 fqdn = 'localhost.localdomain'
102 from cStringIO import StringIO
104 # Which SMTP server to do we connect to? Empty string means localhost.
105 MAILHOST = 'localhost'
108 # Diff trimming stuff
111 DIFF_TRUNCATE_IF_LARGER = 1000
118 PROGRAM = sys.argv[0]
120 BINARY_EXPLANATION_LINES = [
121 "(This appears to be a binary file; contents omitted.)\n"
124 REVCRE = re.compile("^(NONE|[0-9.]+)$")
125 NOVERSION = "Couldn't generate diff; no version number found in filespec: %s"
126 BACKSLASH = "Couldn't generate diff: backslash in filespec's filename: %s"
130 def usage(code, msg=''):
131 print __doc__ % globals()
138 def calculate_diff(filespec, contextlines):
139 file, oldrev, newrev = string.split(filespec, ',')
140 # Make sure we can find a CVS version number
141 if not REVCRE.match(oldrev):
142 return NOVERSION % filespec
143 if not REVCRE.match(newrev):
144 return NOVERSION % filespec
146 if string.find(file, '\\') <> -1:
147 # I'm sorry, a file name that contains a backslash is just too much.
148 # XXX if someone wants to figure out how to escape the backslashes in
149 # a safe way to allow filenames containing backslashes, this is the
150 # place to do it. --Zooko 2002-03-17
151 return BACKSLASH % filespec
153 if string.find(file, "'") <> -1:
154 # Those crazy users put single-quotes in their file names! Now we
155 # have to escape everything that is meaningful inside double-quotes.
156 filestr = string.replace(file, '`', '\`')
157 filestr = string.replace(filestr, '"', '\"')
158 filestr = string.replace(filestr, '$', '\$')
159 # and quote it with double-quotes.
160 filestr = '"' + filestr + '"'
162 # quote it with single-quotes.
163 filestr = "'" + file + "'"
166 if os.path.exists(file):
169 update_cmd = "cvs -fn update -r %s -p %s" % (newrev, filestr)
170 fp = os.popen(update_cmd)
171 lines = fp.readlines()
173 # Is this a binary file? Let's look at the first few
174 # lines to figure it out:
175 for line in lines[:5]:
176 for c in string.rstrip(line):
177 if c in string.whitespace:
179 if c < ' ' or c > chr(127):
180 lines = BINARY_EXPLANATION_LINES[:]
182 lines.insert(0, '--- NEW FILE: %s ---\n' % file)
184 lines = ['***** Error reading new file: ',
185 str(e), '\n***** file: ', file, ' cwd: ', os.getcwd()]
186 elif newrev == 'NONE':
187 lines = ['--- %s DELETED ---\n' % file]
189 # This /has/ to happen in the background, otherwise we'll run into CVS
190 # lock contention. What a crock.
192 difftype = "-C " + str(contextlines)
195 diffcmd = "/usr/bin/cvs -f diff -kk %s --minimal -r %s -r %s %s" \
196 % (difftype, oldrev, newrev, filestr)
197 fp = os.popen(diffcmd)
198 lines = fp.readlines()
200 # ignore the error code, it always seems to be 1 :(
202 ## return 'Error code %d occurred during diff\n' % (sts >> 8)
203 if len(lines) > DIFF_TRUNCATE_IF_LARGER:
204 removedlines = len(lines) - DIFF_HEAD_LINES - DIFF_TAIL_LINES
205 del lines[DIFF_HEAD_LINES:-DIFF_TAIL_LINES]
206 lines.insert(DIFF_HEAD_LINES,
207 '[...%d lines suppressed...]\n' % removedlines)
208 return string.join(lines, '')
212 def blast_mail(subject, people, filestodiff, contextlines, fromhost):
213 # cannot wait for child process or that will cause parent to retain cvs
214 # lock for too long. Urg!
217 # give up the lock you cvs thang!
219 # Create the smtp connection to the localhost
220 conn = smtplib.SMTP()
221 conn.connect(MAILHOST, MAILPORT)
222 user = pwd.getpwuid(os.getuid())[0]
223 domain = fromhost or getfqdn()
224 author = '%s@%s' % (user, domain)
232 ''' % {'author' : author,
233 'people' : string.join(people, COMMASPACE),
236 s.write(sys.stdin.read())
237 # append the diffs if available
239 for file in filestodiff:
240 print calculate_diff(file, contextlines)
242 sys.stdout = sys.__stdout__
243 resp = conn.sendmail(author, people, s.getvalue())
249 # scan args for options
252 opts, args = getopt.getopt(
253 sys.argv[1:], 'hC:cuqf:',
254 ['fromhost=', 'context=', 'cvsroot=', 'help', 'quiet'])
255 except getopt.error, msg:
262 for opt, arg in opts:
263 if opt in ('-h', '--help'):
265 elif opt == '--cvsroot':
266 os.environ['CVSROOT'] = arg
267 elif opt in ('-C', '--context'):
268 contextlines = int(arg)
270 if contextlines <= 0:
274 elif opt in ('-q', '--quiet'):
276 elif opt in ('-f', '--fromhost'):
279 # What follows is the specification containing the files that were
280 # modified. The argument actually must be split, with the first component
281 # containing the directory the checkin is being made in, relative to
282 # $CVSROOT, followed by the list of files that are changing.
284 usage(1, 'No CVS module specified')
286 fileargs = args[1:-1]
288 specs.append(subject)
289 while len(fileargs) > 0:
290 specs.append(string.join(fileargs[:3], ','))
291 fileargs = fileargs[3:]
294 usage(1, 'No recipients specified')
296 people.append(args[-1])
298 # The remaining args should be the email addresses
299 # Now do the mail command
302 print 'Mailing %s...' % string.join(people, COMMASPACE)
304 if specs == ['-', 'Imported', 'sources']:
306 if specs[-3:] == ['-', 'New', 'directory']:
312 if string.count(prev, ',') < 2:
313 L[-1] = "%s %s" % (prev, s)
319 print 'Generating notification message...'
320 blast_mail(subject, people, specs[1:], contextlines, fromhost)
322 print 'Generating notification message... done.'
326 if __name__ == '__main__':