| #!/usr/bin/perl |
| |
| # List people who might be interested in a patch. Useful as the argument to |
| # git-send-email --cc-cmd option, and in other situations. |
| # |
| # Usage: git contacts <file | rev-list option> ... |
| |
| use strict; |
| use warnings; |
| use IPC::Open2; |
| |
| my $since = '5-years-ago'; |
| my $min_percent = 10; |
| my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc/i; |
| my %seen; |
| |
| sub format_contact { |
| my ($name, $email) = @_; |
| return "$name <$email>"; |
| } |
| |
| sub parse_commit { |
| my ($commit, $data) = @_; |
| my $contacts = $commit->{contacts}; |
| my $inbody = 0; |
| for (split(/^/m, $data)) { |
| if (not $inbody) { |
| if (/^author ([^<>]+) <(\S+)> .+$/) { |
| $contacts->{format_contact($1, $2)} = 1; |
| } elsif (/^$/) { |
| $inbody = 1; |
| } |
| } elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) { |
| $contacts->{format_contact($1, $2)} = 1; |
| } |
| } |
| } |
| |
| sub import_commits { |
| my ($commits) = @_; |
| return unless %$commits; |
| my $pid = open2 my $reader, my $writer, qw(git cat-file --batch); |
| for my $id (keys(%$commits)) { |
| print $writer "$id\n"; |
| my $line = <$reader>; |
| if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) { |
| my ($cid, $len) = ($1, $2); |
| die "expected $id but got $cid\n" unless $id eq $cid; |
| my $data; |
| # cat-file emits newline after data, so read len+1 |
| read $reader, $data, $len + 1; |
| parse_commit($commits->{$id}, $data); |
| } |
| } |
| close $reader; |
| close $writer; |
| waitpid($pid, 0); |
| die "git-cat-file error: $?\n" if $?; |
| } |
| |
| sub get_blame { |
| my ($commits, $source, $from, $ranges) = @_; |
| return unless @$ranges; |
| open my $f, '-|', |
| qw(git blame --porcelain -C), |
| map({"-L$_->[0],+$_->[1]"} @$ranges), |
| '--since', $since, "$from^", '--', $source or die; |
| while (<$f>) { |
| if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) { |
| my $id = $1; |
| $commits->{$id} = { id => $id, contacts => {} } |
| unless $seen{$id}; |
| $seen{$id} = 1; |
| } |
| } |
| close $f; |
| } |
| |
| sub blame_sources { |
| my ($sources, $commits) = @_; |
| for my $s (keys %$sources) { |
| for my $id (keys %{$sources->{$s}}) { |
| get_blame($commits, $s, $id, $sources->{$s}{$id}); |
| } |
| } |
| } |
| |
| sub scan_patches { |
| my ($sources, $id, $f) = @_; |
| my $source; |
| while (<$f>) { |
| if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) { |
| $id = $1; |
| $seen{$id} = 1; |
| } |
| next unless $id; |
| if (m{^--- (?:a/(.+)|/dev/null)$}) { |
| $source = $1; |
| } elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) { |
| my $len = defined($2) ? $2 : 1; |
| push @{$sources->{$source}{$id}}, [$1, $len] if $len; |
| } |
| } |
| } |
| |
| sub scan_patch_file { |
| my ($commits, $file) = @_; |
| open my $f, '<', $file or die "read failure: $file: $!\n"; |
| scan_patches($commits, undef, $f); |
| close $f; |
| } |
| |
| sub parse_rev_args { |
| my @args = @_; |
| open my $f, '-|', |
| qw(git rev-parse --revs-only --default HEAD --symbolic), @args |
| or die; |
| my @revs; |
| while (<$f>) { |
| chomp; |
| push @revs, $_; |
| } |
| close $f; |
| return @revs if scalar(@revs) != 1; |
| return "^$revs[0]", 'HEAD' unless $revs[0] =~ /^-/; |
| return $revs[0], 'HEAD'; |
| } |
| |
| sub scan_rev_args { |
| my ($commits, $args) = @_; |
| my @revs = parse_rev_args(@$args); |
| open my $f, '-|', qw(git rev-list --reverse), @revs or die; |
| while (<$f>) { |
| chomp; |
| my $id = $_; |
| $seen{$id} = 1; |
| open my $g, '-|', qw(git show -C --oneline), $id or die; |
| scan_patches($commits, $id, $g); |
| close $g; |
| } |
| close $f; |
| } |
| |
| sub mailmap_contacts { |
| my ($contacts) = @_; |
| my %mapped; |
| my $pid = open2 my $reader, my $writer, qw(git check-mailmap --stdin); |
| for my $contact (keys(%$contacts)) { |
| print $writer "$contact\n"; |
| my $canonical = <$reader>; |
| chomp $canonical; |
| $mapped{$canonical} += $contacts->{$contact}; |
| } |
| close $reader; |
| close $writer; |
| waitpid($pid, 0); |
| die "git-check-mailmap error: $?\n" if $?; |
| return \%mapped; |
| } |
| |
| if (!@ARGV) { |
| die "No input revisions or patch files\n"; |
| } |
| |
| my (@files, @rev_args); |
| for (@ARGV) { |
| if (-e) { |
| push @files, $_; |
| } else { |
| push @rev_args, $_; |
| } |
| } |
| |
| my %sources; |
| for (@files) { |
| scan_patch_file(\%sources, $_); |
| } |
| if (@rev_args) { |
| scan_rev_args(\%sources, \@rev_args) |
| } |
| |
| my $toplevel = `git rev-parse --show-toplevel`; |
| chomp $toplevel; |
| chdir($toplevel) or die "chdir failure: $toplevel: $!\n"; |
| |
| my %commits; |
| blame_sources(\%sources, \%commits); |
| import_commits(\%commits); |
| |
| my $contacts = {}; |
| for my $commit (values %commits) { |
| for my $contact (keys %{$commit->{contacts}}) { |
| $contacts->{$contact}++; |
| } |
| } |
| $contacts = mailmap_contacts($contacts); |
| |
| my $ncommits = scalar(keys %commits); |
| for my $contact (keys %$contacts) { |
| my $percent = $contacts->{$contact} * 100 / $ncommits; |
| next if $percent < $min_percent; |
| print "$contact\n"; |
| } |