File Coverage

blib/lib/GitHub/Extract.pm
Criterion Covered Total %
statement 67 88 76.1
branch 9 26 34.6
condition 0 6 0.0
subroutine 16 19 84.2
pod 6 6 100.0
total 98 145 67.5


line stmt bran cond sub pod time code
1             package GitHub::Extract;
2              
3             =pod
4              
5             =head1 NAME
6              
7             GitHub::Extract - Extract an exported copy of a GitHub project
8              
9             =head1 SYNOPSIS
10              
11             my $project = GitHub::Extract->new(
12             username => 'adamkennedy',
13             project => 'PPI',
14             );
15            
16             $project->extract( to => '/my/directory' );
17              
18             =head1 DESCRIPTION
19              
20             L<GitHub::Extract> is a simple light weight interface to
21             L<http://github.com/> for the sole purpose of retrieving and extracting
22             a "zipball" of a public (and likely open source) project.
23              
24             It makes use of the plain route used by the user interface "zip" button,
25             and as a result it avoids the need to use the full GitHub API and client.
26              
27             This module shares extends (and emulates where needed) the API of
28             L<Archive::Extract>. Any existing tooling code which uses L<Archive::Extract>
29             to work with release tarballs should be trivially upgradable to work with
30             projects directly from GitHub instead.
31              
32             =head1 METHODS
33              
34             =cut
35              
36 2     2   162204 use 5.008;
  2         41  
  2         97  
37 2     2   13 use strict;
  2         5  
  2         84  
38 2     2   13 use warnings;
  2         14  
  2         95  
39 2     2   12 use Carp ();
  2         3  
  2         43  
40 2     2   12 use File::Spec 3.30 ();
  2         54  
  2         53  
41 2     2   4132 use File::Temp 0.21 ();
  2         67646  
  2         114  
42 2     2   2319 use File::pushd 1.00 ();
  2         7746  
  2         4568  
43 2     2   42955 use HTTP::Tiny 0.019 ();
  2         98763  
  2         71  
44 2     2   2341 use Params::Util 1.00 ();
  2         7932  
  2         72  
45 2     2   2630 use Archive::Extract 0.56 ();
  2         392814  
  2         93  
46 2     2   722577 use IO::Socket::SSL 1.56 (); # Needed for HTTP::Tiny SSL
  2         542687  
  2         272  
47              
48             our $VERSION = '0.02';
49             our $WARN = 1;
50             our $DEBUG = 0;
51              
52 2         1429 use Object::Tiny 1.01 qw{
53             username
54             repository
55             branch
56             url
57             http
58             archive
59             archive_extract
60 2     2   3553 };
  2         958  
