Björn Stenberg | 0375f31 | 2002-04-11 08:49:16 +0000 | [diff] [blame] | 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 | |
| 82 | def usage(code, msg=''): |
| 83 | print __doc__ % globals() |
| 84 | if msg: |
| 85 | print msg |
| 86 | sys.exit(code) |
| 87 | |
| 88 | |
| 89 | |
| 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 = "-uN" |
Björn Stenberg | 6ec42d9 | 2002-05-23 12:06:31 +0000 | [diff] [blame] | 118 | diffcmd = '/usr/bin/cvs -f diff -kk %s -b -r %s -r %s %s' % ( |
Björn Stenberg | 0375f31 | 2002-04-11 08:49:16 +0000 | [diff] [blame] | 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 | |
| 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 | |
| 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 | print 'Generating notification message...' |
| 202 | blast_mail(mailcmd, specs[1:], contextlines) |
| 203 | print 'Generating notification message... done.' |
| 204 | |
| 205 | |
| 206 | |
| 207 | if __name__ == '__main__': |
| 208 | main() |
| 209 | sys.exit(0) |
| 210 | |