#!/usr/bin/perl -w ##--------------------------------------------------------------------------## ## File: ## $Id: svnrreport 142 2013-12-31 03:47:31Z ehood $ ## Description: ## Generate report files of revisions from a remote subversion ## repository. ## See POD after __END__. ##--------------------------------------------------------------------------## ## Copyright (C) 2013 Earl Hood ## ## This program is free software; you can redistribute it and/or ## modify it under the same terms as Perl itself. ##--------------------------------------------------------------------------## use Data::Dumper; use FileHandle; use Getopt::Long; use constant LOCK_MODE_DIR => 0; use constant LOCK_MODE_FLOCK => 1; my %opt = (); my $D = 0; # Debug flag my $DO_DIFF = 1; my $INC_REPLY_TO = 1; my $INC_TO = 0; my $MAIL_DOMAIN = ''; my $MAIL_EXE = 'sendmail -i'; my $NOTIFY = 0; my $NOTIFY_TO = ""; my $NOTIFY_FROM = ""; my $OUTDIR = '.'; my $REPO_INFO = {}; my $REV_CUR = 0; my $REV_END = 0; my $REV_START = 1; my $STATE_FILE = '.svnrreport'; my $STDOUT = \*STDOUT; my $SUBJECT_PREFIX = '[SVN] '; my $SVN_EXE = 'svn'; my $USER_MAP = {}; my $V = 0; # Verbose flag my $ISLOCK = 0; my $LOCK_FILE = ".svnrreport.lck"; my $LOCK_TRIES = 10; my $LOCK_SLEEP = 5; my $LOCK_FORCE = 0; my $_lock_file = undef; my $_flock_fh = undef; my $LockFunc = undef; my $UnlockFunc = undef; ############################################################################# MAIN: { $Data::Dumper::Terse = 1; $Data::Dumper::Indent = 1; set_lock_mode(LOCK_MODE_FLOCK); GetOptions(\%opt, 'config=s', # Pathname to configuration file 'domain=s', # Default mail domain 'diff!', # Do diff or not 'mailexe=s', # Program for sending mail 'notify!', # To notify via email (best with -update) 'outdir=s', # Location to put output 'incto!', # Should To: header be included in message 'increplyto!',# Should Reply-To: header be included in message 'repo=s', # URL to repo 'r=s', # Revision start, or range 'statefile=s',# File containing last execution state 'subjectprefix=s', # File containing last execution state 'svnexe=s', # svn command-line executable to use 'to=s@', # Notify recipients 'from=s', # From address for notify 'update!', # Update archive 'usermap=s', # File mapping user names to addresses 'verbose', # Print out progress 'debug', # Print debugging info. 'lockfile=s', # Lock filename 'force', # Force lock 'help|?', # Brief help message 'man' ); usage(1, 0) if ($opt{'help'}); usage(2, 0) if ($opt{'man'}); $D = $opt{'debug'} || $D; $V = $opt{'verbose'} || $V || $D; $OUTDIR = $opt{'outdir'} || $OUTDIR; $REPO = $opt{'repo'} || $REPO; $SVN_EXE = $opt{'svnexe'} || $SVN_EXE; $STATE_FILE = $opt{'statefile'} || $STATE_FILE; $STATE_FILE = join("/", $OUTDIR, $STATE_FILE); $LOCK_FILE = $opt{'lockfile'} || $LOCK_FILE; $LOCK_FILE = join("/", $OUTDIR, $LOCK_FILE); $LOCK_FORCE = defined($opt{'force'}) ? $opt{'force'} : $LOCK_FORCE; if (!lock_archive()) { die qq/Unable to lock archive "$OUTDIR"\n/; } my $is_update = $opt{'update'}; my $r; if ($is_update) { if (! -e $STATE_FILE) { warn qq/Warning: State file "$STATE_FILE" does not exist\n/; } else { my $state = require($STATE_FILE); $REPO = $state->{'repo'}; $REV_CUR = $state->{'rev'}; $REV_START = $state->{'rev'}+1; $DO_DIFF = $state->{'diff'}; $MAIL_DOMAIN = $state->{'domain'}; $MAIL_EXE = $state->{'mailexe'}; $NOTIFY = $state->{'notify'}; $NOTIFY_FROM = $state->{'notifyfrom'}; $NOTIFY_TO = $state->{'notifyto'}; $INC_TO = $state->{'incto'} || $INC_TO; $INC_REPLY_TO = $state->{'increplyto'} || $INC_REPLY_TO; $SUBJECT_PREFIX = $state->{'subjectprefix'}; $USER_MAP = $state->{'usermap'} || $USER_MAP; } } $DO_DIFF = defined($opt{'diff'})? $opt{'diff'}: $DO_DIFF; $MAIL_EXE = $opt{'mailexe'} || $MAIL_EXE; $NOTIFY = defined($opt{'notify'}) ? $opt{'notify'} : $NOTIFY; $NOTIFY_FROM = defined($opt{'from'}) ? $opt{'from'} : $NOTIFY_FROM; $NOTIFY_TO = defined($opt{'to'}) ? join(', ', @{$opt{'to'}}) : $NOTIFY_FROM; $MAIL_DOMAIN = defined($opt{'domain'}) ? $opt{'domain'} : $MAIL_DOMAIN; $SUBJECT_PREFIX = defined($opt{'subjectprefix'}) ? $opt{'subjectprefix'} : $SUBJECT_PREFIX; $INC_TO = defined($opt{'incto'}) ? $opt{'incto'} : $INC_TO; $INC_REPLY_TO = defined($opt{'increplyto'}) ? $opt{'increplyto'} : $INC_REPLY_TO; if ($opt{'usermap'}) { $USER_MAP = read_user_map($opt{'usermap'}); } verbose(qq/Getting information for "$REPO".../) if $V; $REPO_INFO = svn_info($REPO); if ($REPO_INFO->{'repository root'} ne $REPO) { # Make sure we are working with the repo root URL $REPO = $REPO_INFO->{'repository root'}; $REPO_INFO = svn_info($REPO); } if ($opt{'r'}) { $r = $opt{'r'}; my($rstart, $rend) = split(/:/, $r, 2); if ($rstart ne '') { $REV_START = $rstart; } $REV_END = $rend; } debug('$REPO_INFO='.Dumper($REPO_INFO)) if $D; $REV_END = $REPO_INFO->{'revision'} unless $REV_END; verbose("Running report from revision $REV_START to $REV_END") if $V; eval { for ($r=$REV_START; $r <= $REV_END; ++$r) { run_report($r); $REV_CUR = $r; } }; my $status = $@; my $errno = $!; if ($status) { warn "$status\n"; } unlock_archive(); save_state(); exit(($status)? 1: 0); } sub run_report { my $r = shift; my $outfile = join('/', $OUTDIR, $r); local(*OUT); verbose(qq/Running report on revision $r to file "$outfile".../) if $V; my $log_info = svn_log($r); open(OUT, '>'.$outfile) || die qq/ERROR: Unable to open "$outfile": $!\n/; # Parse date from svn log my($time, $zone, $date) = $log_info->{'date'} =~ /(\d+:\d+:\d+)\s+([\-+]\d+)\s+\(([^)]+)\)/; my $commit_user = $log_info->{'user'}; my $mail_date = join('', $date, ' ', $time, ' ', $zone); my $mail_from = add_mail_domain( $NOTIFY_FROM || getlogin() || scalar(getpwuid($<)), $MAIL_DOMAIN); my $mail_reply = add_mail_domain( map_commit_user($commit_user) || $mail_from, $MAIL_DOMAIN); my $mail_to = add_mail_domain( ($NOTIFY_TO || $mail_from), $MAIL_DOMAIN); my $mail_subject = join('', $SUBJECT_PREFIX, "(", $log_info->{'user'}, ") ", $log_info->{'rev'}, " - ", $REPO); print OUT 'To: ', $mail_to, "\n" if ($INC_TO); print OUT 'From: ', $mail_from, "\n"; print OUT 'Reply-To: ', $mail_reply, "\n" if ($INC_REPLY_TO); print OUT 'Subject: ', $mail_subject, "\n"; print OUT 'Date: ', $mail_date, "\n"; print OUT "\n"; print OUT $log_info->{'log'}; svn_diff(\*OUT, $r, $log_info) if $DO_DIFF; close(OUT); if ($NOTIFY) { mail_file($outfile); } } sub save_state { my $state = { rev => $REV_CUR, repo => $REPO, mailexe => $MAIL_EXE, diff => $DO_DIFF, notify => $NOTIFY, notifyto => $NOTIFY_TO, notifyfrom => $NOTIFY_FROM, incto => $INC_TO, increplyto => $INC_REPLY_TO, subjectprefix => $SUBJECT_PREFIX, usermap => $USER_MAP, }; local(*STATE); verbose(qq/Saving state to "$STATE_FILE".../) if $V; open(STATE, '>'.$STATE_FILE) || die qq/ERROR: Unable to create "$STATE_FILE": $!\n/; print STATE '+'; print STATE Dumper($state); print STATE ';'; close(STATE); } sub cmd_pipe_open { my $handle = shift; my @cmd = @_; my $child_pid = open($handle, '-|'); if ($child_pid) { # parent return $handle; } else { # child #open(STDERR, '>&STDOUT'); exec(@cmd) || die qq/ERROR: Cannot exec "@cmd": $!\n/; } } sub cmd_pipe_create { my $handle = shift; my @cmd = @_; my $child_pid = open($handle, '|-'); if ($child_pid) { # parent return $handle; } else { # child #open(STDERR, '>&STDOUT'); exec(@cmd) || die qq/ERROR: Cannot exec "@cmd": $!\n/; } } sub svn_info { my $url = shift; $url = '.' unless $url; my $fh = svn_exe('info', $url); my $fields = read_fields($fh); close($fh); return $fields; } sub svn_log { my $r = shift; my $results = { modified => [], # Array of modified lists. }; my $log = ""; my $line; my $fh = svn_exe('log', '-v', '-r', $r, $REPO); # Skip initial dashes $line = <$fh>; $log .= $line; # Read revision line $line = <$fh>; $log .= $line; my ($rev, $user, $date, $rest) = split(/\s+\|\s+/, $line, 4); $results->{'rev'} = $rev; $results->{'user'} = $user; $results->{'date'} = $date; while (defined($line = <$fh>)) { $log .= $line; last if $line =~ /\S/; } if ($line =~ /changed paths:/i) { # Algorithm: We extract entries that are marked modified for subsequent # diff operation. However, repo-side diff fails to provide full the # resource path, so in later post-diff processing (see svn_diff()), we # will insert the full paths. # # Unfortunately, there is a problem when two consecutive resources have # the same basename, making the post-diff processing step problematic. # To get around this problem, when we extract the modified list and # discover consecutive basenames, we start a new list. The final end # result is then a list of lists, where 'svn diff' will be executed on # each list to get the complete diff set of the revision. # # An alternative solution would be to run 'svn diff' separately on each # modified resource, but this can be incredibly expensive since it will # require a separate server request for each resource. my $modified = $results->{'modified'}; my $curlist = []; push(@{$modified}, $curlist); my $last_basename = undef; while (defined($line = <$fh>)) { $log .= $line; last if $line !~ /\S/; chomp $line; my($indicators, $path) = split(/\s+\//, $line, 2); #debug("svn_log: Path line: ($indicators) $path") if $D; if ($indicators =~ /M/) { debug("svn_log: Adding to modified list: $path") if $D; if ($path =~ m{/([^/]+)\Z}) { my $basename = $1; if (defined($last_basename) && ($basename eq $last_basename)) { debug(qq/svn_log: Consecutive basename match "$basename": $path/) if $D; $curlist = []; push(@{$modified}, $curlist); } $last_basename = $basename; } push(@{$curlist}, $path); } } } while (defined($line = <$fh>)) { $log .= $line; } close($fh); $results->{'log'} = $log; $results; } sub svn_diff { my $outfh = shift; my $r = shift; my $log_info = shift; my @modified = @{$log_info->{'modified'}}; if (($r <= 1) || (scalar(@modified) < 1) || (scalar(@{$modified[0]}) < 1)) { return; } my $old_r = $r - 1; my $old = $REPO.'@'.$old_r; my $new = $REPO.'@'.$r; print $outfh "\nContext diff:\n"; print $outfh " Old: $old\n"; print $outfh " New: $new\n"; print $outfh "\n"; # Loop thru each modified list to generate diff foreach my $aref (@modified) { my @list = @{$aref}; # # The '--depth empty' option is *important*. Due to limitations of # the 'svn log' output format, we do not get a clear indication if a # resource is a directory, but it can be marked modified if property # changes were done on it. The empty depth prevents a diff being done # on all the directories descendents. We want to control which # descendents are diffed. # my $fh = svn_exe('diff', "--depth", "empty", "--old=$old", "--new=$new", @list); local($_); my $eol = "\n"; my $last_file = ""; while (<$fh>) { # # Make sure full pathname is listed for file being diffed. The diff # output generated by the 'svn diff' command will only contain the # basename of each resource. # # Note, we have to check for "Property changes" and "Index" since a # resource may be applicable for either one, or both. # if (/^(Property changes on: )(.+)$/) { if ($2 ne $last_file) { $last_file = $2; $_ = qq/$1/.shift(@list).get_eol($_); } else { $_ = qq/$1/.$last_file.get_eol($_); } } elsif (/^(Index: )(.+)$/) { if ($2 ne $last_file) { $last_file = $2; $_ = qq/$1/.shift(@list).get_eol($_); } else { $_ = qq/$1/.$last_file.get_eol($_); } } print $outfh $_; } close($fh); } } sub svn_exe { my @cmd = ($SVN_EXE, @_); my $fh = new FileHandle; debug("Executing: ".join(' ', @cmd)) if $D; return cmd_pipe_open($fh, @cmd); } sub mail_file { local(*IN); local(*MAIL); my $file = shift; my @recipients = add_mail_domain($NOTIFY_TO, $MAIL_DOMAIN); my $cmd = join(" ", $MAIL_EXE, @recipients); verbose("Sending mail notification to ".$NOTIFY_TO) if $V; open(IN, $file) || die qq/Unable to open "$file" for reading: $!\n/; debug("Executing: ".$cmd) if $D; open(MAIL, '|'.$cmd) || die qq/Unable to open pipe to "$cmd": $!\n/; print MAIL ; close(IN); close(MAIL); } sub read_fields { my $fh = shift; my $fields = { }; my $line; while (defined($line = <$fh>)) { next unless $line =~ /\S/; chomp $line; my($name, $value) = split(/:\s*/, $line, 2); debug("read_fields: $name=$value") if $D; $fields->{lc $name} = $value; # we normalize field names to lowecase } return $fields; } sub read_user_map { my $file = shift; my $map = {}; local(*MAP); local($_); open(MAP, $file) || die qq/ERROR: Unable to open "$file": $!\n/; while () { next unless /\S/; next if /^\s*#/; s/\s+\z//; s/^\s+//; if (/^\s*(\S+)\s*:\s*(\S+)/) { $map->{$1} = $2; } } close(MAP); $map; } sub map_commit_user { my $user = shift; $USER_MAP->{$user} || $USER_MAP->{''} || $user; } sub add_mail_domain { my $to = shift; my $domain = shift; my @a = map { if (($domain ne '') && !/@/) { $_.'@'.$domain; } else { $_; } } split(/,/, $to); if (wantarray) { @a; } else { join(", ", @a); } } sub get_eol { if ($_[0] =~ /([\r\n]+)\z/) { return $1; } "\n"; } sub verbose { if ($V) { print $STDOUT @_, "\n"; } } sub debug { if ($D) { print $STDOUT "DEBUG: ",@_,"\n"; } } sub usage { require Pod::Usage; my $verbose = shift || 0; my $exit_code = shift; if ($verbose == 0) { Pod::Usage::pod2usage(-verbose => $verbose); } else { my $pager = $ENV{'PAGER'} || 'more'; local(*PAGER); my $fh = (-t STDOUT && open(PAGER, "|$pager")) ? \*PAGER : \*STDOUT; Pod::Usage::pod2usage(-verbose => $verbose, -output => $fh); close(PAGER) if ($fh == \*PAGER); } defined($exit_code) && exit($exit_code); } sub lock_archive { verbose(qq/Locking "$OUTDIR".../) if $V; return &$LockFunc($LOCK_FILE, $LOCK_TRIES, $LOCK_SLEEP, $LOCK_FORCE); } sub unlock_archive { verbose(qq/Unlocking "$OUTDIR".../) if $V; &$UnlockFunc; } ############################################################################# ## Locking Routines: Copied from MHonArc, but we default to flock method ## for this script. ############################################################################# ##--------------------------------------------------------------------------- ## set_lock_mode(): Set locking method. ## sub set_lock_mode { my $mode = shift; if ($mode =~ /\D/) { STR2NUM: { if ($mode =~ /^\s*flock/) { $mode = LOCK_MODE_FLOCK; last STR2NUM; } $mode = LOCK_MODE_DIR; last STR2NUM; } } if ($mode == LOCK_MODE_FLOCK) { $LockFunc = \&flock_file; $UnlockFunc = \&unflock_file; return $mode; } $mode = LOCK_MODE_DIR; $LockFunc = \&create_lock_dir; $UnlockFunc = \&remove_lock_dir; $mode; } ############################################################################# ## Directory Method of Locking Functions ############################################################################# ##--------------------------------------------------------------------------- ## create_lock_dir() creates a directory to act as a lock. ## sub create_lock_dir { my($file, $tries, $sleep, $force) = @_; my $prtry = 0; my $ret = 0; $_lock_file = $file; while ($tries > 0) { if (mkdir($file, 0777)) { $ISLOCK = 1; $ret = 1; last; } sleep($sleep) if $sleep > 0; $tries--; if (!$prtry && ($tries > 0)) { print STDOUT qq/Trying to create lock ...\n/ unless $QUIET; $prtry = 1; } } if ($force) { $ISLOCK = 1; $ret = 1; } $ret; } ##--------------------------------------------------------------------------- ## remove_lock_dir removes the lock directory ## sub remove_lock_dir { if ($ISLOCK) { if (!rmdir($_lock_file)) { warn "Warning: Unable to remove $_lock_file: $!\n"; return 0; } $ISLOCK = 0; } 1; } ############################################################################# ## Flock Functions ############################################################################# ##--------------------------------------------------------------------------- ## flock_file(): Create archive lock using flock(2). ## sub flock_file { my($file, $tries, $sleep, $force) = @_; eval { require Symbol; require Fcntl; Fcntl->import(':DEFAULT', ':flock'); }; if ($@) { warn qq/Warning: Unable to require modules for flock() lock method: /, qq/$@\n/, qq/\tFalling back to directory method.\n/; set_lock_mode(LOCK_MODE_DIR); return &$LockFunc(@_); } $_lock_file = $file; $_flock_fh = Symbol::gensym; if (!sysopen($_flock_fh, $file, (&O_WRONLY|&O_CREAT), 0666)) { warn(qq/ERROR: Unable to create "$file": $!\n/); return 0; } my $prtry = 0; my $ret = 0; while ($tries > 0) { if (flock($_flock_fh, &LOCK_EX|&LOCK_NB)) { $ISLOCK = 1; $ret = 1; last; } sleep($sleep) if $sleep > 0; $tries--; if (!$prtry && ($tries > 0)) { print STDOUT qq/Trying to create lock ...\n/ unless $QUIET; $prtry = 1; } } if (!$ISLOCK && $force) { $_flock_fh = undef; $ISLOCK = 1; $ret = 1; } $ret; } ##--------------------------------------------------------------------------- sub unflock_file { if (defined($_flock_fh)) { flock($_flock_fh, &LOCK_UN); close($_flock_fh); $_flock_fh = undef; } $ISLOCK = 0; } ##--------------------------------------------------------------------------- ############################################################################# __END__ =head1 NAME svnrreport - Generate revision reports against a remote subversion repository =head1 SYNOPSIS S> [I] =head1 DESCRIPTION B is a Perl program for generating revision reports (e.g. log messages and diffs) against a remote subversion repository. If one has local administrator access to the repository, such a facility can be done via post-commit hooks. However, if you do not have such access and are unable to get those who do to set up such a facility, you can use B. To best illustrate how to use the program, we will start off with an example: svnrrreport -outdir out \ -repo https://svn.example.com/repo \ -to me@example.com \ -from me@example.com In the example provided, reports for all revisions will be written to the directory called C. The C<-r> option can be used to limit the revision range if desired (see L). For each revision, a file is created in the output location, where the name of each file is the revision number the report is associated with. The format of each file is in mail message format. For example: From: me@example.com Reply-To: me@example.com Subject: [SVN] (me) r6215 - https://svn.example.com/repo Date: Fri, 27 Dec 2013 16:43:17 -0600 [...log message for revision here...] [...context diffs of any modified files here...] The C is specified by the C<-from> option. The C is based on the commit user. See C<-domain> and C<-usermap> for more information on how the mail address is determined. The C field is based on the subversion user who committed the revision (denoted in the parentheses), the revision number, and the repository URL. The C field is set to the commit date of the revision. You may be asking the following, "What's with the mail header?" The reasons are as follows: =over =item * If the C<-notify> option is specified, the file is fed directly to the mail program to send notification to those specified by C<-to>. =item * I am a long-time user of B, L, and the output format and structure of the reports allows me to read the reports like any other mail message. =back =head1 OPTIONS =over =item -diff, -nodiff Include, or skip, context diff output for each revision. The default is to include the context diff. B: Context diffs are only done for files that are marked as modified for a revision. =item -domain The mail domain to use for mail addresses that do not include a domain component. If not specified, then no domain component is added to addresses that are missing it. Example: When the C field is printed, the address used is based on the commit user of the revision. So if the commit user is C and C is the specified domain by the C<-domain> option, then the reply-to address will be C. B: If subversion user identities are not equivalent to local names of user addresses, you can use the C<-usermap> option to map subversion user names to mail addresses. =item -force If unable to lock archive, still perform operation. To protect against concurrent B processes from operating on the same report archive, B will perform a lock before modifying the archive. =item -from I
From address to use in mail notifications if C<-notify> is specified. If no from address provided, then the login name of the user running this program is used. =item -increplyto, -noincreplyto Include the C field in the report. The default is to include the C field. B makes an attempt to set the reply-to address to the committer of the revision. Therefore, recipients of the notification can directly reply to the committer. =item -incto, -noincto Include, or not, the C field in the report. The default is to exclude the C field. B: When C<-notify> is enabled, B provides the recipient list to the mail program via the command-line, so the inclusion of the recipient addresses in the message is B required for delivery. B: The reason C is suppressed by default is that some MUAs (I'm talking to you GMail) may not honor the C field when C is present. =item -mailexe I Pathname to program for sending mail notifications. Default value is C. If B is not on your system, a good alternative is B. The program is invoked as follows: =over =item * Each recipient is specified as an argument to the program. =item * The mail message is passed provided via standard input to the mail program. =back The following is shell-equivalent example of how the mail program is invoked if the recipients are C and C: mailexe joe@example.com mary@example.com < file =item -outdir I Archive location to write revision reports to. Default valus is C<.> (the current working directory). =item -notify, -nonotify Send, or not, email notification for each revision. The default is to B send notification. =item -repo I URL to repository to generate reports against. This option should be specified the first time this program is invoked. =item -r I =item -r I:I Revision range to generate reports for. If not specified, reports starting from revision 1 to the latest is done. If just a number if provided, reports are generated starting with the given number to the latest. For example, the following will generate reports from revision 5000 to latest: -r 5000 If a range specification is provided, then reports are generated for revisions in the range, inclusively. For example, the following will generate reports from revision 5000 to 6000, inclusively: -r 5000:6000 B: If you want to create a report for a single revision, then provide a range where the start and end are the same. For example, the following will only generate a report for revision 5000: -r 5000:5000 If in update mode, see C<-update>, and you do not want to generate reports to the latest revision, but to a specific revision number, you can do something like the following: -r :6000 This says to generate reports from the last known revision to revision 6000. B: Svn revision keywords are B supported. Only numbers are supported. This program does B recognize the keywords C, C, C, C. =item -statefile I Filename of the file to store last execution state. The state file is stored relative to the output location specified by C<-outdir>. The default filename used is C<.svnrreport>. The state file is used when C<-update> is specified to recall what was the last revision processed. The state file also includes other option settings so such settings do not have to be respecified each time you run B. B: The state file contains a single Perl data structure. If you are careful, you can manually edit the file directly to make changes to options that will apply to future updates. B uses Perl's B operator to load the file. =item -subjectprefix I Prefix for C field of mail. Default value is C<[SVN] >. =item -svnexe I Name, or pathname, to subversion command-line client. The default value is C. B: The executable provided must support the standard C command-line client options. =item -to I
Recipient address to receive mail notifications, if C<-notify> is specified. Multiple addresses can be specified as follows: -to "joe@example.com, mary@example.com" or, -to joe@example.com -to mary@example.com =item -update, -noupdate Run, or not, in update mode. Default is no update. In update mode, the state file is first read to obtain the last revision reported against and start generating reports for subsequent revisions . B: If the state file is not present when C<-update> is specified, B will generate a warning but will try to continue processing. Therefore, you may want to prime the report archive like the following: svnrrreport -outdir out \ -repo https://svn.example.com/repo \ -to me@example.com \ -from me@example.com \ -r 1000 The above invocation initializes the report archive, starting with revision 1000. When you want to update the archive, you can then invoke B in update mode: svnrreport -outdir out -update If any other option is specified when C<-update> is used, it will override any previous value for the option. For example, if mail notifications were not enabled initially, you can enable them as follows: svnrreport -outdir out -update -notify =item -usermap I Pathname of file mapping names to mail addresses. The syntax of the file is pretty basic, as illustrated by the following example: # Comment lines are specified with the hash character # # The following maps "joe" to his mail address, # where a colon is used to delimit the name from # the address joe: joe.bloe@example.com If you create a mapping that does not include the domain portion of an address, then C<-domain> value will be used, if specified, to generate a full mail address. =item -verbose Prints out status messages during processing. =item -debug Prints out debugging messages during execution. =item -help Prints out the help message. =item -man Prints out the complete manual page. =back =head1 AUTOMATING REPORT GENERATION The best way to automate the generation of reports, and the sending of notifications if C<-notify> is specified, is to use a tool like B. To simplify the crontab entry for generating reports, a wrapper script can be used. Example: We create a wrapper script called C and place it at C<$HOME/cron/gen-svn-reports>. The body of the script is as follows: svnrreport \ -repo https://svn.example.com/repo -from me@example.com \ -to me@example.com \ -mailexe "ssmtp" \ -notify \ -update \ -outdir "$HOME/svn/reports" For your own script edit/add/remove options to reflect your needs. In our crontab, we add the following entry: 0,30 * * * 1-5 $HOME/cron/gen-svn-reports This entry says to run our script every half-hour, Monday through Friday (things are quiet on the weekends :). In your own crontab, set whatever frequency works best for you. =head1 NOTES =over =item * For this program to access subversion repositories, any credentials required for access must be cached by the svn client. If credentials are not cached, then you will need to write a wrapper to C that specifies the credentials. The C<-svnexe> option can be used to point to your wrapper. =item * When generating the context diff on only modified file of a revision, B attempts to do it with a single request to the repository. However, is some cases, multiple requests may be required. See the function C in the source code for the cases where multiple diff requests are required. =back =head1 PREREQUISITES Perl and the subversion, C, command-line client. =head1 AUTHOR Copyright (C) 2013, Earl Hood This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut