gitmail 40.4 KB
Newer Older
1
2
#!/usr/bin/perl -w
#
3
# Copyright (c) 2009-2011 University of Utah and the Flux Group.
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
23
#
24
# To set this script up:
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 1) Copy or link it to .git/hooks/post-receive in your repository. Make sure
#    it's world-readable and executable.
# 2) Set configuration options by editng the values of variables directly
#    below
#       OR
#    Set the simple values with git options: run
#       git config --add hooks.gitmail.<optname> value
#       (for example:
#           git config --add hooks.gitmail.alwaysmail ricci@cs.utah.edu
# 3) Test it by running it with the -d and -t options, which will not send
#    mail and will give you a chance to make sure everything looks right
#
# TODO:
#    Users can add notifications for themselves
#

use strict;
42
use IPC::Open2;
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
use POSIX 'setsid';
use Getopt::Long;
sub get_config($$);
my $CONFIGBASE = "hooks.gitmail";

######################################################################
# Configuration Options
# Options that use get_config can be set using 'git config' - if not
# set, the second parameter is used as the default
######################################################################

#
# If debugging is enabled, prints a lot of messages and doesn't actually send
# mail.
#
my $debug = get_config("debug",undef);

#
# If set, just picks up the most recent commits instead of reading them from
# stdin. Note that this one doesn't look in the git config; that wouldn't
# make any sense.
#
my $testmode = undef;

67
#
68
69
70
# If set, hide trivial merges (those where no diff hunks needed to be modified
# as part of the merge).  Trivial merges that have a non-empty commit message
# are still shown in case the committer decided to document the merge.
71
72
73
#
my $hide_trivial_merges = get_config("hidetrivialmerges",undef);

74
75
76
77
78
#
# Command-line options - have to do this before setting other options, since
# we want to be able to turn on debugging early
#
my %opt;
79
Getopt::Long::Configure("no_ignore_case");
Robert Ricci's avatar
Robert Ricci committed
80
if (!GetOptions(\%opt, 'd', 'h', 't', 'T=s', 'c=s', 'C', 'o=s@')
81
        || @ARGV || $opt{h}) {
82
    print STDERR "Usage: gitmail [-h|-d]\n";
83
84
85
86
    print STDERR "  -h     this message\n";
    print STDERR "  -d     enable debugging output and don't send mail\n";
    print STDERR "  -t     test mode - operate on last 3 commits to master\n";
    print STDERR "  -T br  like '-t', but use branch 'br' instead of master\n";
Robert Ricci's avatar
Robert Ricci committed
87
88
    print STDERR "  -c n   in test mode, use the last n commits for testing \n";
    print STDERR "  -C     in test mode, pretend the ref was just created\n";
89
90
    print STDERR "  -o o=v give option o the value v (may be given multiple\n";
    print STDERR "         times)\n";
91
    exit 1;
92
93
}

94
my $testbranch = "master";
95
my $testcommits = 3;
Robert Ricci's avatar
Robert Ricci committed
96
my $testcreate = 0;
97
98
if ($opt{d}) { $debug    = 1; }
if ($opt{t}) { $testmode = 1; }
99
if ($opt{T}) { $testmode = 1; $testbranch = $opt{T} }
Robert Ricci's avatar
Robert Ricci committed
100
101
if ($opt{c}) { $testcommits = $opt{c} }
if ($opt{C}) { $testcreate = 1; }
102
103
104
105
106
107
108
109

#
# Name of this repository - set it to a string to get it included in the
# mail subject line and the X-Git-Repo header
#
my $reponame = get_config("reponame",undef);

