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.
14 my $subject_prefix = "GIT";
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>]
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)
31 my $repo = POSIX::getcwd();
34 my @rev_list_options = ('--pretty', '--no-abbrev', '--date=iso');
35 my @diff_options = ('-C');
39 open ORIG_STDIN, '<&', \*STDIN;
40 open ORIG_STDOUT, '>&', \*STDOUT;
43 update_ref($ARGV[0], $ARGV[1], $ARGV[2]);
45 while (<ORIG_STDIN>) {
47 my ($old, $new, $ref) = /^(\S+) (\S+) (.*)/ or die "Error parsing hook input ($_)\n";
48 update_ref($ref, $old, $new);
54 # Get branch (different from $ref) whose tip is $new
56 for (`git for-each-ref refs/heads`) {
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;
66 return sprintf("%s [and %d other]", $branches[0], @branches-1);
72 sub scan_commits($$) {
75 for (`git rev-list $old..$new --pretty=format:"# %H (%P) %s"`) {
78 my ($hash, $parents, $subject) = m{^# (\S+) \(([^)]*)\) (.*)} or die;
81 parents => [ split /\s+/, $parents ],
90 print STDOUT "Most recent commits:\n\n";
91 system 'git', 'rev-list', @rev_list_options, '--max-count=20', $new;
100 sub update_branch($$$$$)
102 my ($branch, $old, $new, $out, $headers) = @_;
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";
113 print $out "Created branch $branch ($new).\n\n";
116 } elsif ($new =~ /^0+$/) {
117 # Deletion of a branch
118 $subj .= ' Branch deleted';
119 print $out "Deleted branch $branch ($old).\n";
121 my $lca = `git merge-base $old $new`; die if $?;
124 # Fast forward ... scan all objects
125 my @commits = scan_commits($old, $new);
126 my @nonmerges = grep { @{$_->{parents}} == 1 } @commits;
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};
136 ($c0p->[0] eq $old || $c0p->[1] eq $old) &&
138 $c0->{subject} =~ m{^\s*Merge branch '([^']*)' into (\S+)} &&
139 ($1 eq $branch) != ($2 eq $branch)
141 $c0->{subject} =~ m{^\s*Merge branch '([^']*)'( of |$)}
143 # Pushed a merge of the current branch with another local branch
144 $subj .= ' ' . $c0->{subject};
146 # Otherwise take the subject of the first non-merge commit
147 $subj .= ' ' . $n0->{subject};
149 # If there is none, take the first merge
150 $subj .= ' ' . $c0->{subject};
153 print $out "Push to branch $branch ($old..$new)\n\n";
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";
161 # Print an overall diffstat
162 system 'git', 'diff', '--stat', $old, $new;
164 my $pos_after_header = output_size($out);
166 # Show individual commits with diffs
167 system 'git', 'log', @rev_list_options, @diff_options, '-p', "$old..$new";
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);
173 print $out "Diff was too long, printing just a summary.\n\n";
174 system 'git', 'log', @rev_list_options, "$old..$new";
176 } elsif ($lca eq $new) {
178 $subj .= ' Branch rewound';
179 print $out "Rewound branch $branch ($old..$new).\n\n";
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";
190 $headers->{'Subject'} = $subj;
191 $headers->{'X-Git-Branch'} = $branch;
195 sub update_tag($$$$$)
197 my ($tag, $old, $new, $out, $headers) = @_;
199 my $subj = '[' . $subject_prefix . ']';
200 if ($new =~ /^0+$/) {
201 $subj .= " Deleted tag $tag";
202 print $out "Deleted tag $tag ($old).\n";
204 my $copy_of = get_source(undef, $new);
205 my $cp = defined($copy_of) ? " to branch $copy_of" : "";
207 $subj .= " Created tag $tag$cp";
208 print $out "Created tag $tag$cp ($new).\n\n";
210 $subj .= " Changed tag $tag$cp";
211 print $out "Changed tag $tag$cp ($old..$new).\n\n";
216 $headers->{'Subject'} = $subj;
217 $headers->{'X-Git-Tag'} = $tag;
223 my ($ref, $old, $new) = @_;
224 $old ne $new or return;
225 my ($type, $name) = ($ref =~ m{^refs/([^/]*)/(.*)}) or return;
227 my $out = File::Temp->new() or die;
228 my $outname = $out->filename;
231 open STDOUT, '>&', $out or die;
234 'X-Git-Repo' => $repo,
235 'X-Git-Old-SHA' => $old,
236 'X-Git-New-SHA' => $new,
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); }
245 if (defined $mail_to) {
247 open STDIN, '<', $outname;
252 '-e', 'set charset="utf-8"',
253 '-e', 'set send_charset="us-ascii:iso-8859-2:utf-8"',
255 '-s', $headers->{'Subject'},
257 delete $headers->{'Subject'};
258 push @mutt, map { ('-e', "my_hdr $_: " . $headers->{$_}) } keys %$headers;
259 system @mutt, $mail_to;
261 open STDOUT, '>&', \*ORIG_STDOUT;
262 print map { "$_: " . $headers->{$_} . "\n" } sort keys %$headers;
264 system 'cat', $outname;