61              
62              
63              
64              
65              
66              
67             ######################################################################
68             # Constructor
69              
70             =pod
71              
72             =head2 new
73              
74             my $branch = GitHub::Extract->new(
75             username => 'adamkennedy',
76             repository => 'PPI',
77              
78             # Fetch a branch other than master
79             branch => 'mybranch',
80              
81             # A custom HTTP client can be provided to any constructor
82             http => HTTP::Tiny->new(
83             # Custom HTTP setup goes here
84             ),
85             );
86              
87             The C<new> constructor identifies a project to download (but does not take any
88             immediate action to do the download).
89              
90             It takes a number of simple parameters to control where to download from.
91              
92             =over 4
93              
94             =item username
95              
96             The GitHub username identifying the owner of the repository.
97              
98             =item repository
99              
100             The name of the repository within the account or organisation.
101              
102             =item branch
103              
104             An optional parameter identifying a particular branch to download. If not
105             specificied, the 'master' branch will be fetched.
106              
107             =item http
108              
109             L<GitHub::Extract> will create a L<HTTP::Tiny> object with default settings to
110             download the zipball from GitHub.
111              
112             This parameter allows you to use your own custom L<HTTP::Tiny> client with
113             alternative settings.
114              
115             =back
116              
117             Returns a new L<GitHub::Extract> object, or false on error.
118              
119             =head2 username
120              
121             The C<username> method returns the GitHub username for the request.
122              
123             =head2 repository
124              
125             The C<repository> method returns the GitHub repository name for the request.
126              
127             =head2 branch
128              
129             The C<branch> method returns the name of the branch to be fetched.
130              
131             =head2 url
132              
133             The C<url> method returns the full download URL used to fetch the zipball.
134              
135             =head2 http
136              
137             The C<http> method returns the HTTP client that will be used for the request.
138              
139             =head2 archive
140              
141             The C<archive> method will return the absolute path to the downloaded zip file
142             on disk, if the download was successful.
143              
144             Returns C<undef> if the download was not completed successfully.
145              
146             =head2 archive_extract
147              
148             The C<archive_extract> method will return the L<Archive::Extract> object used
149             to extract the files from the zipball, whether or not it extracted
150             successfully.
151              
152             Returns C<undef> if the download was not completed successfully.
153              
154             =cut
155              
156             sub new {
157 1     1 1 1010 my $self = shift->SUPER::new(@_);
158              
159             # Generate the URL from the pieces
160 1 50       52 unless ( $self->url ) {
161             # Apply defaults
162 1 50       40 unless ( $self->branch ) {
163 1         10 $self->{branch} = 'master';
164             }
165              
166             # Check params to make the url
167 1 50       29 my $username = $self->username or return;
168 1 50       38 my $repository = $self->repository or return;
169 1 50       32 my $branch = $self->branch or return;
170              
171 1         12 $self->{url} = "https://github.com/$username/$repository/zipball/$branch";
172             }
173              
174 1 50       28 unless ( Params::Util::_INSTANCE($self->http, 'HTTP::Tiny') ) {
175 1         23 $self->{http} = HTTP::Tiny->new;
176             }
177              
178 1         120 return $self;
179             }
180              
181              
182              
183              
184              
185             ######################################################################
186             # Main Methods
187              
188             =pod
189              
190             =head2 extract
191              
192             $project->extract( to => '/output/path' );
193              
194             Extracts the archive represented by the L<GitHub::Extract> object to
195             the path of your choice as specified by the C<to> argument. Defaults to
196             C<cwd()>.
197              
198             In the case that you did not specify a C<to> argument, the output
199             file will be the name of the archive file, stripped from its C<.gz>
200             suffix, in the current working directory.
201              
202             It will return true on success, and false on failure.
203              
204             =cut
205              
206             sub extract {
207 1     1 1 2778 my $self = shift;
208 1         3 my @to = @_;
209              
210             # Clear any previous errors
211 1         3 delete $self->{_error_msg};
212 1         3 delete $self->{_error_msg_long};
213              
214             # Download the code as a GitHub "zipball"
215 1         27 my $url = $self->url;
216 1         10 my $tempdir = File::Temp::tempdir( CLEANUP => 1 );
217 1         677 my $archive = File::Spec->catfile( $tempdir, "github-extract.zip" );
218 1         31 my $response = $self->http->mirror( $url, $archive );
219 1 50       1729857 unless ( $response->{success} ) {
220 0         0 return $self->_error("Failed to download $url");
221             }
222 1         5 $self->{archive} = $archive;
223              
224             # Hand off extraction to Archive::Extract
225 1         2 local $Archive::Extract::WARN = $WARN;
226 1         2 local $Archive::Extract::DEBUG = $DEBUG;
227 1         13 $self->{archive_extract} = Archive::Extract->new( archive => $archive );
228 1         448 return $self->{archive_extract}->extract(@to);
229             }
230              
231             =pod
232              
233             =head2 pushd
234              
235             my $guard = $project->pushd( to => '/output/path' );
236              
237             The C<pushd> method downloads and extracts the project from GitHub, and then
238             temporarily changes the current working directory into the extract path of the
239             project.
240              
241             Returns a L<File::pushd> guard object which will return the current working
242             directory to the original location when it is deleted, or false if the archive
243             was not extracted.
244              
245             =cut
246              
247             sub pushd {
248 0     0 1 0 my $self = shift;
249 0         0 my $result = $self->extract(@_);
250 0 0       0 return $result unless $result;
251 0         0 return File::pushd::pushd( $self->extract_path );
252             }
253            
254              
255              
256              
257              
258             ######################################################################
259             # Proxied Methods
260              
261             =pod
262              
263             =head2 extract_path
264              
265             # Prints '/output/path/myproject-0.01-af41bc'
266             if ( $project->extract( to => '/output/path' ) ) {
267             print $project->extract_path;
268             }
269              
270             The C<extract_path> method returns the absolute path of the logical root
271             directory of the zipball, once it has been extracted.
272              
273             Since some archives will contain a single root directory within the zip file
274             with which the content is placed (and some will not) this compensates for the
275             different, detecting the logical root automatically.
276              
277             See L<Archive::Extract/extract> for more details.
278              
279             =cut
280              
281             sub extract_path {
282 2     2 1 25487 my $self = shift;
283 2 50       76 my $extract = $self->archive_extract or return;
284              
285 2         81 return $self->archive_extract->extract_path;
286             }
287              
288             =pod
289              
290             =head2 files
291              
292             The C<files> method returns an array ref with the paths of all the files in the
293             archive, relative to the C<to> argument you specified.
294              
295             To get the full path to an extracted file, you would use:
296              
297             File::Spec->catfile( $to, $ae->files->[0] );
298              
299             See L<Archive::Extract/extract> for more details.
300              
301             =cut
302              
303             sub files {
304 1     1 1 1034 my $self = shift;
305 1 50       33 my $extract = $self->archive_extract or return;
306              
307 1         36 return $self->archive_extract->files;
308             }
309              
310             =pod
311              
312             =head2 error
313              
314             my $simple = $project->error;
315             my $verbose = $project->error(1);
316              
317             The C<error> method returns the last encountered error as string.
318              
319             Pass it a true value to get the detailed output instead, as produced by
320             L<Carp/longmess>.
321              
322             =cut
323              
324             sub error {
325 0     0 1   my $self = shift;
326              
327             # Hand off to the underlying extract object if we got that far
328 0 0         if ( $self->archive_extract ) {
329 0           return $self->archive_extract->error(@_);
330             }
331              
332             # Fall back to showing our own errors
333 0   0       my $aref = $self->{ $_[0] ? '_error_msg_long' : '_error_msg' } || [];
334 0           return join $/, @$aref;
335             }
336              
337             # Add an error system compatible with Archive::Extract
338             sub _error {
339 0     0     my $self = shift;
340 0           my $error = shift;
341 0           my $lerror = Carp::longmess($error);
342              
343 0   0       $self->{_error_msg} ||= [];
344 0   0       $self->{_error_msg_long} ||= [];
345              
346 0           push @{$self->{_error_msg}}, $error;
  0            
347 0           push @{$self->{_error_msg_long}}, $lerror;
  0            
348              
349             # Set $GitHub::Extract::WARN to 0 to disable printing of errors
350 0 0         Carp::carp( $DEBUG ? $lerror : $error ) if $WARN;
    0          
351              
352 0           return;
353             }
354              
355             1;
356              
357             =pod
358              
359             =head1 GLOBAL VARIABLES
360              
361             All global variables share the names and behaviour of the equivalent variables
362             in L<Archive::Extract>. Their value will be propogated down to the equivalent
363             variables in L<Archive::Extract> whenever it is being used.
364              
365             =head2 $GitHub::Extract::DEBUG
366              
367             Set this variable to C<true> to have all calls to command line tools
368             be printed out, including all their output.
369              
370             This also enables C<Carp::longmess> errors, instead of the regular
371             C<carp> errors.
372              
373             Good for tracking down why things don't work with your particular
374             setup.
375              
376             Defaults to C<false>.
377              
378             =head2 $GitHub::Extract::WARN
379              
380             This variable controls whether errors encountered internally by
381             C<GitHub::Extract> should be C<carp>'d or not.
382              
383             Set to false to silence warnings. Inspect the output of the C<error()>
384             method manually to see what went wrong.
385              
386             Defaults to C<true>.
387              
388             =head1 SUPPORT
389              
390             Bugs should be reported via the CPAN bug tracker at
391              
392             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=GitHub-Extract>
393              
394             For other issues, contact the author.
395              
396             =head1 AUTHOR
397              
398             Adam Kennedy E<lt>adamk@cpan.orgE<gt>
399              
400             =head1 SEE ALSO
401              
402             L<Archive::Extract>
403              
404             L<http://github.com/>
405              
406             =head1 COPYRIGHT
407              
408             Copyright 2012-2013 Adam Kennedy.
409              
410             This program is free software; you can redistribute
411             it and/or modify it under the same terms as Perl itself.
412              
413             The full text of the license can be found in the
414             LICENSE file included with this module.
415              
416             =cut