File Coverage

blib/lib/Dist/Zilla/Plugin/Git/Contributors.pm
Criterion Covered Total %
statement 117 123 95.1
branch 32 44 72.7
condition 9 15 60.0
subroutine 31 32 96.8
pod 0 4 0.0
total 189 218 86.7


line stmt bran cond sub pod time code
1 18     18   59197215 use strict;
  18         56  
  18         688  
2 18     18   149 use warnings;
  18         48  
  18         1319  
3             package Dist::Zilla::Plugin::Git::Contributors; # git description: v0.035-12-gb31d49d
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Add contributor names from git to your distribution
6             # KEYWORDS: plugin distribution metadata git contributors authors commits
7              
8             our $VERSION = '0.036';
9              
10 18     18   143 use Moose;
  18         63  
  18         135  
11             with 'Dist::Zilla::Role::MetaProvider',
12             'Dist::Zilla::Role::PrereqSource';
13              
14 18     18   124513 use List::Util 1.33 qw(none any);
  18         564  
  18         1839  
15 18     18   141 use Git::Wrapper 0.035;
  18         409  
  18         665  
16 18     18   137 use Try::Tiny;
  18         46  
  18         1385  
17 18     18   145 use Path::Tiny 0.048;
  18         376  
  18         1026  
18 18     18   139 use Moose::Util::TypeConstraints 'enum';
  18         58  
  18         251  
19 18     18   19841 use List::UtilsBy 0.04 'uniq_by';
  18         38444  
  18         1346  
20 18     18   14170 use Unicode::Collate 0.53;
  18         166228  
  18         982  
21 18     18   176 use version;
  18         43  
  18         173  
22 18     18   1960 use namespace::autoclean;
  18         41  
  18         218  
