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