Commit | Line | Data |
---|---|---|
4d06402b ES |
1 | #!/usr/bin/perl |
2 | ||
3 | # List people who might be interested in a patch. Useful as the argument to | |
4 | # git-send-email --cc-cmd option, and in other situations. | |
5 | # | |
8e7c4a82 | 6 | # Usage: git contacts <file | rev-list option> ... |
4d06402b ES |
7 | |
8 | use strict; | |
9 | use warnings; | |
10 | use IPC::Open2; | |
11 | ||
12 | my $since = '5-years-ago'; | |
13 | my $min_percent = 10; | |
09ac6737 | 14 | my $labels_rx = qr/Signed-off-by|Reviewed-by|Acked-by|Cc|Reported-by/i; |
4d06402b ES |
15 | my %seen; |
16 | ||
17 | sub format_contact { | |
18 | my ($name, $email) = @_; | |
19 | return "$name <$email>"; | |
20 | } | |
21 | ||
22 | sub parse_commit { | |
23 | my ($commit, $data) = @_; | |
24 | my $contacts = $commit->{contacts}; | |
25 | my $inbody = 0; | |
26 | for (split(/^/m, $data)) { | |
27 | if (not $inbody) { | |
28 | if (/^author ([^<>]+) <(\S+)> .+$/) { | |
29 | $contacts->{format_contact($1, $2)} = 1; | |
30 | } elsif (/^$/) { | |
31 | $inbody = 1; | |
32 | } | |
33 | } elsif (/^$labels_rx:\s+([^<>]+)\s+<(\S+?)>$/o) { | |
34 | $contacts->{format_contact($1, $2)} = 1; | |
35 | } | |
36 | } | |
37 | } | |
38 | ||
39 | sub import_commits { | |
40 | my ($commits) = @_; | |
41 | return unless %$commits; | |
42 | my $pid = open2 my $reader, my $writer, qw(git cat-file --batch); | |
43 | for my $id (keys(%$commits)) { | |
44 | print $writer "$id\n"; | |
45 | my $line = <$reader>; | |
46 | if ($line =~ /^([0-9a-f]{40}) commit (\d+)/) { | |
47 | my ($cid, $len) = ($1, $2); | |
48 | die "expected $id but got $cid\n" unless $id eq $cid; | |
49 | my $data; | |
50 | # cat-file emits newline after data, so read len+1 | |
51 | read $reader, $data, $len + 1; | |
52 | parse_commit($commits->{$id}, $data); | |
53 | } | |
54 | } | |
55 | close $reader; | |
56 | close $writer; | |
57 | waitpid($pid, 0); | |
58 | die "git-cat-file error: $?\n" if $?; | |
59 | } | |
60 | ||
61 | sub get_blame { | |
4c70cfbf ES |
62 | my ($commits, $source, $from, $ranges) = @_; |
63 | return unless @$ranges; | |
4d06402b | 64 | open my $f, '-|', |
4c70cfbf ES |
65 | qw(git blame --porcelain -C), |
66 | map({"-L$_->[0],+$_->[1]"} @$ranges), | |
4d06402b ES |
67 | '--since', $since, "$from^", '--', $source or die; |
68 | while (<$f>) { | |
69 | if (/^([0-9a-f]{40}) \d+ \d+ \d+$/) { | |
70 | my $id = $1; | |
71 | $commits->{$id} = { id => $id, contacts => {} } | |
72 | unless $seen{$id}; | |
73 | $seen{$id} = 1; | |
74 | } | |
75 | } | |
76 | close $f; | |
77 | } | |
78 | ||
db8cae7e ES |
79 | sub blame_sources { |
80 | my ($sources, $commits) = @_; | |
81 | for my $s (keys %$sources) { | |
82 | for my $id (keys %{$sources->{$s}}) { | |
4c70cfbf | 83 | get_blame($commits, $s, $id, $sources->{$s}{$id}); |
db8cae7e ES |
84 | } |
85 | } | |
86 | } | |
87 | ||
4d06402b | 88 | sub scan_patches { |
db8cae7e | 89 | my ($sources, $id, $f) = @_; |
8e7c4a82 | 90 | my $source; |
4d06402b ES |
91 | while (<$f>) { |
92 | if (/^From ([0-9a-f]{40}) Mon Sep 17 00:00:00 2001$/) { | |
93 | $id = $1; | |
94 | $seen{$id} = 1; | |
95 | } | |
96 | next unless $id; | |
97 | if (m{^--- (?:a/(.+)|/dev/null)$}) { | |
98 | $source = $1; | |
4d06402b | 99 | } elsif (/^@@ -(\d+)(?:,(\d+))?/ && $source) { |
9ae9ca1f | 100 | my $len = defined($2) ? $2 : 1; |
db8cae7e | 101 | push @{$sources->{$source}{$id}}, [$1, $len] if $len; |
4d06402b ES |
102 | } |
103 | } | |
104 | } | |
105 | ||
106 | sub scan_patch_file { | |
107 | my ($commits, $file) = @_; | |
108 | open my $f, '<', $file or die "read failure: $file: $!\n"; | |
8e7c4a82 ES |
109 | scan_patches($commits, undef, $f); |
110 | close $f; | |
111 | } | |
112 | ||
ccf6b45a ES |
113 | sub parse_rev_args { |
114 | my @args = @_; | |
115 | open my $f, '-|', | |
116 | qw(git rev-parse --revs-only --default HEAD --symbolic), @args | |
117 | or die; | |
118 | my @revs; | |
119 | while (<$f>) { | |
120 | chomp; | |
121 | push @revs, $_; | |
122 | } | |
123 | close $f; | |
124 | return @revs if scalar(@revs) != 1; | |
125 | return "^$revs[0]", 'HEAD' unless $revs[0] =~ /^-/; | |
126 | return $revs[0], 'HEAD'; | |
127 | } | |
128 | ||
8e7c4a82 ES |
129 | sub scan_rev_args { |
130 | my ($commits, $args) = @_; | |
ccf6b45a ES |
131 | my @revs = parse_rev_args(@$args); |
132 | open my $f, '-|', qw(git rev-list --reverse), @revs or die; | |
8e7c4a82 ES |
133 | while (<$f>) { |
134 | chomp; | |
135 | my $id = $_; | |
136 | $seen{$id} = 1; | |
137 | open my $g, '-|', qw(git show -C --oneline), $id or die; | |
138 | scan_patches($commits, $id, $g); | |
139 | close $g; | |
140 | } | |
4d06402b ES |
141 | close $f; |
142 | } | |
143 | ||
7c6d6ff8 ES |
144 | sub mailmap_contacts { |
145 | my ($contacts) = @_; | |
146 | my %mapped; | |
147 | my $pid = open2 my $reader, my $writer, qw(git check-mailmap --stdin); | |
148 | for my $contact (keys(%$contacts)) { | |
149 | print $writer "$contact\n"; | |
150 | my $canonical = <$reader>; | |
151 | chomp $canonical; | |
152 | $mapped{$canonical} += $contacts->{$contact}; | |
153 | } | |
154 | close $reader; | |
155 | close $writer; | |
156 | waitpid($pid, 0); | |
157 | die "git-check-mailmap error: $?\n" if $?; | |
158 | return \%mapped; | |
159 | } | |
160 | ||
4d06402b | 161 | if (!@ARGV) { |
8e7c4a82 | 162 | die "No input revisions or patch files\n"; |
4d06402b ES |
163 | } |
164 | ||
8e7c4a82 | 165 | my (@files, @rev_args); |
4d06402b | 166 | for (@ARGV) { |
8e7c4a82 ES |
167 | if (-e) { |
168 | push @files, $_; | |
169 | } else { | |
170 | push @rev_args, $_; | |
171 | } | |
172 | } | |
173 | ||
db8cae7e | 174 | my %sources; |
8e7c4a82 | 175 | for (@files) { |
db8cae7e | 176 | scan_patch_file(\%sources, $_); |
4d06402b | 177 | } |
8e7c4a82 | 178 | if (@rev_args) { |
db8cae7e | 179 | scan_rev_args(\%sources, \@rev_args) |
8e7c4a82 | 180 | } |
db8cae7e | 181 | |
8fc9f022 ES |
182 | my $toplevel = `git rev-parse --show-toplevel`; |
183 | chomp $toplevel; | |
184 | chdir($toplevel) or die "chdir failure: $toplevel: $!\n"; | |
185 | ||
db8cae7e ES |
186 | my %commits; |
187 | blame_sources(\%sources, \%commits); | |
4d06402b ES |
188 | import_commits(\%commits); |
189 | ||
190 | my $contacts = {}; | |
191 | for my $commit (values %commits) { | |
192 | for my $contact (keys %{$commit->{contacts}}) { | |
193 | $contacts->{$contact}++; | |
194 | } | |
195 | } | |
7c6d6ff8 | 196 | $contacts = mailmap_contacts($contacts); |
4d06402b ES |
197 | |
198 | my $ncommits = scalar(keys %commits); | |
199 | for my $contact (keys %$contacts) { | |
200 | my $percent = $contacts->{$contact} * 100 / $ncommits; | |
201 | next if $percent < $min_percent; | |
202 | print "$contact\n"; | |
203 | } |