File Coverage

blib/lib/Git/CPAN/Patch/Import.pm
Criterion Covered Total %
statement 67 296 22.6
branch 0 98 0.0
condition 0 31 0.0
subroutine 23 37 62.1
pod 0 12 0.0
total 90 474 18.9


line stmt bran cond sub pod time code
1             package Git::CPAN::Patch::Import;
2             our $AUTHORITY = 'cpan:YANICK';
3             $Git::CPAN::Patch::Import::VERSION = '2.3.1';
4 2     2   1368 use 5.10.0;
  2         7  
5              
6 2     2   11 use strict;
  2         4  
  2         40  
7 2     2   8 use warnings;
  2         4  
  2         56  
8              
9             {
10 2     2   9 no warnings;
  2         5  
  2         65  
11 2     2   27 use 5.010;
  2         6  
12              
13 2     2   833 use File::chmod (); # must be before 'autodie' to hush the warnings
  2         4250  
  2         43  
14              
15 2     2   424 use autodie;
  2         10349  
  2         13  
16              
17 2     2   12459 use Archive::Extract;
  2         227496  
  2         98  
18             $Archive::Extract::PREFER_BIN = 1;
19              
20 2     2   36 use File::Find;
  2         5  
  2         112  
21 2     2   12 use File::Basename;
  2         4  
  2         92  
22 2     2   399 use File::Spec::Functions;
  2         689  
  2         148  
23 2     2   14 use File::Temp qw(tempdir);
  2         4  
  2         98  
24 2     2   11 use File::Path;
  2         4  
  2         77  
25 2     2   737 use File::chdir;
  2         2792  
  2         180  
26 2     2   14 use Path::Class qw/ file /;
  2         14  
  2         127  
27 2     2   12 use Cwd qw/ getcwd /;
  2         4  
  2         57  
28 2     2   16 use version;
  2         4  
  2         13  
29 2     2   560 use Git::Repository;
  2         10618  
  2         14  
30 2     2   1102 use CLASS;
  2         498  
  2         10  
31 2     2   1388 use DateTime;
  2         816575  
  2         110  
32              
33 2     2   1349 use CPANPLUS;
  2         360954  
  2         124  
34 2     2   889 use BackPAN::Index;
  2         327968  
  2         47  
35              
36             }
37              
38             our $BackPAN_URL = "http://backpan.perl.org/";
39             our $PERL_GIT_URL = 'git://perl5.git.perl.org/perl.git';
40              
41             sub backpan_index {
42 0     0 0   state $backpan = do {
43 0           say "Loading BackPAN index (this may take a while)";
44 0           BackPAN::Index->new;
45             };
46 0           return $backpan;
47             }
48              
49             sub cpanplus {
50 0     0 0   state $cpanplus = CPANPLUS::Backend->new;
51 0           return $cpanplus;
52             }
53              
54             # Make sure we can read tarballs and change directories
55             sub _fix_permissions {
56 0     0     my $dir = shift;
57              
58 0           File::chmod::chmod "u+rx", $dir;
59             find(sub {
60 0 0   0     -d $_ ? File::chmod::chmod "u+rx", $_ : File::chmod::chmod "u+r", $_;
61 0           }, $dir);
62             }
63              
64             sub init_repo {
65 0     0 0   my $module = shift;
66 0           my $opts = shift;
67              
68 0           my $dirname = ".";
69 0 0         if ( defined $opts->{mkdir} ) {
70 0   0       ( $dirname = $opts->{mkdir} || $module ) =~ s/::/-/g;
71              
72 0 0         if( -d $dirname ) {
73 0 0         die "$dirname already exists\n" unless $opts->{update};
74             }
75             else {
76 0           say "creating directory $dirname";
77              
78             # mkpath() does not play nice with overloaded objects
79 0           mkpath "$dirname";
80             }
81             }
82              
83             {
84 0           local $CWD = $dirname;
  0            
85              
86 0 0         if ( -d '.git' ) {
87 0 0 0       if ( !$opts->{force} and !$opts->{update} ) {
88 0           die "Aborting: git repository already present.\n",
89             "use '--force' if it's really what you want to do\n";
90             }
91             }
92             else {
93 0           Git::Repository->run('init');
94             }
95             }
96              
97 0           return File::Spec->rel2abs($dirname);
98             }
99              
100              
101             sub releases_in_git {
102 0     0 0   my $repo = Git::Repository->new;
103 0 0         return unless contains_git_revisions();
104 0           my @releases = map { m{\bgit-cpan-version:\s*(\S+)}x; $1 }
  0            
  0            
105             grep /^\s*git-cpan-version:/,
106             $repo->run(log => '--pretty=format:%b');
107 0           return @releases;
108             }
109              
110              
111             sub rev_exists {
112 0     0 0   my $rev = shift;
113 0           my $repo = Git::Repository->new;
114              
115 0           return eval { $repo->run( 'rev-parse', $rev ); };
  0            
116             }
117              
118              
119             sub contains_git_revisions {
120 0 0   0 0   return unless -d ".git";
121 0           return rev_exists("HEAD");
122             }
123              
124              
125             sub import_one_backpan_release {
126 0     0 0   my $release = shift;
127 0           my $opts = shift;
128 0   0       my $backpan_urls = $opts->{backpan} || $BackPAN_URL;
129              
130             # allow multiple backpan URLs to be supplied
131 0 0         $backpan_urls = [ $backpan_urls ] unless (ref($backpan_urls) eq 'ARRAY');
132              
133 0           my $repo = Git::Repository->new;
134              
135 0           my( $last_commit, $last_version );
136              
137             # figure out if there is already an imported module
138 0 0         if ( $last_commit = eval { $repo->run("rev-parse", "-q", "--verify", "cpan/master") } ) {
  0            
139 0           $last_version = $repo->run("cpan-last-version");
140             }
141              
142             my $tmp_dir = File::Temp->newdir(
143 0 0         $opts->{tempdir} ? (DIR => $opts->{tempdir}) : ()
144             );
145              
146 0           my $archive_file = catfile($tmp_dir, $release->filename);
147 0           mkpath dirname $archive_file;
148              
149 0           my $response;
150 0           for my $backpan_url (@$backpan_urls) {
151 0           my $release_url = $backpan_url . "/" . $release->prefix;
152              
153 0           say "Downloading $release_url";
154 0           $response = get_from_url($release_url, $archive_file);
155 0 0         last if $response->is_success;
156              
157 0           say " failed @{[ $response->status_line ]}";
  0            
158             }
159              
160 0 0         if( !$response->is_success ) {
161 0           say "Fetch failed. Skipping.";
162 0           return;
163             }
164              
165 0 0         if( !-e $archive_file ) {
166 0           say "$archive_file is missing. Skipping.";
167 0           return;
168             }
169              
170 0           say "extracting distribution";
171 0           my $ae = Archive::Extract->new( archive => $archive_file );
172 0 0         unless( $ae->extract( to => $tmp_dir ) ) {
173 0           say "Couldn't extract $archive_file to $tmp_dir because ".$ae->error;
174 0           say "Skipping";
175 0           return;
176             }
177              
178 0           my $dir = $ae->extract_path;
179 0 0         if( !$dir ) {
180 0           say "The archive is empty, skipping";
181 0           return;
182             }
183 0           _fix_permissions($dir);
184              
185 0           my $tree = do {
186             # don't overwrite the user's index
187 0           local $ENV{GIT_INDEX_FILE} = catfile($tmp_dir, "temp_git_index");
188 0           local $ENV{GIT_DIR} = catfile( getcwd(), '.git' );
189 0           local $ENV{GIT_WORK_TREE} = $dir;
190              
191 0           local $CWD = $dir;
192              
193 0           my $write_tree_repo = Git::Repository->new( work_tree => $dir ) ;
194              
195 0           $write_tree_repo->run( qw(add -v --force .) );
196 0           $write_tree_repo->run( "write-tree" );
197             };
198              
199             # Create a commit for the imported tree object and write it into
200             # refs/remotes/cpan/master
201 0           local %ENV = %ENV;
202 0   0       $ENV{GIT_AUTHOR_DATE} ||= $release->date;
203              
204 0           my $author = $CLASS->cpanplus->author_tree($release->cpanid);
205 0   0       $ENV{GIT_AUTHOR_NAME} ||= $author->author;
206 0   0       $ENV{GIT_AUTHOR_EMAIL} ||= $author->email;
207              
208 0           my @parents = grep { $_ } $last_commit;
  0            
209              
210              
211             # commit message
212 0           my $name = $release->dist;
213 0   0       my $version = $release->version || '';
214 0 0         my $message = join ' ', ( $last_version ? "import" : "initial import of"), "$name $version from CPAN\n";
215 0           $message .= <<"END";
216              
217             git-cpan-module: $name
218             git-cpan-version: $version
219 0           git-cpan-authorid: @{[ $author->cpanid ]}
220 0           git-cpan-file: @{[ $release->prefix ]}
221              
222             END
223              
224             my $commit = $repo->run( { input => $message }, 'commit-tree', $tree,
225 0           map { ( -p => $_ ) } @parents );
  0            
226              
227             # finally, update the fake branch and create a tag for convenience
228 0           my $dist = $release->dist;
229 0           print $repo->run('update-ref', '-m' => "import $dist", 'refs/heads/cpan/master', $commit );
230              
231 0 0         if( $version ) {
232 0           my $tag = $version;
233 0           $tag =~ s{^\.}{0.}; # git does not like a leading . as a tag name
234 0           $tag =~ s{\.$}{}; # nor a trailing one
235 0 0         if( $repo->run( "tag", "-l" => $tag ) ) {
236 0           say "Tag $tag already exists, overwriting";
237             }
238 0           print $repo->run( "tag", "-f" => $tag, $commit );
239 0           say "created tag '$tag' ($commit)";
240             }
241             }
242              
243              
244             sub get_from_url {
245 0     0 0   my($url, $file) = @_;
246              
247 0           require LWP::UserAgent;
248 0           my $ua = LWP::UserAgent->new;
249              
250 0           my $req = HTTP::Request->new( GET => $url );
251 0           my $res = $ua->request($req, $file);
252              
253 0           return $res;
254             }
255              
256              
257             sub import_from_backpan {
258 0     0 0   my ( $distname, $opts ) = @_;
259              
260 0           $distname =~ s/::/-/g;
261              
262             # handle --mkdir and raise an error if the target directory has already been git-initialized
263 0           my $repo_dir = init_repo($distname, $opts);
264              
265 0           local $CWD = $repo_dir;
266              
267 0           my $backpan = $CLASS->backpan_index;
268 0 0         my $dist = $backpan->dist($distname)
269             or die "Error: no distributions found. ",
270             "Are you sure you spelled the module name correctly?\n";
271              
272 0           fixup_repository();
273              
274 0           my %existing_releases;
275 0 0         %existing_releases = map { $_ => 1 } releases_in_git() if $opts->{update};
  0            
276 0           my $release_added = 0;
277 0           for my $release ($dist->releases->search( undef, { order_by => "date" } )) {
278 0 0         next if $existing_releases{$release->version};
279              
280             # skip .ppm files
281 0 0         next if $release->filename =~ m{\.ppm\b};
282              
283 0           say "importing $release";
284 0           import_one_backpan_release(
285             $release,
286             $opts,
287             );
288 0           $release_added++;
289             }
290              
291 0 0         if( !$release_added ) {
292 0 0         if( !keys %existing_releases ) {
293 0           say "Empty repository for $dist. Deleting.";
294              
295             # We can't delete it if we're inside it.
296 0           $CWD = "..";
297 0           rmtree $repo_dir;
298              
299 0           return;
300             }
301             else {
302 0           say "No updates for $dist.";
303 0           return;
304             }
305             }
306              
307 0           my $repo = Git::Repository->new;
308 0 0         if( !rev_exists("master") ) {
309 0           print $repo->run('checkout', '-t', '-b', 'master', 'cpan/master');
310             }
311             else {
312 0           print $repo->run('checkout', 'master', '.'),
313             $repo->run('merge', 'cpan/master');
314             }
315              
316 0           return $repo_dir;
317             }
318              
319              
320             sub fixup_repository {
321 0     0 0   my $repo = Git::Repository->new;
322              
323 0 0         return unless -d ".git";
324              
325             # We do our work in cpan/master, it might not exist if this
326             # repo was cloned from gitpan.
327 0 0 0       if( !rev_exists("cpan/master") and rev_exists("master") ) {
328 0           print $repo->run('branch', '-t', 'cpan/master', 'master');
329             }
330             }
331              
332 2     2   5281 use MetaCPAN::API;
  2         177321  
  2         1668  
