1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
#!/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";
}
|