monitoring-plugins/tools/git-notify
Holger Weiss 765c08d15b Revert "git-notify: Remove unused [...] code"
This reverts commit 5445b9769f.  Alexandre
Julliard pointed out that the code in question was used if git-notify
was explicitly called with the SHA1 name of an annotated tag object.  At
the moment, the code in question actually _is_ unused due to later
modifications, but it wasn't at the time 5445b976 was committed, and
we'll add further changes so that the code will be used again in the
future.

Conflicts:

	tools/git-notify
2009-11-07 02:11:52 +01:00

624 lines
19 KiB
Perl
Executable file

#!/usr/bin/perl -w
#
# 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
# published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
#
#
# This script is meant to be called from .git/hooks/post-receive.
#
# Usage: git-notify [options] [--] old-sha1 new-sha1 refname
#
# -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 Set the file to use for reading and saving state
# -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
#
use strict;
use Fcntl ':flock';
use Encode qw(encode decode);
use Cwd 'realpath';
sub git_config($);
sub get_repos_name();
# some parameters you may want to change
# set this to something that takes "-s"
my $mailer = "/usr/bin/mail";
# CIA notification address
my $cia_address = "cia\@cia.navi.cx";
# debug mode
my $debug = 0;
# number of generated (non-CIA) notifications
my $sent_notices = 0;
# configuration parameters
# base URL of the gitweb repository browser (can be set with the -u option)
my $gitweb_url = git_config( "notify.baseurl" );
# default repository name (can be changed with the -r option)
my $repos_name = git_config( "notify.repository" ) || get_repos_name();
# max size of diffs in bytes (can be changed with the -s option)
my $max_diff_size = git_config( "notify.maxdiff" ) || 10000;
# address for mail notices (can be set with -m option)
my $commitlist_address = git_config( "notify.mail" );
# project name for CIA notices (can be set with -c option)
my $cia_project_name = git_config( "notify.cia" );
# max number of individual notices before falling back to a single global notice (can be set with -n option)
my $max_individual_notices = git_config( "notify.maxnotices" ) || 100;
# branches to include
my @include_list = split /\s+/, git_config( "notify.include" ) || "";
# branches to exclude
my @exclude_list = split /\s+/, git_config( "notify.exclude" ) || "";
# the state file we use (can be changed with the -t option)
my $state_file = git_config( "notify.statefile" ) || "/var/tmp/git-notify.state";
# 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 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 Set the file to use for reading and saving state\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";
exit 1;
}
sub xml_escape($)
{
my $str = shift;
$str =~ s/&/&/g;
$str =~ s/</&lt;/g;
$str =~ s/>/&gt;/g;
my @chars = unpack "U*", $str;
$str = join "", map { ($_ > 127) ? sprintf "&#%u;", $_ : chr($_); } @chars;
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 -e $state_file) # initialize the state file with all hashes
{
save_commits(git_rev_list("--all", "--full-history"));
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 @lines = @_;
my @table;
my $max = 0;
foreach my $line (@lines)
{
next if not defined $line;
my $pos = index($line, ":");
$max = $pos if $pos > $max;
}
foreach my $line (@lines)
{
next if not defined $line;
my ($left, $right) = split(/: */, $line, 2);
push @table, (defined $left and defined $right)
? sprintf("%*s: %s", $max + 1, $left, $right)
: $line;
}
return @table;
}
# format an integer date + timezone as string
# algorithm taken from git's date.c
sub format_date($$)
{
my ($time,$tz) = @_;
if ($tz < 0)
{
my $minutes = (-$tz / 100) * 60 + (-$tz % 100);
$time -= $minutes * 60;
}
else
{
my $minutes = ($tz / 100) * 60 + ($tz % 100);
$time += $minutes * 60;
}
return gmtime($time) . sprintf " %+05d", $tz;
}
# fetch a parameter from the git config file
sub git_config($)
{
my ($param) = @_;
open CONFIG, "-|" or exec "git", "config", $param;
my $ret = <CONFIG>;
chomp $ret if $ret;
close CONFIG or $ret = undef;
return $ret;
}
# parse command line options
sub parse_options()
{
while (@ARGV && $ARGV[0] =~ /^-/)
{
my $arg = shift @ARGV;
if ($arg eq '--') { last; }
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 '-d') { $debug++; }
else { usage(); }
}
if (@ARGV && $#ARGV != 2) { usage(); }
@exclude_list = map { "^$_"; } @exclude_list;
}
# send an email notification
sub mail_notification($$$@)
{
my ($name, $subject, $content_type, @text) = @_;
$subject = encode("MIME-Q",$subject);
if ($debug)
{
binmode STDOUT, ":utf8";
print "---------------------\n";
print "To: $name\n";
print "Subject: $subject\n";
print "Content-Type: $content_type\n";
print "\n", join("\n", @text), "\n";
}
else
{
my $pid = open MAIL, "|-";
return unless defined $pid;
if (!$pid)
{
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 or die $! ? "Cannot execute $mailer: $!" : "$mailer exited with status: $?";
}
}
# get the default repository name
sub get_repos_name()
{
my $dir = `git rev-parse --git-dir`;
chomp $dir;
my $repos = realpath($dir);
$repos =~ s/(.*?)((\.git\/)?\.git)$/$1/;
$repos =~ s/(.*)\/([^\/]+)\/?$/$2/;
return $repos;
}
# 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 %info = ();
my @log = ();
my $do_log = 0;
$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)
{
last if /^-----BEGIN PGP SIGNATURE-----/;
push @log, $_;
}
elsif (/^(author|committer|tagger) ((.*) (<.*>)) (\d+) ([+-]\d+)$/)
{
$info{$1} = $2;
$info{$1 . "_name"} = $3;
$info{$1 . "_email"} = $4;
$info{$1 . "_date"} = $5;
$info{$1 . "_tz"} = $6;
}
elsif (/^tag (.+)/)
{
$info{"tag"} = $1;
}
elsif (/^encoding (.+)/)
{
$info{"encoding"} = $1;
}
elsif (/^$/) { $do_log = 1; }
}
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);
$sent_notices++;
}
# 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);
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: $?";
return if length($diff) == 0;
if ($gitweb_url)
{
open REVPARSE, "-|" or exec "git", "rev-parse", "--short", $obj or die "cannot exec git-rev-parse";
my $short_obj = <REVPARSE>;
close REVPARSE or die $! ? "Cannot execute rev-parse: $!" : "rev-parse exited with status: $?";
$short_obj = $obj if not defined $short_obj;
chomp $short_obj;
$url = "$gitweb_url/?a=$info{type};h=$short_obj";
}
if ($info{"type"} eq "tag")
{
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, format_table(
"Module: $repos_name",
"Branch: $ref",
"Commit: $obj",
"Author:" . $info{"author"},
$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: $?";
if (($max_diff_size == -1) || (length($diff) < $max_diff_size))
{
push @notice, $diff;
}
else
{
push @notice, "Diff: $gitweb_url/?a=commitdiff;h=$obj" if $gitweb_url;
}
$subject = $info{"author_name"};
}
$subject .= ": " . truncate_str(${$info{"log"}}[0],50);
$_ = decode($info{"encoding"}, $_) for @notice;
mail_notification($commitlist_address, $subject, "text/plain; charset=UTF-8", @notice);
$sent_notices++;
}
# send a commit notice to the CIA server
sub send_cia_notice($$)
{
my ($ref,$commit) = @_;
my %info = get_object_info($commit);
my @cia_text = ();
return if $info{"type"} ne "commit";
push @cia_text,
"<message>",
" <generator>",
" <name>git-notify script for CIA</name>",
" </generator>",
" <source>",
" <project>" . xml_escape($cia_project_name) . "</project>",
" <module>" . xml_escape($repos_name) . "</module>",
" <branch>" . xml_escape($ref). "</branch>",
" </source>",
" <body>",
" <commit>",
" <revision>" . substr($commit,0,10) . "</revision>",
" <author>" . xml_escape($info{"author"}) . "</author>",
" <log>" . xml_escape(join "\n", @{$info{"log"}}) . "</log>",
" <files>";
open COMMIT, "-|" or exec "git", "diff-tree", "--name-status", "-r", "-M", $commit or die "cannot run git-diff-tree";
while (<COMMIT>)
{
chomp;
if (/^([AMD])\t(.*)$/)
{
my ($action, $file) = ($1, $2);
my %actions = ( "A" => "add", "M" => "modify", "D" => "remove" );
next unless defined $actions{$action};
push @cia_text, " <file action=\"$actions{$action}\">" . xml_escape($file) . "</file>";
}
elsif (/^R\d+\t(.*)\t(.*)$/)
{
my ($old, $new) = ($1, $2);
push @cia_text, " <file action=\"rename\" to=\"" . xml_escape($new) . "\">" . xml_escape($old) . "</file>";
}
}
close COMMIT or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?";
push @cia_text,
" </files>",
$gitweb_url ? " <url>" . xml_escape("$gitweb_url/?a=commit;h=$commit") . "</url>" : "",
" </commit>",
" </body>",
" <timestamp>" . $info{"author_date"} . "</timestamp>",
"</message>";
mail_notification($cia_address, "DeliverXML", "text/xml", @cia_text);
}
# send a global commit notice when there are too many commits for individual mails
sub send_global_notice($$$)
{
my ($ref, $old_sha1, $new_sha1) = @_;
my $notice = git_rev_list("--pretty", "^$old_sha1", "$new_sha1", @exclude_list);
foreach my $rev (@$notice)
{
$rev =~ s/^commit /URL: $gitweb_url\/?a=commit;h=/ if $gitweb_url;
}
mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @$notice);
$sent_notices++;
}
# send all the notices
sub send_all_notices($$$)
{
my ($old_sha1, $new_sha1, $ref) = @_;
my ($reftype, $refname, $action, @notice);
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 ($new_sha1 eq '0' x 40)
{
$action = "removed";
@notice = ( "Old SHA1: $old_sha1" );
}
elsif ($old_sha1 eq '0' x 40)
{
$action = "created";
@notice = ( "SHA1: $new_sha1" );
}
elsif ($reftype eq "tag")
{
$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);
unless ($reftype eq "tag" or $new_sha1 eq '0' x 40)
{
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;
}
else
{
foreach my $commit (@$commits)
{
send_commit_notice( $refname, $commit ) if $commitlist_address;
send_cia_notice( $refname, $commit ) if $cia_project_name;
}
}
if ($sent_notices == 0 and $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;
if (@ARGV)
{
send_all_notices( $ARGV[0], $ARGV[1], $ARGV[2] );
}
else # read them from stdin
{
while (<>)
{
chomp;
if (/^([0-9a-f]{40}) ([0-9a-f]{40}) (.*)$/) { send_all_notices( $1, $2, $3 ); }
}
}
exit 0;