a loginfo/log_accum/commitprep script for cvs to email commit logs and diffs upon commits
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}