mirror of
https://gitlab.archlinux.org/archlinux/infrastructure.git
synced 2025-01-18 08:06:16 +01:00
This fixes an issue with utf8 encoding in From headers that starts appearing with perl 5.24. Dont't ask me why it worked before. Signed-off-by: Florian Pritz <bluewind@xinu.at>
1093 lines
39 KiB
Perl
Executable File
1093 lines
39 KiB
Perl
Executable File
#!/usr/bin/perl -w
|
|
our $ID = q($Id: svnlog,v 1.15 2009-02-28 23:02:39 eagle Exp $ );
|
|
#
|
|
# svnlog -- Mail Subversion commit notifications.
|
|
#
|
|
# Written by Russ Allbery <rra@stanford.edu>
|
|
# Copyright 2005, 2006, 2007, 2009
|
|
# Board of Trustees, Leland Stanford Jr. University
|
|
# Based on cvslog, copyright 1998, 1999, 2000, 2001, 2002, 2003, 2004
|
|
# Board of Trustees, Leland Stanford Jr. University
|
|
#
|
|
# This program is free software; you may redistribute it and/or modify it
|
|
# under the same terms as Perl itself.
|
|
|
|
##############################################################################
|
|
# Modules and declarations
|
|
##############################################################################
|
|
|
|
# I want to use open (FOO, '-|', @command).
|
|
require 5.008;
|
|
use strict;
|
|
|
|
use Date::Parse qw(str2time);
|
|
use Encode qw(decode encode FB_CROAK);
|
|
use Getopt::Long qw(GetOptions);
|
|
use IPC::Open2 qw(open2);
|
|
use POSIX qw(strftime);
|
|
|
|
# The path to the repository, set later.
|
|
our $REPOSITORY;
|
|
|
|
# The default path to svnlook, which can be overridden on the command line.
|
|
our $SVNLOOK = 'svnlook';
|
|
|
|
# Debugging is off by default.
|
|
our $DEBUG = 0;
|
|
|
|
# Clean up $0 for errors.
|
|
our $FULLPATH = $0;
|
|
$0 =~ s%^.*/%%;
|
|
|
|
##############################################################################
|
|
# Utility functions
|
|
##############################################################################
|
|
|
|
# Given a prefix and a reference to an array, return a list of all strings in
|
|
# that array with the common prefix stripped off. Also strip off any leading
|
|
# ./ if present.
|
|
sub simplify {
|
|
my ($prefix, $list) = @_;
|
|
my @stripped = @$list;
|
|
for (@stripped) {
|
|
s%^\Q$prefix\E/*%%;
|
|
s%^\./+%%;
|
|
}
|
|
return @stripped;
|
|
}
|
|
|
|
# Run the given command, capturing its output and returning the output as an
|
|
# array. Be aware for huge inputs that the output is read entirely into
|
|
# memory.
|
|
sub read_command {
|
|
my (@command) = @_;
|
|
my $pid = open (CMD, '-|', @command) or die "$0: cannot fork @command\n";
|
|
my @output = <CMD>;
|
|
close CMD;
|
|
my $status = $?;
|
|
if ($status != 0) {
|
|
my $error = "$0: pipe from '@command' failed:";
|
|
my $exit = $status >> 8;
|
|
my $signal = $status & 127;
|
|
$error .= " status=$exit" if $exit;
|
|
$error .= " signal=$signal" if $exit;
|
|
die "$error\n";
|
|
}
|
|
if ($DEBUG) {
|
|
print "Output from @command:\n";
|
|
print @output;
|
|
print '-' x 78, "\n";
|
|
}
|
|
return @output;
|
|
}
|
|
|
|
##############################################################################
|
|
# Parsing functions
|
|
##############################################################################
|
|
|
|
# Given a hash to hold the data we're accumulating and the revision number of
|
|
# the change, get the general information about the commit message. This
|
|
# gives us the user making the change, the date, and the commit message. Sets
|
|
# author, date, and message in the provided hash, with date being the Unix
|
|
# timestamp.
|
|
sub parse_info {
|
|
my ($data) = @_;
|
|
my $rev = $$data{revision};
|
|
my @info = read_command ($SVNLOOK, 'info', $REPOSITORY, '-r', $rev);
|
|
chomp (@info);
|
|
|
|
# User, date, and then size of log message (ignored).
|
|
$$data{author} = shift @info;
|
|
$$data{date} = str2time (shift @info);
|
|
shift @info;
|
|
|
|
# Remainder is the log message, although strip off any trailing blank
|
|
# lines.
|
|
pop @info while (@info && $info[-1] =~ /^\s*$/);
|
|
$$data{message} = @info ? join ("\n", @info, '') : '';
|
|
}
|
|
|
|
# Given a hash to hold the data we're accumulating and the revision number of
|
|
# the change, obtain the list of files affected. For right now, we ignore
|
|
# property modifications. Anything other than an add (A) or delete (D) is
|
|
# treated as a modification. Sets prefix, added, modified, deleted, and files
|
|
# in the provided hash.
|
|
sub parse_files {
|
|
my ($data) = @_;
|
|
my $rev = $$data{revision};
|
|
my @info = read_command ($SVNLOOK, 'changed', $REPOSITORY, '-r', $rev);
|
|
|
|
# Go through the files and in the process, calculate the common prefix.
|
|
# Start with the dirname of the first file and remove a level of directory
|
|
# structure until we find a match for every file.
|
|
my $prefix;
|
|
my (@added, @modified, @deleted);
|
|
for (@info) {
|
|
my ($flag, $props, $file) = /^(.)(.) (.*)$/;
|
|
next unless $file;
|
|
if ($file !~ m%/$% && $flag ne '_') {
|
|
$$data{summarizable} = 1;
|
|
}
|
|
if ($flag eq 'A') { push (@added, $file) }
|
|
elsif ($flag eq 'D') { push (@deleted, $file) }
|
|
else {
|
|
push (@modified, $file);
|
|
if ($props ne ' ') {
|
|
$$data{propchange}{$file} = 1;
|
|
}
|
|
if ($flag eq 'U') {
|
|
$$data{contents}{$file} = 1;
|
|
}
|
|
}
|
|
if (defined $prefix) {
|
|
while ($prefix && index ($file, $prefix) != 0) {
|
|
$prefix =~ s%/*([^/]+)/*$%%;
|
|
}
|
|
} else {
|
|
$prefix = $file;
|
|
$prefix =~ s%/*([^/]+)/*$%%;
|
|
}
|
|
}
|
|
unless (@added || @deleted || @modified) {
|
|
die "$0: unable to get list of changed files from svnlook\n";
|
|
}
|
|
|
|
# Stash the data.
|
|
$$data{prefix} = $prefix;
|
|
$$data{added} = [ sort @added ];
|
|
$$data{modified} = [ sort @modified ];
|
|
$$data{deleted} = [ sort @deleted ];
|
|
$$data{files} = [ sort @added, @modified, @deleted ];
|
|
}
|
|
|
|
# Given the data hash and a reference to the diff output, build the
|
|
# metainformation about what changed. Right now, this just grabs information
|
|
# about copies.
|
|
sub parse_metadata {
|
|
my ($data, $diff) = @_;
|
|
my ($propchange, $property);
|
|
for my $line (@$diff) {
|
|
if ($line =~ /^Copied: (.*?) \((from rev \d+, .*)\)$/) {
|
|
$$data{copied}{$1} = $2;
|
|
undef $propchange;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Given the revision, get the diff for that revision. Note that this stores
|
|
# the full diff output in memory.
|
|
sub parse_diff {
|
|
my ($rev) = @_;
|
|
my @diff = read_command ($SVNLOOK, 'diff', $REPOSITORY, '-r', $rev);
|
|
return @diff;
|
|
}
|
|
|
|
# Given the data hash, parse the commit message and look for Debian bug
|
|
# closers. Return all of the bugs found.
|
|
sub parse_debbugs {
|
|
my ($data) = @_;
|
|
|
|
# The regex for matching the bug closers, taken from the Debian
|
|
# Developer's Reference Manual with capturing parens added to grab the bug
|
|
# numbers.
|
|
my $close = qr/closes:\s*(?:bug)?\#\s*(\d+)(?:,\s*(?:bug)?\#\s*(\d+))*/i;
|
|
|
|
# Scan through the commit message looking for added bug closers.
|
|
my @bugs = ($$data{message} =~ /$close/g);
|
|
@bugs = map { defined $_ ? $_ : () } @bugs if @bugs;
|
|
return @bugs;
|
|
}
|
|
|
|
##############################################################################
|
|
# Formatting functions
|
|
##############################################################################
|
|
|
|
# Determine the From header of the message from the user provided. Right now,
|
|
# this is dead simple, but I'm guessing it will get more complex later as I
|
|
# find more ways of getting this information.
|
|
#
|
|
# We currently assume that all names containing non-ASCII characters are
|
|
# encoded in UTF-8. If they're not, we send them verbatim in the mail
|
|
# message.
|
|
sub build_from {
|
|
my ($data) = @_;
|
|
my $user = $$data{author};
|
|
my $name = (getpwnam $user)[6] || '';
|
|
$name =~ s/,.*//;
|
|
my $utf8 = eval { decode ('utf-8', $name, FB_CROAK) };
|
|
unless ($@) {
|
|
$name = encode ('MIME-Q', $utf8);
|
|
}
|
|
if ($name =~ /[^\w ]/) {
|
|
$name = '"' . $name . '"';
|
|
}
|
|
return "From: " . ($name ? "$name <$user>" : $user) . "\n";
|
|
}
|
|
|
|
# Takes the data hash, a prefix to add to the subject header, and a flag
|
|
# saying whether to give a full list of files no matter how long it is. Form
|
|
# the subject line of our message. Try to keep the subject under 78
|
|
# characters by just giving a count of files if there are a lot of them.
|
|
sub build_subject {
|
|
my ($data, $prefix, $long) = @_;
|
|
my $subject = "Subject: " . $prefix . $$data{prefix} . ' ';
|
|
my $length = 78 - length ($subject);
|
|
$length = 8 if $length < 8;
|
|
my @files = simplify ($$data{prefix}, $$data{files});
|
|
my $files = join (' ', map { my $f = $_; $f =~ s%/$%%; $f } @files);
|
|
$files =~ s/[\n\r]/ /g;
|
|
if (!$long && length ($files) > $length) {
|
|
$subject .= '(' . @files . (@files > 1 ? " files" : " file") . ')';
|
|
} else {
|
|
$subject .= "($files)";
|
|
}
|
|
return $subject;
|
|
}
|
|
|
|
# Generate file lists, one file per line, with the appropriate heading and
|
|
# including additional information for modified files. Takes the data hash
|
|
# and the heading.
|
|
sub build_filelist {
|
|
my ($data, $heading) = @_;
|
|
my $key = lc $heading;
|
|
return '' unless $$data{$key} && @{ $$data{$key} };
|
|
my $output = "$heading:\n";
|
|
for my $file (@{ $$data{$key} }) {
|
|
my $cleanfile = $file;
|
|
$cleanfile =~ s%/+$%%;
|
|
if ($key ne 'modified') {
|
|
$output .= " $file\n";
|
|
} else {
|
|
$output .= " $file";
|
|
if ($$data{propchange}{$file}) {
|
|
my @attrs = ('properties');
|
|
unshift (@attrs, 'contents') if $$data{contents}{$file};
|
|
my $attrs = '(' . join (', ', @attrs) . ')';
|
|
my $length = length ($file) + 2;
|
|
$length += 8 - $length % 8;
|
|
if (78 - $length < length ($attrs)) {
|
|
$output .= "\n $attrs\n";
|
|
} else {
|
|
$output .= "\t$attrs\n";
|
|
}
|
|
} else {
|
|
$output .= "\n";
|
|
}
|
|
}
|
|
if ($key eq 'added') {
|
|
if ($$data{copied}{$cleanfile}) {
|
|
$output .= " ($$data{copied}{$cleanfile})\n";
|
|
}
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
# Build the subheader of the report, listing the files changed and some other
|
|
# information about the change. Takes the data hash, the revision, and a flag
|
|
# saying whether to show the author of the change. Returns the header as a
|
|
# string.
|
|
sub build_header {
|
|
my ($data, $rev, $showauthor) = @_;
|
|
my $date = strftime ('%A, %B %e, %Y @ %T', localtime $$data{date});
|
|
$date =~ s/ / /;
|
|
my $header = " Date: $date\n";
|
|
$header .= " Author: $$data{author}\n" if $showauthor;
|
|
$header .= "Revision: $rev\n";
|
|
return $header;
|
|
}
|
|
|
|
# Build a report for a particular commit; this includes the list of affected
|
|
# files and the commit message. Returns the report as a string. Takes the
|
|
# data hash.
|
|
sub build_message {
|
|
my ($data) = @_;
|
|
my $message = '';
|
|
if ($$data{message}) {
|
|
$message .= $$data{message};
|
|
if ($$data{added} || $$data{modified} || $$data{deleted}) {
|
|
$message .= "\n";
|
|
}
|
|
}
|
|
$message .= build_filelist ($data, 'Added');
|
|
$message .= build_filelist ($data, 'Modified');
|
|
$message .= build_filelist ($data, 'Deleted');
|
|
return $message;
|
|
}
|
|
|
|
# Given the revision and the diff, check to see if it's longer than 200KB
|
|
# (this limit should be configurable). If it is, suppress the diff and
|
|
# print out a message explaining why. Otherwise, return the diff as a text
|
|
# string. (We use lots of memory. I guess I don't care that much.)
|
|
sub build_diff {
|
|
my ($rev, $diff) = @_;
|
|
my $length = 0;
|
|
for (@$diff) {
|
|
$length += length ($_);
|
|
}
|
|
if ($length > 200 * 1024) {
|
|
my $old = $rev - 1;
|
|
return "The diff is longer than the limit of 200KB.\n"
|
|
. "Use svn diff -r $old:$rev to see the changes.\n";
|
|
} else {
|
|
return join ('', @$diff);
|
|
}
|
|
}
|
|
|
|
# Build a summary of the changes by running the patch through diffstat. This
|
|
# gives a basic idea of the order of magnitude of the changes. Takes the data
|
|
# hash (for file prefix), revision, and the path to diffstat as arguments, and
|
|
# optionally takes a reference to the array holding the diff if it was already
|
|
# obtained for other reasons (if not, we'll run svnlook ourselves).
|
|
#
|
|
# Doesn't handle binary files properly yet (they'll just disappear in the
|
|
# diffstat output rather than being marked binary). I need to see the svn
|
|
# diff output again to know what to do.
|
|
sub build_summary {
|
|
my ($data, $diffstat, $diff) = @_;
|
|
$diffstat ||= 'diffstat';
|
|
unless ($diff) {
|
|
$diff = [ parse_diff ($data) ];
|
|
}
|
|
open2 (\*OUT, \*IN, $diffstat, '-w', '78')
|
|
or die "$0: cannot fork $diffstat: $!\n";
|
|
for (@$diff) {
|
|
s%^(\*\*\*|---|\+\+\+) \Q$$data{prefix}\E/*%$1 %;
|
|
s%^(Added|Modified|Deleted): \Q$$data{prefix}\E/*%$1: %;
|
|
print IN $_;
|
|
}
|
|
close IN;
|
|
my @stats = <OUT>;
|
|
close OUT;
|
|
if ($DEBUG) {
|
|
print "Output from diffstat:\n";
|
|
print @stats;
|
|
print '-' x 78, "\n";
|
|
}
|
|
my $offset = index ($stats[0], '|');
|
|
unshift (@stats, '-' x $offset, "+\n");
|
|
return @stats;
|
|
}
|
|
|
|
##############################################################################
|
|
# Configuration handling
|
|
##############################################################################
|
|
|
|
# svnlog takes all of the same parameters as both command-line arguments and
|
|
# configuration file parameters. Right now, we only support a simple
|
|
# configuration file that's the same for every path; that will probably change
|
|
# in the future.
|
|
#
|
|
# This function takes in a list of Getopt::Long options and parses both the
|
|
# configuration file and the command line. Normally, the command-line options
|
|
# override the configuration file; the exceptions are the address and header
|
|
# parameters, where all provided options are merged together into one list.
|
|
# There are three options only supported as command-line options, not as
|
|
# configuration file parameters: help, version, and config.
|
|
#
|
|
# Returns a reference to a config hash that contains the parameters provided
|
|
# as keys.
|
|
sub configure {
|
|
my (@rules) = @_;
|
|
my %config;
|
|
|
|
# Parse command-line options.
|
|
Getopt::Long::Configure ('bundling', 'no_ignore_case', 'require_order');
|
|
GetOptions (\%config, @rules) or exit 1;
|
|
|
|
# Handle special options.
|
|
if ($config{help}) {
|
|
print "Feeding myself to perldoc, please wait....\n";
|
|
exec ('perldoc', '-t', $FULLPATH);
|
|
} elsif ($config{version}) {
|
|
my @version = split (' ', $ID);
|
|
shift @version if $ID =~ /^\$Id/;
|
|
my $version = join (' ', @version[0..2]);
|
|
$version =~ s/,v\b//;
|
|
$version =~ s/(\S+)$/($1)/;
|
|
$version =~ tr%/%-%;
|
|
print $version, "\n";
|
|
exit;
|
|
}
|
|
|
|
# If a config file was specified, read it. All of the options in @rules
|
|
# are also allowed in the config file except help, version, and config.
|
|
if ($config{config}) {
|
|
my %keys = map {
|
|
my $key = $_;
|
|
$key =~ s/\|.*//;
|
|
$key =~ s/=.*//;
|
|
if ($key =~ /^(help|version|config)$/) {
|
|
();
|
|
} else {
|
|
$key => 1;
|
|
}
|
|
} @rules;
|
|
open (CONFIG, '<', $config{config})
|
|
or die "$0: cannot open $config{config}: $!\n";
|
|
local $_;
|
|
while (<CONFIG>) {
|
|
next if /^\s*\#/;
|
|
next if /^\s*$/;
|
|
chomp;
|
|
my ($key, $value) = /^\s*(\S+):\s+(.*)/;
|
|
unless ($value) {
|
|
warn "$0:$config{config}:$.: invalid config syntax: $_\n";
|
|
next;
|
|
}
|
|
unless ($keys{$key}) {
|
|
warn "$0:$config{config}:$.: unrecognized config line: $_\n";
|
|
next;
|
|
}
|
|
$value =~ s/\s+$//;
|
|
$value =~ s/^\"(.*)\"$/$1/;
|
|
if ($key eq 'address' || $key eq 'header') {
|
|
if ($config{$key}) {
|
|
push (@{ $config{$key} }, $value);
|
|
} else {
|
|
$config{$key} = [ $value ];
|
|
}
|
|
} else {
|
|
$config{$key} = $value unless defined $config{$key};
|
|
}
|
|
}
|
|
close CONFIG;
|
|
}
|
|
|
|
# Set some defaults if needed and then return.
|
|
$config{diffstat} ||= 'diffstat';
|
|
unless ($config{sendmail}) {
|
|
my ($path) = grep { -x $_ } qw(/usr/sbin/sendmail /usr/lib/sendmail);
|
|
$config{sendmail} = $path || '/usr/lib/sendmail';
|
|
}
|
|
$config{subject} ||= 'Commit in ';
|
|
return %config;
|
|
}
|
|
|
|
##############################################################################
|
|
# Main routine
|
|
##############################################################################
|
|
|
|
# Parse the command line and load the configuration file, if any. The long
|
|
# option has to be listed first to get the hash table loaded properly,
|
|
# unfortunately.
|
|
my @options = ('address|a=s@', 'config|c=s', 'confirm', 'debug|D',
|
|
'debbugs=s', 'diff|d', 'diffstat=s', 'from|f=s',
|
|
'header|H=s@', 'help|h', 'include|i=s@', 'long-subject|l',
|
|
'omit-author|o', 'sendmail=s', 'subject|p=s', 'summary|s',
|
|
'svnlook=s', 'version|v');
|
|
my %config = configure (@options);
|
|
die "$0: no addresses specified\n" unless ($config{address});
|
|
$DEBUG = $config{debug} if $config{debug};
|
|
$SVNLOOK = $config{svnlook} if $config{svnlook};
|
|
|
|
# Unbuffer output for debugging.
|
|
$| = 1 if $DEBUG;
|
|
|
|
# There must now be two command-line arguments remaining: the path to the
|
|
# repository and the revision number of the change.
|
|
die "Usage: $0 [options] <repository> <revision>\n" unless @ARGV == 2;
|
|
$REPOSITORY = shift;
|
|
my $revision = shift;
|
|
|
|
# Get the data.
|
|
my %data;
|
|
$data{revision} = $revision;
|
|
parse_info (\%data);
|
|
parse_files (\%data);
|
|
|
|
# If we were told to include only particular files, exit if none of those
|
|
# files were modified.
|
|
if ($config{include}) {
|
|
my $found = 0;
|
|
for my $file (@{ $data{files} }) {
|
|
if (grep { $file =~ /$_/ } @{ $config{include} }) {
|
|
$found = 1;
|
|
}
|
|
}
|
|
exit unless $found;
|
|
}
|
|
|
|
# Get the diff. Right now, this is unconditional because of parse_metadata.
|
|
my @diff = parse_diff ($revision);
|
|
|
|
# Get additional metadata.
|
|
parse_metadata (\%data, \@diff);
|
|
|
|
# Get the From header.
|
|
my $from = $config{from} ? "From: $config{from}\n" : build_from (\%data);
|
|
|
|
# Prompt the user if we should ask for confirmation.
|
|
if ($config{confirm}) {
|
|
my $response;
|
|
open (TTY, '>/dev/tty') or exit;
|
|
do {
|
|
print TTY "\nSend notification to @{ $config{address} } (y/n)? ";
|
|
$response = <STDIN>;
|
|
} until ($response =~ /^[yn]/i);
|
|
exit if $response =~ /^n/i;
|
|
close TTY;
|
|
}
|
|
|
|
# Send debbugs control message if needed.
|
|
if ($config{debbugs}) {
|
|
my @bugs = parse_debbugs (\%data);
|
|
if (@bugs) {
|
|
if ($DEBUG) {
|
|
open (MAIL, '>&STDOUT')
|
|
or die "$0: cannot dup standard output: $!\n";
|
|
} else {
|
|
open (MAIL, "| $config{sendmail} -t -oi -oem")
|
|
or die "$0: cannot fork $config{sendmail}: $!\n";
|
|
}
|
|
print MAIL "To: $config{debbugs}\n";
|
|
print MAIL $from;
|
|
print MAIL "\n";
|
|
print MAIL "tag $_ pending\n" for @bugs;
|
|
print MAIL "thanks\n";
|
|
close MAIL;
|
|
unless ($? == 0) {
|
|
warn "$0: debbugs sendmail exit status " . ($? >> 8) . "\n";
|
|
}
|
|
if ($DEBUG) {
|
|
print '-' x 78, "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
# Open our mail program for the main mail message.
|
|
if ($DEBUG) {
|
|
open (MAIL, '>&STDOUT') or die "$0: cannot dup standard output: $!\n";
|
|
} else {
|
|
open (MAIL, "| $config{sendmail} -t -oi -oem")
|
|
or die "$0: cannot fork $config{sendmail}: $!\n";
|
|
}
|
|
my $oldfh = select MAIL;
|
|
$| = 1;
|
|
select $oldfh;
|
|
|
|
# Build the mail headers.
|
|
print MAIL "To: ", join (', ', @{ $config{address} }), "\n";
|
|
print MAIL $from;
|
|
if ($config{header}) {
|
|
for my $header (@{ $config{header} }) {
|
|
print MAIL $header, ($header =~ /\n\z/ ? '' : "\n");
|
|
}
|
|
}
|
|
my $subj = build_subject (\%data, $config{subject}, $config{'long-subject'});
|
|
print MAIL $subj, "\n";
|
|
|
|
# Add a User-Agent header with the version of svnlog.
|
|
my @version = split (' ', $ID);
|
|
shift @version if $ID =~ /^\$Id/;
|
|
print MAIL "User-Agent: svnlog/$version[1]\n";
|
|
print MAIL "\n";
|
|
|
|
# Build the rest of the message and write it out. Is it the right thing to
|
|
# always try to do the summary and diff, or is this going to bite me for tags?
|
|
print MAIL build_header (\%data, $revision, !$config{'omit-author'});
|
|
print MAIL "\n", build_message (\%data);
|
|
print MAIL "\n", build_summary (\%data, $config{diffstat}, \@diff)
|
|
if $config{summary} && $data{summarizable};
|
|
print MAIL "\n", build_diff ($revision, \@diff) if $config{diff};
|
|
|
|
# Make sure sending mail succeeded.
|
|
close MAIL;
|
|
unless ($? == 0) { die "$0: sendmail exit status " . ($? >> 8) . "\n" }
|
|
exit 0;
|
|
__END__
|
|
|
|
##############################################################################
|
|
# Documentation
|
|
##############################################################################
|
|
|
|
=head1 NAME
|
|
|
|
svnlog - Mail Subversion commit notifications
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
B<svnlog> [B<-Ddhlosv>] [B<-a> I<address> ...] [B<--confirm>] [B<-c> I<file>]
|
|
[B<--debbugs>=I<address>] [B<--diffstat>=I<path>] [B<-f> I<from>]
|
|
[B<-H> I<header> ...] [B<-i> I<regex> ...] [B<--sendmail>=I<path>]
|
|
[B<-p> I<prefix>] [B<--svnlook>=I<path>] I<repository> I<revision>
|
|
|
|
=head1 REQUIREMENTS
|
|
|
|
Subversion, including the B<svnlook> command-line tool; Perl 5.8.0 or later;
|
|
B<diffstat> for the B<-s> option; and a sendmail command that can accept
|
|
formatted mail messages for delivery.
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
B<svnlog> is intended to be run out of Subversion's post-commit hook,
|
|
generates a commit notification, and sends it via e-mail. The format is
|
|
almost identical to the format of mail generated by B<cvslog>. By default,
|
|
the commit message will contain the author, date, and revision number; the
|
|
files added, deleted, or modified; and the commit message. Optionally, the
|
|
full diff of the commit or a diffstat(1) summary of the changes, or both,
|
|
can be added to the notification.
|
|
|
|
For information on how to add B<svnlog> to your Subversion repository, see
|
|
L<"INSTALLATION"> below. All necessary configuration options can be given
|
|
on the command line, but if you would prefer you can also use the B<-c>
|
|
option to point B<svnlog> at a configuration file. See L<"CONFIG FILE">
|
|
below for the syntax.
|
|
|
|
The full path to the repository and the revision number of the change must
|
|
be given on the command line. These are the same parameters that Subversion
|
|
passes to its post-commit hook. B<svnlog> must be able to find B<svnlook>
|
|
to work; if B<svnlook> isn't on the default path, specify it via either the
|
|
B<--svlook> command-line option or the I<svnlook> configuration parameter.
|
|
|
|
At least one B<-a> option must be given or at least one I<address> parameter
|
|
must be set in the configuration file; otherwise, B<svnlog> has nowhere to
|
|
send mail and nothing to do.
|
|
|
|
=head1 OPTIONS
|
|
|
|
All of these options except B<-c>, B<-h>, and B<-v> can also be set in a
|
|
configuration file specified with B<-c>. See L<"CONFIG FILE"> below for the
|
|
syntax.
|
|
|
|
=over 4
|
|
|
|
=item B<-a> I<address>, B<--address>=I<address>
|
|
|
|
Send the commit notification to I<address>. This option may occur more than
|
|
once, and all specified addresses will receive a copy of the notification
|
|
(including any addresses set in the configuration file, if any). There is
|
|
no way to override the addresses set in the configuration file, just add
|
|
more addresses.
|
|
|
|
=item B<-c> I<file>, B<--config>=I<file>
|
|
|
|
Load configuration options from I<file>. See L<"CONFIG FILE"> below for the
|
|
syntax. Normally, command-line options override values set in the
|
|
configuration file, but settings for B<-a> (I<address>) and B<-H>
|
|
(I<header>) are cumulative between the command line and the configuration
|
|
file.
|
|
|
|
=item B<--confirm>
|
|
|
|
Prompt for user confirmation before sending the commit notification. The
|
|
user must answer C<y> or C<n>, and if they answer C<n>, B<svnlog> will exit
|
|
without doing anything further.
|
|
|
|
This option is something of a hack. It will only work if it can open the
|
|
user's tty to print the prompt, which means that it may only work with local
|
|
repositories or remote repositories accessed via ssh. If it cannot open the
|
|
tty, it will exit quietly (as if the user said C<n>), but if this function
|
|
is called by a daemon that may have a tty of F</dev/console>, its operation
|
|
is unpredictable. Recommended only for use with local repositories.
|
|
|
|
=item B<-D>, B<--debug>
|
|
|
|
Prints out the information B<svnlog> got from Subversion as it works. This
|
|
option is mostly useful for developing B<svnlog> and checking exactly what
|
|
data Subversion provides.
|
|
|
|
=item B<--debbugs>=I<address>
|
|
|
|
This is a special feature probably only of use to Debian developers. If this
|
|
flag is given, scan the commit message for any bug closers (using the same
|
|
syntax as is used in Debian changelog files). If any bug closers are found,
|
|
send a separate mail message to I<address> with the debbugs commands to tag
|
|
those bugs as pending. The easiest way to get the appropriate bug closers
|
|
into the commit message when working on Debian packages is to use B<debcommit>
|
|
to commit changes.
|
|
|
|
In versions of svnlog prior to 1.11, this option instead checked the diff for
|
|
modifications to F<debian/changelog>. This proved too likely to tag bugs
|
|
spuriously when importing old source and was changed to only look in the
|
|
commit message.
|
|
|
|
=item B<-d>, B<--diff>
|
|
|
|
Append the full diff output for each change to the notification message
|
|
provided that it is less than 200KB. If longer than that, just append a
|
|
note saying that the diff is too large and giving the B<svn> command that
|
|
would display it.
|
|
|
|
=item B<--diffstat>=I<path>
|
|
|
|
Sets the path to B<diffstat>. If this option isn't given, B<svnlog>
|
|
defaults to attempting to find B<diffstat> in its path (and what its path is
|
|
will depend a great deal on your Subversion configuration).
|
|
|
|
=item B<-f> I<from>, B<--from>=I<from>
|
|
|
|
Sets the From header of the mail generated by B<svnlog> to I<from>. This
|
|
must be syntactically valid as the contents of an RFC 2822 From header (a
|
|
simple address of course qualifies). If this option is not given, the
|
|
From header will be set to the user who made the commit, with their full
|
|
name added if they're found in the local password file or equivalent.
|
|
(Normally, the local mail system will then append a local domain to form
|
|
an e-mail address.)
|
|
|
|
I<from> should be specified in UTF-8. If it is, it will be encoded using
|
|
RFC 2047 encoding if needed. If it is encoded in some other character
|
|
set, B<svnlog> will try to use it in the mail message without modification
|
|
or encoding, which may produce surprising results.
|
|
|
|
=item B<-H> I<header>, B<--header>=I<header>
|
|
|
|
Include I<header> in the headers of the mail message. I<header> must be a
|
|
valid mail header (including the keyword, colon, and space). This option
|
|
may be given multiple times, and all of the headers (including any specified
|
|
in the configuration file) will be included in the message. There is no way
|
|
to override the headers set in the configuration file, just add to them.
|
|
|
|
=item B<-h>, B<--help>
|
|
|
|
Print out this documentation (which is done simply by feeding the script to
|
|
C<perldoc -t>).
|
|
|
|
=item B<-i> I<regex>, B<--include>=I<regex>
|
|
|
|
Only send a notification message if one of the files modified matches the
|
|
regular expression I<regex>. If none do, the commit will be silently
|
|
ignored. This option may be given multiple times, and a notification
|
|
message will be sent if any of the files in the commit match any of the
|
|
provided regular expressions. There is no way to override include patterns
|
|
set in the configuration file, just add to them.
|
|
|
|
If the notification is sent, all details about that commit will be sent,
|
|
including listing files that don't match the regexes, including other
|
|
changes in summary and diff output, and so forth.
|
|
|
|
=item B<-l>, B<--long-subject>
|
|
|
|
Normally, B<svnlog> will just list the number of changed files rather than
|
|
the complete list of them if the subject would otherwise be too long (more
|
|
than 78 characters). This flag disables that behavior and includes the full
|
|
list of modified files in the subject header of the mail, no matter how long
|
|
it is. Currently, B<svnlog> does not wrap the header, so using this option
|
|
can result in a message that is not syntactically valid.
|
|
|
|
=item B<-o>, B<--omit-author>
|
|
|
|
Omit the author information from the commit notification. This is useful
|
|
where all commits are done by the same person (so the author information is
|
|
just noise) or where the author information isn't actually available. The
|
|
author will still appear in the From header of the commit message unless the
|
|
B<-f> option is also given.
|
|
|
|
=item B<--sendmail>=I<path>
|
|
|
|
Sets the path to B<sendmail>. If this option is not given, B<svnlog> will
|
|
search for sendmail in F</usr/sbin> and then F</usr/lib>, falling back on
|
|
using F</usr/lib/sendmail> (and probably failing) if neither appears to
|
|
exist.
|
|
|
|
=item B<-p> I<prefix>, B<--subject>=I<prefix>
|
|
|
|
Sets the Subject prefix for all commit messages. Appended to this prefix
|
|
will be the relative path in the repository to the lowermost parent
|
|
directory of all changed files and then a list of files (or a count of
|
|
files) in parentheses. If this option is not given, the default prefix is
|
|
C<Commit in >. Note that you will normally want I<prefix> to end in a
|
|
trailing space.
|
|
|
|
=item B<-s>, B<--summary>
|
|
|
|
Append to each commit notification a summary of the changes, produced by
|
|
generating diffs and feeding those diffs to diffstat(1). diffstat(1) must
|
|
be installed to use this option; see also the B<--diffstat> option to
|
|
specify the path to the program.
|
|
|
|
=item B<--svnlook>=I<path>
|
|
|
|
Sets the path to B<svnlook>. If this option is not given, B<svnlog> attempt
|
|
to find B<diffstat> in its path (and what its path is will depend a great
|
|
deal on your Subversion configuration).
|
|
|
|
=item B<-v>, B<--version>
|
|
|
|
Print out the version of B<svnlog> and exit.
|
|
|
|
=back
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
B<svnlog> can be told to read a configuration file with the B<-c> option.
|
|
The syntax of this file is one configuration parameter per line in the
|
|
format:
|
|
|
|
parameter: value
|
|
|
|
The value may be enclosed in double-quotes and must be enclosed in
|
|
double-quotes if there is leading or trailing whitespace that should be part
|
|
of the value. Whitespace after the colon and before the value (or an
|
|
opening double quote) is ignored. There is no way to continue a line; each
|
|
parameter must be a single line. Lines beginning with C<#> are comments and
|
|
ignored, as are blank lines.
|
|
|
|
The configuration parameters are exactly the same as the long names of the
|
|
command-line options, with the exception that B<--config>, B<--help>, and
|
|
B<--version> have no corresponding parameters (for obvious reasons). As
|
|
with the command-line options, the I<address> and I<header> parameters may
|
|
be given multiple times; for all other parameters, the last value is taken
|
|
if the paramter occurs multiple times.
|
|
|
|
When setting a boolean parameter like I<summary> or I<diff>, the value
|
|
should be C<1> if you want to turn it on and C<0> if you want to turn it
|
|
off.
|
|
|
|
Command-line options always override the configuration file except for the
|
|
B<--address> and B<--header> options, as noted.
|
|
|
|
=head1 INSTALLATION
|
|
|
|
Follow these steps to add B<svnlog> to your project:
|
|
|
|
=over 4
|
|
|
|
=item 1.
|
|
|
|
Install B<svnlog> on your system in some convenient location. You may need
|
|
to change the path to Perl on the first line of the script if your Perl
|
|
isn't in F</usr/bin/perl>. B<svnlog> does not have to be inside the
|
|
F<hooks> directory of the Subversion repository; it can be anywhere
|
|
(F</usr/local/bin>, for instance).
|
|
|
|
=item 2.
|
|
|
|
In the F<hooks> directory of your Subversion repository, create a file named
|
|
F<post-commit> with the contents:
|
|
|
|
#!/bin/sh
|
|
/path/to/svnlog <options> "$1" "$2"
|
|
|
|
where C</path/to/svnlog> is the full path to wherever you installed
|
|
B<svnlog> and <options> are whatever options you wish to use. Be sure to
|
|
include at least one B<-a> option (or a B<-c> option pointing to a
|
|
configuration file that includes at least one I<address> parameter setting)
|
|
so that B<svnlog> knows where to send the mail.
|
|
|
|
It's best to always specify the path to B<svnlook> with the B<--svnlook>
|
|
option (and the path to B<diffstat> with B<--diffstat> if you're using it)
|
|
even if you think that program will be in the default path, since Subversion
|
|
can be run under unusual situations (such as by a web server) and the path
|
|
may not be what you expect.
|
|
|
|
=back
|
|
|
|
That's all there is to it. Try committing to your repository and see if
|
|
mail is sent as you expect. Note that if you're configuring B<svnlog> to
|
|
send commit notifications to a mailing list, you may need to use the B<-f>
|
|
option to set the From header to an address that's allowed to mail that
|
|
list. It's a good idea to test your configuration with B<-a> pointing to
|
|
your own e-mail address so that you see what the From header looks like and
|
|
can find any problems.
|
|
|
|
=head1 EXAMPLES
|
|
|
|
The following examples show the B<svnlog> command line that would go into
|
|
the Subversion post-commit hook, so they assume that $1 is the path to the
|
|
repository and $2 is the revision number.
|
|
|
|
A minimalist configuration, sending mail to commits@example.com:
|
|
|
|
/usr/local/bin/svnlog -a commits@example.com "$1" "$2"
|
|
|
|
A more typical configuration, specifying the paths to B<svnlog> and
|
|
B<diffstat>, sending commit notifications from commit-mailer@example.com
|
|
(perhaps a special address that can send to mailing lists and discards any
|
|
replies), sending them to proj-commits@example.com and to user@example.com,
|
|
and including diffstat output.
|
|
|
|
/usr/local/bin/svnlog --svnlook=/usr/bin/svnlook \
|
|
--diffstat=/usr/bin/diffstat --summary \
|
|
--from="Subversion Commits <commit-mailer@example.com>" \
|
|
-a proj-commits@example.com -a user@example.com \
|
|
"$1" "$2"
|
|
|
|
Here is the same example, but taking all of its parameters from a
|
|
configuration file:
|
|
|
|
/usr/local/bin/svnlog -c /usr/local/etc/svnlog.conf "$1" "$2"
|
|
|
|
where F</usr/local/etc/svnlog.conf> contains:
|
|
|
|
# svnlog configuration
|
|
svnlook: /usr/bin/svnlook
|
|
diffstat: /usr/bin/diffstat
|
|
summary: 1
|
|
from: Subversion Commits <commit-mailer@example.com>
|
|
address: proj-commits@example.com
|
|
address: user@example.com
|
|
|
|
Finally, here is an example for a Debian package maintainer. Send all
|
|
commits, including diffs, to pkg-debian@lists.example.com, and cc
|
|
control@bugs.debian.org with tag commands when appropriate. Also add the
|
|
headers:
|
|
|
|
X-Debian-Commit: yes
|
|
X-Comment: Generated by svlog
|
|
|
|
to each commit message.
|
|
|
|
/usr/local/bin/svnlog --svnlook=/usr/bin/svnlook \
|
|
--diff --debbugs=control@bugs.debian.org \
|
|
--header="X-Debian-Commit: yes" \
|
|
--header="X-Comment: Generated by svnlog" \
|
|
--address=pkg-debian@lists.debian.org \
|
|
"$1" "$2"
|
|
|
|
The From header on the mail will be the default, derived from the user
|
|
making the commit and the name from the local password file.
|
|
|
|
=head1 DIAGNOSTICS
|
|
|
|
=over 4
|
|
|
|
=item cannot dup standard output: %s
|
|
|
|
(Fatal) B<svnlog> is running in debug mode (B<-D>) and was unable to send
|
|
the output that would normally go into a mail message to standard output
|
|
instead.
|
|
|
|
=item cannot fork %s: %s
|
|
|
|
(Fatal) B<svnlog> was unable to run a program that it wanted to run. This
|
|
may result in no notification being sent or in information missing.
|
|
Generally this means that the program in question was missing or B<svnlog>
|
|
couldn't find it for some reason.
|
|
|
|
=item cannot open %s: %s
|
|
|
|
(Fatal) B<svnlog> was unable to open the configuration file specified with
|
|
B<-c> or B<--config>. The file may be missing or not readable by the user
|
|
running B<svnlog>.
|
|
|
|
=item pipe from '%s' failed: %s
|
|
|
|
(Fatal) Some program invoked by B<svnlog> to get some information (usually
|
|
B<svnlook>) failed or died unexpectedly. There may be other error messages
|
|
before this one that explain what happened. The exit status and any fatal
|
|
signal received by the program are appended.
|
|
|
|
=item invalid config syntax: %s
|
|
|
|
(Warning) The given line in the configuration file was syntactically
|
|
invalid. See L<"CONFIG FILE"> for the correct syntax.
|
|
|
|
=item no addresses specified
|
|
|
|
(Fatal) There was no B<-a> or B<--address> command-line option and no
|
|
B<address> parameter in a config file. At least one recipient address must
|
|
be specified for the Subversion commit notification.
|
|
|
|
=item unable to get list of changed files from svnlook
|
|
|
|
(Fatal) B<svnlog> was unable to understand any of the output of C<svnlook
|
|
changed> and therefore was unable to tell what files were changed by this
|
|
commit. There may be other error messages before this one that explain what
|
|
happened.
|
|
|
|
=item unrecognized config line: %s
|
|
|
|
(Warning) The given configuration parameter isn't one of the ones that
|
|
B<svnlog> knows about.
|
|
|
|
=back
|
|
|
|
=head1 FILES
|
|
|
|
=over 4
|
|
|
|
=item F</usr/sbin/sendmail>
|
|
|
|
=item F</usr/lib/sendmail>
|
|
|
|
The default paths to B<sendmail> if the B<--sendmail> option and I<sendmail>
|
|
configuration parameter are not given. B<svnlog> first tries the former and
|
|
then the latter.
|
|
|
|
=back
|
|
|
|
=head1 ENVIRONMENT
|
|
|
|
=over 4
|
|
|
|
=item PATH
|
|
|
|
Used to find B<svnlook> (and B<diffstat> when the B<-s> option is in
|
|
effect) if the B<--svnlook> and B<--diffstat> command-line options or
|
|
configuration parameters are not given.
|
|
|
|
=back
|
|
|
|
=head1 CAVEATS
|
|
|
|
B<svnlog> assumes that the full name in the local password file or
|
|
equivalent is encoded in UTF-8. If it appears to be valid UTF-8, it will
|
|
be used as UTF-8, which may mangle the name if it were actually in a
|
|
different encoding. If it's not valid UTF-8, B<svnlog> will use the full
|
|
name without modification in the mail message, which may result in a
|
|
syntactically invalid mail message and unexpected results.
|
|
|
|
=head1 BUGS
|
|
|
|
B<svnlog>, when given the B<-l> option, can generate syntactically invalid
|
|
e-mail messages. The Subject header needs to be properly wrapped and some
|
|
solution found to filenames longer than 998 characters (probably using RFC
|
|
2047 encoding).
|
|
|
|
No MIME type is declared in the mail message, which means that if your log
|
|
messages (or file names or diffs) aren't US-ASCII, the results are unlikely
|
|
to be what you want. You can work around this, if all of your commit
|
|
messages are in the same character set, with command-line options like:
|
|
|
|
--header="MIME-Version: 1.0"
|
|
--header="Content-Type: text/plain; charset=utf-8"
|
|
|
|
but B<svnlog> should really be able to figure this out for itself and should
|
|
do the right thing with log messages in multiple character sets.
|
|
|
|
URLs for the diffs are not yet supported. If you want this added, please
|
|
e-mail me the details of how to construct an appropriate URL and I'll see
|
|
what I can do.
|
|
|
|
B<svnlog> always reads the entire diff into memory. This may not be
|
|
desirable on low-memory machines when processing huge changes such as
|
|
merges. The functions that need the diff output to gather information
|
|
should be able to scan the diff a line at a time when needed.
|
|
|
|
The diff size limit should be configurable, and what to include in the diff
|
|
output should also be configurable. B<mailer.py> supports specifying
|
|
whether one wants diffs for adds, deletes, and modifications separately.
|
|
|
|
B<svnlog> runs the command-line B<svnlook> program and parses its output
|
|
rather than using the Perl bindings. It was easier, but the Perl bindings
|
|
would be more efficient.
|
|
|
|
=head1 SEE ALSO
|
|
|
|
debcommit(1), diffstat(1), svnlook(1).
|
|
|
|
The Subversion manual at L<http://svnbook.red-bean.com/>.
|
|
|
|
diffstat is at L<http://invisible-island.net/diffstat/>.
|
|
|
|
Current versions of this program are available from its web site at
|
|
L<http://www.eyrie.org/~eagle/software/svnlog/>.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Russ Allbery <rra@stanford.edu>.
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
Copyright 2005, 2006, 2007, 2009 Board of Trustees, Leland Stanford Jr.
|
|
University.
|
|
|
|
Based on cvslog, copyright 1998, 1999, 2000, 2001, 2002, 2003, 2004 Board of
|
|
Trustees, Leland Stanford Jr. University.
|
|
|
|
This program is free software; you may redistribute it and/or modify it
|
|
under the same terms as Perl itself.
|
|
|
|
=cut
|