File Coverage

lib/App/GitFind.pm
Criterion Covered Total %
statement 56 114 49.1
branch 5 36 13.8
condition 5 23 21.7
subroutine 17 23 73.9
pod 2 2 100.0
total 85 198 42.9


line stmt bran cond sub pod time code
1             package App::GitFind;
2              
3 2     2   1940 use 5.010;
  2         6  
4 2     2   8 use strict;
  2         2  
  2         33  
5 2     2   7 use warnings;
  2         2  
  2         80  
6              
7 2     2   346 use App::GitFind::Base;
  2         4  
  2         221  
8              
9 2     2   11 use parent 'App::GitFind::Class';
  2         3  
  2         8  
10 2     2   199 use Class::Tiny _qwc <<'EOT';
  2         4  
  2         6  
11             argv # the args we parse
12             _expr # the expression (AST from parsing)
13             _revs # the revs in the search scope, or ']]' for filesystem
14             _repo # The Git::Raw::Repository of the superproject we are in
15             _repotop # The working directory of _repo. Path::Class::Dir.
16             _searchbase # Path::Class::Dir with respect to which results
17             # are reported.
18             _searcher # App::GitFind::Searcher subclass that finds entries
19             _scan_submodules # If truthy, scan submodules.
20             EOT
21              
22 2     2   2360 use App::GitFind::Actions;
  2         7  
  2         11  
23 2     2   2565 use App::GitFind::cmdline;
  2         4  
  2         48  
24 2     2   394 use App::GitFind::FileProcessor;
  2         11  
  2         54  
25 2     2   1306 use Getopt::Long 2.34 ();
  2         34496  
  2         146  
26 2     2   359 use Git::Raw;
  2         18107  
  2         44  
27 2     2   476 use IO::Handle;
  2         4995  
  2         92  
28 2     2   1040 use Iterator::Simple qw(ichain iflatten igrep imap iter iterator);
  2         5108  
  2         184  
29             #use Path::Class; # TODO see if we can do without Path::Class for speed
30 2     2   530 use App::GitFind::PathClassMicro;
  2         4  
  2         2072  
