File Coverage

blib/lib/App/cpangitify.pm
Criterion Covered Total %
statement 170 194 87.6
branch 37 58 63.7
condition 4 11 36.3
subroutine 25 28 89.2
pod 0 2 0.0
total 236 293 80.5


line stmt bran cond sub pod time code
1             package App::cpangitify;
2              
3 2     2   656744 use strict;
  2         18  
  2         59  
4 2     2   13 use warnings;
  2         4  
  2         56  
5 2     2   10 use autodie qw( :system );
  2         3  
  2         14  
6 2     2   22699 use 5.010001;
  2         16  
7 2     2   1537 use Getopt::Long qw( GetOptions );
  2         21622  
  2         11  
8 2     2   1658 use Pod::Usage qw( pod2usage );
  2         80034  
  2         194  
9 2     2   1205 use Path::Class qw( file dir );
  2         37064  
  2         131  
10 2     2   1092 use Git::Wrapper;
  2         32566  
  2         94  
11 2     2   21 use File::Temp qw( tempdir );
  2         6  
  2         131  
12 2     2   24 use File::chdir;
  2         6  
  2         220  
13 2     2   1785 use JSON::PP qw( decode_json );
  2         29507  
  2         168  
14 2     2   24 use URI;
  2         5  
  2         59  
15 2     2   1269 use PerlX::Maybe qw( maybe );
  2         5453  
  2         9  
16 2     2   1407 use File::Copy::Recursive qw( rcopy );
  2         14540  
  2         206  
17 2     2   24 use File::Basename qw( basename );
  2         6  
  2         267  
18 2     2   1548 use Archive::Extract;
  2         315216  
  2         138  
19 2     2   23 use File::Spec;
  2         4  
  2         45  
20 2     2   1234 use CPAN::ReleaseHistory;
  2         137774  
  2         116  
21 2     2   26 use HTTP::Tiny;
  2         6  
  2         1578  