#
110
111
112
113
114
115
116
117
# Data structure mapping branch names and path names to email address. Each
# entry is a triple:
#    Branch name
#    File path
#    Mail address
# If *both* the branch and path match for a commit, mail will be sent to the
# associated address. The branch and paths are interpreted as perl regexps,
# with the special value 'undef' matching anything at all. Any of these may
118
# be array references: for the branch and path fields, if *any* of the
119
120
# elements in the array match, the field is considered to match. For the email
# address field, the mail will be sent to all addresses in the array.
121
122
#
# *NOTE* This are perl regexps, not shell globs! *NOTE*
123
# *NOTE* This can also be read from a file - see below *NOTE*
124
#
125
126
127
128
129
130
131
132
133
my @mailto = (
  # Branch              # Path             # Send mail to
# Examples
# [ undef,              undef,             'cvs-testbed@flux.utah.edu' ],
# [ undef,              'snmpit',         ['ricci@flux.utah.edu',
#                                          'sklower@vangogh.cs.berkeley.edu']],
# [ ['^ricci-',
#    '^test-'],         'assign/',         'ricci+assign@flux.utah.edu'],
# [ 'gitmail',          'tools/git',       'ricci+git@flux.utah.edu']
134
135
);

136
137
138
139
140
141
142
143
144
145
#
# If set, look in a file for the @mailto structure.  This file should be
# executable perl code that returns an array following the same format as the
# @mailto array below. For example, the file could contain:
# ( [ undef, undef, 'me@example.com'] )
#
# *NOTE* If this is set, overrides the @mailto setting above *NOTE*
#
my $mailconfigfile = get_config("mailconfigfile",undef);

146
147
148
149
150
151
152
153
154
155
#
# Default mail address - if none of the more specific regular expressions
# match, send to this address
#
my $defmail = get_config("defmail",undef);

#
# If set, *always* send mail to this address (even if one or more regexps
# match). ($defmail will still be used if no regexps match)
#
156
157
my @alwaysmail = get_config("alwaysmail",undef);

158
159
160
161
162
163
164
#
# This works exactly like alwaysmail, but it causes seperate mail to get sent
# to each address (this mail is also seperate from the 'main' message that will
# get sent to all of the other addresses)
#
my @archivemail = get_config("archivemail",undef);

165
166
167
168
169
#
# If set, set the 'Reply-To' header in the mail, so that discussion can
# take place on, for example, a particular development mailing list
#
my $replyto = get_config("replyto",undef);
170
171
172
173
174
175
176
177
178
179
180
181
182
183

#
# If set to true, detach and run in background - the push doesn't return until
# the hook finishes, so doing this means the pusher doesn't have to wait for
# us to finish
# Note: Not well tested!
#
my $detach = get_config("detach",undef);

#
# If set to true, send a separate mail message for every single commit. If
# false, pushes of multiple commits along a branch get included in the same
# mail.
#
184
185
186
my $separate_mail = get_config("separatemail",undef);

#
187
188
189
190
191
# Select a style for displaying commits.
# Supported styles are:
#   default - default style, includes commig log and summary of changed files
#   diff    - include a diff, like 'git show' does by default
#   wdiff    - like diff, but word-diff (useful for LaTeX, etc.)
192
#
193
194
195
196
197
198
199
my $commit_style = get_config("commitstyle","default");

#
# If set, overrides the options to 'git show' that would be set by the
# 'commitstyle' option.
#
my $showcommit_args = get_config("showcommitargs",undef);
200

Ryan Jackson's avatar
Ryan Jackson committed
201
202
203
204
#
# If set, check all commit objects to see if they exist in the repository
# at the specified path.  If a commit object exists in this repository,
# do not include its log in the email message.
205
206
207
# Note: This should point at the .git directory - for a 'bare' repository,
# this is just the path the repository. For a 'reglar' repository, this is the
# .git/ directory *inside* the repositry.
Ryan Jackson's avatar
Ryan Jackson committed
208
209
210
#
my $exclude_repo = get_config("excluderepo",undef);

211
212
213
214
215
216
217
#
# If set, these values will be used to create 'X-Git-Repo-Keyword' headers, one
# for each value.  This is to facilitate filtering of commit mails for those only
# interested in certain projects.
#
my @repo_keywords = get_config("keyword",undef);

218
219
220
221
#
# If defined, produce a summary at the top of the mail if there are at least
# this many commits
#
222
223
224
225
226
227
228
my $summary_threshold = get_config("summarythreshold",2);

#
# Disable summry messages; they are now on by default
# this many commits
#
my $disable_summary = get_config("nosummary",undef);
229