31              
32             our $VERSION = '0.000002';
33              
34             # === Documentation === {{{1
35              
36             =head1 NAME
37              
38             App::GitFind - Find files anywhere in a Git repository
39              
40             =head1 SYNOPSIS
41              
42             This is the implementation of the L<git-find> command (q.v.). To use it
43             from Perl code:
44              
45             use App::GitFind;
46             exit App::GitFind->new(\@ARGV)->run;
47              
48             =head1 SUBROUTINES/METHODS
49              
50             =cut
51              
52             # }}}1
53              
54             # Later items:
55             # TODO add an option to report absolute paths instead of relative
56             # TODO skip .git and .gitignored files unless -u
57             # TODO optimization: if possible, add a filter function
58             # (e.g., for a top-level -type filter)
59              
60             =head2 BUILD
61              
62             Process the arguments. Usage:
63              
64             my $gf = App::GitFind->new(-argv => \@ARGV, -searchbase => Cwd::getcwd);
65              
66             May modify the provided array. May C<exit()>, e.g., on C<--help>.
67              
68             =cut
69              
70             sub BUILD {
71 1     1 1 25 my ($self, $hrArgs) = @_;
72 1 50       4 croak "Need a -argv arrayref" unless ref $hrArgs->{argv} eq 'ARRAY';
73 1 50       4 croak "Need a -searchbase" unless defined $hrArgs->{searchbase};
74              
75 1         3 my $details = _process_options($hrArgs->{argv});
76              
77             # Handle default -print
78 0 0       0 if(!$details->{expr}) { # Default: -print
    0          
79 0         0 $details->{expr} = App::GitFind::Actions::argdetails('print');
80              
81             } elsif(!$details->{saw_nonprune_action}) { # With an expr: -print unless an action
82             # other than -prune was given
83             $details->{expr} = +{
84 0         0 AND => [ $details->{expr}, App::GitFind::Actions::argdetails('print') ]
85             };
86             }
87              
88             # Add default for missing revs
89 0 0       0 unless($details->{revs}) {
90 0         0 $details->{revs} = [undef];
91 0   0     0 $details->{saw_non_rr} ||= true;
92             }
93 0 0       0 if(@{$details->{revs}} > 1) {
  0         0  
94 0         0 require List::SomeUtils;
95 0         0 $details->{revs} = [List::SomeUtils::uniq(@{$details->{revs}})];
  0         0  
96             }
97              
98 0     0   0 vlog { "Options:", ddc $details } 2;
  0         0  
99              
100             # Check the scope. TODO permit searching both ]] and non-]] in one run
101 0 0 0     0 if($details->{saw_rr} && $details->{saw_non_rr}) {
102 0         0 die "I don't know how to search both ']]' and a Git rev at once."
103             }
104              
105             # Copy information into our instance fields
106 0         0 $self->_expr($details->{expr});
107 0         0 $self->_revs($details->{revs});
108             $self->_searchbase(
109             App::GitFind::PathClassMicro::Dir->new($hrArgs->{searchbase})
110 0         0 );
111              
112 0         0 $self->_find_repo;
113              
114             } #BUILD()
115              
116             # Initialize _repo and _repotop. Dies on error.
117             sub _find_repo {
118 0     0   0 my $self = shift;
119             # Find the repo we're in. If we're in a submodule, that will be the
120             # repo of that submodule.
121 0         0 my $repo = eval { Git::Raw::Repository->discover('.'); };
  0         0  
122 0 0       0 die "Not in a Git repository: $@\n" if $@;
123 0         0 $self->_repo($repo);
124              
125 0         0 $self->_repotop( App::GitFind::PathClassMicro::Dir->new($self->_repo->workdir) );
126             # $repo->path is .git/
127             vlog {
128 0     0   0 "Repository:", $self->_repo->path,
129             "\nWorking dir:", $self->_repotop,
130             "\nSearch base:", $self->_searchbase,
131 0         0 };
132              
133             # Are we in a submodule? If so, move outward to the parent
134 0 0       0 if($self->_repo->path =~ qr{^(.+?[\\\/]\.git)[\\\/]modules[\\\/]}) {
135 0         0 my $parent_path = $1;
136 0         0 $repo = eval { Git::Raw::Repository->open($parent_path); };
  0         0  
137 0 0       0 die "Could not open parent Git repository in $parent_path: $@\n" if $@;
138 0         0 $self->_repo($repo);
139 0     0   0 vlog { 'Moved to outer repo', $parent_path };
  0         0  
140             }
141             } #_find_repo()
142              
143             =head2 run
144              
145             Does the work. Call as C<< exit($obj->run()) >>. Returns a shell exit code.
146              
147             =cut
148              
149             sub run {
150 0     0 1 0 my $self = shift;
151 0         0 my $runner = App::GitFind::FileProcessor->new(-expr => $self->_expr);
152 0         0 my $callback = $runner->callback($VERBOSE>=3);
153              
154 0 0       0 if($VERBOSE) {
155 0         0 STDOUT->autoflush(true);
156 0         0 STDERR->autoflush(true);
157             }
158              
159 0         0 for my $rev (@{$self->_revs}) {
  0         0  
160 0         0 my $searcher = $self->_make_searcher($rev, $self->_repo);
161             # TODO? deduplicate?
162 0         0 $searcher->run($callback);
163             # TODO? early stop?
164             }
165              
166 0         0 return 0; # TODO? return 1 if any -exec failed?
167             } #run()
168              
169             =head1 INTERNALS
170              
171             =head2 _make_searcher
172              
173             Create an iterator for the entries to be processed. Returns an
174             L<App::GitFind::Searcher>. Usage:
175              
176             my $searcher = $self->_make_searcher('rev', -in => $repo);
177              
178             =cut
179              
180             sub _make_searcher {
181 0     0   0 my ($self, %args) = getparameters('self', [qw(rev in)], @_);
182 0         0 my $rev = $args{rev};
183 0         0 my $repo = $args{in};
184              
185             # TODO find files in scope $self->_revs, repo $repo
186              
187 0 0       0 if(!defined $rev) { # The index of the current repo
    0          
188 0         0 require App::GitFind::Searcher::Git;
189 0         0 return App::GitFind::Searcher::Git->new(
190             -repo => $repo,
191             -searchbase=>$self->_searchbase
192             );
193              
194             } elsif($rev eq ']]') { # The current working directory
195 0         0 require App::GitFind::Searcher::FileSystem;
196 0         0 return App::GitFind::Searcher::FileSystem->new(
197             -repo => $repo,
198             -searchbase=>$self->_searchbase
199             );
200              
201             } else {
202 0         0 die "I don't yet know how to search through rev $_";
203             }
204              
205             } #_make_searcher
206              
207             =head2 _process_options
208              
209             Process the options and return a hashref. Any remaining arguments are
210             stored under key C<_>.
211              
212             =cut
213              
214             sub _process_options {
215 1   50 1   3 my $lrArgv = shift // [];
216 1         2 my $hrOpts;
217 1   33 2   4 local *have = sub { $hrOpts->{switches}->{$_[0] // $_} };
  2         10  
218              
219 1 50       3 $hrOpts = App::GitFind::cmdline::Parse($lrArgv)
220             or die 'Could not parse options successfully';
221              
222 1   50     2 $VERBOSE = scalar @{$hrOpts->{switches}->{v} // []};
  1         7  
223 1   50     2 $QUIET = scalar @{$hrOpts->{switches}->{q} // []};
  1         6  
224              
225 1 50       3 Getopt::Long::HelpMessage(-exitval => 0, -verbose => 2) if have('man');
226 1 50 33     3 Getopt::Long::HelpMessage(-exitval => 0, -verbose => 1)
227             if have('h') || have('help');
228 0 0 0       Getopt::Long::HelpMessage(-exitval => 0) if have('?') || have('usage');
229 0 0 0       Getopt::Long::VersionMessage(-exitval => 0) if have('V')||have('version');
230              
231 0           return $hrOpts;
232             } #_process_options
233              
234             1; # End of App::GitFind
235             __END__
236              
237             # === Rest of the docs === {{{1
238              
239             =head1 AUTHOR
240              
241             Christopher White, C<< <cxw at cpan.org> >>
242              
243             =head1 LICENSE AND COPYRIGHT
244              
245             Copyright 2019 Christopher White.
246             Portions copyright 2019 D3 Engineering, LLC.
247              
248             This program is distributed under the MIT (X11) License:
249             L<http://www.opensource.org/licenses/mit-license.php>
250              
251             Permission is hereby granted, free of charge, to any person
252             obtaining a copy of this software and associated documentation
253             files (the "Software"), to deal in the Software without
254             restriction, including without limitation the rights to use,
255             copy, modify, merge, publish, distribute, sublicense, and/or sell
256             copies of the Software, and to permit persons to whom the
257             Software is furnished to do so, subject to the following
258             conditions:
259              
260             The above copyright notice and this permission notice shall be
261             included in all copies or substantial portions of the Software.
262              
263             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
264             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
265             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
266             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
267             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
268             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
269             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
270             OTHER DEALINGS IN THE SOFTWARE.
271              
272             =cut
273              
274             # }}}1
275             # vi: set fdm=marker fdl=0: #