Thu, 21 Aug 2014 21:01:32 +0200
Update to finished configuration of summer software package stack.
michael@516 | 1 | #!/usr/bin/perl -w |
michael@516 | 2 | # |
michael@516 | 3 | # Tool to send git commit notifications |
michael@516 | 4 | # |
michael@516 | 5 | # Copyright 2005 Alexandre Julliard |
michael@516 | 6 | # |
michael@516 | 7 | # This program is free software; you can redistribute it and/or |
michael@516 | 8 | # modify it under the terms of the GNU General Public License as |
michael@516 | 9 | # published by the Free Software Foundation; either version 2 of |
michael@516 | 10 | # the License, or (at your option) any later version. |
michael@516 | 11 | # |
michael@516 | 12 | # |
michael@516 | 13 | # This script is meant to be called from .git/hooks/post-receive. |
michael@516 | 14 | # |
michael@516 | 15 | # Usage: git-notify [options] [--] old-sha1 new-sha1 refname |
michael@516 | 16 | # |
michael@516 | 17 | # -c name Send CIA notifications under specified project name |
michael@516 | 18 | # -m addr Send mail notifications to specified address |
michael@516 | 19 | # -n max Set max number of individual mails to send |
michael@516 | 20 | # -r name Set the git repository name |
michael@516 | 21 | # -s bytes Set the maximum diff size in bytes (-1 for no limit) |
michael@516 | 22 | # -u url Set the URL to the gitweb browser |
michael@516 | 23 | # -i branch If at least one -i is given, report only for specified branches |
michael@516 | 24 | # -x branch Exclude changes to the specified branch from reports |
michael@516 | 25 | # -X Exclude merge commits |
michael@516 | 26 | # |
michael@516 | 27 | |
michael@516 | 28 | use strict; |
michael@516 | 29 | use open ':utf8'; |
michael@516 | 30 | use Encode 'encode'; |
michael@516 | 31 | use Cwd 'realpath'; |
michael@516 | 32 | |
michael@516 | 33 | binmode STDIN, ':utf8'; |
michael@516 | 34 | binmode STDOUT, ':utf8'; |
michael@516 | 35 | |
michael@516 | 36 | sub git_config($); |
michael@516 | 37 | sub get_repos_name(); |
michael@516 | 38 | |
michael@516 | 39 | # some parameters you may want to change |
michael@516 | 40 | |
michael@516 | 41 | # CIA notification address |
michael@516 | 42 | my $cia_address = "cia\@cia.navi.cx"; |
michael@516 | 43 | |
michael@516 | 44 | # debug mode |
michael@516 | 45 | my $debug = 0; |
michael@516 | 46 | |
michael@516 | 47 | # configuration parameters |
michael@516 | 48 | |
michael@516 | 49 | # base URL of the gitweb repository browser (can be set with the -u option) |
michael@516 | 50 | my $gitweb_url = git_config( "notify.baseurl" ); |
michael@516 | 51 | |
michael@516 | 52 | # default repository name (can be changed with the -r option) |
michael@516 | 53 | my $repos_name = git_config( "notify.repository" ) || get_repos_name(); |
michael@516 | 54 | |
michael@516 | 55 | # max size of diffs in bytes (can be changed with the -s option) |
michael@516 | 56 | my $max_diff_size = git_config( "notify.maxdiff" ) || 10000; |
michael@516 | 57 | |
michael@516 | 58 | # address for mail notices (can be set with -m option) |
michael@516 | 59 | my $commitlist_address = git_config( "notify.mail" ); |
michael@516 | 60 | |
michael@516 | 61 | # project name for CIA notices (can be set with -c option) |
michael@516 | 62 | my $cia_project_name = git_config( "notify.cia" ); |
michael@516 | 63 | |
michael@516 | 64 | # max number of individual notices before falling back to a single global notice (can be set with -n option) |
michael@516 | 65 | my $max_individual_notices = git_config( "notify.maxnotices" ) || 100; |
michael@516 | 66 | |
michael@516 | 67 | # branches to include |
michael@516 | 68 | my @include_list = split /\s+/, git_config( "notify.include" ) || ""; |
michael@516 | 69 | |
michael@516 | 70 | # branches to exclude |
michael@516 | 71 | my @exclude_list = split /\s+/, git_config( "notify.exclude" ) || ""; |
michael@516 | 72 | |
michael@516 | 73 | # set this to something that takes "-s" |
michael@516 | 74 | my $mailer = git_config( "notify.mailer" ) || "/usr/bin/mail"; |
michael@516 | 75 | |
michael@516 | 76 | # Extra options to git rev-list |
michael@516 | 77 | my @revlist_options; |
michael@516 | 78 | |
michael@516 | 79 | sub usage() |
michael@516 | 80 | { |
michael@516 | 81 | print "Usage: $0 [options] [--] old-sha1 new-sha1 refname\n"; |
michael@516 | 82 | print " -c name Send CIA notifications under specified project name\n"; |
michael@516 | 83 | print " -m addr Send mail notifications to specified address\n"; |
michael@516 | 84 | print " -n max Set max number of individual mails to send\n"; |
michael@516 | 85 | print " -r name Set the git repository name\n"; |
michael@516 | 86 | print " -s bytes Set the maximum diff size in bytes (-1 for no limit)\n"; |
michael@516 | 87 | print " -u url Set the URL to the gitweb browser\n"; |
michael@516 | 88 | print " -i branch If at least one -i is given, report only for specified branches\n"; |
michael@516 | 89 | print " -x branch Exclude changes to the specified branch from reports\n"; |
michael@516 | 90 | print " -X Exclude merge commits\n"; |
michael@516 | 91 | exit 1; |
michael@516 | 92 | } |
michael@516 | 93 | |
michael@516 | 94 | sub xml_escape($) |
michael@516 | 95 | { |
michael@516 | 96 | my $str = shift; |
michael@516 | 97 | $str =~ s/&/&/g; |
michael@516 | 98 | $str =~ s/</</g; |
michael@516 | 99 | $str =~ s/>/>/g; |
michael@516 | 100 | my @chars = unpack "U*", $str; |
michael@516 | 101 | $str = join "", map { ($_ > 127) ? sprintf "&#%u;", $_ : chr($_); } @chars; |
michael@516 | 102 | return $str; |
michael@516 | 103 | } |
michael@516 | 104 | |
michael@516 | 105 | # format an integer date + timezone as string |
michael@516 | 106 | # algorithm taken from git's date.c |
michael@516 | 107 | sub format_date($$) |
michael@516 | 108 | { |
michael@516 | 109 | my ($time,$tz) = @_; |
michael@516 | 110 | |
michael@516 | 111 | if ($tz < 0) |
michael@516 | 112 | { |
michael@516 | 113 | my $minutes = (-$tz / 100) * 60 + (-$tz % 100); |
michael@516 | 114 | $time -= $minutes * 60; |
michael@516 | 115 | } |
michael@516 | 116 | else |
michael@516 | 117 | { |
michael@516 | 118 | my $minutes = ($tz / 100) * 60 + ($tz % 100); |
michael@516 | 119 | $time += $minutes * 60; |
michael@516 | 120 | } |
michael@516 | 121 | return gmtime($time) . sprintf " %+05d", $tz; |
michael@516 | 122 | } |
michael@516 | 123 | |
michael@516 | 124 | # fetch a parameter from the git config file |
michael@516 | 125 | sub git_config($) |
michael@516 | 126 | { |
michael@516 | 127 | my ($param) = @_; |
michael@516 | 128 | |
michael@516 | 129 | open CONFIG, "-|" or exec "git", "config", $param; |
michael@516 | 130 | my $ret = <CONFIG>; |
michael@516 | 131 | chomp $ret if $ret; |
michael@516 | 132 | close CONFIG or $ret = undef; |
michael@516 | 133 | return $ret; |
michael@516 | 134 | } |
michael@516 | 135 | |
michael@516 | 136 | # parse command line options |
michael@516 | 137 | sub parse_options() |
michael@516 | 138 | { |
michael@516 | 139 | while (@ARGV && $ARGV[0] =~ /^-/) |
michael@516 | 140 | { |
michael@516 | 141 | my $arg = shift @ARGV; |
michael@516 | 142 | |
michael@516 | 143 | if ($arg eq '--') { last; } |
michael@516 | 144 | elsif ($arg eq '-c') { $cia_project_name = shift @ARGV; } |
michael@516 | 145 | elsif ($arg eq '-m') { $commitlist_address = shift @ARGV; } |
michael@516 | 146 | elsif ($arg eq '-n') { $max_individual_notices = shift @ARGV; } |
michael@516 | 147 | elsif ($arg eq '-r') { $repos_name = shift @ARGV; } |
michael@516 | 148 | elsif ($arg eq '-s') { $max_diff_size = shift @ARGV; } |
michael@516 | 149 | elsif ($arg eq '-u') { $gitweb_url = shift @ARGV; } |
michael@516 | 150 | elsif ($arg eq '-i') { push @include_list, shift @ARGV; } |
michael@516 | 151 | elsif ($arg eq '-x') { push @exclude_list, shift @ARGV; } |
michael@516 | 152 | elsif ($arg eq '-X') { push @revlist_options, "--no-merges"; } |
michael@516 | 153 | elsif ($arg eq '-d') { $debug++; } |
michael@516 | 154 | else { usage(); } |
michael@516 | 155 | } |
michael@516 | 156 | if (@ARGV && $#ARGV != 2) { usage(); } |
michael@516 | 157 | @exclude_list = map { "^$_"; } @exclude_list; |
michael@516 | 158 | } |
michael@516 | 159 | |
michael@516 | 160 | # send an email notification |
michael@516 | 161 | sub mail_notification($$$@) |
michael@516 | 162 | { |
michael@516 | 163 | my ($name, $subject, $content_type, @text) = @_; |
michael@516 | 164 | $subject = encode("MIME-Q",$subject); |
michael@516 | 165 | if ($debug) |
michael@516 | 166 | { |
michael@516 | 167 | print "---------------------\n"; |
michael@516 | 168 | print "To: $name\n"; |
michael@516 | 169 | print "Subject: $subject\n"; |
michael@516 | 170 | print "Content-Type: $content_type\n"; |
michael@516 | 171 | print "\n", join("\n", @text), "\n"; |
michael@516 | 172 | } |
michael@516 | 173 | else |
michael@516 | 174 | { |
michael@516 | 175 | my $pid = open MAIL, "|-"; |
michael@516 | 176 | return unless defined $pid; |
michael@516 | 177 | if (!$pid) |
michael@516 | 178 | { |
michael@516 | 179 | exec $mailer, "-s", $subject, "-a", "Content-Type: $content_type", $name or die "Cannot exec $mailer"; |
michael@516 | 180 | } |
michael@516 | 181 | print MAIL join("\n", @text), "\n"; |
michael@516 | 182 | close MAIL; |
michael@516 | 183 | } |
michael@516 | 184 | } |
michael@516 | 185 | |
michael@516 | 186 | # get the default repository name |
michael@516 | 187 | sub get_repos_name() |
michael@516 | 188 | { |
michael@516 | 189 | my $dir = `git rev-parse --git-dir`; |
michael@516 | 190 | chomp $dir; |
michael@516 | 191 | my $repos = realpath($dir); |
michael@516 | 192 | $repos =~ s/(.*?)((\.git\/)?\.git)$/$1/; |
michael@516 | 193 | $repos =~ s/(.*)\/([^\/]+)\/?$/$2/; |
michael@516 | 194 | return $repos; |
michael@516 | 195 | } |
michael@516 | 196 | |
michael@516 | 197 | # extract the information from a commit or tag object and return a hash containing the various fields |
michael@516 | 198 | sub get_object_info($) |
michael@516 | 199 | { |
michael@516 | 200 | my $obj = shift; |
michael@516 | 201 | my %info = (); |
michael@516 | 202 | my @log = (); |
michael@516 | 203 | my $do_log = 0; |
michael@516 | 204 | |
michael@516 | 205 | open TYPE, "-|" or exec "git", "cat-file", "-t", $obj or die "cannot run git-cat-file"; |
michael@516 | 206 | my $type = <TYPE>; |
michael@516 | 207 | chomp $type; |
michael@516 | 208 | close TYPE; |
michael@516 | 209 | |
michael@516 | 210 | open OBJ, "-|" or exec "git", "cat-file", $type, $obj or die "cannot run git-cat-file"; |
michael@516 | 211 | while (<OBJ>) |
michael@516 | 212 | { |
michael@516 | 213 | chomp; |
michael@516 | 214 | if ($do_log) |
michael@516 | 215 | { |
michael@516 | 216 | last if /^-----BEGIN PGP SIGNATURE-----/; |
michael@516 | 217 | push @log, $_; |
michael@516 | 218 | } |
michael@516 | 219 | elsif (/^(author|committer|tagger) ((.*)(<.*>)) (\d+) ([+-]\d+)$/) |
michael@516 | 220 | { |
michael@516 | 221 | $info{$1} = $2; |
michael@516 | 222 | $info{$1 . "_name"} = $3; |
michael@516 | 223 | $info{$1 . "_email"} = $4; |
michael@516 | 224 | $info{$1 . "_date"} = $5; |
michael@516 | 225 | $info{$1 . "_tz"} = $6; |
michael@516 | 226 | } |
michael@516 | 227 | elsif (/^tag (.*)$/) |
michael@516 | 228 | { |
michael@516 | 229 | $info{"tag"} = $1; |
michael@516 | 230 | } |
michael@516 | 231 | elsif (/^$/) { $do_log = 1; } |
michael@516 | 232 | } |
michael@516 | 233 | close OBJ; |
michael@516 | 234 | |
michael@516 | 235 | $info{"type"} = $type; |
michael@516 | 236 | $info{"log"} = \@log; |
michael@516 | 237 | return %info; |
michael@516 | 238 | } |
michael@516 | 239 | |
michael@516 | 240 | # send a commit notice to a mailing list |
michael@516 | 241 | sub send_commit_notice($$) |
michael@516 | 242 | { |
michael@516 | 243 | my ($ref,$obj) = @_; |
michael@516 | 244 | my %info = get_object_info($obj); |
michael@516 | 245 | my @notice = (); |
michael@516 | 246 | my $subject; |
michael@516 | 247 | |
michael@516 | 248 | if ($info{"type"} eq "tag") |
michael@516 | 249 | { |
michael@516 | 250 | push @notice, |
michael@516 | 251 | "Module: $repos_name", |
michael@516 | 252 | "Branch: $ref", |
michael@516 | 253 | "Tag: $obj", |
michael@516 | 254 | $gitweb_url ? "URL: $gitweb_url/?a=tag;h=$obj\n" : "", |
michael@516 | 255 | "Tagger: " . $info{"tagger"}, |
michael@516 | 256 | "Date: " . format_date($info{"tagger_date"},$info{"tagger_tz"}), |
michael@516 | 257 | "", |
michael@516 | 258 | join "\n", @{$info{"log"}}; |
michael@516 | 259 | $subject = "Tag " . $info{"tag"} . " : " . $info{"tagger_name"} . ": " . ${$info{"log"}}[0]; |
michael@516 | 260 | } |
michael@516 | 261 | else |
michael@516 | 262 | { |
michael@516 | 263 | push @notice, |
michael@516 | 264 | "Module: $repos_name", |
michael@516 | 265 | "Branch: $ref", |
michael@516 | 266 | "Commit: $obj", |
michael@516 | 267 | $gitweb_url ? "URL: $gitweb_url/?a=commit;h=$obj\n" : "", |
michael@516 | 268 | "Author: " . $info{"author"}, |
michael@516 | 269 | "Date: " . format_date($info{"author_date"},$info{"author_tz"}), |
michael@516 | 270 | "", |
michael@516 | 271 | join "\n", @{$info{"log"}}, |
michael@516 | 272 | "", |
michael@516 | 273 | "---", |
michael@516 | 274 | ""; |
michael@516 | 275 | |
michael@516 | 276 | open STAT, "-|" or exec "git", "diff-tree", "--stat", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; |
michael@516 | 277 | push @notice, join("", <STAT>); |
michael@516 | 278 | close STAT; |
michael@516 | 279 | |
michael@516 | 280 | open DIFF, "-|" or exec "git", "diff-tree", "-p", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; |
michael@516 | 281 | my $diff = join( "", <DIFF> ); |
michael@516 | 282 | close DIFF; |
michael@516 | 283 | |
michael@516 | 284 | if (($max_diff_size == -1) || (length($diff) < $max_diff_size)) |
michael@516 | 285 | { |
michael@516 | 286 | push @notice, $diff; |
michael@516 | 287 | } |
michael@516 | 288 | else |
michael@516 | 289 | { |
michael@516 | 290 | push @notice, "Diff: $gitweb_url/?a=commitdiff;h=$obj" if $gitweb_url; |
michael@516 | 291 | } |
michael@516 | 292 | |
michael@516 | 293 | $subject = $info{"author_name"} . ": " . ${$info{"log"}}[0]; |
michael@516 | 294 | } |
michael@516 | 295 | |
michael@516 | 296 | mail_notification($commitlist_address, $subject, "text/plain; charset=UTF-8", @notice); |
michael@516 | 297 | } |
michael@516 | 298 | |
michael@516 | 299 | # send a commit notice to the CIA server |
michael@516 | 300 | sub send_cia_notice($$) |
michael@516 | 301 | { |
michael@516 | 302 | my ($ref,$commit) = @_; |
michael@516 | 303 | my %info = get_object_info($commit); |
michael@516 | 304 | my @cia_text = (); |
michael@516 | 305 | |
michael@516 | 306 | return if $info{"type"} ne "commit"; |
michael@516 | 307 | |
michael@516 | 308 | push @cia_text, |
michael@516 | 309 | "<message>", |
michael@516 | 310 | " <generator>", |
michael@516 | 311 | " <name>git-notify script for CIA</name>", |
michael@516 | 312 | " </generator>", |
michael@516 | 313 | " <source>", |
michael@516 | 314 | " <project>" . xml_escape($cia_project_name) . "</project>", |
michael@516 | 315 | " <module>" . xml_escape($repos_name) . "</module>", |
michael@516 | 316 | " <branch>" . xml_escape($ref). "</branch>", |
michael@516 | 317 | " </source>", |
michael@516 | 318 | " <body>", |
michael@516 | 319 | " <commit>", |
michael@516 | 320 | " <revision>" . substr($commit,0,10) . "</revision>", |
michael@516 | 321 | " <author>" . xml_escape($info{"author"}) . "</author>", |
michael@516 | 322 | " <log>" . xml_escape(join "\n", @{$info{"log"}}) . "</log>", |
michael@516 | 323 | " <files>"; |
michael@516 | 324 | |
michael@516 | 325 | open COMMIT, "-|" or exec "git", "diff-tree", "--name-status", "-r", "-M", $commit or die "cannot run git-diff-tree"; |
michael@516 | 326 | while (<COMMIT>) |
michael@516 | 327 | { |
michael@516 | 328 | chomp; |
michael@516 | 329 | if (/^([AMD])\t(.*)$/) |
michael@516 | 330 | { |
michael@516 | 331 | my ($action, $file) = ($1, $2); |
michael@516 | 332 | my %actions = ( "A" => "add", "M" => "modify", "D" => "remove" ); |
michael@516 | 333 | next unless defined $actions{$action}; |
michael@516 | 334 | push @cia_text, " <file action=\"$actions{$action}\">" . xml_escape($file) . "</file>"; |
michael@516 | 335 | } |
michael@516 | 336 | elsif (/^R\d+\t(.*)\t(.*)$/) |
michael@516 | 337 | { |
michael@516 | 338 | my ($old, $new) = ($1, $2); |
michael@516 | 339 | push @cia_text, " <file action=\"rename\" to=\"" . xml_escape($new) . "\">" . xml_escape($old) . "</file>"; |
michael@516 | 340 | } |
michael@516 | 341 | } |
michael@516 | 342 | close COMMIT; |
michael@516 | 343 | |
michael@516 | 344 | push @cia_text, |
michael@516 | 345 | " </files>", |
michael@516 | 346 | $gitweb_url ? " <url>" . xml_escape("$gitweb_url/?a=commit;h=$commit") . "</url>" : "", |
michael@516 | 347 | " </commit>", |
michael@516 | 348 | " </body>", |
michael@516 | 349 | " <timestamp>" . $info{"author_date"} . "</timestamp>", |
michael@516 | 350 | "</message>"; |
michael@516 | 351 | |
michael@516 | 352 | mail_notification($cia_address, "DeliverXML", "text/xml", @cia_text); |
michael@516 | 353 | } |
michael@516 | 354 | |
michael@516 | 355 | # send a global commit notice when there are too many commits for individual mails |
michael@516 | 356 | sub send_global_notice($$$) |
michael@516 | 357 | { |
michael@516 | 358 | my ($ref, $old_sha1, $new_sha1) = @_; |
michael@516 | 359 | my @notice = (); |
michael@516 | 360 | |
michael@516 | 361 | push @revlist_options, "--pretty"; |
michael@516 | 362 | open LIST, "-|" or exec "git", "rev-list", @revlist_options, "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list"; |
michael@516 | 363 | while (<LIST>) |
michael@516 | 364 | { |
michael@516 | 365 | chomp; |
michael@516 | 366 | s/^commit /URL: $gitweb_url\/?a=commit;h=/ if $gitweb_url; |
michael@516 | 367 | push @notice, $_; |
michael@516 | 368 | } |
michael@516 | 369 | close LIST; |
michael@516 | 370 | |
michael@516 | 371 | mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @notice); |
michael@516 | 372 | } |
michael@516 | 373 | |
michael@516 | 374 | # send all the notices |
michael@516 | 375 | sub send_all_notices($$$) |
michael@516 | 376 | { |
michael@516 | 377 | my ($old_sha1, $new_sha1, $ref) = @_; |
michael@516 | 378 | |
michael@516 | 379 | $ref =~ s/^refs\/heads\///; |
michael@516 | 380 | |
michael@516 | 381 | return if (@include_list && !grep {$_ eq $ref} @include_list); |
michael@516 | 382 | |
michael@516 | 383 | if ($old_sha1 eq '0' x 40) # new ref |
michael@516 | 384 | { |
michael@516 | 385 | send_commit_notice( $ref, $new_sha1 ) if $commitlist_address; |
michael@516 | 386 | return; |
michael@516 | 387 | } |
michael@516 | 388 | |
michael@516 | 389 | my @commits = (); |
michael@516 | 390 | |
michael@516 | 391 | open LIST, "-|" or exec "git", "rev-list", @revlist_options, "^$old_sha1", "$new_sha1", @exclude_list or die "cannot exec git-rev-list"; |
michael@516 | 392 | while (<LIST>) |
michael@516 | 393 | { |
michael@516 | 394 | chomp; |
michael@516 | 395 | die "invalid commit $_" unless /^[0-9a-f]{40}$/; |
michael@516 | 396 | unshift @commits, $_; |
michael@516 | 397 | } |
michael@516 | 398 | close LIST; |
michael@516 | 399 | |
michael@516 | 400 | if (@commits > $max_individual_notices) |
michael@516 | 401 | { |
michael@516 | 402 | send_global_notice( $ref, $old_sha1, $new_sha1 ) if $commitlist_address; |
michael@516 | 403 | return; |
michael@516 | 404 | } |
michael@516 | 405 | |
michael@516 | 406 | foreach my $commit (@commits) |
michael@516 | 407 | { |
michael@516 | 408 | send_commit_notice( $ref, $commit ) if $commitlist_address; |
michael@516 | 409 | send_cia_notice( $ref, $commit ) if $cia_project_name; |
michael@516 | 410 | } |
michael@516 | 411 | } |
michael@516 | 412 | |
michael@516 | 413 | parse_options(); |
michael@516 | 414 | |
michael@516 | 415 | # append repository path to URL |
michael@516 | 416 | $gitweb_url .= "/$repos_name.git" if $gitweb_url; |
michael@516 | 417 | |
michael@516 | 418 | if (@ARGV) |
michael@516 | 419 | { |
michael@516 | 420 | send_all_notices( $ARGV[0], $ARGV[1], $ARGV[2] ); |
michael@516 | 421 | } |
michael@516 | 422 | else # read them from stdin |
michael@516 | 423 | { |
michael@516 | 424 | while (<>) |
michael@516 | 425 | { |
michael@516 | 426 | chomp; |
michael@516 | 427 | if (/^([0-9a-f]{40}) ([0-9a-f]{40}) (.*)$/) { send_all_notices( $1, $2, $3 ); } |
michael@516 | 428 | } |
michael@516 | 429 | } |
michael@516 | 430 | |
michael@516 | 431 | exit 0; |