230
231
232
233
#
# Style used for displaying summaries. Supported styles:
#   hashes - Include hash and summary
#   bare   - Include summary only
234
235
#   list   - Like bare, but formatted to look more like a list
#   name   - Prefix summary with author name
236
#
237
my $summary_style = get_config("summarystyle","list");
238

239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#
# If set, puts a set of patches at the bottom of the mail, after all
# commit messages
#
my $include_patches = get_config("includepatches",undef);

#
# Patch style: currently supported options are 'default' and 'word'
#
my $patch_style = get_config("patchstyle","default");

#
# Limit to the number of lines in an individual patch
#
my $patch_size_limit = get_config("patchsizelimit",10000);

#
# Limit to the number of patches to include in a single mail
#
my $max_patches = get_config("maxpatches",20);

260
261
262
263
264
265
266
#
# URL for a gitweb server for this repository. If set, links will
# be sent for each commit pointing to the entry on gitweb
# For example: http://git-public.flux.utah.edu/?p=emulab-devel.git
#
my $gitweb_url = get_config("gitweburl",undef);

267
268
269
270
271
272
#
# If true, include clone URLs in the mail. These will be guessed from
# the local hostname and the path
#
my $include_urls = get_config("includeurls",1);

273
274
275
276
277
278
279
280
281
######################################################################
# Constants
######################################################################

#
# Programs we use
#
my $GIT = "git";
my $SENDMAIL = get_config("sendmail","sendmail");
282
my $HOSTNAME = get_config("hostname","hostname");
283
284
285
286
287
288

#
# Magic 'hash' that indicates an empty or non-existant rev
#
my $EMPTYREV = "0"x40;

289
290
291
292
293
294
295
296
297
298
#
# Separator between commits
#
my $SEP = "\n" . "-"x72 . "\n\n";

#
# Separator between body parts
#
my $BODYSEP = "\n" . "="x72 . "\n\n";

299
300
301
302
303
304
#
# Types of changes
#
my $CT_CREATE = "create";
my $CT_UPDATE = "update";
my $CT_DELETE = "delete";
305
306
my $CT_REWIND = "rewind";
my $CT_REBASE = "rebase";
307

308
309
310
311
312
313
#
# Types of clone URLs
#
my $CLONE_SSH    = "ssh";
my $CLONE_PUBLIC = "public";

314
315
316
317
318
319
320
321
#
# Tired of typing this and getting it wrong
#
my $STDERRNULL = " 2> /dev/null";

######################################################################
# Function Prototypes
######################################################################
322
sub fix_truefalse($);
323
324
sub change_type($$$);
sub ref_type($);
325
326
sub rev_type($);
sub revparse($);
Robert Ricci's avatar
Robert Ricci committed
327
sub short_hash($);
328
sub changed_files(@);
329
sub get_mail_addresses($@);
330
sub get_merge_base($$);
331
sub get_summary($);
332
333
sub uniq(@);
sub flatten_arrayref($);
Robert Ricci's avatar
Robert Ricci committed
334
sub commit_mail($$$\@$@);
335
sub get_commits($$$);
Robert Ricci's avatar
Robert Ricci committed
336
sub send_mail($$$@);
337
338
sub short_refname($);
sub debug(@);
339
340
sub object_exists($$);
sub filter_out_objects_in_repo($@);
341
sub generate_messageid();
342
343
sub get_hostname();
sub get_pathname();
344
sub get_clone_url($);
345
346
347
348
349
350
351

######################################################################
# Main Body
######################################################################

debug("starting");

352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#
# Read from the mail config file, if requested. We do this before detaching
# so that we can report an error if one occurs.
#
if ($mailconfigfile) {
    #
    # We open the file, read the contents, then eval them. If we didn't get any
    # errors, the result becomes the new contents of the @mailto array
    #
    if (!open(MCF,"<$mailconfigfile")) {
        warn "gitmail: Unable to open config file $mailconfigfile - $!\n";
    } else {
        my @mailcode = <MCF>;
        close MCF;

        #
        # Have to turn array back into a single string before we can call
        # eval on it. Put the result in a temp. variable so that we don't
        # overwrite @mailto if there is an error
        #
        my @mailconfig = eval join("\n",@mailcode);

        #
        # If there were any errors in the eval, they will be found in the magic
        # variable $@ - however, they will also have been printed to stderr, so
        # don't print again
        #
        if ($@) {
380
            warn "gitmail: Error in $mailconfigfile: $@\n";
381
382
383
384
385
386
387
388
389
        } else {
            @mailto = @mailconfig;
        }
    }
}

