diff options
Diffstat (limited to 'libexec/git-notify')
-rwxr-xr-x | libexec/git-notify | 676 |
1 files changed, 676 insertions, 0 deletions
diff --git a/libexec/git-notify b/libexec/git-notify new file mode 100755 index 0000000..8514296 --- /dev/null +++ b/libexec/git-notify | |||
@@ -0,0 +1,676 @@ | |||
1 | #!/usr/bin/perl -w | ||
2 | # | ||
3 | # Tool to send git commit notifications | ||
4 | # | ||
5 | # Copyright 2005 Alexandre Julliard | ||
6 | # Copyright 2009 Nagios Plugins Development Team | ||
7 | # | ||
8 | # This program is free software; you can redistribute it and/or | ||
9 | # modify it under the terms of the GNU General Public License as | ||
10 | # published by the Free Software Foundation; either version 2 of | ||
11 | # the License, or (at your option) any later version. | ||
12 | # | ||
13 | # | ||
14 | # This script is meant to be called from .git/hooks/post-receive. | ||
15 | # | ||
16 | # Usage: git-notify [options] [--] old-sha1 new-sha1 refname | ||
17 | # | ||
18 | # -A Omit the author name from the mail subject | ||
19 | # -C Show committer in the body if different from the author | ||
20 | # -c name Send CIA notifications under specified project name | ||
21 | # -m addr Send mail notifications to specified address | ||
22 | # -n max Set max number of individual mails to send | ||
23 | # -r name Set the git repository name | ||
24 | # -S Enable compatibility with SourceForge's gitweb URLs | ||
25 | # -s bytes Set the maximum diff size in bytes (-1 for no limit) | ||
26 | # -T Prefix the mail subject with a [repository name] tag | ||
27 | # -t file Prevent duplicate notifications by saving state to this file | ||
28 | # -U mask Set the umask for creating the state file | ||
29 | # -u url Set the URL to the gitweb browser | ||
30 | # -i branch If at least one -i is given, report only for specified branches | ||
31 | # -x branch Exclude changes to the specified branch from reports | ||
32 | # -X Exclude merge commits | ||
33 | # -z Try to abbreviate the SHA1 name within gitweb URLs (unsafe) | ||
34 | # | ||
35 | |||
36 | use strict; | ||
37 | use Fcntl ':flock'; | ||
38 | use Encode qw(encode decode); | ||
39 | use Cwd 'realpath'; | ||
40 | |||
41 | sub git_config($); | ||
42 | sub get_repos_name(); | ||
43 | |||
44 | # some parameters you may want to change | ||
45 | |||
46 | # sendmail's pathname | ||
47 | my $sendmail = "/usr/sbin/sendmail"; | ||
48 | |||
49 | # CIA notification address | ||
50 | my $cia_address = "cia\@cia.vc"; | ||
51 | |||
52 | # debug mode | ||
53 | my $debug = 0; | ||
54 | |||
55 | # configuration parameters | ||
56 | |||
57 | # omit the author from the mail subject (can be set with the -A option) | ||
58 | my $omit_author = git_config( "notify.omitauthor" ); | ||
59 | |||
60 | # prefix the mail subject with a [repository name] tag (can be set with the -T option) | ||
61 | my $emit_repo = git_config( "notify.emitrepository" ); | ||
62 | |||
63 | # show the committer if different from the author (can be set with the -C option) | ||
64 | my $show_committer = git_config( "notify.showcommitter" ); | ||
65 | |||
66 | # base URL of the gitweb repository browser (can be set with the -u option) | ||
67 | my $gitweb_url = git_config( "notify.baseurl" ); | ||
68 | |||
69 | # abbreviate the SHA1 name within gitweb URLs (can be set with the -z option) | ||
70 | my $abbreviate_url = git_config( "notify.shorturls" ); | ||
71 | |||
72 | # don't report merge commits (can be set with the -X option) | ||
73 | my $ignore_merges = git_config( "notify.ignoremerges" ); | ||
74 | |||
75 | # enable compatibility with SourceForge's gitweb (can be set with the -S option) | ||
76 | my $sourceforge = git_config( "notify.sourceforge" ); | ||
77 | |||
78 | # default repository name (can be changed with the -r option) | ||
79 | my $repos_name = git_config( "notify.repository" ) || get_repos_name(); | ||
80 | |||
81 | # max size of diffs in bytes (can be changed with the -s option) | ||
82 | my $max_diff_size = git_config( "notify.maxdiff" ) || 10000; | ||
83 | |||
84 | # address for mail notices (can be set with -m option) | ||
85 | my $commitlist_address = git_config( "notify.mail" ); | ||
86 | |||
87 | # project name for CIA notices (can be set with -c option) | ||
88 | my $cia_project_name = git_config( "notify.cia" ); | ||
89 | |||
90 | # max number of individual notices before falling back to a single global notice (can be set with -n option) | ||
91 | my $max_individual_notices = git_config( "notify.maxnotices" ) || 100; | ||
92 | |||
93 | # branches to include | ||
94 | my @include_list = split /\s+/, git_config( "notify.include" ) || ""; | ||
95 | |||
96 | # branches to exclude | ||
97 | my @exclude_list = split /\s+/, git_config( "notify.exclude" ) || ""; | ||
98 | |||
99 | # the state file we use (can be set with the -t option) | ||
100 | my $state_file = git_config( "notify.statefile" ); | ||
101 | |||
102 | # umask for creating the state file (can be set with -U option) | ||
103 | my $mode_mask = git_config( "notify.umask" ) || 002; | ||
104 | |||
105 | sub usage() | ||
106 | { | ||
107 | print "Usage: $0 [options] [--] old-sha1 new-sha1 refname\n"; | ||
108 | print " -A Omit the author name from the mail subject\n"; | ||
109 | print " -C Show committer in the body if different from the author\n"; | ||
110 | print " -c name Send CIA notifications under specified project name\n"; | ||
111 | print " -m addr Send mail notifications to specified address\n"; | ||
112 | print " -n max Set max number of individual mails to send\n"; | ||
113 | print " -r name Set the git repository name\n"; | ||
114 | print " -S Enable compatibility with SourceForge's gitweb URLs\n"; | ||
115 | print " -s bytes Set the maximum diff size in bytes (-1 for no limit)\n"; | ||
116 | print " -T Prefix the mail subject with a [repository name] tag\n"; | ||
117 | print " -t file Prevent duplicate notifications by saving state to this file\n"; | ||
118 | print " -U mask Set the umask for creating the state file\n"; | ||
119 | print " -u url Set the URL to the gitweb browser\n"; | ||
120 | print " -i branch If at least one -i is given, report only for specified branches\n"; | ||
121 | print " -x branch Exclude changes to the specified branch from reports\n"; | ||
122 | print " -X Exclude merge commits\n"; | ||
123 | print " -z Try to abbreviate the SHA1 name within gitweb URLs (unsafe)\n"; | ||
124 | exit 1; | ||
125 | } | ||
126 | |||
127 | sub xml_escape($) | ||
128 | { | ||
129 | my $str = shift; | ||
130 | $str =~ s/&/&/g; | ||
131 | $str =~ s/</</g; | ||
132 | $str =~ s/>/>/g; | ||
133 | my @chars = unpack "U*", $str; | ||
134 | $str = join "", map { ($_ > 127) ? sprintf "&#%u;", $_ : chr($_); } @chars; | ||
135 | return $str; | ||
136 | } | ||
137 | |||
138 | # execute git-rev-list(1) with the given parameters and return the output | ||
139 | sub git_rev_list(@) | ||
140 | { | ||
141 | my @args = @_; | ||
142 | my $revlist = []; | ||
143 | my $pid = open REVLIST, "-|"; | ||
144 | |||
145 | die "Cannot open pipe: $!" if not defined $pid; | ||
146 | if (!$pid) | ||
147 | { | ||
148 | exec "git", "rev-list", "--reverse", @args or die "Cannot execute rev-list: $!"; | ||
149 | } | ||
150 | while (<REVLIST>) | ||
151 | { | ||
152 | chomp; | ||
153 | unless (grep {$_ eq "--pretty"} @args) | ||
154 | { | ||
155 | die "Invalid commit: $_" if not /^[0-9a-f]{40}$/; | ||
156 | } | ||
157 | push @$revlist, $_; | ||
158 | } | ||
159 | close REVLIST or die $! ? "Cannot execute rev-list: $!" : "rev-list exited with status: $?"; | ||
160 | return $revlist; | ||
161 | } | ||
162 | |||
163 | # append the given commit hashes to the state file | ||
164 | sub save_commits($) | ||
165 | { | ||
166 | my $commits = shift; | ||
167 | |||
168 | open STATE, ">>", $state_file or die "Cannot open $state_file: $!"; | ||
169 | flock STATE, LOCK_EX or die "Cannot lock $state_file"; | ||
170 | print STATE "$_\n" for @$commits; | ||
171 | flock STATE, LOCK_UN or die "Cannot unlock $state_file"; | ||
172 | close STATE or die "Cannot close $state_file: $!"; | ||
173 | } | ||
174 | |||
175 | # for the given range, return the new hashes (and append them to the state file) | ||
176 | sub get_new_commits($$) | ||
177 | { | ||
178 | my ($old_sha1, $new_sha1) = @_; | ||
179 | my ($seen, @args); | ||
180 | my $newrevs = []; | ||
181 | |||
182 | @args = ( "^$old_sha1" ) unless $old_sha1 eq '0' x 40; | ||
183 | push @args, $new_sha1, @exclude_list; | ||
184 | unshift @args, "--no-merges" if $ignore_merges; | ||
185 | |||
186 | my $revlist = git_rev_list(@args); | ||
187 | |||
188 | if (not defined $state_file or not -e $state_file) | ||
189 | { | ||
190 | save_commits(git_rev_list("--all", "--full-history")) if defined $state_file; | ||
191 | return $revlist; | ||
192 | } | ||
193 | |||
194 | open STATE, $state_file or die "Cannot open $state_file: $!"; | ||
195 | flock STATE, LOCK_SH or die "Cannot lock $state_file"; | ||
196 | while (<STATE>) | ||
197 | { | ||
198 | chomp; | ||
199 | die "Invalid commit: $_" if not /^[0-9a-f]{40}$/; | ||
200 | $seen->{$_} = 1; | ||
201 | } | ||
202 | flock STATE, LOCK_UN or die "Cannot unlock $state_file"; | ||
203 | close STATE or die "Cannot close $state_file: $!"; | ||
204 | |||
205 | # FIXME: if another git-notify process reads the $state_file at *this* | ||
206 | # point, that process might generate duplicates of our notifications. | ||
207 | |||
208 | save_commits($revlist); | ||
209 | |||
210 | foreach my $commit (@$revlist) | ||
211 | { | ||
212 | push @$newrevs, $commit unless $seen->{$commit}; | ||
213 | } | ||
214 | return $newrevs; | ||
215 | } | ||
216 | |||
217 | # truncate the given string if it exceeds the specified number of characters | ||
218 | sub truncate_str($$) | ||
219 | { | ||
220 | my ($str, $max) = @_; | ||
221 | |||
222 | if (length($str) > $max) | ||
223 | { | ||
224 | $str = substr($str, 0, $max); | ||
225 | $str =~ s/\s+\S+$//; | ||
226 | $str .= " ..."; | ||
227 | } | ||
228 | return $str; | ||
229 | } | ||
230 | |||
231 | # right-justify the left column of "left: right" elements, omit undefined elements | ||
232 | sub format_table(@) | ||
233 | { | ||
234 | my @lines = @_; | ||
235 | my @table; | ||
236 | my $max = 0; | ||
237 | |||
238 | foreach my $line (@lines) | ||
239 | { | ||
240 | next if not defined $line; | ||
241 | my $pos = index($line, ":"); | ||
242 | |||
243 | $max = $pos if $pos > $max; | ||
244 | } | ||
245 | |||
246 | foreach my $line (@lines) | ||
247 | { | ||
248 | next if not defined $line; | ||
249 | my ($left, $right) = split(/: */, $line, 2); | ||
250 | |||
251 | push @table, (defined $left and defined $right) | ||
252 | ? sprintf("%*s: %s", $max + 1, $left, $right) | ||
253 | : $line; | ||
254 | } | ||
255 | return @table; | ||
256 | } | ||
257 | |||
258 | # format an integer date + timezone as string | ||
259 | # algorithm taken from git's date.c | ||
260 | sub format_date($$) | ||
261 | { | ||
262 | my ($time,$tz) = @_; | ||
263 | |||
264 | if ($tz < 0) | ||
265 | { | ||
266 | my $minutes = (-$tz / 100) * 60 + (-$tz % 100); | ||
267 | $time -= $minutes * 60; | ||
268 | } | ||
269 | else | ||
270 | { | ||
271 | my $minutes = ($tz / 100) * 60 + ($tz % 100); | ||
272 | $time += $minutes * 60; | ||
273 | } | ||
274 | return gmtime($time) . sprintf " %+05d", $tz; | ||
275 | } | ||
276 | |||
277 | # fetch a parameter from the git config file | ||
278 | sub git_config($) | ||
279 | { | ||
280 | my ($param) = @_; | ||
281 | |||
282 | open CONFIG, "-|" or exec "git", "config", $param; | ||
283 | my $ret = <CONFIG>; | ||
284 | chomp $ret if $ret; | ||
285 | close CONFIG or $ret = undef; | ||
286 | return $ret; | ||
287 | } | ||
288 | |||
289 | # parse command line options | ||
290 | sub parse_options() | ||
291 | { | ||
292 | while (@ARGV && $ARGV[0] =~ /^-/) | ||
293 | { | ||
294 | my $arg = shift @ARGV; | ||
295 | |||
296 | if ($arg eq '--') { last; } | ||
297 | elsif ($arg eq '-A') { $omit_author = 1; } | ||
298 | elsif ($arg eq '-C') { $show_committer = 1; } | ||
299 | elsif ($arg eq '-c') { $cia_project_name = shift @ARGV; } | ||
300 | elsif ($arg eq '-m') { $commitlist_address = shift @ARGV; } | ||
301 | elsif ($arg eq '-n') { $max_individual_notices = shift @ARGV; } | ||
302 | elsif ($arg eq '-r') { $repos_name = shift @ARGV; } | ||
303 | elsif ($arg eq '-S') { $sourceforge = 1; } | ||
304 | elsif ($arg eq '-s') { $max_diff_size = shift @ARGV; } | ||
305 | elsif ($arg eq '-T') { $emit_repo = 1; } | ||
306 | elsif ($arg eq '-t') { $state_file = shift @ARGV; } | ||
307 | elsif ($arg eq '-U') { $mode_mask = shift @ARGV; } | ||
308 | elsif ($arg eq '-u') { $gitweb_url = shift @ARGV; } | ||
309 | elsif ($arg eq '-i') { push @include_list, shift @ARGV; } | ||
310 | elsif ($arg eq '-X') { $ignore_merges = 1; } | ||
311 | elsif ($arg eq '-x') { push @exclude_list, shift @ARGV; } | ||
312 | elsif ($arg eq '-z') { $abbreviate_url = 1; } | ||
313 | elsif ($arg eq '-d') { $debug++; } | ||
314 | else { usage(); } | ||
315 | } | ||
316 | if (@ARGV && $#ARGV != 2) { usage(); } | ||
317 | @exclude_list = map { "^$_"; } @exclude_list; | ||
318 | } | ||
319 | |||
320 | # send an email notification | ||
321 | sub mail_notification($$$@) | ||
322 | { | ||
323 | my ($name, $subject, $content_type, @text) = @_; | ||
324 | |||
325 | $subject = "[$repos_name] $subject" if ($emit_repo and $name ne $cia_address); | ||
326 | $subject = encode("MIME-Q",$subject); | ||
327 | |||
328 | my @header = ("To: $name", "Subject: $subject", "Content-Type: $content_type"); | ||
329 | |||
330 | if ($debug) | ||
331 | { | ||
332 | binmode STDOUT, ":utf8"; | ||
333 | print "---------------------\n"; | ||
334 | print join("\n", @header), "\n\n", join("\n", @text), "\n"; | ||
335 | } | ||
336 | else | ||
337 | { | ||
338 | my $pid = open MAIL, "|-"; | ||
339 | return unless defined $pid; | ||
340 | if (!$pid) | ||
341 | { | ||
342 | exec $sendmail, "-t", "-oi", "-oem" or die "Cannot exec $sendmail"; | ||
343 | } | ||
344 | binmode MAIL, ":utf8"; | ||
345 | print MAIL join("\n", @header), "\n\n", join("\n", @text), "\n"; | ||
346 | close MAIL or warn $! ? "Cannot execute $sendmail: $!" : "$sendmail exited with status: $?"; | ||
347 | } | ||
348 | } | ||
349 | |||
350 | # get the default repository name | ||
351 | sub get_repos_name() | ||
352 | { | ||
353 | my $dir = `git rev-parse --git-dir`; | ||
354 | chomp $dir; | ||
355 | my $repos = realpath($dir); | ||
356 | $repos =~ s/(.*?)((\.git\/)?\.git)$/$1/; | ||
357 | $repos =~ s/(.*)\/([^\/]+)\/?$/$2/; | ||
358 | return $repos; | ||
359 | } | ||
360 | |||
361 | # return the type of the given object | ||
362 | sub get_object_type($) | ||
363 | { | ||
364 | my $obj = shift; | ||
365 | |||
366 | open TYPE, "-|" or exec "git", "cat-file", "-t", $obj or die "cannot run git-cat-file"; | ||
367 | my $type = <TYPE>; | ||
368 | chomp $type; | ||
369 | close TYPE or die $! ? "Cannot execute cat-file: $!" : "cat-file exited with status: $?"; | ||
370 | return $type; | ||
371 | } | ||
372 | |||
373 | # extract the information from a commit or tag object and return a hash containing the various fields | ||
374 | sub get_object_info($) | ||
375 | { | ||
376 | my $obj = shift; | ||
377 | my %info = (); | ||
378 | my @log = (); | ||
379 | my $do_log = 0; | ||
380 | |||
381 | $info{"encoding"} = "utf-8"; | ||
382 | |||
383 | my $type = get_object_type($obj); | ||
384 | |||
385 | open OBJ, "-|" or exec "git", "cat-file", $type, $obj or die "cannot run git-cat-file"; | ||
386 | while (<OBJ>) | ||
387 | { | ||
388 | chomp; | ||
389 | if ($do_log) | ||
390 | { | ||
391 | last if /^-----BEGIN PGP SIGNATURE-----/; | ||
392 | push @log, $_; | ||
393 | } | ||
394 | elsif (/^(author|committer|tagger) ((.*) (<.*>)) (\d+) ([+-]\d+)$/) | ||
395 | { | ||
396 | $info{$1} = $2; | ||
397 | $info{$1 . "_name"} = $3; | ||
398 | $info{$1 . "_email"} = $4; | ||
399 | $info{$1 . "_date"} = $5; | ||
400 | $info{$1 . "_tz"} = $6; | ||
401 | } | ||
402 | elsif (/^tag (.+)/) | ||
403 | { | ||
404 | $info{"tag"} = $1; | ||
405 | } | ||
406 | elsif (/^encoding (.+)/) | ||
407 | { | ||
408 | $info{"encoding"} = $1; | ||
409 | } | ||
410 | elsif (/^$/) { $do_log = 1; } | ||
411 | } | ||
412 | close OBJ or die $! ? "Cannot execute cat-file: $!" : "cat-file exited with status: $?"; | ||
413 | |||
414 | $info{"type"} = $type; | ||
415 | $info{"log"} = \@log; | ||
416 | return %info; | ||
417 | } | ||
418 | |||
419 | # send a ref change notice to a mailing list | ||
420 | sub send_ref_notice($$@) | ||
421 | { | ||
422 | my ($ref, $action, @notice) = @_; | ||
423 | my ($reftype, $refname) = ($ref =~ /^refs\/(head|tag)s\/(.+)/); | ||
424 | |||
425 | $reftype =~ s/^head$/branch/; | ||
426 | |||
427 | @notice = (format_table( | ||
428 | "Module: $repos_name", | ||
429 | ($reftype eq "tag" ? "Tag:" : "Branch:") . $refname, | ||
430 | @notice, | ||
431 | ($action ne "removed" and $gitweb_url) | ||
432 | ? "URL: ${gitweb_url}a=shortlog;h=$ref" : undef), | ||
433 | "", | ||
434 | "The $refname $reftype has been $action."); | ||
435 | |||
436 | mail_notification($commitlist_address, "$refname $reftype $action", | ||
437 | "text/plain; charset=us-ascii", @notice); | ||
438 | } | ||
439 | |||
440 | # send a commit notice to a mailing list | ||
441 | sub send_commit_notice($$) | ||
442 | { | ||
443 | my ($ref,$obj) = @_; | ||
444 | my %info = get_object_info($obj); | ||
445 | my @notice = (); | ||
446 | my ($url,$subject,$obj_string); | ||
447 | |||
448 | if ($gitweb_url) | ||
449 | { | ||
450 | if ($abbreviate_url) | ||
451 | { | ||
452 | open REVPARSE, "-|" or exec "git", "rev-parse", "--short", $obj or die "cannot exec git-rev-parse"; | ||
453 | $obj_string = <REVPARSE>; | ||
454 | chomp $obj_string if defined $obj_string; | ||
455 | close REVPARSE or die $! ? "Cannot execute rev-parse: $!" : "rev-parse exited with status: $?"; | ||
456 | } | ||
457 | $obj_string = $obj if not defined $obj_string; | ||
458 | $url = "${gitweb_url}a=$info{type};h=$obj_string"; | ||
459 | } | ||
460 | |||
461 | if ($info{"type"} eq "tag") | ||
462 | { | ||
463 | push @notice, format_table( | ||
464 | "Module: $repos_name", | ||
465 | "Tag: $ref", | ||
466 | "SHA1: $obj", | ||
467 | "Tagger:" . $info{"tagger"}, | ||
468 | "Date:" . format_date($info{"tagger_date"},$info{"tagger_tz"}), | ||
469 | $url ? "URL: $url" : undef), | ||
470 | "", | ||
471 | join "\n", @{$info{"log"}}; | ||
472 | |||
473 | $subject = "Tag " . $info{"tag"} . ": "; | ||
474 | $subject .= $info{"tagger_name"} . ": " unless $omit_author; | ||
475 | } | ||
476 | else | ||
477 | { | ||
478 | push @notice, format_table( | ||
479 | "Module: $repos_name", | ||
480 | "Branch: $ref", | ||
481 | "Commit: $obj", | ||
482 | "Author:" . $info{"author"}, | ||
483 | $show_committer && $info{"committer"} ne $info{"author"} ? "Committer:" . $info{"committer"} : undef, | ||
484 | "Date:" . format_date($info{"author_date"},$info{"author_tz"}), | ||
485 | $url ? "URL: $url" : undef), | ||
486 | "", | ||
487 | @{$info{"log"}}, | ||
488 | "", | ||
489 | "---", | ||
490 | ""; | ||
491 | |||
492 | open STAT, "-|" or exec "git", "diff-tree", "--stat", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; | ||
493 | push @notice, join("", <STAT>); | ||
494 | close STAT or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?"; | ||
495 | |||
496 | open DIFF, "-|" or exec "git", "diff-tree", "-p", "-M", "--no-commit-id", $obj or die "cannot exec git-diff-tree"; | ||
497 | my $diff = join("", <DIFF>); | ||
498 | close DIFF or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?"; | ||
499 | |||
500 | if (($max_diff_size == -1) || (length($diff) < $max_diff_size)) | ||
501 | { | ||
502 | push @notice, $diff; | ||
503 | } | ||
504 | else | ||
505 | { | ||
506 | push @notice, "Diff: ${gitweb_url}a=commitdiff;h=$obj_string" if $gitweb_url; | ||
507 | } | ||
508 | $subject = $info{"author_name"} . ": " unless $omit_author; | ||
509 | } | ||
510 | |||
511 | $subject .= truncate_str(${$info{"log"}}[0],50); | ||
512 | $_ = decode($info{"encoding"}, $_) for @notice; | ||
513 | mail_notification($commitlist_address, $subject, "text/plain; charset=UTF-8", @notice); | ||
514 | } | ||
515 | |||
516 | # send a commit notice to the CIA server | ||
517 | sub send_cia_notice($$) | ||
518 | { | ||
519 | my ($ref,$commit) = @_; | ||
520 | my %info = get_object_info($commit); | ||
521 | my @cia_text = (); | ||
522 | |||
523 | return if $info{"type"} ne "commit"; | ||
524 | |||
525 | push @cia_text, | ||
526 | "<message>", | ||
527 | " <generator>", | ||
528 | " <name>git-notify script for CIA</name>", | ||
529 | " </generator>", | ||
530 | " <source>", | ||
531 | " <project>" . xml_escape($cia_project_name) . "</project>", | ||
532 | " <module>" . xml_escape($repos_name) . "</module>", | ||
533 | " <branch>" . xml_escape($ref). "</branch>", | ||
534 | " </source>", | ||
535 | " <body>", | ||
536 | " <commit>", | ||
537 | " <revision>" . substr($commit,0,10) . "</revision>", | ||
538 | " <author>" . xml_escape($info{"author"}) . "</author>", | ||
539 | " <log>" . xml_escape(join "\n", @{$info{"log"}}) . "</log>", | ||
540 | " <files>"; | ||
541 | |||
542 | open COMMIT, "-|" or exec "git", "diff-tree", "--name-status", "-r", "-M", $commit or die "cannot run git-diff-tree"; | ||
543 | while (<COMMIT>) | ||
544 | { | ||
545 | chomp; | ||
546 | if (/^([AMD])\t(.*)$/) | ||
547 | { | ||
548 | my ($action, $file) = ($1, $2); | ||
549 | my %actions = ( "A" => "add", "M" => "modify", "D" => "remove" ); | ||
550 | next unless defined $actions{$action}; | ||
551 | push @cia_text, " <file action=\"$actions{$action}\">" . xml_escape($file) . "</file>"; | ||
552 | } | ||
553 | elsif (/^R\d+\t(.*)\t(.*)$/) | ||
554 | { | ||
555 | my ($old, $new) = ($1, $2); | ||
556 | push @cia_text, " <file action=\"rename\" to=\"" . xml_escape($new) . "\">" . xml_escape($old) . "</file>"; | ||
557 | } | ||
558 | } | ||
559 | close COMMIT or die $! ? "Cannot execute diff-tree: $!" : "diff-tree exited with status: $?"; | ||
560 | |||
561 | push @cia_text, | ||
562 | " </files>", | ||
563 | $gitweb_url ? " <url>" . xml_escape("${gitweb_url}a=commit;h=$commit") . "</url>" : "", | ||
564 | " </commit>", | ||
565 | " </body>", | ||
566 | " <timestamp>" . $info{"author_date"} . "</timestamp>", | ||
567 | "</message>"; | ||
568 | |||
569 | mail_notification($cia_address, "DeliverXML", "text/xml", @cia_text); | ||
570 | } | ||
571 | |||
572 | # send a global commit notice when there are too many commits for individual mails | ||
573 | sub send_global_notice($$$) | ||
574 | { | ||
575 | my ($ref, $old_sha1, $new_sha1) = @_; | ||
576 | my $notice = git_rev_list("--pretty", "^$old_sha1", "$new_sha1", @exclude_list); | ||
577 | |||
578 | foreach my $rev (@$notice) | ||
579 | { | ||
580 | $rev =~ s/^commit /URL: ${gitweb_url}a=commit;h=/ if $gitweb_url; | ||
581 | } | ||
582 | |||
583 | mail_notification($commitlist_address, "New commits on branch $ref", "text/plain; charset=UTF-8", @$notice); | ||
584 | } | ||
585 | |||
586 | # send all the notices | ||
587 | sub send_all_notices($$$) | ||
588 | { | ||
589 | my ($old_sha1, $new_sha1, $ref) = @_; | ||
590 | my ($reftype, $refname, $tagtype, $action, @notice); | ||
591 | |||
592 | return if ($ref =~ /^refs\/remotes\// | ||
593 | or (@include_list && !grep {$_ eq $ref} @include_list)); | ||
594 | die "The name \"$ref\" doesn't sound like a local branch or tag" | ||
595 | if not (($reftype, $refname) = ($ref =~ /^refs\/(head|tag)s\/(.+)/)); | ||
596 | |||
597 | if ($reftype eq "tag") | ||
598 | { | ||
599 | $tagtype = get_object_type($ref) eq "tag" ? "annotated" : "lightweight"; | ||
600 | } | ||
601 | |||
602 | if ($new_sha1 eq '0' x 40) | ||
603 | { | ||
604 | $action = "removed"; | ||
605 | @notice = ( "Old SHA1: $old_sha1" ); | ||
606 | } | ||
607 | elsif ($old_sha1 eq '0' x 40) | ||
608 | { | ||
609 | if ($reftype eq "tag" and $tagtype eq "annotated") | ||
610 | { | ||
611 | send_commit_notice( $refname, $new_sha1 ) if $commitlist_address; | ||
612 | return; | ||
613 | } | ||
614 | $action = "created"; | ||
615 | @notice = ( "SHA1: $new_sha1" ); | ||
616 | } | ||
617 | elsif ($reftype eq "tag") | ||
618 | { | ||
619 | $action = "updated"; | ||
620 | @notice = ( "Old SHA1: $old_sha1", "New SHA1: $new_sha1" ); | ||
621 | } | ||
622 | elsif (not grep( $_ eq $old_sha1, @{ git_rev_list( $new_sha1, "--full-history" ) } )) | ||
623 | { | ||
624 | $action = "rewritten"; | ||
625 | @notice = ( "Old SHA1: $old_sha1", "New SHA1: $new_sha1" ); | ||
626 | } | ||
627 | |||
628 | send_ref_notice( $ref, $action, @notice ) if ($commitlist_address and $action); | ||
629 | |||
630 | unless ($reftype eq "tag" or $new_sha1 eq '0' x 40) | ||
631 | { | ||
632 | my $commits = get_new_commits ( $old_sha1, $new_sha1 ); | ||
633 | |||
634 | if (@$commits > $max_individual_notices) | ||
635 | { | ||
636 | send_global_notice( $refname, $old_sha1, $new_sha1 ) if $commitlist_address; | ||
637 | } | ||
638 | elsif (@$commits > 0) | ||
639 | { | ||
640 | foreach my $commit (@$commits) | ||
641 | { | ||
642 | send_commit_notice( $refname, $commit ) if $commitlist_address; | ||
643 | send_cia_notice( $refname, $commit ) if $cia_project_name; | ||
644 | } | ||
645 | } | ||
646 | elsif ($commitlist_address) | ||
647 | { | ||
648 | @notice = ( "Old SHA1: $old_sha1", "New SHA1: $new_sha1" ); | ||
649 | send_ref_notice( $ref, "modified", @notice ); | ||
650 | } | ||
651 | } | ||
652 | } | ||
653 | |||
654 | parse_options(); | ||
655 | |||
656 | umask( $mode_mask ); | ||
657 | |||
658 | # append repository path to URL | ||
659 | if ($gitweb_url) { | ||
660 | $gitweb_url .= $sourceforge ? "/$repos_name;" : "/$repos_name.git/?"; | ||
661 | } | ||
662 | |||
663 | if (@ARGV) | ||
664 | { | ||
665 | send_all_notices( $ARGV[0], $ARGV[1], $ARGV[2] ); | ||
666 | } | ||
667 | else # read them from stdin | ||
668 | { | ||
669 | while (<>) | ||
670 | { | ||
671 | chomp; | ||
672 | if (/^([0-9a-f]{40}) ([0-9a-f]{40}) (.*)$/) { send_all_notices( $1, $2, $3 ); } | ||
673 | } | ||
674 | } | ||
675 | |||
676 | exit 0; | ||