]> mj.ucw.cz Git - git-tools.git/blob - update2
Makefile: Fixed release machinery
[git-tools.git] / update2
1 #!/usr/bin/perl
2 # This is a generic update/post-receive hook script for GIT repositories.
3 # Written by Martin Mares <mj@ucw.cz> and placed into public domain.
4
5 use strict;
6 use warnings;
7
8 use Getopt::Long;
9 use IO::File;
10 use File::Temp;
11 use POSIX;
12
13 my $mail_to;
14 my $subject_prefix = "GIT";
15 my $max_diff_size;
16
17 GetOptions(
18         'mail-to=s' => \$mail_to,
19         'subject-prefix=s' => \$subject_prefix,
20         'max-diff-size=s' => \$max_diff_size,
21 ) and (@ARGV == 3 || !@ARGV) or die <<AMEN ;
22 Usage as update hook: $0 [<options>] <refname> <sha1-old> <sha1-new>
23 Usage as post-receive hook: $0 [<options>]
24
25 Options:
26 --mail-to=<address>     Send mail to the given address
27 --max-diff-size=<bytes> If the diff is too long, send just a summary
28 --subject-prefix=<px>   Prefix subjects with [<px>] (default: GIT)
29 AMEN
30
31 my $repo = POSIX::getcwd();
32 $repo =~ s{.*/}{};
33
34 my @rev_list_options = ('--pretty', '--no-abbrev', '--date=iso');
35 my @diff_options = ('-C');
36
37 sub update_ref($$$);
38
39 open ORIG_STDIN, '<&', \*STDIN;
40 open ORIG_STDOUT, '>&', \*STDOUT;
41
42 if (@ARGV) {
43         update_ref($ARGV[0], $ARGV[1], $ARGV[2]);
44 } else {
45         while (<ORIG_STDIN>) {
46                 chomp;
47                 my ($old, $new, $ref) = /^(\S+) (\S+) (.*)/ or die "Error parsing hook input ($_)\n";
48                 update_ref($ref, $old, $new);
49         }
50 }
51
52 sub get_source($$) {
53         my ($ref, $new) = @_;
54         # Get branch (different from $ref) whose tip is $new
55         my @branches = ();
56         for (`git for-each-ref refs/heads`) {
57                 chomp;
58                 my ($sha, $type, $name) = m{^(\S+) (\S+)\trefs/heads/(\S+)$} or die;
59                 if ((!defined($ref) || $name ne $ref) && $sha eq $new && $type eq 'commit') {
60                         push @branches, $name;
61                 }
62         }
63         if (@branches == 1) {
64                 return $branches[0];
65         } elsif (@branches) {
66                 return sprintf("%s [and %d other]", $branches[0], @branches-1);
67         } else {
68                 return;
69         }
70 }
71
72 sub scan_commits($$) {
73         my ($old, $new) = @_;
74         my @commits = ();
75         for (`git rev-list $old..$new --pretty=format:"# %H (%P) %s"`) {
76                 chomp;
77                 /^# / or next;
78                 my ($hash, $parents, $subject) = m{^# (\S+) \(([^)]*)\) (.*)} or die;
79                 push @commits, {
80                         hash => $hash,
81                         parents => [ split /\s+/, $parents ],
82                         subject => $subject,
83                 };
84         }
85         return @commits;
86 }
87
88 sub most_recent($) {
89         my ($new) = @_;
90         print STDOUT "Most recent commits:\n\n";
91         system 'git', 'rev-list', @rev_list_options, '--max-count=20', $new;
92 }
93
94 sub output_size($) {
95         my ($out) = @_;
96         $out->seek(0, 2);
97         return $out->tell;
98 }
99
100 sub update_branch($$$$$)
101 {
102         my ($branch, $old, $new, $out, $headers) = @_;
103
104         my $subj = '[' . $subject_prefix . ($branch eq 'master' ? '' : "/$branch") . ']';
105         if ($old =~ /^0+$/) {
106                 # Creation of a branch
107                 $subj .= ' Created branch';
108                 my $copy_of = get_source($branch, $new);
109                 if (defined $copy_of) {
110                         $subj .= " as a copy of $copy_of";
111                         print $out "Created branch $branch as a copy of $copy_of ($new).\n";
112                 } else {
113                         print $out "Created branch $branch ($new).\n\n";
114                         most_recent($new);
115                 }
116         } elsif ($new =~ /^0+$/) {
117                 # Deletion of a branch
118                 $subj .= ' Branch deleted';
119                 print $out "Deleted branch $branch ($old).\n";
120         } else {
121                 my $lca = `git merge-base $old $new`; die if $?;
122                 chomp $lca;
123                 if ($lca eq $old) {
124                         # Fast forward ... scan all objects
125                         my @commits = scan_commits($old, $new);
126                         my @nonmerges = grep { @{$_->{parents}} == 1 } @commits;
127                         @commits or return;
128
129                         # Construct subject
130                         # Try to recognize simple merges and display them as such
131                         my $c0 = $commits[0];
132                         my $n0 = $nonmerges[0];
133                         my $c0p = $c0->{parents};
134
135                         if (@{$c0p} == 2 &&
136                             ($c0p->[0] eq $old || $c0p->[1] eq $old) &&
137                             (
138                                 $c0->{subject} =~ m{^\s*Merge branch '([^']*)' into (\S+)} &&
139                                 ($1 eq $branch) != ($2 eq $branch)
140                             ) || (
141                                 $c0->{subject} =~ m{^\s*Merge branch '([^']*)'( of |$)}
142                             )) {
143                                 # Pushed a merge of the current branch with another local branch
144                                 $subj .= ' ' . $c0->{subject};
145                         } elsif ($n0) {
146                                 # Otherwise take the subject of the first non-merge commit
147                                 $subj .= ' ' . $n0->{subject};
148                         } else {
149                                 # If there is none, take the first merge
150                                 $subj .= ' ' . $c0->{subject};
151                         }
152
153                         print $out "Push to branch $branch ($old..$new)\n\n";
154
155                         # If there are multiple commits, mention that
156                         if (@nonmerges > 1) {
157                                 $subj .= ' [' . (scalar @commits) . ' commits]';
158                                 print $out 'Pushed ', (scalar @commits), " commits. Overall diffstat:\n\n";
159                         }
160
161                         # Print an overall diffstat
162                         system 'git', 'diff', '--stat', $old, $new;
163                         print $out "\n";
164                         my $pos_after_header = output_size($out);
165
166                         # Show individual commits with diffs
167                         system 'git', 'log', @rev_list_options, @diff_options, '-p', "$old..$new";
168
169                         # If the file is too long, truncate it and print just a summary
170                         if (defined($max_diff_size) && output_size($out) > $max_diff_size) {
171                                 $out->truncate($pos_after_header);
172                                 output_size($out);
173                                 print $out "Diff was too long, printing just a summary.\n\n";
174                                 system 'git', 'log', @rev_list_options, "$old..$new";
175                         }
176                 } elsif ($lca eq $new) {
177                         # Rewind
178                         $subj .= ' Branch rewound';
179                         print $out "Rewound branch $branch ($old..$new).\n\n";
180                         most_recent($new);
181                 } else {
182                         # Otherwise it is a rebase
183                         $subj .= ' Branch rebased';
184                         print $out "Rebased branch $branch ($old..$new).\n\n";
185                         print $out "Commits from common ancestor:\n\n";
186                         system 'git', 'rev-list', @rev_list_options, $new, "^$old";
187                 }
188         }
189
190         $headers->{'Subject'} = $subj;
191         $headers->{'X-Git-Branch'} = $branch;
192         return 1;
193 }
194
195 sub update_tag($$$$$)
196 {
197         my ($tag, $old, $new, $out, $headers) = @_;
198
199         my $subj = '[' . $subject_prefix . ']';
200         if ($new =~ /^0+$/) {
201                 $subj .= " Deleted tag $tag";
202                 print $out "Deleted tag $tag ($old).\n";
203         } else {
204                 my $copy_of = get_source(undef, $new);
205                 my $cp = defined($copy_of) ? " to branch $copy_of" : "";
206                 if ($old =~ /^0+/) {
207                         $subj .= " Created tag $tag$cp";
208                         print $out "Created tag $tag$cp ($new).\n\n";
209                 } else {
210                         $subj .= " Changed tag $tag$cp";
211                         print $out "Changed tag $tag$cp ($old..$new).\n\n";
212                 }
213                 most_recent($new);
214         }
215
216         $headers->{'Subject'} = $subj;
217         $headers->{'X-Git-Tag'} = $tag;
218         return 1;
219 }
220
221 sub update_ref($$$)
222 {
223         my ($ref, $old, $new) = @_;
224         $old ne $new or return;
225         my ($type, $name) = ($ref =~ m{^refs/([^/]*)/(.*)}) or return;
226
227         my $out = File::Temp->new() or die;
228         my $outname = $out->filename;
229         $out->autoflush(1);
230         close STDOUT;
231         open STDOUT, '>&', $out or die;
232
233         my $headers = {
234                 'X-Git-Repo' => $repo,
235                 'X-Git-Old-SHA' => $old,
236                 'X-Git-New-SHA' => $new,
237         };
238
239         my $send;
240         if ($type eq 'heads') { $send = update_branch($name, $old, $new, $out, $headers); }
241         elsif ($type eq 'tags') { $send = update_tag($name, $old, $new, $out, $headers); }
242         $out->close();
243         $send or return;
244
245         if (defined $mail_to) {
246                 close STDIN;
247                 open STDIN, '<', $outname;
248                 my @mutt = (
249                         'mutt',
250                         '-F/dev/null',
251                         '-x',
252                         '-e', 'set charset="utf-8"',
253                         '-e', 'set send_charset="us-ascii:iso-8859-2:utf-8"',
254                         '-e', 'set record=',
255                         '-s', $headers->{'Subject'},
256                 );
257                 delete $headers->{'Subject'};
258                 push @mutt, map { ('-e', "my_hdr $_: " . $headers->{$_}) } keys %$headers;
259                 system @mutt, $mail_to;
260         } else {
261                 open STDOUT, '>&', \*ORIG_STDOUT;
262                 print map { "$_: " . $headers->{$_} . "\n" } sort keys %$headers;
263                 print "\n";
264                 system 'cat', $outname;
265                 print "\n";
266         }
267 }