#
# Get the actual references
#
390
391
my @reflines;
if ($testmode) {
392
    my $fullref = `$GIT rev-parse --symbolic-full-name $testbranch`;
393
394
395
    if (!$fullref) {
	    exit(1);
    }
396
    my $newrev = `$GIT rev-parse $fullref $STDERRNULL`;
397
398
    chomp $newrev;

Robert Ricci's avatar
Robert Ricci committed
399
400
401
402
403
404
405
    my $oldrev;
    if ($testcreate) {
        $oldrev = $EMPTYREV;
    } else {
        $oldrev = "$newrev~'$testcommits'";
    }

406
    #
Robert Ricci's avatar
Robert Ricci committed
407
    # Provide a simple way to grab some commits 
408
    #
Robert Ricci's avatar
Robert Ricci committed
409
    @reflines = ("$oldrev $newrev $fullref");
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
} else {
    #
    # Get all of the references that are being pushed from stdin - we do this in
    # one slurp so that we can detach below
    #
    @reflines = <STDIN>;
    debug("finished reading stdin");
}

#
# Detach?
#
if ($detach && !$debug) {
    # Stolen from perlipc manpage
    chdir '/'               or die "Can't chdir to /: $!";
    open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
    open STDOUT, '>/dev/null'
                            or die "Can't write to /dev/null: $!";
    defined(my $pid = fork) or die "Can't fork: $!";
    exit if $pid;
    setsid                  or die "Can't start a new session: $!";
    open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
}

434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
#
# Figure out args we'll use to 'git show'
#
if (!defined($showcommit_args)) {
    if ($commit_style eq "default") {
        $showcommit_args = "--numstat --shortstat";
    } elsif ($commit_style eq "diff") {
        $showcommit_args = "";
    } elsif ($commit_style eq "wdiff") {
        $showcommit_args = "--word-diff";
    } else {
        warn "gitmail: Bad commit display style '$commit_style'\n";
        $showcommit_args = "--numstat --shortstat";
    }
    debug("Using predefined commit args: '$showcommit_args'");
} else {
    debug("Using showcommit_args: '$showcommit_args'");
}

