File Coverage

blib/lib/App/DualLivedDiff.pm
Criterion Covered Total %
statement 30 264 11.3
branch 0 126 0.0
condition 0 18 0.0
subroutine 10 25 40.0
pod 0 13 0.0
total 40 446 8.9


line stmt bran cond sub pod time code
1             package App::DualLivedDiff;
2 1     1   27145 use strict;
  1         3  
  1         44  
3 1     1   5 use warnings;
  1         2  
  1         43  
4              
5             our $VERSION = '1.07';
6              
7 1     1   1312 use Getopt::Long;
  1         21506  
  1         6  
8 1     1   16194 use Parse::CPAN::Meta ();
  1         1321  
  1         26  
9 1     1   876 use LWP::Simple;
  1         116728  
  1         10  
10 1     1   1958 use File::Temp ();
  1         26249  
  1         24  
11 1     1   10 use File::Spec;
  1         2  
  1         20  
12 1     1   1150 use Archive::Extract;
  1         238479  
  1         44  
13 1     1   9 use File::Find ();
  1         2  
  1         14  
14 1     1   4913 use CPAN ();
  1         283950  
  1         4526  
15              
16             our $diff_cmd = 'diff';
17             our @exclude_files = (
18             qr(\.{1,2}$),
19             qr(\.svn$),
20             qr(\.git$),
21             );
22              
23             sub usage {
24 0 0   0 0   print "@_\n\n" if @_;
25 0           print <
26             Usage: $0 -d source-dist -b /path/to/blead/checkout
27             Does a diff FROM a dual lived module distribution TO blead perl
28              
29             -b/--blead blead perl path
30             -d/--dual dual lived module distribution path, archive, URL,
31             or module- or distribution name (default: .)
32             -r/--reverse reverses the diff (blead to lib)
33             -c/--config name of the configuration file with file mappings
34             (defaults to .dualLivedDiffConfig in the module path or current path)
35             -o/--output file name for output (defaults to STDOUT)
36             useful to separate diff from CPAN.pm output
37             -p/--paths show the "dual"-lived module paths or the "blead" paths?
38             defaults to "blead" or to "dual" if in --reverse mode
39             -w/--ignore-all-space (same as for normal gnu diff)
40              
41             Check perldoc "App::DualLivedDiff" for more info on the usage.
42             The "diff" command is assumed to be in your PATH and will be run with the
43             -u and -N options by default.
44             HERE
45 0           exit(1);
46             }
47              
48             my (
49             $bleadpath, $dualmodule, $reverse,
50             $default_config_file, $config_file,
51             $output_file, $paths, $ignore_space
52             );
53              
54             sub run {
55 0     0 0   $bleadpath = undef;
56 0           $dualmodule = '.';
57 0           $reverse = 0;
58 0           $default_config_file = '.dualLivedDiffConfig';
59 0           $config_file = $default_config_file;
60 0           $output_file = undef;
61 0           $paths = undef;
62 0           $ignore_space = undef;
63 0           GetOptions(
64             'b|blead=s' => \$bleadpath,
65             'h|help' => \&usage,
66             'r|reverse' => \$reverse,
67             'd|dual=s' => \$dualmodule,
68             'c|conf|config|configfile=s' => \$config_file,
69             'o|out|output=s' => \$output_file,
70             'p|path|paths=s' => \$paths,
71             'w|ignore-all-space' => \$ignore_space,
72             );
73              
74 0 0         if (defined $output_file) {
75 0 0         open my $fh, '>', $output_file or die "Could not open file '$output_file' for writing: $!";
76 0           $output_file = $fh;
77             }
78              
79 0 0 0       usage() if not defined $bleadpath or not -d $bleadpath;
80              
81 0 0         if (not defined $paths) {
82 0 0         $paths = $reverse ? "blead" : "dual";
83             }
84             else {
85 0 0         if ($paths =~ /^blead$/i) {
    0          
86 0           $paths = 'blead';
87             } elsif ($paths =~ /^dual$/i) {
88 0           $paths = 'dual';
89             } else {
90 0           die "Invalid path setting: --paths must be either 'dual' or 'blead'!\n";
91             }
92             }
93              
94 0           my $workdir = get_dual_lived_distribution_dir($dualmodule);
95 0           my $config = get_config($workdir, $config_file);
96              
97 0   0       my $files = $config->{"files"} || {};
98 0 0         my $exclude_regexes = [ map {qr/$_/} @{$config->{"exclude-regexes"} || []} ];
  0            
  0            
99 0   0       my $dirs_flat = $config->{"dirs-flat"} || {};
100 0   0       my $dirs_recursive = $config->{"dirs-recursive"} || {};
101            
102 0           my $blead_module_base_path = $config->{"base-path-in-blead"};
103              
104 0           my $pathspec = {
105             blead_path => $bleadpath,
106             source_path => $workdir,
107             blead_module_path => $blead_module_base_path,
108             };
109              
110 0           foreach my $source_file (keys %$files) {
111             # commented out since explicitly mapped files trump exclusion
112             #if (grep {$source_file =~ $_} @$exclude_regexes) { next; }
113 0           my $blead_file = $files->{$source_file};
114 0           $pathspec->{blead_file} = $blead_file;
115 0           $pathspec->{source_file} = $source_file;
116              
117 0           my $absolute_source_file = File::Spec->catdir($workdir, $source_file);
118              
119 0 0         if (-f $absolute_source_file) {
    0          
120 0           file_diff( $output_file, $pathspec, $paths );
121             }
122             elsif (-d $absolute_source_file) {
123 0           warn "'$absolute_source_file' is not a file but a directory. Use the 'dirs-flat' or 'dirs-recursive' config options instead!";
124 0           next;
125             }
126             else {
127 0           warn "Explicitly mapped file '$source_file' missing from dual lived module source tree!";
128 0           next;
129             }
130             }
131              
132 0           foreach my $source_dir (keys %$dirs_flat) {
133 0 0         if (grep {$source_dir =~ $_} @$exclude_regexes) {
  0            
134 0           warn "Explicitly mapped directory '$source_dir' is also excluded explicitly. Skipping it.";
135 0           next;
136             }
137 0           my $blead_dir = $dirs_flat->{$source_dir};
138 0           $pathspec->{blead_file} = $blead_dir;
139 0           $pathspec->{source_file} = $source_dir;
140              
141 0           my $absolute_source_dir = File::Spec->catdir($workdir, $source_dir);
142 0 0         if (-f $absolute_source_dir) {
    0          
143 0           warn "'$absolute_source_dir' is not a directory but a file. Use the 'files' config option instead!";
144 0           next;
145             }
146             elsif (-d $absolute_source_dir) {
147 0           dir_diff( $output_file, $pathspec, $paths, 0, $exclude_regexes );
148             }
149             else {
150 0           warn "Explicitly mapped directory '$source_dir' missing from dual lived module source tree!";
151 0           next;
152             }
153             }
154              
155 0           foreach my $source_dir (keys %$dirs_recursive) {
156 0 0         if (grep {$source_dir =~ $_} @$exclude_regexes) {
  0            
157 0           warn "Explicitly mapped directory '$source_dir' is also excluded explicitly. Skipping it.";
158 0           next;
159             }
160 0           my $blead_dir = $dirs_recursive->{$source_dir};
161 0           $pathspec->{blead_file} = $blead_dir;
162 0           $pathspec->{source_file} = $source_dir;
163              
164 0           my $absolute_source_dir = File::Spec->catdir($workdir, $source_dir);
165 0 0         if (-f $absolute_source_dir) {
    0          
166 0           warn "'$absolute_source_dir' is not a directory but a file. Use the 'files' config option instead!";
167 0           next;
168             }
169             elsif (-d $absolute_source_dir) {
170 0           dir_diff( $output_file, $pathspec, $paths, 1, $exclude_regexes );
171             }
172             else {
173 0           warn "Explicitly mapped directory '$source_dir' missing from dual lived module source tree!";
174 0           next;
175             }
176             }
177             }
178              
179             # given a source specification, get the path to an extracted distribution
180             sub get_dual_lived_distribution_dir {
181 0     0 0   my $source = shift;
182 0 0         usage("Bad source of the dual lived module distribution '$source'")
183             if not defined $source;
184            
185 0           my $distfile;
186 0 0         if (-d $source) {
    0          
    0          
    0          
187             # already extracted or checkout
188 0           return $source;
189             }
190             elsif (-f $source) {
191             # distribution file
192 0           $distfile = $source;
193             }
194             elsif ($source =~ m{^(?:ftp|https?)://}) {
195 0           $distfile = download_distribution($source);
196             }
197             elsif ($source =~ m{^[^:/]+://}) {
198 0           die "Support for VCS checkout and fancy protocols not implemented";
199             }
200             else {
201             # fallback, treat as module or distribution
202 0           my $url = module_or_dist_to_url($source);
203 0 0         die "Could not find CPAN module of that name ($source)" if not defined $url;
204 0           $distfile = download_distribution($url);
205             }
206              
207             # extract distribution
208 0           my $tmpdir = File::Temp::tempdir( CLEANUP => 1 );
209 0           my $ae = Archive::Extract->new( archive => $distfile );
210 0 0         $ae->extract( to => $tmpdir )
211             or die "Failed to extract distribution '$distfile' to temp. dir: " . $ae->error();
212              
213             # find the extracted distribution dir
214 0 0         opendir my $dh, $tmpdir
215             or die "Could not opendir '$tmpdir': $!";
216 0           my @stuff = readdir($dh);
217 0           my @files = grep {-f File::Spec->catfile($tmpdir, $_)} @stuff;
  0            
218 0 0         my @dirs = grep {!/^\.\.?$/ and -d File::Spec->catdir($tmpdir, $_)} @stuff;
  0            
219 0           closedir $dh;
220              
221 0 0 0       if (@files or @dirs != 1) {
222 0           die "Failed to find extracted distribution directory in '$tmpdir'. Found ".scalar(@files)." files and ".scalar(@dirs)." dirs";
223             }
224              
225 0           return File::Spec->catdir($tmpdir, shift(@dirs));
226             }
227              
228             sub download_distribution {
229 0     0 0   my $url = shift;
230 0           my $disttmpdir = File::Temp::tempdir( CLEANUP => 1 );
231 0 0         $url =~ m{/([^/]+)$} or die;
232 0           my $file = File::Spec->catfile($disttmpdir, $1);
233 0 0         if (is_success(getstore( $url, $file ))) {
234 0           return $file;
235             }
236             else {
237 0           die "Could not fetch '$url'";
238             }
239             }
240              
241             # find and load the configuration file
242             sub get_config {
243 0     0 0   my $source_dir = shift;
244 0           my $config_file = shift;
245 0           my $yaml;
246 0 0         if (-f $config_file) {
    0          
    0          
247 0           $yaml = Parse::CPAN::Meta::LoadFile($config_file);
248             }
249             elsif ( -f File::Spec->catfile($source_dir, $config_file) ) {
250 0           $yaml = Parse::CPAN::Meta::LoadFile(
251             File::Spec->catfile($source_dir, $config_file)
252             );
253             }
254             elsif ( -f File::Spec->catfile($source_dir, $default_config_file) ) {
255 0           $yaml = Parse::CPAN::Meta::LoadFile(
256             File::Spec->catfile($source_dir, $default_config_file)
257             );
258             }
259             else {
260 0           die "Could not find nor load configuration file";
261             }
262              
263 0 0         $yaml = $yaml->[0] if ref($yaml) eq 'ARRAY';
264              
265 0           return $yaml;
266             }
267              
268             # given the two base dirs and two relative paths, transform a
269             # directory mapping into file mappings recursively
270             sub dirs_to_filemapping {
271 0     0 0   my $pathspec = shift;
272 0           my $recursive = shift;
273            
274 0           my $full_source_dir = File::Spec->catdir($pathspec->{source_path}, $pathspec->{source_file});
275 0           my $full_blead_dir = get_full_blead_path($pathspec, $pathspec->{blead_file});
276              
277 0 0         if (not -d $full_blead_dir) {
278 0           warn "Specified directory '$pathspec->{blead_file}' could not be found in blead perl source tree (as $full_blead_dir)!";
279 0           return();
280             }
281 0 0         if (not -d $full_source_dir) {
282 0           warn "Specified directory '$pathspec->{source_file}' could not be found in dual lived module source tree (as $full_source_dir)!";
283 0           return();
284             }
285              
286 0 0         my @source_files = $recursive ? recur_get_all_files($full_source_dir) : get_all_files($full_source_dir);
287 0 0         if (!@source_files) {
288 0           warn "Specified source directory '$pathspec->{source_file}' does not contain any files!";
289 0           return({});
290             }
291              
292 0           my $mapping = {};
293             $mapping->{File::Spec->catfile($pathspec->{source_file}, $_)} = File::Spec->catfile($pathspec->{blead_file}, $_)
294 0           for @source_files;
295              
296 0           return $mapping;
297             }
298              
299             # get all files in a path with relative paths
300             sub recur_get_all_files {
301 0     0 0   my $path = shift;
302              
303 0           my @files;
304 0 0         return() if not -d $path;
305            
306             File::Find::find(
307             {
308             preprocess => sub {
309 0     0     my @return;
310 0           FILE: foreach my $file (@_) {
311 0           foreach my $exclude_regex (@exclude_files) {
312 0 0         next FILE if $file =~ $exclude_regex;
313             }
314 0           push @return, $file;
315             }
316 0           return(@return);
317             },
318             wanted => sub {
319 0     0     foreach my $exclude_regex (@exclude_files) {
320 0 0         return if $_ =~ $exclude_regex;
321             }
322 0 0         return unless -f $_;
323 0           s{^\Q$path\E[\\/]*}{};
324 0           push @files, $_;
325             },
326 0           no_chdir => 1,
327             },
328             $path
329             );
330 0           return(@files);
331             }
332              
333             # get all files in a path with relative paths
334             sub get_all_files {
335 0     0 0   my $path = shift;
336              
337 0 0         return() if not -d $path;
338            
339 0 0         opendir my $dh, $path or die "Could not open path '$path': $!";
340 0           my @files = readdir($dh);
341 0           closedir $dh;
342              
343 0           my @use_files;
344 0           FILE: foreach my $file (@files) {
345 0           foreach my $exclude_regex (@exclude_files) {
346 0 0         next FILE if $file =~ $exclude_regex;
347             }
348 0 0         push @use_files, $file if -f File::Spec->catfile($path, $file);
349             }
350              
351 0           return(@use_files);
352             }
353              
354             # produce the diff of a full directory
355             sub dir_diff {
356 0     0 0   my $output_file = shift;
357 0           my $pathspec = shift;
358 0           my $paths = shift;
359 0           my $recursive = shift;
360 0           my $exclude_regexes = shift;
361              
362 0           my $map = dirs_to_filemapping( $pathspec, $recursive );
363              
364 0           foreach my $source_file (keys %$map) {
365 0 0         next if grep {$source_file =~ $_} @$exclude_regexes;
  0            
366 0           my $pathspec = {%$pathspec};
367 0           $pathspec->{source_file} = $source_file;
368 0           $pathspec->{blead_file} = $map->{$source_file};
369 0           file_diff( $output_file, $pathspec, $paths );
370             }
371             }
372              
373             # produce the diff of a single file
374             sub file_diff {
375 0     0 0   my $output_file = shift;
376 0           my $pathspec = shift;
377 0           my $paths = shift;
378              
379 0           my $absolute_source_file = File::Spec->catfile($pathspec->{source_path}, $pathspec->{source_file});
380 0           my $absolute_blead_file = get_full_blead_path( $pathspec, $pathspec->{blead_file} );
381             #warn "Diffing '$absolute_source_file' to '$absolute_blead_file'";
382              
383 0 0         my @cmd = ($diff_cmd, ($ignore_space ? ('-w') : ()), qw(-u -N));
384 0 0         if ($reverse) {
385 0           push @cmd, $absolute_blead_file, $absolute_source_file;
386             }
387             else {
388 0           push @cmd, $absolute_source_file, $absolute_blead_file;
389             }
390 0           my $result = `@cmd`;
391 0 0         my $blead_prefix = quotemeta($reverse ? '---' : '+++');
392 0 0         my $source_prefix = quotemeta($reverse ? '+++' : '---');
393              
394 0           my $patched_filename;
395 0 0         my $bleadpath_patched_filename =
396             defined($pathspec->{blead_module_path})
397             ? File::Spec->catfile( $pathspec->{blead_module_path}, $pathspec->{blead_file} )
398             : $pathspec->{blead_file};
399              
400 0 0         if ($paths eq 'dual') {
    0          
401 0           $patched_filename = $pathspec->{source_file};
402             } elsif ($paths eq 'blead') {
403 0           $patched_filename = $bleadpath_patched_filename;
404             } else {
405 0 0         $patched_filename = $reverse ? $bleadpath_patched_filename : $pathspec->{source_file};
406             }
407             #my $patched_filename = $reverse ? $source_file : $blead_file;
408              
409             #$result =~ s{^($blead_prefix\s*)(\S+)}{$1 . remove_path_prefix($2, $blead_base_dir)}gme;
410             #$result =~ s{^($source_prefix\s*)(\S+)}{$1 . remove_path_prefix($2, $source_base_dir)}gme;
411            
412 0           $result =~ s{^($blead_prefix\s+)(\S+)}{$1 . $patched_filename}gme;
  0            
413 0           $result =~ s{^($source_prefix\s+)(\S+)}{$1 . $patched_filename}gme;
  0            
414              
415 0 0         if (defined $output_file) {
416 0           print $output_file $result;
417             }
418             else {
419 0           print $result;
420             }
421             }
422              
423             # remove a prefix from a path
424             sub remove_path_prefix {
425 0     0 0   my $path = shift;
426 0           my $prefix = shift;
427 0           $path =~ s/^\Q$prefix\E//;
428 0           $path =~ s/^[\/\\]+//;
429 0           return $path;
430             }
431              
432             # turn something that may look like a module or
433             # distribution into an URL using CPAN
434             sub module_or_dist_to_url {
435 0     0 0   my $module_name = shift;
436             #my $use_dev_versions = shift;
437              
438 0           my $distro;
439 0 0         if ($module_name =~ /[\/.]/) {
440 0           my $dist = CPAN::Shell->expandany($module_name);
441 0 0         if (not defined $dist) {
442 0           warn "Could not find distribution '$module_name' on CPAN\n";
443 0           return();
444             }
445 0 0         $dist = $dist->distribution() if ref($dist) eq 'CPAN::Module';
446 0 0         if (not ref($dist) eq 'CPAN::Distribution') {
447 0           warn "Could not find distribution '$module_name' on CPAN\n";
448 0           return();
449             }
450 0           $distro = $dist->pretty_id();
451 0           warn "Assuming you specified a distribution name. Found the '$distro' distribution on CPAN\n";
452             }
453             else {
454 0           my $module = CPAN::Shell->expand("Module", $module_name);
455 0 0         if (not defined $module) {
456 0           warn "Could not find module '$module_name' on CPAN\n";
457 0           return();
458             }
459 0           $distro = $module->distribution()->pretty_id();
460 0           warn "Assuming you specified a module name. Found the '$distro' distribution on CPAN\n";
461             }
462              
463 0 0         $distro =~ /^([^\/]+)/ or die;
464 0           $distro = substr($1, 0, 1) . "/" . substr($1, 0, 2) . "/" . $distro;
465              
466 0           my $mirrors = $CPAN::Config->{urllist};
467 0 0 0       if (not defined $mirrors or not ref($mirrors) eq 'ARRAY' or not @$mirrors) {
      0        
468 0           warn "Could not determine CPAN mirror";
469 0           return();
470             }
471              
472 0           my $url = $mirrors->[0];
473 0           $url =~ s/\/+$//;
474 0           return $url . '/authors/id/' . $distro;
475             }
476              
477             sub get_full_blead_path {
478 0     0 0   my $pathspec = shift;
479 0           my $path = shift;
480 0 0         if (defined $pathspec->{blead_module_path}) {
481 0           return File::Spec->catdir($pathspec->{blead_path}, $pathspec->{blead_module_path}, $path);
482             }
483             else {
484 0           return File::Spec->catdir($pathspec->{blead_path}, $path);
485             }
486             }
487              
488             1;
489             __END__