repairing
[fp-git.git] / CVSROOT / syncmail
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
81 \f
82 def usage(code, msg=''):
83     print __doc__ % globals()
84     if msg:
85         print msg
86     sys.exit(code)
87
88
89 \f
90 def calculate_diff(filespec, contextlines):
91     try:
92         file, oldrev, newrev = string.split(filespec, ',')
93     except ValueError:
94         # No diff to report
95         return '***** Bogus filespec: %s' % filespec
96     if oldrev == 'NONE':
97         try:
98             if os.path.exists(file):
99                 fp = open(file)
100             else:
101                 update_cmd = 'cvs -fn update -r %s -p %s' % (newrev, file)
102                 fp = os.popen(update_cmd)
103             lines = fp.readlines()
104             fp.close()
105             lines.insert(0, '--- NEW FILE: %s ---\n' % file)
106         except IOError, e:
107             lines = ['***** Error reading new file: ',
108                      str(e), '\n***** file: ', file, ' cwd: ', os.getcwd()]
109     elif newrev == 'NONE':
110         lines = ['--- %s DELETED ---\n' % file]
111     else:
112         # This /has/ to happen in the background, otherwise we'll run into CVS
113         # lock contention.  What a crock.
114         if contextlines > 0:
115             difftype = "-C " + str(contextlines)
116         else:
117             difftype = "-u"
118         diffcmd = "/usr/bin/cvs -f diff -kk %s --minimal -r %s -r %s '%s'" % (
119             difftype, oldrev, newrev, file)
120         fp = os.popen(diffcmd)
121         lines = fp.readlines()
122         sts = fp.close()
123         # ignore the error code, it always seems to be 1 :(
124 ##        if sts:
125 ##            return 'Error code %d occurred during diff\n' % (sts >> 8)
126     if len(lines) > DIFF_TRUNCATE_IF_LARGER:
127         removedlines = len(lines) - DIFF_HEAD_LINES - DIFF_TAIL_LINES
128         del lines[DIFF_HEAD_LINES:-DIFF_TAIL_LINES]
129         lines.insert(DIFF_HEAD_LINES,
130                      '[...%d lines suppressed...]\n' % removedlines)
131     return string.join(lines, '')
132
133
134 \f
135 def blast_mail(mailcmd, filestodiff, contextlines):
136     # cannot wait for child process or that will cause parent to retain cvs
137     # lock for too long.  Urg!
138     if not os.fork():
139         # in the child
140         # give up the lock you cvs thang!
141         time.sleep(2)
142         fp = os.popen(mailcmd, 'w')
143         fp.write(sys.stdin.read())
144         fp.write('\n')
145         # append the diffs if available
146         for file in filestodiff:
147             fp.write(calculate_diff(file, contextlines))
148             fp.write('\n')
149         fp.close()
150         # doesn't matter what code we return, it isn't waited on
151         os._exit(0)
152
153
154 \f
155 # scan args for options
156 def main():
157     contextlines = 2
158     try:
159         opts, args = getopt.getopt(sys.argv[1:], 'hC:cu',
160                                    ['context=', 'cvsroot=', 'help'])
161     except getopt.error, msg:
162         usage(1, msg)
163
164     # parse the options
165     for opt, arg in opts:
166         if opt in ('-h', '--help'):
167             usage(0)
168         elif opt == '--cvsroot':
169             os.environ['CVSROOT'] = arg
170         elif opt in ('-C', '--context'):
171             contextlines = int(arg)
172         elif opt == '-c':
173             if contextlines <= 0:
174                 contextlines = 2
175         elif opt == '-u':
176             contextlines = 0
177
178     # What follows is the specification containing the files that were
179     # modified.  The argument actually must be split, with the first component
180     # containing the directory the checkin is being made in, relative to
181     # $CVSROOT, followed by the list of files that are changing.
182     if not args:
183         usage(1, 'No CVS module specified')
184     SUBJECT = args[0]
185     specs = string.split(args[0])
186     del args[0]
187
188     # The remaining args should be the email addresses
189     if not args:
190         usage(1, 'No recipients specified')
191
192     # Now do the mail command
193     PEOPLE = string.join(args)
194     mailcmd = MAILCMD % vars()
195
196     print 'Mailing %s...' % PEOPLE
197     if specs == ['-', 'Imported', 'sources']:
198         return
199     if specs[-3:] == ['-', 'New', 'directory']:
200         del specs[-3:]
201     elif len(specs) > 2:
202         L = specs[:2]
203         for s in specs[2:]:
204             prev = L[-1]
205             if string.count(prev, ",") < 2:
206                 L[-1] = "%s %s" % (prev, s)
207             else:
208                 L.append(s)
209         specs = L
210     print 'Generating notification message...'
211     blast_mail(mailcmd, specs[1:], contextlines)
212     print 'Generating notification message... done.'
213
214
215 \f
216 if __name__ == '__main__':
217     main()
218     sys.exit(0)