453
454
455
456
#
# Loop over all of the references we got on stdin
#
foreach my $refline (@reflines) {
457
458
459
    my @commits;
    my @changed_files;

460
461
462
463
464
465
466
467
468
    chomp $refline;
    debug("Read line '$refline'");

    #
    # Each line we get on stdin gives us an old revision, a new revision, and
    # a reference (such as the branch name). It's our job to figure out what
    # happened in the middle
    #
    my ($oldrev, $newrev, $refname) = split(/\s+/, $refline);
469
    my $ref_type = ref_type($refname);
470
471
472
473
474
475
476
477
478
479
480
481

    #
    # Use rev-parse so that fancy symbolic names, etc. can be used
    # Note: revparse can die if the name given is bogus
    #
    $oldrev = revparse($oldrev);
    $newrev = revparse($newrev);

    #
    # Figure out what type of change occured (update, creation, deletion, etc.)
    # and what type of objects (commit, tree, etc.) we got
    #
482
    my $ct = change_type($oldrev,$newrev,$ref_type);
483
484
485
486
487
488
    my $old_type = rev_type($oldrev);
    my $new_type = rev_type($newrev);

    debug("Change type: $ct ($old_type,$new_type)");

    #
489
    # For now, only handle commit objects.  Tag objects require extra work.
490
    #
491
    if ($new_type && $new_type ne "commit") {
492
493
494
495
496
        debug("Skipping non-commit");
        next;
    }

    #
497
498
    # Figure out which commits we're interested in based on reference type
    # and change type.
499
    #
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
    if ($ref_type eq 'tag') {
	if ($ct eq $CT_DELETE) {
	    # We want to know where the tag used to point before deletion
	    push @commits, $oldrev;
	} else {
	    # Tags only have delete, create, and update.  Rewind and rebase
	    # don't make sense in tag context.
	    #
	    # We only care about the new value of the tag here.
	    push @commits, $newrev;
	}
    } elsif ($ref_type eq 'branch') {
	if ($ct eq $CT_DELETE) {
	    # We want to know where the branch used to point before deletion
	    push @commits, $oldrev;
	} elsif ($ct eq $CT_REWIND) {
	    # There's no new history to show, but we still want to see where
	    # the branch now points.
	    push @commits, $newrev;
	} else {
	    @commits = get_commits($oldrev,$newrev,$refname);
	    # We only want to see *new* commits, which means that commits already
	    # in the main repository need to be excluded too.
	    if (defined $exclude_repo) {
		@commits = filter_out_objects_in_repo($exclude_repo, @commits);
	    }
	}
    }

    next unless (@commits);
Ryan Jackson's avatar
Ryan Jackson committed
530
531
    debug("commits are: ", join(" ",@commits));

532
    @changed_files = changed_files(@commits);
533
534
535
536
537
538
539
540
541
542
543
    debug("Changed files: ", join(",",@changed_files));

    #
    # Based on the list of files, figure out who to mail
    #
    my @mailaddrs = get_mail_addresses($refname,@changed_files);

    #
    # Send off the mail!
    #
    if (@mailaddrs) {
Robert Ricci's avatar
Robert Ricci committed
544
        commit_mail($ct,$oldrev,$newrev,@commits,$refname,@mailaddrs);
545
546
547
548
549
550
551
552
553
554
555
556
557
    }
}

debug("finishing");

######################################################################
# Functions
######################################################################

#
# Does this change represent the creation, deletion, or update of an object?
# Takes old and new revs
#
558
559
sub change_type($$$) {
    my ($oldrev, $newrev, $ref_type) = @_;
560

561
562
563
564
565
566
567
568
    #
    # We can detect creates and deletes by looking for a special 'null'
    # revision
    #
    if ($oldrev eq $EMPTYREV) {
        return $CT_CREATE;
    } elsif ($newrev eq $EMPTYREV) {
        return $CT_DELETE;
569
570
    } elsif ($ref_type eq 'tag') {
	    return $CT_UPDATE;
571
    } else {
572
	my $merge_base = get_merge_base($oldrev,$newrev);
573
	my $oldrev = revparse($oldrev);
574
	my $newrev = revparse($newrev);
575
	if ($merge_base eq $oldrev) {
576
577
578
	    return $CT_UPDATE;
	} elsif ($merge_base eq $newrev) {
	    return $CT_REWIND;
579
	} else {
580
	    return $CT_REBASE;
581
	}
582
583
584
585
586
587
588
589
    }
}

#
# Find out what type an object has
#
sub rev_type($) {
    my ($rev) = @_;
590
    my $rev_type = `$GIT cat-file -t '$rev' $STDERRNULL`;
591
592
593
594
    chomp $rev_type;
    return $rev_type;
}

595
596
597
598
599
600
601
602
603
604
605
606
607
608
#
# Find out what type of reference this is
#
sub ref_type($) {
    my ($ref) = @_;
    my $type;
    if ($ref =~ m#^refs/heads/#) {
	    $type = 'branch';
    } elsif ($ref =~ m#^refs/tags/#) {
	    $type = 'tag';
    }
    return $type;
}

609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
#
# Parse (possibly) symbolic revision name into hash
# Note: Dies if the revision name is bogus!
#
sub revparse($) {
    my ($rev) = @_;
    open(RP,"$GIT rev-parse $rev $STDERRNULL |");
    my $parsedrev = <RP>;
    my $okay = close(RP);
    if (!$okay) {
        die "gitmail: $rev is not a valid revision\n";
    }
    chomp $parsedrev;
    return $parsedrev;
}

Robert Ricci's avatar
Robert Ricci committed
625
626
627
628
629
630
631
632
633
634
#
# Take a git hash and return the short (abbreviated) form.
#
sub short_hash($) {
    my ($hash) = @_;
    my $short = `$GIT rev-parse --short $hash $STDERRNULL`;
    chomp($short);
    return $short;
}

