1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/git/git-notify Fri Aug 10 14:40:03 2012 +0200 1.3 @@ -0,0 +1,431 @@ 1.4 +#!/usr/bin/perl -w 1.5 +# 1.6 +# Tool to send git commit notifications 1.7 +# 1.8 +# Copyright 2005 Alexandre Julliard 1.9 +# 1.10 +# This program is free software; you can redistribute it and/or 1.11 +# modify it under the terms of the GNU General Public License as 1.12 +# published by the Free Software Foundation; either version 2 of 1.13 +# the License, or (at your option) any later version. 1.14 +# 1.15 +# 1.16 +# This script is meant to be called from .git/hooks/post-receive. 1.17 +# 1.18 +# Usage: git-notify [options] [--] old-sha1 new-sha1 refname 1.19 +# 1.20 +# -c name Send CIA notifications under specified project name 1.21 +# -m addr Send mail notifications to specified address 1.22 +# -n max Set max number of individual mails to send 1.23 +# -r name Set the git repository name 1.24 +# -s bytes Set the maximum diff size in bytes (-1 for no limit) 1.25 +# -u url Set the URL to the gitweb browser 1.26 +# -i branch If at least one -i is given, report only for specified branches 1.27 +# -x branch Exclude changes to the specified branch from reports 1.28 +# -X Exclude merge commits 1.29 +# 1.30 + 1.31 +use strict; 1.32 +use open ':utf8'; 1.33 +use Encode 'encode'; 1.34 +use Cwd 'realpath'; 1.35 + 1.36 +binmode STDIN, ':utf8'; 1.37 +binmode STDOUT, ':utf8'; 1.38 + 1.39 +sub git_config($); 1.40 +sub get_repos_name(); 1.41 + 1.42 +# some parameters you may want to change 1.43 + 1.44 +# CIA notification address 1.45 +my $cia_address = "cia\@cia.navi.cx"; 1.46 + 1.47 +# debug mode 1.48 +my $debug = 0; 1.49 + 1.50 +# configuration parameters 1.51 + 1.52 +# base URL of the gitweb repository browser (can be set with the -u option) 1.53 +my $gitweb_url = git_config( "notify.baseurl" ); 1.54 + 1.55 +# default repository name (can be changed with the -r option) 1.56 +my $repos_name = git_config( "notify.repository" ) || get_repos_name(); 1.57 + 1.58 +# max size of diffs in bytes (can be changed with the -s option) 1.59 +my $max_diff_size = git_config( "notify.maxdiff" ) || 10000; 1.60 + 1.61 +# address for mail notices (can be set with -m option) 1.62 +my $commitlist_address = git_config( "notify.mail" ); 1.63 + 1.64 +# project name for CIA notices (can be set with -c option) 1.65 +my $cia_project_name = git_config( "notify.cia" ); 1.66 + 1.67 +# max number of individual notices before falling back to a single global notice (can be set with -n option) 1.68 +my $max_individual_notices = git_config( "notify.maxnotices" ) || 100; 1.69 + 1.70 +# branches to include 1.71 +my @include_list = split /\s+/, git_config( "notify.include" ) || ""; 1.72 + 1.73 +# branches to exclude 1.74 +my @exclude_list = split /\s+/, git_config( "notify.exclude" ) || ""; 1.75 + 1.76 +# set this to something that takes "-s" 1.77 +my $mailer = git_config( "notify.mailer" ) || "/usr/bin/mail"; 1.78 + 1.79 +# Extra options to git rev-list 1.80 +my @revlist_options; 1.81 + 1.82 +sub usage() 1.83 +{ 1.84 + print "Usage: $0 [options] [--] old-sha1 new-sha1 refname\n"; 1.85 + print " -c name Send CIA notifications under specified project name\n"; 1.86 + print " -m addr Send mail notifications to specified address\n"; 1.87 + print " -n max Set max number of individual mails to send\n"; 1.88 + print " -r name Set the git repository name\n"; 1.89 + print " -s bytes Set the maximum diff size in bytes (-1 for no limit)\n"; 1.90 + print " -u url Set the URL to the gitweb browser\n"; 1.91 + print " -i branch If at least one -i is given, report only for specified branches\n"; 1.92 + print " -x branch Exclude changes to the specified branch from reports\n"; 1.93 + print " -X Exclude merge commits\n"; 1.94 + exit 1; 1.95 +} 1.96 + 1.97 +sub xml_escape($) 1.98 +{ 1.99 + my $str = shift; 1.100 + $str =~ s/&/&/g; 1.101 + $str =~ s/</</g; 1.102 + $str =~ s/>/>/g; 1.103 + my @chars = unpack "U*", $str; 1.104 + $str = join "", map { ($_ > 127) ? sprintf "&#%u;", $_ : chr($_); } @chars; 1.105 + return $str; 1.106 +} 1.107 + 1.108 +# format an integer date + timezone as string 1.109 +# algorithm taken from git's date.c 1.110 +sub format_date($$) 1.111 +{ 1.112 + my ($time,$tz) = @_; 1.113 + 1.114 + if ($tz < 0) 1.115 + { 1.116 + my $minutes = (-$tz / 100) * 60 + (-$tz % 100); 1.117 + $time -= $minutes * 60; 1.118 + } 1.119 + else 1.120 + { 1.121 + my $minutes = ($tz / 100) * 60 + ($tz % 100); 1.122 + $time += $minutes * 60; 1.123 + } 1.124 + return gmtime($time) . sprintf " %+05d", $tz; 1.125 +} 1.126 + 1.127 +# fetch a parameter from the git config file 1.128 +sub git_config($) 1.129 +{ 1.130 + my ($param) = @_; 1.131 + 1.132 + open CONFIG, "-|" or exec "git", "config", $param; 1.133 + my $ret = <CONFIG>; 1.134 + chomp $ret if $ret; 1.135 + close CONFIG or $ret = undef; 1.136 + return $ret; 1.137 +} 1.138 + 1.139 +# parse command line options 1.140 +sub parse_options() 1.141 +{ 1.142 + while (@ARGV && $ARGV[0] =~ /^-/) 1.143 + { 1.144 + my $arg = shift @ARGV; 1.145 + 1.146 + if ($arg eq '--') { last; } 1.147 + elsif ($arg eq '-c') { $cia_project_name = shift @ARGV; } 1.148 + elsif ($arg eq '-m') { $commitlist_address = shift @ARGV; } 1.149 + elsif ($arg eq '-n') { $max_individual_notices = shift @ARGV; } 1.150 + elsif ($arg eq '-r') { $repos_name = shift @ARGV; } 1.151 + elsif ($arg eq '-s') { $max_diff_size = shift @ARGV; } 1.152 + elsif ($arg eq '-u') { $gitweb_url = shift @ARGV; } 1.153 + elsif ($arg eq '-i') { push @include_list, shift @ARGV; } 1.154 + elsif ($arg eq '-x') { push @exclude_list, shift @ARGV; } 1.155 + elsif ($arg eq '-X') { push @revlist_options, "--no-merges"; } 1.156 + elsif ($arg eq '-d') { $debug++; } 1.157 + else { usage(); } 1.158 + } 1.159 + if (@ARGV && $#ARGV != 2) { usage(); } 1.160 + @exclude_list = map { "^$_"; } @exclude_list; 1.161 +} 1.162 + 1.163 +# send an email notification 1.164 +sub mail_notification($$$@) 1.165 +{ 1.166 + my ($name, $subject, $content_type, @text) = @_; 1.167 + $subject = encode("MIME-Q",$subject); 1.168 + if ($debug) 1.169 + { 1.170 + print "---------------------\n"; 1.171 + print "To: $name\n"; 1.172 + print "Subject: $subject\n"; 1.173 + print "Content-Type: $content_type\n"; 1.174 + print "\n", join("\n", @text), "\n"; 1.175 + } 1.176 + else 1.177 + { 1.178 + my $pid = open MAIL, "|-"; 1.179 + return unless defined $pid; 1.180 + if (!$pid) 1.181 + { 1.182 + exec $mailer, "-s", $subject, "-a", "Content-Type: $content_type", $name or die "Cannot exec $mailer"; 1.183 + } 1.184 + print MAIL join("\n", @text), "\n"; 1.185 + close MAIL; 1.186 + } 1.187 +} 1.188 + 1.189 +# get the default repository name 1.190 +sub get_repos_name() 1.191 +{ 1.192 + my $dir = `git rev-parse --git-dir`; 1.193 + chomp $dir; 1.194 + my $repos = realpath($dir); 1.195 + $repos =~ s/(.*?)((\.git\/)?\.git)$/$1/; 1.196 + $repos =~ s/(.*)\/([^\/]+)\/?$/$2/; 1.197 + return $repos; 1.198 +} 1.199 + 1.200 +# extract the information from a commit or tag object and return a hash containing the various fields 1.201 +sub get_object_info($) 1.202 +{ 1.203 + my $obj = shift; 1.204 + my %info = (); 1.205 + my @log = (); 1.206 + my $do_log = 0; 1.207 + 1.208 + open TYPE, "-|" or exec "git", "cat-file", "-t", $obj or die "cannot run git-cat-file"; 1.209 + my $type = <TYPE>; 1.210 + chomp $type; 1.211 + close TYPE; 1.212 + 1.213 + open OBJ, "-|" or exec "git", "cat-file", $type, $obj or die "cannot run git-cat-file"; 1.214 + while (<OBJ>) 1.215 + { 1.216 + chomp; 1.217 + if ($do_log) 1.218 + { 1.219 + last if /^-----BEGIN PGP SIGNATURE-----/; 1.220 + push @log, $_; 1.221 + } 1.222 + elsif (/^(author|committer|tagger) ((.*)(<.*>)) (\d+) ([+-]\d+)$/) 1.223 + { 1.224 + $info{$1} = $2; 1.225 + $info{$1 . "_name"} = $3; 1.226 + $info{$1 . "_email"} = $4; 1.227 + $info{$1 . "_date"} = $5; 1.228 + $info{$1 . "_tz"} = $6; 1.229 + } 1.230 + elsif (/^tag (.*)$/) 1.231 + { 1.232 + $info{"tag"} = $1; 1.233 + } 1.234 + elsif (/^$/) { $do_log = 1; } 1.235 + } 1.236 + close OBJ; 1.237 + 1.238 + $info{"type"} = $type; 1.239 + $info{"log"} = \@log; 1.240 + return %info; 1.241 +} 1.242 + 1.243 +# send a commit notice to a mailing list 1.244 +sub send_commit_notice($$) 1.245 +{ 1.246 + my ($ref,$obj) = @_; 1.247 + my %info = get_object_info($obj); 1.248 + my @notice = (); 1.249 + my $subject; 1.250 + 1.251 + if ($info{"type"} eq "tag") 1.252 + { 1.253 + push @notice, 1.254 + "Module: $repos_name", 1.255 + "Branch: $ref", 1.256 + "Tag: $obj", 1.257 + $gitweb_url ? "URL: $gitweb_url/?a=tag;h=$obj\n" : "", 1.258 + "Tagger: " . $info{"tagger"}, 1.259 + "Date: " . format_date($info{"tagger_date"},$info{"tagger_tz"}), 1.260 + "", 1.261 + join "\n", @{$info{"log"}}; 1.262 + $subject = "Tag " . $info{"tag"} . " : " . $info{"tagger_name"} . ": " . ${$info{"log"}}[0]; 1.263 + } 1.264 + else 1.265 + { 1.266 + push @notice, 1.267 + "Module: $repos_name", 1.268 + "Branch: $ref", 1.269 + "Commit: $obj", 1.270 + $gitweb_url ? "URL: $gitweb_url/?a=commit;h=$obj\n" : "", 1.271 + "Author: " . $info{"author"}, 1.272 + "Date: " . format_date($info{"author_date"},$info{"author_tz"}), 1.273 + "", 1.274 + join "\n", @{$info{"log"}}, 1.275 + "", 1.276 + "---", 1.277 + ""; 1.278 + 1.279 + open STAT, "-|" or exec "git", "diff-tree", "--stat", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; 1.280 + push @notice, join("", <STAT>); 1.281 + close STAT; 1.282 + 1.283 + open DIFF, "-|" or exec "git", "diff-tree", "-p", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; 1.284 + my $diff = join( "", <DIFF> ); 1.285 + close DIFF; 1.286 + 1.287 + if (($max_diff_size == -1) || (length($diff) < $max_diff_size)) 1.288 + { 1.289 + push @notice, $diff; 1.290 + } 1.291 + else 1.292 + { 1.293 + push @notice, "Diff: $gitweb_url/?a=commitdiff;h=$obj" if $gitweb_url; 1.294 + } 1.295 + 1.296 + $subject = $info{"author_name"} . ": " . ${$info{"log"}}[0]; 1.297 + } 1.298 + 1.299 + mail_notification($commitlist_address, $subject, "text/plain; charset=UTF-8", @notice); 1.300 +} 1.301 + 1.302 +# send a commit notice to the CIA server 1.303 +sub send_cia_notice($$) 1.304 +{ 1.305 + my ($ref,$commit) = @_; 1.306 + my %info = get_object_info($commit); 1.307 + my @cia_text = (); 1.308 + 1.309 + return if $info{"type"} ne "commit"; 1.310 + 1.311 + push @cia_text, 1.312 + "<message>", 1.313 + " <generator>", 1.314 + " <name>git-notify script for CIA</name>", 1.315 + " </generator>", 1.316 + " <source>", 1.317 + " <project>" . xml_escape($cia_project_name) . "</project>", 1.318 + " <module>" . xml_escape($repos_name) . "</module>", 1.319 + " <branch>" . xml_escape($ref). "</branch>", 1.320 + " </source>", 1.321 + " <body>", 1.322 + " <commit>", 1.323 + " <revision>" . substr($commit,0,10) . "</revision>", 1.324 + " <author>" . xml_escape($info{"author"}) . "</author>", 1.325 + " <log>" . xml_escape(join "\n", @{$info{"log"}}) . "</log>", 1.326 + " <files>"; 1.327 + 1.328 + open COMMIT, "-|" or exec "git", "diff-tree", "--name-status", "-r", "-M", $commit or die "cannot run git-diff-tree"; 1.329 + while (<COMMIT>) 1.330 + { 1.331 + chomp; 1.332 + if (/^([AMD])\t(.*)$/) 1.333 + { 1.334 + my ($action, $file) = ($1, $2); 1.335 + my %actions = ( "A" => "add", "M" => "modify", "D" => "remove" ); 1.336 + next unless defined $actions{$action}; 1.337 + push @cia_text, " <file action=\"$actions{$action}\">" . xml_escape($file) . "</file>"; 1.338 + } 1.339 + elsif (/^R\d+\t(.*)\t(.*)$/) 1.340 + { 1.341 + my ($old, $new) = ($1, $2); 1.342 + push @cia_text, " <file action=\"rename\" to=\"" . xml_escape($new) . "\">" . xml_escape($old) . "</file>"; 1.343 + } 1.344 + } 1.345 + close COMMIT; 1.346 + 1.347 + push @cia_text, 1.348 + " </files>", 1.349 + $gitweb_url ? " <url>" . xml_escape("$gitweb_url/?a=commit;h=$commit") . "</url>" : "", 1.350 + " </commit>", 1.351 + " </body>", 1.352 + " <timestamp>" . $info{"author_date"} . "</timestamp>", 1.353 + "</message>"; 1.354 + 1.355 + mail_notification($cia_address, "DeliverXML", "text/xml", @cia_text); 1.356 +} 1.357 + 1.358 +# send a global commit notice when there are too many commits for individual mails 1.359 +sub send_global_notice($$$) 1.360 +{ 1.361 + my ($ref, $old_sha1, $new_sha1) = @_; 1.362 + my @notice = (); 1.363 + 1.364 + push @revlist_options, "--pretty"; 1.365 + open LIST, "-|" or exec "git", "rev-list", @revlist_options, "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list"; 1.366 + while (<LIST>) 1.367 + { 1.368 + chomp; 1.369 + s/^commit /URL: $gitweb_url\/?a=commit;h=/ if $gitweb_url; 1.370 + push @notice, $_; 1.371 + } 1.372 + close LIST; 1.373 + 1.374 + mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @notice); 1.375 +} 1.376 + 1.377 +# send all the notices 1.378 +sub send_all_notices($$$) 1.379 +{ 1.380 + my ($old_sha1, $new_sha1, $ref) = @_; 1.381 + 1.382 + $ref =~ s/^refs\/heads\///; 1.383 + 1.384 + return if (@include_list && !grep {$_ eq $ref} @include_list); 1.385 + 1.386 + if ($old_sha1 eq '0' x 40) # new ref 1.387 + { 1.388 + send_commit_notice( $ref, $new_sha1 ) if $commitlist_address; 1.389 + return; 1.390 + } 1.391 + 1.392 + my @commits = (); 1.393 + 1.394 + open LIST, "-|" or exec "git", "rev-list", @revlist_options, "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list"; 1.395 + while (<LIST>) 1.396 + { 1.397 + chomp; 1.398 + die "invalid commit $_" unless /^[0-9a-f]{40}$/; 1.399 + unshift @commits, $_; 1.400 + } 1.401 + close LIST; 1.402 + 1.403 + if (@commits > $max_individual_notices) 1.404 + { 1.405 + send_global_notice( $ref, $old_sha1, $new_sha1 ) if $commitlist_address; 1.406 + return; 1.407 + } 1.408 + 1.409 + foreach my $commit (@commits) 1.410 + { 1.411 + send_commit_notice( $ref, $commit ) if $commitlist_address; 1.412 + send_cia_notice( $ref, $commit ) if $cia_project_name; 1.413 + } 1.414 +} 1.415 + 1.416 +parse_options(); 1.417 + 1.418 +# append repository path to URL 1.419 +$gitweb_url .= "/$repos_name.git" if $gitweb_url; 1.420 + 1.421 +if (@ARGV) 1.422 +{ 1.423 + send_all_notices( $ARGV[0], $ARGV[1], $ARGV[2] ); 1.424 +} 1.425 +else # read them from stdin 1.426 +{ 1.427 + while (<>) 1.428 + { 1.429 + chomp; 1.430 + if (/^([0-9a-f]{40}) ([0-9a-f]{40}) (.*)$/) { send_all_notices( $1, $2, $3 ); } 1.431 + } 1.432 +} 1.433 + 1.434 +exit 0;