diff --git a/tools/git-notify b/tools/git-notify
index 848cfe0d97785ad5acc488f628982f9fc11fb8da..289a5f645a0f9f69b427edab8f6a063138d2c47f 100755 (executable)
--- a/tools/git-notify
+++ b/tools/git-notify
# Tool to send git commit notifications
#
# Copyright 2005 Alexandre Julliard
+# Copyright 2009 Nagios Plugins Development Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
#
# Usage: git-notify [options] [--] old-sha1 new-sha1 refname
#
+# -C Show committer in the body if different from the author
# -c name Send CIA notifications under specified project name
# -m addr Send mail notifications to specified address
# -n max Set max number of individual mails to send
# -r name Set the git repository name
# -s bytes Set the maximum diff size in bytes (-1 for no limit)
+# -t file Prevent duplicate notifications by saving state to this file
+# -U mask Set the umask for creating the state file
# -u url Set the URL to the gitweb browser
# -i branch If at least one -i is given, report only for specified branches
# -x branch Exclude changes to the specified branch from reports
# -X Exclude merge commits
+# -z Try to abbreviate the SHA1 name within gitweb URLs (unsafe)
#
use strict;
-use open ':utf8';
-use Encode 'encode';
+use Fcntl ':flock';
+use Encode qw(encode decode);
use Cwd 'realpath';
-binmode STDIN, ':utf8';
-binmode STDOUT, ':utf8';
-
sub git_config($);
sub get_repos_name();
# configuration parameters
+# show the committer if different from the author (can be set with the -C option)
+my $show_committer = git_config( "notify.showcommitter" );
+
# base URL of the gitweb repository browser (can be set with the -u option)
my $gitweb_url = git_config( "notify.baseurl" );
+# abbreviate the SHA1 name within gitweb URLs (can be set with the -z option)
+my $abbreviate_url = git_config( "notify.shorturls" );
+
# default repository name (can be changed with the -r option)
my $repos_name = git_config( "notify.repository" ) || get_repos_name();
# branches to exclude
my @exclude_list = split /\s+/, git_config( "notify.exclude" ) || "";
+# the state file we use (can be set with the -t option)
+my $state_file = git_config( "notify.statefile" );
+
+# umask for creating the state file (can be set with -U option)
+my $mode_mask = git_config( "notify.umask" ) || 002;
+
# Extra options to git rev-list
my @revlist_options;
sub usage()
{
print "Usage: $0 [options] [--] old-sha1 new-sha1 refname\n";
+ print " -C Show committer in the body if different from the author\n";
print " -c name Send CIA notifications under specified project name\n";
print " -m addr Send mail notifications to specified address\n";
print " -n max Set max number of individual mails to send\n";
print " -r name Set the git repository name\n";
print " -s bytes Set the maximum diff size in bytes (-1 for no limit)\n";
+ print " -t file Prevent duplicate notifications by saving state to this file\n";
+ print " -U mask Set the umask for creating the state file\n";
print " -u url Set the URL to the gitweb browser\n";
print " -i branch If at least one -i is given, report only for specified branches\n";
print " -x branch Exclude changes to the specified branch from reports\n";
print " -X Exclude merge commits\n";
+ print " -z Try to abbreviate the SHA1 name within gitweb URLs (unsafe)\n";
exit 1;
}
return $str;
}
+# execute git-rev-list(1) with the given parameters and return the output
+sub git_rev_list(@)
+{
+ my @args = @_;
+ my $revlist = [];
+ my $pid = open REVLIST, "-|";
+
+ die "Cannot open pipe: $!" if not defined $pid;
+ if (!$pid)
+ {
+ exec "git", "rev-list", @revlist_options, @args or die "Cannot execute rev-list: $!";
+ }
+ while (<REVLIST>)
+ {
+ chomp;
+ die "Invalid commit: $_" if not /^[0-9a-f]{40}$/;
+ push @$revlist, $_;
+ }
+ close REVLIST or die $! ? "Cannot execute rev-list: $!" : "rev-list exited with status: $?";
+ return $revlist;
+}
+
+# append the given commit hashes to the state file
+sub save_commits($)
+{
+ my $commits = shift;
+
+ open STATE, ">>", $state_file or die "Cannot open $state_file: $!";
+ flock STATE, LOCK_EX or die "Cannot lock $state_file";
+ print STATE "$_\n" for @$commits;
+ flock STATE, LOCK_UN or die "Cannot unlock $state_file";
+ close STATE or die "Cannot close $state_file: $!";
+}
+
+# for the given range, return the new hashes (and append them to the state file)
+sub get_new_commits($$)
+{
+ my ($old_sha1, $new_sha1) = @_;
+ my ($seen, @args);
+ my $newrevs = [];
+
+ @args = ( "^$old_sha1" ) unless $old_sha1 eq '0' x 40;
+ push @args, $new_sha1, @exclude_list;
+
+ my $revlist = git_rev_list(@args);
+
+ if (not defined $state_file or not -e $state_file)
+ {
+ save_commits(git_rev_list("--all", "--full-history")) if defined $state_file;
+ return $revlist;
+ }
+
+ open STATE, $state_file or die "Cannot open $state_file: $!";
+ flock STATE, LOCK_SH or die "Cannot lock $state_file";
+ while (<STATE>)
+ {
+ chomp;
+ die "Invalid commit: $_" if not /^[0-9a-f]{40}$/;
+ $seen->{$_} = 1;
+ }
+ flock STATE, LOCK_UN or die "Cannot unlock $state_file";
+ close STATE or die "Cannot close $state_file: $!";
+
+ # FIXME: if another git-notify process reads the $state_file at *this*
+ # point, that process might generate duplicates of our notifications.
+
+ save_commits($revlist);
+
+ foreach my $commit (@$revlist)
+ {
+ push @$newrevs, $commit unless $seen->{$commit};
+ }
+ return $newrevs;
+}
+
+# truncate the given string if it exceeds the specified number of characters
+sub truncate_str($$)
+{
+ my ($str, $max) = @_;
+
+ if (length($str) > $max)
+ {
+ $str = substr($str, 0, $max);
+ $str =~ s/\s+\S+$//;
+ $str .= " ...";
+ }
+ return $str;
+}
+
# right-justify the left column of "left: right" elements, omit undefined elements
sub format_table(@)
{
my $arg = shift @ARGV;
if ($arg eq '--') { last; }
+ elsif ($arg eq '-C') { $show_committer = 1; }
elsif ($arg eq '-c') { $cia_project_name = shift @ARGV; }
elsif ($arg eq '-m') { $commitlist_address = shift @ARGV; }
elsif ($arg eq '-n') { $max_individual_notices = shift @ARGV; }
elsif ($arg eq '-r') { $repos_name = shift @ARGV; }
elsif ($arg eq '-s') { $max_diff_size = shift @ARGV; }
+ elsif ($arg eq '-t') { $state_file = shift @ARGV; }
+ elsif ($arg eq '-U') { $mode_mask = shift @ARGV; }
elsif ($arg eq '-u') { $gitweb_url = shift @ARGV; }
elsif ($arg eq '-i') { push @include_list, shift @ARGV; }
elsif ($arg eq '-x') { push @exclude_list, shift @ARGV; }
elsif ($arg eq '-X') { push @revlist_options, "--no-merges"; }
+ elsif ($arg eq '-z') { $abbreviate_url = 1; }
elsif ($arg eq '-d') { $debug++; }
else { usage(); }
}
$subject = encode("MIME-Q",$subject);
if ($debug)
{
+ binmode STDOUT, ":utf8";
print "---------------------\n";
print "To: $name\n";
print "Subject: $subject\n";
{
exec $mailer, "-s", $subject, "-a", "Content-Type: $content_type", $name or die "Cannot exec $mailer";
}
+ binmode MAIL, ":utf8";
print MAIL join("\n", @text), "\n";
- close MAIL;
+ close MAIL or warn $! ? "Cannot execute $mailer: $!" : "$mailer exited with status: $?";
}
}
return $repos;
}
-# extract the information from a commit object and return a hash containing the various fields
+# extract the information from a commit or tag object and return a hash containing the various fields
sub get_object_info($)
{
my $obj = shift;
my @log = ();
my $do_log = 0;
- open OBJ, "-|" or exec "git", "cat-file", "commit", $obj or die "cannot run git-cat-file";
+ $info{"encoding"} = "utf-8";
+
+ open TYPE, "-|" or exec "git", "cat-file", "-t", $obj or die "cannot run git-cat-file";
+ my $type = <TYPE>;
+ chomp $type;
+ close TYPE or die $! ? "Cannot execute cat-file: $!" : "cat-file exited with status: $?";
+
+ open OBJ, "-|" or exec "git", "cat-file", $type, $obj or die "cannot run git-cat-file";
while (<OBJ>)
{
chomp;
- if ($do_log) { push @log, $_; }
- elsif (/^$/) { $do_log = 1; }
- elsif (/^(author|committer) ((.*) (<.*>)) (\d+) ([+-]\d+)$/)
+ if ($do_log)
+ {
+ last if /^-----BEGIN PGP SIGNATURE-----/;
+ push @log, $_;
+ }
+ elsif (/^(author|committer|tagger) ((.*) (<.*>)) (\d+) ([+-]\d+)$/)
{
$info{$1} = $2;
$info{$1 . "_name"} = $3;
$info{$1 . "_date"} = $5;
$info{$1 . "_tz"} = $6;
}
+ elsif (/^tag (.+)/)
+ {
+ $info{"tag"} = $1;
+ }
+ elsif (/^encoding (.+)/)
+ {
+ $info{"encoding"} = $1;
+ }
+ elsif (/^$/) { $do_log = 1; }
}
- close OBJ;
+ close OBJ or die $! ? "Cannot execute cat-file: $!" : "cat-file exited with status: $?";
+ $info{"type"} = $type;
$info{"log"} = \@log;
return %info;
}
+# send a ref change notice to a mailing list
+sub send_ref_notice($$@)
+{
+ my ($ref, $action, @notice) = @_;
+ my ($reftype, $refname) = ($ref =~ /^refs\/(head|tag)s\/(.+)/);
+
+ $reftype =~ s/^head$/branch/;
+
+ @notice = (format_table(
+ "Module: $repos_name",
+ ($reftype eq "tag" ? "Tag:" : "Branch:") . $refname,
+ @notice,
+ ($action ne "removed" and $gitweb_url)
+ ? "URL: $gitweb_url/?a=shortlog;h=$ref" : undef),
+ "",
+ "The $refname $reftype has been $action.");
+
+ mail_notification($commitlist_address, "$refname $reftype $action",
+ "text/plain; charset=us-ascii", @notice);
+}
+
# send a commit notice to a mailing list
sub send_commit_notice($$)
{
my ($ref,$obj) = @_;
my %info = get_object_info($obj);
my @notice = ();
+ my ($url,$subject,$obj_string);
- open DIFF, "-|" or exec "git", "diff-tree", "-p", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree";
- my $diff = join("", <DIFF>);
- close DIFF;
-
- return if length($diff) == 0;
-
- push @notice, format_table(
- "Module: $repos_name",
- "Branch: $ref",
- "Commit: $obj",
- $gitweb_url ? "URL: $gitweb_url/?a=commit;h=$obj" : undef),
- "Author:" . $info{"author"},
- $info{"committer"} ne $info{"author"} ? "Committer:" . $info{"committer"} : undef,
- "Date:" . format_date($info{"author_date"},$info{"author_tz"}),
- "",
- @{$info{"log"}},
- "",
- "---",
- "";
-
- open STAT, "-|" or exec "git", "diff-tree", "--stat", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree";
- push @notice, join("", <STAT>);
- close STAT;
+ if ($gitweb_url)
+ {
+ if ($abbreviate_url)
+ {
+ open REVPARSE, "-|" or exec "git", "rev-parse", "--short", $obj or die "cannot exec git-rev-parse";
+ $obj_string = <REVPARSE>;
+ chomp $obj_string if defined $obj_string;
+ close REVPARSE or die $! ? "Cannot execute rev-parse: $!" : "rev-parse exited with status: $?";
+ }
+ $obj_string = $obj if not defined $obj_string;
+ $url = "$gitweb_url/?a=$info{type};h=$obj_string";
+ }
- if (($max_diff_size == -1) || (length($diff) < $max_diff_size))
+ if ($info{"type"} eq "tag")
{
- push @notice, $diff;
+ push @notice, format_table(
+ "Module: $repos_name",
+ "Branch: $ref",
+ "Tag: $obj",
+ "Tagger:" . $info{"tagger"},
+ "Date:" . format_date($info{"tagger_date"},$info{"tagger_tz"}),
+ $url ? "URL: $url" : undef),
+ "",
+ join "\n", @{$info{"log"}};
+
+ $subject = "Tag " . $info{"tag"} . ": " . $info{"tagger_name"};
}
else
{
- push @notice, "Diff: $gitweb_url/?a=commitdiff;h=$obj" if $gitweb_url;
+ push @notice, format_table(
+ "Module: $repos_name",
+ "Branch: $ref",
+ "Commit: $obj",
+ "Author:" . $info{"author"},
+ $show_committer && $info{"committer"} ne $info{"author"} ? "Committer:" . $info{"committer"} : undef,
+ "Date:" . format_date($info{"author_date"},$info{"author_tz"}),
+ $url ? "URL: $url" : undef),
+ "",
+ @{$info{"log"}},
+ "",
+ "---",
+ "";
+
+ open STAT, "-|" or exec "git", "diff-tree", "--stat", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree";
+ push @notice, join("", <STAT>);
+ close STAT or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?";
+
+ open DIFF, "-|" or exec "git", "diff-tree", "-p", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree";
+ my $diff = join("", <DIFF>);
+ close DIFF or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?";
+
+ if (($max_diff_size == -1) || (length($diff) < $max_diff_size))
+ {
+ push @notice, $diff;
+ }
+ else
+ {
+ push @notice, "Diff: $gitweb_url/?a=commitdiff;h=$obj_string" if $gitweb_url;
+ }
+ $subject = $info{"author_name"};
}
- mail_notification($commitlist_address,
- $info{"author_name"} . ": " . ${$info{"log"}}[0],
- "text/plain; charset=UTF-8", @notice);
+ $subject .= ": " . truncate_str(${$info{"log"}}[0],50);
+ $_ = decode($info{"encoding"}, $_) for @notice;
+ mail_notification($commitlist_address, $subject, "text/plain; charset=UTF-8", @notice);
}
# send a commit notice to the CIA server
my %info = get_object_info($commit);
my @cia_text = ();
+ return if $info{"type"} ne "commit";
+
push @cia_text,
"<message>",
" <generator>",
push @cia_text, " <file action=\"rename\" to=\"" . xml_escape($new) . "\">" . xml_escape($old) . "</file>";
}
}
- close COMMIT;
+ close COMMIT or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?";
push @cia_text,
" </files>",
sub send_global_notice($$$)
{
my ($ref, $old_sha1, $new_sha1) = @_;
- my @notice = ();
+ my $notice = git_rev_list("--pretty", "^$old_sha1", "$new_sha1", @exclude_list);
- push @revlist_options, "--pretty";
- open LIST, "-|" or exec "git", "rev-list", @revlist_options, "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list";
- while (<LIST>)
+ foreach my $rev (@$notice)
{
- chomp;
- s/^commit /URL: $gitweb_url\/?a=commit;h=/ if $gitweb_url;
- push @notice, $_;
+ $rev =~ s/^commit /URL: $gitweb_url\/?a=commit;h=/ if $gitweb_url;
}
- close LIST;
- mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @notice);
+ mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @$notice);
}
# send all the notices
sub send_all_notices($$$)
{
my ($old_sha1, $new_sha1, $ref) = @_;
+ my ($reftype, $refname, $action, @notice);
- $ref =~ s/^refs\/heads\///;
-
- return if (@include_list && !grep {$_ eq $ref} @include_list);
+ return if ($ref =~ /^refs\/remotes\//
+ or (@include_list && !grep {$_ eq $ref} @include_list));
+ die "The name \"$ref\" doesn't sound like a local branch or tag"
+ if not (($reftype, $refname) = ($ref =~ /^refs\/(head|tag)s\/(.+)/));
- if ($old_sha1 eq '0' x 40) # new ref
+ if ($new_sha1 eq '0' x 40)
{
- send_commit_notice( $ref, $new_sha1 ) if $commitlist_address;
- return;
+ $action = "removed";
+ @notice = ( "Old SHA1: $old_sha1" );
}
-
- my @commits = ();
-
- open LIST, "-|" or exec "git", "rev-list", @revlist_options, "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list";
- while (<LIST>)
+ elsif ($old_sha1 eq '0' x 40)
{
- chomp;
- die "invalid commit $_" unless /^[0-9a-f]{40}$/;
- unshift @commits, $_;
+ $action = "created";
+ @notice = ( "SHA1: $new_sha1" );
}
- close LIST;
-
- if (@commits > $max_individual_notices)
+ elsif ($reftype eq "tag")
{
- send_global_notice( $ref, $old_sha1, $new_sha1 ) if $commitlist_address;
- return;
+ $action = "updated";
+ @notice = ( "Old SHA1: $old_sha1", "New SHA1: $new_sha1" );
}
+ elsif (not grep( $_ eq $old_sha1, @{ git_rev_list( $new_sha1, "--full-history" ) } ))
+ {
+ $action = "rewritten";
+ @notice = ( "Old SHA1: $old_sha1", "New SHA1: $new_sha1" );
+ }
+
+ send_ref_notice( $ref, $action, @notice ) if ($commitlist_address and $action);
- foreach my $commit (@commits)
+ unless ($reftype eq "tag" or $new_sha1 eq '0' x 40)
{
- send_commit_notice( $ref, $commit ) if $commitlist_address;
- send_cia_notice( $ref, $commit ) if $cia_project_name;
+ my $commits = get_new_commits ( $old_sha1, $new_sha1 );
+
+ if (@$commits > $max_individual_notices)
+ {
+ send_global_notice( $refname, $old_sha1, $new_sha1 ) if $commitlist_address;
+ }
+ elsif (@$commits > 0)
+ {
+ foreach my $commit (@$commits)
+ {
+ send_commit_notice( $refname, $commit ) if $commitlist_address;
+ send_cia_notice( $refname, $commit ) if $cia_project_name;
+ }
+ }
+ elsif ($commitlist_address)
+ {
+ @notice = ( "Old SHA1: $old_sha1", "New SHA1: $new_sha1" );
+ send_ref_notice( $ref, "modified", @notice );
+ }
}
}
parse_options();
+umask( $mode_mask );
+
# append repository path to URL
$gitweb_url .= "/$repos_name.git" if $gitweb_url;