a loginfo/log_accum/commitprep script for cvs to email commit logs and diffs upon commits
at master 415 lines 11 kB view raw
1#!/usr/bin/perl 2# $Id: loginfo.pl,v 1.20 2007/01/18 06:52:00 jcs Exp $ 3# vim:ts=4 4# 5# loginfo.pl 6# a cvs loginfo script to handle changelog writing and emailing, similar to 7# the log_accum script included with cvs, but not nearly as hideous. also 8# supports emailing diffs and rdiff/cvsweb information. 9# 10# Copyright (c) 2004-2007 joshua stein <jcs@jcs.org> 11# 12# Redistribution and use in source and binary forms, with or without 13# modification, are permitted provided that the following conditions 14# are met: 15# 16# 1. Redistributions of source code must retain the above copyright 17# notice, this list of conditions and the following disclaimer. 18# 2. Redistributions in binary form must reproduce the above copyright 19# notice, this list of conditions and the following disclaimer in the 20# documentation and/or other materials provided with the distribution. 21# 3. The name of the author may not be used to endorse or promote products 22# derived from this software without specific prior written permission. 23# 24# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR 25# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 26# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 27# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34# 35 36# 37# to process all subdirectories at once, this script will need to be called 38# from commitinfo in "prep" mode (emulating commit_prep): 39# ALL perl $CVSROOT/CVSROOT/loginfo.pl -p 40# 41# then call the script normally from loginfo: 42# ALL perl $CVSROOT/CVSROOT/loginfo.pl -c $CVSROOT/CVSROOT/ChangeLog \ 43# -C http://example.com/cgi-bin/cvsweb.cgi -D \ 44# -m somelist@example.com -d %{sVv} 45# 46 47use strict; 48 49my ($curdir, $donewdir, $doimport, $lastdir, $prepdir, $module, $branch); 50my (@diffcmds, @diffrevs, %modfiles, %addfiles, %delfiles, @message, @log); 51 52# configuration options taken from args 53my ($changelog, $cvsweburibase, $dodiffs, $dordiffcmds, @emailrecips); 54 55# temporary files used between runs 56my $tmpdir = "/tmp"; 57my $tmp_lastdir = ".cvs.lastdir" . getpgrp(); 58my $tmp_modfiles = ".cvs.modfiles" . getpgrp(); 59my $tmp_addfiles = ".cvs.addfiles" . getpgrp(); 60my $tmp_delfiles = ".cvs.delfiles" . getpgrp(); 61my $tmp_diffcmd = ".cvs.diffcmd" . getpgrp(); 62 63while (@ARGV) { 64 if ($ARGV[0] eq "-p") { 65 # prep mode: only args should be a directory (and a file we don't use) 66 $prepdir = $ARGV[1]; 67 68 # remove the cvsroot and slash 69 $prepdir = substr($prepdir, length($ENV{"CVSROOT"}) + 1); 70 71 last; 72 } 73 74 # configuration options 75 elsif ($ARGV[0] eq "-c") { 76 $changelog = $ARGV[1]; 77 shift(@ARGV); 78 } elsif ($ARGV[0] eq "-C") { 79 $cvsweburibase = $ARGV[1]; 80 shift(@ARGV); 81 } elsif ($ARGV[0] eq "-d") { 82 $dodiffs = 1; 83 # no args 84 } elsif ($ARGV[0] eq "-D") { 85 $dordiffcmds = 1; 86 # no args 87 } elsif ($ARGV[0] eq "-m") { 88 push @emailrecips, $ARGV[1]; 89 shift(@ARGV); 90 } 91 92 # args passed by cvs 93 elsif ($ARGV[0] =~ /^(.+) - New directory$/) { 94 $donewdir = $1; 95 96 if ($donewdir =~ /^(.+?)\/(.+)/) { 97 $module = $1; 98 $donewdir = $2; 99 } 100 101 last; 102 } elsif ($ARGV[0] =~ /^(.+) - Imported sources$/) { 103 $doimport = $1; 104 105 if ($doimport =~ /^(.+?)\/(.+)/) { 106 $module = $1; 107 $doimport = $2; 108 } 109 110 last; 111 } else { 112 # read list of files, assuming a format of %{sVv} giving us: 113 # something/here/blah file1,1.1,1.2 file2,NONE,1.3 114 115 $ARGV[0] =~ /^(.+?) (.+)/; 116 117 # our "module" is the first component of the path 118 $module = (split("/", $1))[0]; 119 120 # and our current directory is the rest of the path 121 $curdir = (split("/", $1, 2))[1]; 122 if ($curdir eq "") { 123 $curdir = "."; 124 } 125 126 # our files are the second part of argv[0] 127 my $filelist = $2; 128 129 # init 130 @{$modfiles{$curdir}} = @{$addfiles{$curdir}} = 131 @{$delfiles{$curdir}} = (); 132 133 # read list of changed files and their versions 134 while ($filelist =~ /^((.+?),([\d\.]+|NONE),([\d\.]+|NONE))($| (.+))/) { 135 $filelist = $6; 136 137 if ($3 eq "NONE") { 138 push @{$addfiles{$curdir}}, $2; 139 } elsif ($4 eq "NONE") { 140 push @{$delfiles{$curdir}}, $2; 141 } else { 142 push @{$modfiles{$curdir}}, $2; 143 push @diffcmds, "-r" . $3 . " -r" . $4 . " " . $module . "/" 144 . ($curdir eq "." ? "" : $curdir . "/") . $2; 145 } 146 } 147 } 148 shift(@ARGV); 149} 150 151if ($prepdir) { 152 # if we're in prep dir, just record this as the last directory we've seen 153 # and exit 154 unlink($tmpdir . "/" . $tmp_lastdir); 155 156 # XXX: this is not safe 157 open(LASTDIR, ">" . $tmpdir . "/" . $tmp_lastdir) or 158 die "can't prep to " . $tmpdir . "/" . $tmp_lastdir . ": " . $!; 159 print LASTDIR $prepdir . "\n"; 160 close(LASTDIR); 161 162 exit; 163} 164 165if ($donewdir eq "") { 166 if ($doimport eq "") { 167 # we're in loginfo mode, so read the last directory prep mode found 168 open(LASTDIR, "<" . $tmpdir . "/" . $tmp_lastdir) or 169 die "can't read " . $tmpdir . "/" . $tmp_lastdir . ": " . $!; 170 chop($lastdir = <LASTDIR>); 171 close(LASTDIR); 172 } 173 174 # read log message 175 my $startlog = 0; 176 while (my $line = <STDIN>) { 177 if ($startlog) { 178 push @log, $line; 179 180 # specifying 'nodiff' in the cvs log will disable sending diffs for 181 # this commit, useful if the commit includes sensitive information 182 # or if the diff will be huge 183 if ($line =~ /nodiff/i) { 184 $dodiffs = 0; 185 } 186 } else { 187 if ($line =~ /^[ \t]+Tag: (.+)/) { 188 $branch = $1; 189 } elsif ($line =~ /^Log Message:/) { 190 $startlog++; 191 } 192 } 193 } 194 195 # remove trailing empty lines from the log 196 for (my $x = $#log; $x >= 0; $x--) { 197 if ($log[$x] eq "" || $log[$x] eq "\n") { 198 pop(@log); 199 } else { 200 last; 201 } 202 } 203 204 # dump what we have 205 foreach my $dir (keys %modfiles) { 206 my @files = @{$modfiles{$dir}}; 207 add_formatted_files($dir, \@files, $tmp_modfiles); 208 } 209 210 foreach my $dir (keys %addfiles) { 211 my @files = @{$addfiles{$dir}}; 212 add_formatted_files($dir, \@files, $tmp_addfiles); 213 } 214 215 foreach my $dir (keys %delfiles) { 216 my @files = @{$delfiles{$dir}}; 217 add_formatted_files($dir, \@files, $tmp_delfiles); 218 } 219 220 if ($#diffcmds > -1) { 221 open(DIFFCMDS, ">>" . $tmpdir . "/" . $tmp_diffcmd) or 222 die "can't append to " . $tmpdir . "/" . $tmp_diffcmd . ": " . $!; 223 print DIFFCMDS join("\n", @diffcmds) . "\n"; 224 close(DIFFCMDS); 225 } 226 227 if ($doimport eq "") { 228 # we have more directories to look at 229 if ($module . ($curdir eq "." ? "" : "/" . $curdir) ne $lastdir) { 230 exit; 231 } 232 } 233} 234 235# start building e-mail header 236my ($login, $gecos, $fullname, $email); 237 238# determine our user 239if (($login = $ENV{"USER"}) ne "") { 240 $gecos = (getpwnam($login))[6]; 241} else { 242 ($login, $gecos) = (getpwuid ($<))[0,6]; 243} 244$fullname = $gecos; 245$fullname =~ s/,.*//; 246chop(my $hostname = `hostname`); 247$email = $login . "\@" . $hostname; 248 249push @message, "CVSROOT: " . $ENV{"CVSROOT"} . "\n"; 250push @message, "Module name: " . $module . "\n"; 251if ($branch) { 252 push @message, "Branch: " . $branch . "\n"; 253} 254my ($sec, $min, $hour, $mday, $mon, $year) = localtime(time); 255push @message,"Changes by: " . $email . " " 256 . sprintf("%02d/%02d/%02d %02d:%02d:%02d", ($year % 100), ($mon + 1), 257 $mday, $hour, $min, $sec) . "\n"; 258 259push @message, "\n"; 260 261if ($donewdir ne "") { 262 push @message, "Created directory " . $donewdir . "\n"; 263} else { 264 # add file groups 265 if (-f $tmpdir . "/" . $tmp_modfiles) { 266 push @message, "Modified files:\n"; 267 268 open(MODFILES, "<" . $tmpdir . "/" . $tmp_modfiles) or 269 die "can't read from " . $tmpdir . "/" . $tmp_modfiles . ": " . $!; 270 while (my $line = <MODFILES>) { 271 push @message, $line; 272 } 273 close(MODFILES); 274 } 275 if (-f $tmpdir . "/" . $tmp_addfiles) { 276 push @message, "Added files:\n"; 277 278 open(ADDFILES, "<" . $tmpdir . "/" . $tmp_addfiles) or 279 die "can't read from " . $tmpdir . "/" . $tmp_addfiles . ": " . $!; 280 while (my $line = <ADDFILES>) { 281 push @message, $line; 282 } 283 close(ADDFILES); 284 } 285 if (-f $tmpdir . "/" . $tmp_delfiles) { 286 push @message, "Removed files:\n"; 287 288 open(DELFILES, "<" . $tmpdir . "/" . $tmp_delfiles) or 289 die "can't read from " . $tmpdir . "/" . $tmp_delfiles . ": " . $!; 290 while (my $line = <DELFILES>) { 291 push @message, $line; 292 } 293 close(DELFILES); 294 } 295 296 if ($doimport eq "") { 297 push @message, "\n"; 298 } 299 300 push @message, "Log message:\n"; 301 foreach my $line (@log) { 302 push @message, " " . $line; 303 } 304} 305 306# if we're saving to a changelog, do it now before we add the diffs 307if ($changelog) { 308 open(CHANGELOG, ">>" . $changelog) or 309 warn "can't write to " . $changelog . ": " . $!; 310 foreach my $line (@message) { 311 print CHANGELOG $line; 312 } 313 print CHANGELOG "\n"; 314 close(CHANGELOG); 315} 316 317if ($donewdir eq "" && ($dodiffs || $dordiffcmds || $cvsweburibase) && 318-f $tmpdir . "/" . $tmp_diffcmd) { 319 # generate diffs and record revision numbers 320 @diffcmds = @diffrevs = (); 321 open(DIFFCMDS, "<" . $tmpdir . "/" . $tmp_diffcmd) or 322 die "can't read " . $tmpdir . "/" . $tmp_diffcmd . ": " . $!; 323 while (chop(my $line = <DIFFCMDS>)) { 324 push @diffcmds, "cvs -nQq rdiff -u " . $line; 325 326 if ($cvsweburibase) { 327 my ($r1, $r2, $mod) = $line =~ /^-r([0-9\.]+) -r([0-9\.]+) (\S+)/; 328 push @diffrevs, [ $r1, $r2, $mod ]; 329 } 330 } 331 close(DIFFCMDS); 332 333 if ($#diffcmds > -1) { 334 if ($dordiffcmds) { 335 push @message, "\n" 336 . "Diff commands:\n"; 337 push @message, join("\n", @diffcmds) . "\n"; 338 } 339 340 if ($cvsweburibase) { 341 push @message, "\n" 342 . "CVSWeb:\n"; 343 344 foreach my $diffrevr (@diffrevs) { 345 my ($r1, $r2, $mod) = @{$diffrevr}; 346 347 # encode special chars in filenames for valid urls 348 $mod =~ s/([^\w\/\.-])/sprintf("%%%02X", ord($1))/seg; 349 350 push @message, $cvsweburibase . "/" . $mod . "?r1=" . $r1 351 . ";r2=" . $r2 . "\n"; 352 } 353 } 354 355 if ($dodiffs) { 356 push @message, "\n" 357 . "Diffs:\n"; 358 359 foreach my $diffcmd (@diffcmds) { 360 my @args = split(" ", $diffcmd, 7); 361 open(DIFF, "-|") || exec @args; 362 while (my $line = <DIFF>) { 363 push @message, $line; 364 } 365 close(DIFF); 366 } 367 } 368 } 369} 370 371# send emails 372foreach my $recip (@emailrecips) { 373 open(SENDMAIL, "| /usr/sbin/sendmail -t") or 374 die "can't run sendmail: " . $!; 375 print SENDMAIL "From: " . $fullname . " <" . $email . ">\n"; 376 print SENDMAIL "Reply-To: " . $email . "\n"; 377 print SENDMAIL "To: " . $recip . "\n"; 378 print SENDMAIL "Subject: CVS: " . $hostname . ": " . $module . "\n"; 379 print SENDMAIL "\n"; 380 foreach my $line (@message) { 381 print SENDMAIL $line; 382 } 383 close(SENDMAIL); 384} 385 386# clean up 387unlink($tmpdir . "/" . $tmp_lastdir); 388unlink($tmpdir . "/" . $tmp_modfiles); 389unlink($tmpdir . "/" . $tmp_addfiles); 390unlink($tmpdir . "/" . $tmp_delfiles); 391unlink($tmpdir . "/" . $tmp_diffcmd); 392 393exit; 394 395sub add_formatted_files { 396 my $dir = $_[0]; 397 my $files = $_[1]; 398 my $tmpfile = $_[2]; 399 400 if (@$files) { 401 open(FILES, ">>" . $tmpdir . "/" . $tmpfile) or 402 die "can't append to " . $tmpdir . "/" . $tmpfile . ": " . $!; 403 404 print FILES " " . $dir . (" " x (15 - length($dir))) . " :"; 405 406 # TODO: wrap files and indent 407 foreach my $file (@$files) { 408 print FILES " " . $file; 409 } 410 411 print FILES "\n"; 412 413 close(FILES); 414 } 415}