333             my $mcpan = MetaCPAN::API->new;
334              
335             sub find_release {
336 0     0 0   my $input = shift;
337              
338             return eval { $mcpan->release(
339             distribution => $mcpan->module($input)->{distribution}
340             ) }
341 0   0       || eval { $mcpan->release( distribution => $input ) }
342             || die "could not find release for '$input' on metacpan\n";
343              
344             }
345              
346             sub main {
347 0     0 0   my $module = shift;
348 0           my $opts = shift;
349              
350 0 0         if ( delete $opts->{backpan} ) {
351 0           return import_from_backpan( $module, $opts );
352             }
353              
354 0           my $repo = Git::Repository->new;
355              
356 0           my ( $last_commit, $last_version );
357              
358             # figure out if there is already an imported module
359 0 0         if ( $last_commit = eval { $repo->run("rev-parse", "-q", "--verify", "cpan/master") } ) {
  0            
360 0   0       $module ||= $repo->run("cpan-which");
361 0           $last_version = $repo->run("cpan-last-version");
362             }
363              
364 0 0         die("Usage: git-cpan import Foo::Bar\n") unless $module;
365              
366             # first we figure out a module object from the module argument
367              
368 0           my $release = find_release($module);
369              
370             # based on the version number it figured out for us we decide whether or not to
371             # actually import.
372              
373 0           my $name = $release->{name};
374 0           my $version = $release->{version};
375 0           my $dist = $release->{distribution};
376              
377 0 0         if ( $dist eq 'perl' ) {
378 0           say "$name is a core modules, ",
379             "clone perl from $PERL_GIT_URL instead.";
380 0           exit;
381             }
382              
383 0           my $prettyname = $dist . ( " ($module)" x ( $dist ne $module ) );
384              
385 0 0 0       if ( $last_version and $opts->{checkversion} ) {
386             # if last_version is defined this is an update
387 0           my $imported = version->new($last_version);
388 0           my $will_import = version->new($release->{version});
389              
390 0 0         die "$name has already been imported\n" if $imported == $will_import;
391              
392 0 0         die "imported version $imported is more recent than $will_import, can't import\n"
393             if $imported > $will_import;
394              
395 0           say "updating $prettyname from $imported to $will_import";
396              
397             } else {
398 0           say "importing $prettyname";
399             }
400              
401 0           require LWP::UserAgent;
402              
403 0           my $ua = LWP::UserAgent->new;
404              
405             # download the dist and extract into a temporary directory
406 0           my $tmp_dir = tempdir( CLEANUP => 0 );
407              
408 0           say "downloading $dist";
409              
410 0           my $tarball = file( $tmp_dir, $release->{archive} );
411              
412             $ua->mirror(
413 0 0         $release->{download_url} => $tarball
414             ) or die "couldn't fetch tarball\n";
415              
416 0           say "extracting distribution";
417              
418 0           my $archive = Archive::Extract->new( archive => $tarball );
419 0           $archive->extract( to => $tmp_dir );
420              
421 0 0         my $dist_dir = $archive->extract_path
422             or die "extraction failed\n";
423              
424             # create a tree object for the CPAN module
425             # this imports the source code without touching the user's working directory or
426             # index
427              
428 0           my $tree = do {
429             # don't overwrite the user's index
430 0           local $ENV{GIT_INDEX_FILE} = catfile($tmp_dir, "temp_git_index");
431 0           local $ENV{GIT_DIR} = catfile( getcwd(), '.git' );
432 0           local $ENV{GIT_WORK_TREE} = $dist_dir;
433              
434 0           local $CWD = $dist_dir;
435              
436 0           my $write_tree_repo = Git::Repository->new( work_tree => $dist_dir );
437              
438 0           $write_tree_repo->run( qw(add -v --force .) );
439 0           $write_tree_repo->run( "write-tree" );
440             };
441              
442             # create a commit for the imported tree object and write it into
443             # refs/heads/cpan/master
444              
445             {
446 0           local %ENV = %ENV;
  0            
447              
448 0           my $author_obj = $mcpan->author($release->{author});
449              
450             # try to find a date for the version using the backpan index
451             # secondly, if the CPANPLUS author object is a fake one (e.g. when importing a
452             # URI), get the user object by using the ID from the backpan index
453 0 0         unless ( $ENV{GIT_AUTHOR_DATE} ) {
454 0           my $mtime = eval {
455 0           DateTime->from_epoch( epoch => $release->{stat}{mtime})->ymd;
456             };
457              
458 0 0         warn $@ if $@;
459              
460             # CPAN::Checksums makes YYYY-MM-DD dates, but GIT_AUTHOR_DATE
461             # doesn't support that.
462 0 0         $mtime .= 'T00:00::00'
463             if $mtime =~ m/\A (\d\d\d\d) - (\d\d?) - (\d\d?) \z/x;
464              
465 0 0         if ( $mtime ) {
466 0           $ENV{GIT_AUTHOR_DATE} = $mtime;
467             } else {
468 0           my %dists;
469              
470 0 0         if ( $opts->{backpan} ) {
471             # we need the backpan index for dates
472 0           my $backpan = $CLASS->backpan_index;
473              
474 0           %dists = map { $_->filename => $_ }
475 0           $backpan->releases($release->{name});
476             }
477              
478 0 0         if ( my $bp_dist = $dists{$dist} ) {
479              
480 0           $ENV{GIT_AUTHOR_DATE} = $bp_dist->date;
481              
482 0 0         if ( $author_obj->isa("CPANPLUS::Module::Author::Fake") ) {
483 0           $author_obj = $mcpan->author_tree($bp_dist->cpanid);
484             }
485             } else {
486 0           say "Couldn't find upload date for $dist";
487              
488 0 0         if ( $author_obj->isa("CPANPLUS::Module::Author::Fake") ) {
489 0           say "Couldn't find author for $dist";
490             }
491             }
492             }
493             }
494              
495             # create the commit object
496 0 0         $ENV{GIT_AUTHOR_NAME} = $author_obj->{name} unless $ENV{GIT_AUTHOR_NAME};
497 0 0         $ENV{GIT_AUTHOR_EMAIL} = $author_obj->{email}[0] unless $ENV{GIT_AUTHOR_EMAIL};
498              
499 0 0         my @parents = ( grep { $_ } $last_commit, @{ $opts->{parent} || [] } );
  0            
  0            
500              
501 0 0         my $message = join ' ',
502             ( $last_version ? "import" : "initial import of" ),
503             "$name $version from CPAN\n";
504 0           $message .= <<"END";
505              
506             git-cpan-module: $name
507             git-cpan-version: $version
508 0           git-cpan-authorid: @{[ $author_obj->{pauseid} ]}
509              
510             END
511              
512             my $commit = $repo->run(
513             { input => $message },
514 0           'commit-tree', $tree, map { ( -p => $_ ) } @parents );
  0            
515              
516             # finally, update the fake remote branch and create a tag for convenience
517              
518 0           print $repo->run('update-ref', '-m' => "import $dist", 'refs/remotes/cpan/master', $commit );
519              
520 0           print $repo->run( tag => $version, $commit );
521              
522 0           say "created tag '$version' ($commit)";
523             }
524              
525             }
526              
527             1;
528              
529             =pod
530              
531             =encoding UTF-8
532              
533             =head1 NAME
534              
535             Git::CPAN::Patch::Import
536              
537             =head1 VERSION
538              
539             version 2.3.1
540              
541             =head1 DESCRIPTION
542              
543             This is the guts of Git::CPAN::Patch::Import moved here to make it callable
544             as a function so git-backpan-init goes faster.
545              
546             =head1 NAME
547              
548             Git::CPAN::Patch::Import - The meat of git-cpan-import
549              
550             =head1 AUTHOR
551              
552             Yanick Champoux <yanick@cpan.org>
553              
554             =head1 COPYRIGHT AND LICENSE
555              
556             This software is copyright (c) 2017 by Yanick Champoux.
557              
558             This is free software; you can redistribute it and/or modify it under
559             the same terms as the Perl 5 programming language system itself.
560              
561             =cut
562              
563             __END__
564              
565              
566             1;