22              
23             # ABSTRACT: Convert cpan distribution from BackPAN to a git repository
24             our $VERSION = '0.18'; # VERSION
25              
26              
27             our $ua = HTTP::Tiny->new;
28             our $opt_metacpan_url;
29              
30             sub _rm_rf
31             {
32 83     83   8156 my($file) = @_;
33              
34 83 100 66     339 if($file->is_dir && ! -l $file)
35             {
36 18         889 _rm_rf($_) for $file->children;
37             }
38              
39 83 50       2558 $file->remove || die "unable to delete $file";
40             }
41              
42             our $_run_cb = sub {};
43             our $original_run = \&Git::Wrapper::RUN;
44             our $ignore_error = 0;
45             our $trace = 0;
46              
47             sub _run_wrapper
48             {
49 32     32   1509 my($self,@command) = @_;
50 32         81 my @display;
51 32         232 foreach my $arg (@command)
52             {
53 64 100       274 if(ref($arg) eq 'HASH')
54             {
55 7         366 foreach my $k (keys %$arg)
56             {
57 28         171 my $v = $arg->{$k};
58 28         101 push @display, "--$k";
59 28 50       366 push @display, $v =~ /\s/ ? "'$v'" : $v
    100          
60             if $v ne '1'; # yes there is a weird exception for this :P
61             }
62             }
63             else
64             {
65 57         164 push @display, $arg;
66             }
67             }
68 32         643 $_run_cb->($self, @display);
69 32 50       40481 say "+ git @display" if $trace;
70 32         242 $original_run->($self, @command);
71             }
72              
73             sub author($)
74             {
75 7     7 0 87 state $cache = {};
76              
77 7         55 my $cpanid = shift;
78              
79 7 100       129 unless(defined $cache->{$cpanid})
80             {
81 3         165 my $uri = URI->new($opt_metacpan_url . "v1/author/" . $cpanid);
82 3         1058 my $res = $ua->get($uri);
83 3 50       10209 unless($res->{success})
84             {
85 0         0 say "error fetching $uri";
86 0         0 say $res->{reason};
87 0         0 return 2;
88             }
89             $cache->{$cpanid} = decode_json($res->{content})
90 3         84 }
91              
92 7         14572 my $email = $cache->{$cpanid}->{email};
93 7 100       115 $email = $email->[0] if ref($email) eq 'ARRAY';
94 7         412 sprintf "%s <%s>", $cache->{$cpanid}->{name}, $email;
95             }
96              
97             sub main
98             {
99 2     2 0 97668 my $class = shift;
100 2         30 local @ARGV = @_;
101 2     2   19 no warnings 'redefine';
  2         5  
  2         172  
102 2         54 local *Git::Wrapper::RUN = \&_run_wrapper;
103 2     2   16 use warnings;
  2         5  
  2         2916  
104              
105 2         23 my %skip;
106             my $opt_backpan_index_url;
107 2         28 my $opt_backpan_url = "http://backpan.perl.org/authors/id";
108 2         38 $opt_metacpan_url = "http://fastapi.metacpan.org/";
109 2         14 my $opt_trace = 0;
110 2         19 my $opt_output;
111             my $opt_resume;
112 2         16 my $opt_branch = 'main';
113              
114             GetOptions(
115             'backpan_index_url=s' => \$opt_backpan_index_url,
116             'backpan_url=s' => \$opt_backpan_url,
117             'metacpan_url=s' => \$opt_metacpan_url,
118             'trace' => \$opt_trace,
119 0     0   0 'skip=s' => sub { $skip{$_} = 1 for split /,/, $_[1] },
120             'resume' => \$opt_resume,
121             'output|o=s' => \$opt_output,
122 0     0   0 'help|h' => sub { pod2usage({ -verbose => 2}) },
123             'branch|b=s' => \$opt_branch,
124             'version' => sub {
125 0   0 0   0 say 'cpangitify version ', ($App::cpangitify::VERSION // 'dev');
126 0         0 exit 1;
127             },
128 2 50       111 ) || pod2usage(1);
129              
130 2         3251 local $trace = $opt_trace;
131              
132 2         23 my @names = @ARGV;
133 2         26 s/::/-/g for @names;
134 2         13 my %names = map { $_ => 1 } @names;
  3         45  
135 2         20 my $name = $names[0];
136              
137 2 50       21 pod2usage(1) unless $name;
138              
139 2 50       45 my $dest = $opt_output ? dir($opt_output)->absolute : dir()->absolute->subdir($name);
140              
141 2 50 33     575 if(-e $dest && ! $opt_resume)
142             {
143 0         0 say "already exists: $dest";
144 0         0 say "you may be able to update with the --resume option";
145 0         0 say "but any local changes to your repository will be overwritten by upstream";
146 0         0 return 2;
147             }
148              
149 2         225 say "creating/updating index...";
150 2         146 my $history = CPAN::ReleaseHistory->new(
151             maybe url => $opt_backpan_index_url
152             )->release_iterator;
153              
154 2         65055 say "searching...";
155 2         11 my @rel;
156 2         15 while(my $release = $history->next_release)
157             {
158 22 100       12280 next unless defined $release->distinfo->dist;
159 8 100       1048 next unless $names{$release->distinfo->dist};
160 7         132 push @rel, $release;
161             }
162              
163 2 50 33     96 if($@ || @rel == 0)
164             {
165 0         0 say "no releases found for $name";
166 0         0 return 2;
167             }
168              
169 2         144 say "mkdir $dest";
170 2         242 $dest->mkpath(0,0700);
171              
172 2         597 my $git = Git::Wrapper->new($dest->stringify);
173              
174 2 50       540 if($opt_resume)
175             {
176 0 0       0 if($git->status->is_dirty)
177             {
178 0         0 die "the appear to be uncommited changes";
179             }
180 0         0 $skip{$_} = 1 for $git->tag;
181             }
182             else
183             {
184 2         39 $git->init;
185 2         25451 $git->checkout( -b => $opt_branch );
186             }
187              
188 2         18112 foreach my $rel (@rel)
189             {
190 7         8033 my $path = $rel->path;
191 7         910 my $version = $rel->distinfo->version;
192 7         313 my $time = $rel->timestamp;
193 7         188 my $cpanid = $rel->distinfo->cpanid;
194              
195 7         427 say "$path [ $version ]";
196              
197 7 50       76 if($skip{$version})
198             {
199 0         0 say "skipping ...";
200 0         0 next;
201             }
202              
203 7         192 my $tmp = dir( tempdir( CLEANUP => 1 ) );
204              
205 7         8035 local $CWD = $tmp->stringify;
206              
207 7         923 my $uri = URI->new(join('/', $opt_backpan_url, $path));
208 7         1438 say "fetch ... $uri";
209 7         757 my $res = $ua->get($uri);
210 7 50       17863 unless($res->{success})
211             {
212 0         0 say "error fetching $uri";
213 0         0 say $res->{reason};
214 0         0 return 2;
215             }
216              
217 7         25 do {
218 7         45 my $fn = basename $uri->path;
219              
220 7         1710 open my $fh, '>', $fn;
221 7         40 binmode $fh;
222 7         358 print $fh $res->{content};
223 7         175 close $fh;
224              
225 7         123 say "unpack... $fn";
226 7         259 my $archive = Archive::Extract->new( archive => $fn );
227 7 50       3664 $archive->extract( to => File::Spec->curdir ) || die $archive->error;
228 7         1374101 unlink $fn;
229 7 50       508 if($trace)
230             {
231 0         0 say "- extract $fn $_" for @{ $archive->files };
  0         0  
232             }
233             };
234              
235 7         51 my $source = do {
236 7         173 my @children = map { $_->absolute } dir()->children;
  7         6300  
237 7 50       939 if(@children != 1)
238             {
239 0         0 say "archive doesn't contain exactly one child: @children";
240             }
241              
242 7         44 $CWD = $children[0]->stringify;
243 7         652 $children[0];
244             };
245              
246 7         212 say "merge...";
247              
248 7         90 foreach my $child ($dest->children)
249             {
250 67 100       17947 next if $child->basename eq '.git';
251 60         482 _rm_rf($child);
252             }
253              
254 7         700 foreach my $child ($source->children)
255             {
256 84 50       151544 next if $child->basename eq '.git';
257 84 100       1139 if(-d $child)
258             {
259 14 50       634 rcopy($child, $dest->subdir($child->basename)) || die "unable to copy $child $!";
260             }
261             else
262             {
263 70 50       3816 rcopy($child, $dest->file($child->basename)) || die "unable to copy $child $!";
264             }
265             }
266              
267 7         11290 say "commit and tag...";
268 7         346 $git->add('.');
269 7         103296 $git->add('-u');
270 7         72495 $git->commit({
271             message => "version $version",
272             date => "$time +0000",
273             author => author $cpanid,
274             'allow-empty' => 1,
275             });
276 7         120174 eval { local $ignore_error = 1; $git->tag($version) };
  7         78  
  7         303  
277 7 50       70083 warn $@ if $@;
278             }
279              
280 2         2815 return 0;
281             }
282              
283             1;
284              
285             __END__