635
#
636
637
# Given a list of commit object hashes, return the list of files changed by
# all commits.
638
#
639
640
sub changed_files(@) {
    my %files;
641

642
643
    debug("running '$GIT diff-tree --stdin -r --name-only --no-commit-id' on '@_'");
    my $pid = open2(\*OUT, \*IN, "$GIT diff-tree --stdin -r --name-only --no-commit-id");
644

645
646
647
648
649
650
651
652
    print IN "$_\n" for (@_);
    close(IN);

    while (<OUT>) {
	    chomp;
	    $files{$_} = 1;
    }
    close(OUT);
Ryan Jackson's avatar
Ryan Jackson committed
653

654
655
656
657
658
659
660
    waitpid($pid, 0);
    my $rc = $? >> 8;
    if ($rc) {
	    die "'git diff-tree' exited with return code $rc\n";
    }

    return keys(%files);
Ryan Jackson's avatar
Ryan Jackson committed
661
662
}

663
664
665
666
667
668
#
# Given a refname and a list of filenames, return the set of email addresses
# the report should be sent to
#
sub get_mail_addresses($@) {
    my ($refname, @changedfiles) = @_;
669
    my (@addrs,@archiveaddrs);
670
671
672
673
674
675
676
677
678
679
    my $matched = 0;

    #
    # Note: we use flatten_arrayref so that either individual strings or array
    # references containing lists of addresses can be used
    #

    #
    # If there's an address we're always supposed to send to, include that now
    #
680
681
682
    if (@alwaysmail) {
        push @addrs, @alwaysmail;
        debug("Used alwaysmail address(es) " . join(",",@alwaysmail));
683
684
    }

685
686
687
688
689
690
691
692
693
694
    #
    # If there are any 'archive' mail addresses, put them into array refs,
    # which will cause them to get sent separately
    #
    if (@archivemail) {
        push @archiveaddrs, map {[$_]} @archivemail;
        debug("Used archivemail address(es) " . join(",",@archivemail));
    }


695
    #
696
697
698
    # Find out if this is a branch, and of so, what it's name is. If it's not,
    # set the branch name to be empty, so that only empty regexps will match
    # it
699
    #
700
    my $branchname = "";
701
    if ($refname =~ /^refs\/heads\/(.*)/) {
702
       $branchname = $1;
703
704
705
    }

    #
706
    # Loop through each entry, making sure both branch and path match
707
    #
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
    ENTRY: foreach my $entry (@mailto) {
        my ($branches, $paths, $addresses) = @$entry;
        my @branches = flatten_arrayref($branches);
        my @paths = flatten_arrayref($paths);
        my @addresses = flatten_arrayref($addresses);

        #
        # If the branch doesn't match, go on to the next entry
        #
        my $branch_matched = 0;
        BRANCH: foreach my $branchRE (@branches) {
            if (!defined($branchRE)) {
                debug("Empty branch matched");
                $branch_matched = 1;
                last BRANCH;
            } elsif ($branchname =~ $branchRE) {
                debug("Matched branch regexp /$branchRE/");
                $branch_matched = 1;
                last BRANCH;
727
728
            }
        }
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765

        if (!$branch_matched) {
            next ENTRY;
        }

        #
        # If the path doesn't match, go on to the next entry
        #
        my $paths_matched = 0;
        PATH: foreach my $regexp (@paths) {
            if (!defined($regexp)) {
                debug("  Empty path matched");
                $paths_matched = 1;
                last PATH;
            }
            # Have to check against every file in the changeset
            foreach my $file (@changedfiles) {
                if ($file =~ $regexp) {
                    debug("  Matched path regexp /$regexp/");
                    $paths_matched = 1;
                    last PATH;
                }
            }
        }

        if (!$paths_matched) {
            debug("  Path match failed");
            next ENTRY;
        }

        #
        # Great, made it through - we add all addresses, we'll weed out
        # duplicates later
        #
        debug("  Adding adddresses ", join(",",@addresses));
        $matched = 1;
        push @addrs, @addresses;
766
767
768
769
770
771
772
773
774
    }

    #
    # Fall back to default if no matches (note that an earlier match with an
    # empty list of addresses will cause this case to not be triggered - this
    # is intentional)
    #
    if (!$matched && defined($defmail)) {
        @addrs = flatten_arrayref($defmail);
775
        debug("Used default address $defmail");
776
    }
777

778
779
780
    #
    # Pull out unique values to return
    #
781
    return (uniq(@addrs),@archiveaddrs);
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
}