23              
24 19     19 0 76814 sub mvp_multivalue_args { qw(paths remove) }
25 19     19 0 3245 sub mvp_aliases { return { path => 'paths' } }
26              
27             has include_authors => (
28             is => 'ro', isa => 'Bool',
29             default => 0,
30             );
31              
32             has include_releaser => (
33             is => 'ro', isa => 'Bool',
34             default => 1,
35             );
36              
37             has order_by => (
38             is => 'ro', isa => enum([qw(name commits)]),
39             default => 'name',
40             );
41              
42             has paths => (
43             isa => 'ArrayRef[Str]',
44             lazy => 1,
45             default => sub { [] },
46             traits => ['Array'],
47             handles => { paths => 'elements' },
48             );
49              
50             has remove => (
51             isa => 'ArrayRef[Str]',
52             lazy => 1,
53             default => sub { [] },
54             traits => ['Array'],
55             handles => { remove => 'elements' },
56             );
57              
58             around dump_config => sub
59             {
60             my ($orig, $self) = @_;
61             my $config = $self->$orig;
62              
63             my $dist_root = path($self->zilla->root)->realpath;
64              
65             $config->{+__PACKAGE__} = {
66             include_authors => $self->include_authors ? 1 : 0,
67             include_releaser => $self->include_releaser ? 1 : 0,
68             order_by => $self->order_by,
69             paths => [ sort $self->paths ],
70             $self->remove ? ( remove => '...' ) : (),
71             git_version => $self->_git('version'),
72             blessed($self) ne __PACKAGE__ ? ( version => $VERSION ) : (),
73             };
74              
75             return $config;
76             };
77              
78             sub metadata
79             {
80 19     19 0 173535 my $self = shift;
81              
82 19         1279 my $contributors = $self->_contributors;
83 19 100       287 return if not @$contributors;
84              
85 16         282 $self->_check_podweaver;
86 16         271 +{ x_contributors => $contributors };
87             }
88              
89             sub register_prereqs
90             {
91 19     19 0 3714375 my $self = shift;
92              
93 19 100   27   558 return if none { /[^[:ascii:]]/ } @{ $self->_contributors };
  27         492  
  19         1294  
94              
95 2         186 my $prereqs = $self->zilla->prereqs;
96 2         220 my $all_prereqs = $prereqs->requirements_for(qw(runtime requires))
97             ->clone
98             ->add_requirements($prereqs->requirements_for(qw(configure requires)))
99             ->add_requirements($prereqs->requirements_for(qw(build requires)))
100             ->add_requirements($prereqs->requirements_for(qw(test requires)))
101             ->as_string_hash;
102              
103 2         2794 my $perl_prereq = $all_prereqs->{perl};
104              
105 2 50       66 $self->log_debug([ 'found non-ascii characters in contributor names; perl prereq so far is %s',
106             defined $perl_prereq ? $perl_prereq : 'unknown' ]);
107 2 50       2174 $perl_prereq = 0 if not defined $perl_prereq;
108 2         71 $perl_prereq = version->parse($perl_prereq)->numify;
109 2 100       51 return if "$perl_prereq" >= '5.008006';
110              
111             # many Dist::Zilla-using distributions don't have an explicit minimum
112             # perl, but we know that Dist::Zilla doesn't work until 5.8.7
113 1 50   1   18 return if any { /^Dist::Zilla/ } keys %$all_prereqs;
  1         6  
114              
115             # if dynamic_config is set, the user gets another chance to read the file, via fallback code:
116             # < haarg> eumm loads META, updates prereqs, and writes out MYMETA
117             # < haarg> so in a working system, x_contributors will be included
118             # < haarg> in a broken system, it will fail to load META, regenerate it from parameters including META_ADD/MERGE, then write it out
119             # < haarg> so if there isn't any utf8 data in the parameters given to EUMM, it will produce a file that can be read by a "bad" JSON::PP
120 1 50       48 return if $self->zilla->distmeta->{dynamic_config};
121              
122             # see https://github.com/makamaka/JSON-PP/pull/9 for for details
123 1         10095 $self->log('Warning: distribution has non-ascii characters in contributor names. META.json will be unparsable on perls <= 5.8.6 when JSON::PP is lower than 2.27300');
124              
125 1         804 $self->zilla->register_prereqs(
126             {
127             phase => 'configure',
128             type => 'suggests',
129             },
130             'JSON::PP' => '2.27300',
131             );
132             }
133              
134             # should not be called before the MetaProvider phase
135             has _contributors => (
136             is => 'ro', isa => 'ArrayRef[Str]',
137             lazy => 1,
138             builder => '_build_contributors',
139             );
140              
141             sub _build_contributors
142             {
143 19     19   138 my $self = shift;
144              
145             # note that ->status does something different.
146 19 100       385 return [] if not $self->_git(RUN => 'status');
147              
148 18         3722 my @data = $self->_git(shortlog =>
149             {
150             email => 1,
151             summary => 1,
152             numbered => 1,
153             },
154             'HEAD', '--', $self->paths,
155             );
156              
157             # [ count, email ]
158 18         693 my @counts_and_contributors = map [ split ' ', $_, 2 ], @data;
159              
160             my $fc = "$]" >= '5.016001'
161             ? \&CORE::fc
162 18 50       777 : do {
163 0         0 $self->log_debug('case-folding not available; falling back to lower-cased comparisons');
164 0     0   0 sub { lc $_[0] } # not callable via \&CORE::lc
165 0         0 };
166              
167             # remove duplicates by email address, keeping the latest associated name
168 18         873 my $count = @counts_and_contributors;
169 18     47   922 @counts_and_contributors = uniq_by { $fc->(($_->[1] =~ /(<[^>]+>)/g)[-1]) } @counts_and_contributors;
  47         2521  
170              
171 18 100       1406 $self->log('multiple names with the same email found: you may want to use a .mailmap file (https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html#_mapping_authors)') if @counts_and_contributors != $count;
172              
173             # sort by name or count depending on choice (numeric descending, name ascending)
174 18         4493 my $Collator = Unicode::Collate->new(level => 1);
175              
176             my $sort_sub =
177 41     41   70236 $self->order_by eq 'name' ? sub { $Collator->cmp($a->[1], $b->[1]) }
178 1 50   1   41 : $self->order_by eq 'commits' ? sub { $b->[0] <=> $a->[0] || $Collator->cmp($a->[1], $b->[1]) }
179 18 50       940402 : die 'unrecognized option order_by=', $self->order_by;
    100          
180              
181 18         432 my @contributors =
182             map $_->[1],
183             sort $sort_sub
184             @counts_and_contributors;
185              
186             $self->log_debug([ 'extracted contributors from git: %s',
187             sub {
188 18     18   22731 require Data::Dumper;
189 18         119847 chomp(my $str = Data::Dumper->new([ \@contributors ])->Indent(2)->Terse(1)->Dump);
190 18         2117 $str;
191 18         22939 } ]);
192              
193 18 100       8430 if (not $self->include_authors)
194             {
195 14 50       41 my @authors = eval { Dist::Zilla->VERSION('7.000') } ? $self->zilla->authors : @{ $self->zilla->authors };
  14         1099  
  14         673  
196              
197 14         1888 my @author_emails = map /(<[^>]+>)/g, @authors;
198             @contributors = grep {
199 14         69 my $contributor = $_;
  26         76  
200 26     46   239 none { $contributor =~ /\Q$_\E/i } @author_emails;
  46         647  
201             } @contributors;
202             }
203              
204 18 100 100     911 if (not $self->include_releaser and my $releaser = $self->_releaser)
205             {
206 4         240 @contributors = grep $fc->($_) ne $fc->($releaser), @contributors;
207             }
208              
209 18 100       1768 if ($self->remove)
210             {
211             @contributors = grep {
212 1     2   5 my $contributor = $_; none { $contributor =~ /\Q$_\E/ } $self->remove
  2         5  
  2         108  
  2         28  
213             } @contributors;
214             }
215              
216 18         14970 return \@contributors;
217             }
218              
219             sub _releaser
220             {
221 7     7   40267 my $self = shift;
222              
223 7         26 my ($username, $email);
224             try {
225 7     7   390 ($username) = $self->_git(config => 'user.name');
226 7         145 ($email) = $self->_git(config => 'user.email');
227 7         130 };
228 7 100 66     762 if (not $username or not $email)
229             {
230 1         20 $self->log('could not extract user.name and user.email configs from git');
231 1         572 return;
232             }
233 6         231 $username . ' <' . $email . '>';
234             }
235              
236             sub _check_podweaver
237             {
238 16     16   118 my $self = shift;
239              
240             # check if the module is loaded, not just that it is installed
241             $self->log('WARNING! You appear to be using Pod::Weaver::Section::Contributors, but it is not new enough to take data directly from distmeta. Upgrade to version 0.008!')
242 16         753 if eval { Pod::Weaver::Section::Contributors->VERSION(0); 1 }
  0         0  
243 16 50 33     112 and not eval { Pod::Weaver::Section::Contributors->VERSION(0.007001); 1 };
  0         0  
  0         0  
244             }
245              
246             has __git => (
247             is => 'ro',
248             isa => 'Git::Wrapper',
249             lazy => 1,
250             default => sub { Git::Wrapper->new(path(shift->zilla->root)->absolute->stringify) },
251             );
252              
253             sub _git
254             {
255 64     64   652 my ($self, $command, @args) = @_;
256              
257 64 50       566 die 'no command?!' if not $command;
258 64         4056 my $git = $self->__git;
259             my @result = try {
260 64     64   7708 $git->$command(@args);
261             } catch {
262 4 50 33 4   52734 $self->log(blessed($_) && $_->isa('Git::Wrapper::Exception') ? $_->error : $_);
263 4         6085 ();
264 64         1392 };
265 64         639014 my $err = $git->ERR;
266 64 50 66     2280 $self->log(@$err) if $err and @$err;
267              
268             # TODO Git::Wrapper should really be decoding this for us, via a new
269             # (defaulting-to-false) utf8 flag
270 64         1187 utf8::decode($_) foreach @result;
271 64         2475 return @result;
272             }
273              
274             __PACKAGE__->meta->make_immutable;
275              
276             __END__
277              
278             =pod
279              
280             =encoding UTF-8
281              
282             =head1 NAME
283              
284             Dist::Zilla::Plugin::Git::Contributors - Add contributor names from git to your distribution
285              
286             =head1 VERSION
287              
288             version 0.036
289              
290             =head1 SYNOPSIS
291              
292             In your F<dist.ini>:
293              
294             [Git::Contributors]
295              
296             =head1 DESCRIPTION
297              
298             This is a L<Dist::Zilla> plugin that extracts all names and email addresses
299             from git commits in your repository and adds them to the distribution metadata
300             under the C<x_contributors> key. It takes a minimalist approach to this -- no
301             data is stuffed into other locations, including stashes -- if other plugins
302             wish to work with this information, they should extract it from the
303             distribution metadata.
304              
305             =for stopwords unicode casefolding
306              
307             =head1 RECOMMENDED PERL VERSION
308              
309             This module uses unicode comparison routines as well as casefolding semantics
310             (when available); Perl 5.016 is recommended.
311              
312             =head1 CONFIGURATION OPTIONS
313              
314             =head2 C<include_authors>
315              
316             When true, authors (as defined by the preamble section in your F<dist.ini>)
317             are added to the list of contributors. When false, authors
318             are filtered out of the list of contributors. Defaults to false.
319              
320             =head2 C<include_releaser>
321              
322             Defaults to true; set to false to remove the current user (who is doing the
323             distribution release) from the contributors list. It is applied after
324             C<include_authors>, so you will be removed from the list even if you are (one
325             of the) distribution author(s) and C<include_authors = 1>.
326              
327             You probably don't want this option -- it was added experimentally to change
328             how contributors are displayed on L<http://metacpan.org>, but it was decided
329             that this should be managed at a different layer than the metadata.
330              
331             =head2 C<order_by>
332              
333             When C<order_by = name>, contributors are sorted alphabetically
334             (ascending); when C<order_by = commits>, contributors are sorted by number of
335             commits made to the repository (descending). The default value is C<name>.
336              
337             =head2 C<path>
338              
339             Available since version 0.007.
340              
341             Indicates a path, relative to the repository root, to search for commits in.
342             Technically: "Consider only commits that are enough to explain how the files that match the specified paths came to be."
343             Defaults to the repository root. Can be used more than once.
344             I<You should almost certainly not need this.>
345              
346             =head2 C<remove>
347              
348             Available since version 0.011.
349              
350             =for stopwords unanchored
351              
352             Any contributor entry matching this (unanchored, case-sensitive) regular expression is removed
353             from inclusion.
354             Can be used more than once.
355              
356             =for stopwords canonicalizing
357              
358             =head1 CANONICALIZING NAMES AND ADDRESSES
359              
360             If you or a contributor uses multiple names and/or email addresses to make
361             commits and would like them mapped to a canonical value (e.g. their
362             C<cpan.org> address), you can do this by
363             adding a F<.mailmap> file to your git repository, with entries formatted as
364             described in "MAPPING AUTHORS" in C<git help shortlog>
365             (L<https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html>).
366              
367             Duplicate names that share the same email address will be removed
368             automatically (keeping the form associated with the latest commit).
369              
370             =head1 ADDING CONTRIBUTORS TO POD DOCUMENTATION
371              
372             You can add the contributor names to your module documentation by using
373             L<Pod::Weaver> in conjunction with L<Pod::Weaver::Section::Contributors>.
374              
375             =head1 UNICODE SUPPORT
376              
377             =for stopwords ascii
378              
379             This module aims to properly handle non-ascii characters in contributor names.
380             However, on Windows you might need to do a bit more: see
381             L<https://github.com/msysgit/msysgit/wiki/Git-for-Windows-Unicode-Support> for
382             supported versions and extra configurations you may need to apply.
383              
384             =head1 SEE ALSO
385              
386             =over 4
387              
388             =item *
389              
390             L<How I'm using Dist::Zilla to give credit to contributors|http://www.dagolden.com/index.php/1921/how-im-using-distzilla-to-give-credit-to-contributors/>
391              
392             =item *
393              
394             L<Pod::Weaver::Section::Contributors> - weaves x_contributors data into a pod section
395              
396             =item *
397              
398             L<Dist::Zilla::Plugin::Meta::Contributors> - adds an explicit list of names to x_contributors
399              
400             =item *
401              
402             L<Dist::Zilla::Plugin::ContributorsFromGit> - more dependencies, problematic tests, passes around a lot of extra data in stashes unnecessarily, not unicode-clean
403              
404             =item *
405              
406             L<Dist::Zilla::Plugin::ContributorsFromPod> - takes the list of contributors from pod
407              
408             =item *
409              
410             L<Module::Install::Contributors>
411              
412             =item *
413              
414             L<Dist::Zilla::Plugin::ContributorsFile> - adds CONTRIBUTORS file, containing names from x_contributors metadata
415              
416             =back
417              
418             =for Pod::Coverage mvp_multivalue_args mvp_aliases metadata register_prereqs
419              
420             =head1 SUPPORT
421              
422             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Plugin-Git-Contributors>
423             (or L<bug-Dist-Zilla-Plugin-Git-Contributors@rt.cpan.org|mailto:bug-Dist-Zilla-Plugin-Git-Contributors@rt.cpan.org>).
424              
425             There is also a mailing list available for users of this distribution, at
426             L<http://dzil.org/#mailing-list>.
427              
428             There is also an irc channel available for users of this distribution, at
429             L<C<#distzilla> on C<irc.perl.org>|irc://irc.perl.org/#distzilla>.
430              
431             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.freenode.org>.
432              
433             =head1 AUTHOR
434              
435             Karen Etheridge <ether@cpan.org>
436              
437             =head1 CONTRIBUTORS
438              
439             =for stopwords Kent Fredric Ioan Rogers Klaus Eichner Matthew Horsfall Mohammad S Anwar Ricardo Signes
440              
441             =over 4
442              
443             =item *
444              
445             Kent Fredric <kentnl@cpan.org>
446              
447             =item *
448              
449             Ioan Rogers <ioan@dirtysoft.ca>
450              
451             =item *
452              
453             Klaus Eichner <klaus03@gmail.com>
454              
455             =item *
456              
457             Matthew Horsfall <wolfsage@gmail.com>
458              
459             =item *
460              
461             Mohammad S Anwar <mohammad.anwar@yahoo.com>
462              
463             =item *
464              
465             Ricardo Signes <rjbs@cpan.org>
466              
467             =back
468              
469             =head1 COPYRIGHT AND LICENCE
470              
471             This software is copyright (c) 2014 by Karen Etheridge.
472              
473             This is free software; you can redistribute it and/or modify it under
474             the same terms as the Perl 5 programming language system itself.
475              
476             =cut