#
# Return only the unique elements of the supplied list. Input does not have
# to be sorted, sort order of output is undefined.
#
sub uniq(@) {
    my %uniq;
    map { $uniq{$_} = 1 } @_;
    return keys %uniq;
}

#
# If the parameter is a scalar, just return a one-element array containing the
# scalar. If it's a reference to an array, return the array referenced.
#
sub flatten_arrayref($) {
    my ($ref) = @_;
    if (ref($ref) eq "ARRAY") {
        return @$ref;
    } else {
        return ($ref);
    }
}

#
# Send mail about a regular update commit
#
810
sub commit_mail($$$\@$@) {
Robert Ricci's avatar
Robert Ricci committed
811
    my ($ct,$oldrev,$newrev,$commits,$refname,@mailaddrs) = @_;
812
813
814

    #
    # Construct the subject line. For now, we just say what repo (if defined)
815
    # and what branch/tag it happened on
816
817
    #
    my $subject = "git commit: ";
818
    my $ref_type;
819
    my $short_rev;
820
821
822
    if (defined($reponame)) {
        $subject .= "[$reponame] ";
    }
823

824
    $ref_type = ref_type($refname);
825
826

    $subject .= $ref_type . ' ' . short_refname($refname);
827

828
    my $what_happened;
829
    if ($ct eq $CT_UPDATE) {
830
831
832
833
834
        $what_happened .= 'updated';
    } elsif ($ct eq $CT_REWIND) {
        $what_happened .= 'rewound';
    } elsif ($ct eq $CT_REBASE) {
        $what_happened .= 'rebased';
835
    } elsif ($ct eq $CT_CREATE) {
836
        $what_happened .= 'created';
837
    } elsif ($ct eq $CT_DELETE) {
838
        $what_happened .= 'deleted';
839
840
    }

841
    $subject .= ' ' . $what_happened;
842
    my $actionstring = ucfirst($ref_type) . ' ' . short_refname($refname) .
843
                       " has been $what_happened";
844

845
    $short_rev = `$GIT rev-parse --short $refname $STDERRNULL`;
846
847
848
    chomp $short_rev;
    $subject .= " ($short_rev)" if ($short_rev);

849
850
851
852
853
854
    if ($ct eq $CT_REBASE) {
        $actionstring .= ".  The following commits are new or have been modified:";
    } elsif ($ct eq $CT_REWIND) {
        $actionstring .= "  to point to the following commit:";
    } elsif ($ct eq $CT_DELETE) {
	$actionstring .= ".  It previously pointed to the following commit:";
Robert Ricci's avatar
Robert Ricci committed
855
856
857
858
    } elsif ($ct eq $CT_UPDATE) {
        my $oldshort = short_hash($oldrev);
        my $newshort = short_hash($newrev);
	$actionstring .= ": $oldshort..$newshort";
859
860
    }

861
862
    $actionstring .= "\n\n";

863
864
865
    #
    # Make a pretty summary of commits if there are enough of them
    #
866
    my $summary = "";
867
868
    if (!$disable_summary && defined($summary_threshold)
            && scalar(@$commits) >= $summary_threshold && !$separate_mail) {
869
870
871
872
        my @summaries;
        foreach my $rev (@$commits) {
            push @summaries, get_summary($rev);
        }
873
        $summary = join("\n",@summaries) . "\n" . $BODYSEP;
874
875
    }

876
877
    my @commitmessages;
    my @patches;
Ryan Jackson's avatar
Ryan Jackson committed
878
    foreach my $rev (@$commits) {
879
880
881
882
        #
        # Just use regular git show command, with purty +/- summary at the
        # bottom (formatted to be narrow enough for email)
        #
883
        my $showcommand = "$GIT show $showcommit_args '$rev'";
884
        debug("running '$showcommand'");
885
        my @commit_text = `$showcommand`;
886
887
888
889
890
        
        if ($hide_trivial_merges) {
	        my $is_merge = 0;
	        my $body_lines = 0;
	        my $subject;
891
892
893
894
895
896
	        my @diff_lines;

		# This is a bit of a hack.  It assumes that the output
		# of git-show will not change.  We could use --pretty
		# to produce exactly the format we want, but since we've
		# already called git-show we'll just use that.
897
	        for (@commit_text) {
898
899
900
901
902
903
904
905
906
		        $is_merge = 1 if (/^Merge:/);
			if (/^ /) {
				if (not defined $subject) {
					$subject = $_;
				} else {
					$body_lines++;
				}
			}
		}
907
908
909
910

		if ($is_merge) {
			@diff_lines = `$GIT diff-tree --no-commit-id --cc $rev`;
		}
911
		
912
913
914
915
		if ($is_merge && !@diff_lines && !$body_lines) {
			# We have a trivial merge, i.e. no hunks were modified as
			# part of the merge.  We still want to show the commit if
			# the commit message is non-standard.
916
917
918
			next;
		}
	}
919

920
921
922
923
924
925
926
927
        #
        # Add a URL to look at the commit if set for this repo
        #
	if (defined($gitweb_url)) {
            my $shortrev = short_hash($rev);
	    push @commit_text, "\n${gitweb_url};a=commitdiff;h=$shortrev\n";
        }

928
929
        #
        # Grab patches - we do this seperately so that we can put them down
930
931
        # below the commit logs. (Note: we don't print patches when a ref is
        # deleted, that doesn't make any sense.)
932
933
        #
	my @patch_text;
934
935
	if ($ct ne $CT_DELETE &&
                defined($include_patches) && $include_patches &&
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
                scalar(@$commits) <= $max_patches) {
	    my $patchcommand = "$GIT show --format='format:commit %H'";
	    if ($patch_style eq "word") {
	        $patchcommand .= " --word-diff";
            }
	    $patchcommand .= " '$rev'";
	    debug("running '$patchcommand'");
	    @patch_text = `$patchcommand $STDERRNULL`;

            # Just nuke the patch if it's too long
            if (scalar(@patch_text) > $patch_size_limit) {
                @patch_text = ();
            }
        }

951
952
        if ($separate_mail) {
            # Send this message by itself
953
954
955
956
957
            send_mail($subject,
                $actionstring . join("",@commit_text) . "\n" .
                    join("",@patch_text),
                short_refname($refname),
                @mailaddrs);
958
        } else {
959
960
961
962
            push @commitmessages,\@commit_text;
            if (scalar(@patch_text)) {
                push @patches, \@patch_text;
            }
963
964
965
        }
    }

966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
    #
    # Show clone URLs
    #
    my $cloneurls = $BODYSEP;
    if ($include_urls) {
        my $ssh_url = get_clone_url($CLONE_SSH);
        my $public_url = get_clone_url($CLONE_PUBLIC);

        $cloneurls .= "Clone via ssh: $ssh_url\n";
        if ($public_url) {
            # This one might not be set, since the repo may only be
            # accessable through ssh
            $cloneurls .= "Clone read-only: $public_url\n";
        }
        $cloneurls .= "\n";
    }

983
984
985
    #
    # Send all the changes together in one message
    #
986
    if (!$separate_mail && @commitmessages) {
987
        send_mail($subject,
988
989
            $actionstring .
            $summary .
990
991
            join($SEP, map { join "",@$_} @commitmessages) .
            (scalar(@patches) ? "$BODYSEP" : "") .
992
993
            join("\n", map { join "",@$_} @patches) .
            $cloneurls,
Robert Ricci's avatar
Robert Ricci committed
994
            short_refname($refname),
995
996
997
998
999
1000
            @mailaddrs);
    }
}

#
# Given two revisions, get a list of the commits that occured between them
For faster browsing, not all history is shown. View entire blame