File Coverage

blib/lib/App/GitGot/Command.pm
Criterion Covered Total %
statement 210 275 76.3
branch 65 130 50.0
condition 13 26 50.0
subroutine 47 58 81.0
pod 7 8 87.5
total 342 497 68.8


line stmt bran cond sub pod time code
1             package App::GitGot::Command;
2             our $AUTHORITY = 'cpan:GENEHACK';
3             $App::GitGot::Command::VERSION = '1.337';
4             # ABSTRACT: Base class for App::GitGot commands
5 20     20   12850 use 5.014;
  20         78  
6              
7 20     19   186 use App::Cmd::Setup -command;
  19         63  
  19         125  
8 19     16   3095 use Cwd;
  16         43  
  16         968  
9 16     15   6612 use File::HomeDir::Tiny ();
  15         4272  
  15         367  
10 15     15   107 use List::Util qw/ max first /;
  15         37  
  15         1588  
11 15     15   108 use Path::Tiny;
  15         30  
  15         697  
12 15     15   98 use Try::Tiny;
  15         31  
  15         913  
13 15     15   8979 use Types::Standard -types;
  15         1123011  
  15         165  
14 15     15   72436 use YAML qw/ DumpFile LoadFile /;
  15         60  
  15         894  
15              
16 15     15   7170 use App::GitGot::Repo::Git;
  15         59  
  15         575  
17 15     15   8477 use App::GitGot::Repositories;
  15         69  
  15         1134  
18 15     15   120 use App::GitGot::Types -all;
  15         37  
  15         123  
19              
20 15     15   31634 use Moo;
  15         54  
  15         93  
21 15     15   7317 use MooX::HandlesVia;
  15         40  
  15         118  
22 15     15   2004 use namespace::autoclean;
  15         48  
  15         137  
23              
24             sub opt_spec {
25 42     42 1 352978 my( $class , $app ) = @_;
26              
27             return (
28 42         1750 [ 'all|a' => 'use all available repositories' ] ,
29             [ 'by_path|p' => 'if set, output will be sorted by repo path (default: sort by repo name)' ] ,
30             [ 'color_scheme|c=s' => 'name of color scheme to use' => { default => 'dark' } ] ,
31             [ 'configfile|f=s' => 'path to config file' => { default => path( File::HomeDir::Tiny::home() , '.gitgot') , required => 1 } ] ,
32             [ 'no_color|C' => 'do not use colored output' => { default => 0 } ] ,
33             [ 'quiet|q' => 'keep it down' ] ,
34             [ 'skip_tags|T=s@' => 'select repositories not tagged with these words' ] ,
35             [ 'tags|t=s@' => 'select repositories tagged with these words' ] ,
36             [ 'verbose|v' => 'bring th\' noise'] ,
37             $class->options($app)
38             );
39             }
40              
41       8 0   sub options {}
42              
43             has active_repo_list => (
44             is => 'lazy',
45             isa => ArrayRef[GotRepo] ,
46             handles_via => 'Array' ,
47             handles => {
48             active_repos => 'elements' ,
49             } ,
50             );
51              
52             sub _build_active_repo_list {
53 21     21   908 my ( $self ) = @_;
54              
55             return $self->full_repo_list
56 21 100 66     493 if $self->all or ! $self->tags and ! $self->skip_tags and ! @{ $self->args };
      100        
57              
58 4         47 my $list = _expand_arg_list( $self->args );
59              
60 4         13 my @repos;
61 4         109 REPO: foreach my $repo ( $self->all_repos ) {
62 12 100       610 if ( grep { $_ eq $repo->number or $_ eq $repo->name } @$list ) {
  12 100       139  
63 4         13 push @repos, $repo;
64 4         15 next REPO;
65             }
66              
67 8 50       144 if ( $self->skip_tags ) {
68 0         0 foreach my $tag ( @{ $self->skip_tags } ) {
  0         0  
69 0 0       0 next REPO if grep { $repo->tags =~ /\b$_\b/ } $tag;
  0         0  
70             }
71             }
72              
73 8 50       331 if ( $self->tags ) {
74 0         0 foreach my $tag ( @{ $self->tags } ) {
  0         0  
75 0 0       0 if ( grep { $repo->tags =~ /\b$_\b/ } $tag ) {
  0         0  
76 0         0 push @repos, $repo;
77 0         0 next REPO;
78             }
79             }
80             }
81 8 50 33     312 push @repos, $repo unless $self->tags or @$list;
82             }
83              
84 4         152 return \@repos;
85             }
86              
87             has args => (
88             is => 'rwp' ,
89             isa => ArrayRef ,
90             );
91              
92             has full_repo_list => (
93             is => 'lazy',
94             isa => ArrayRef[GotRepo] ,
95             writer => 'set_full_repo_list' ,
96             handles_via => 'Array' ,
97             handles => {
98             add_repo => 'push' ,
99             all_repos => 'elements' ,
100             } ,
101             );
102              
103             sub _build_full_repo_list {
104 37     37   1257 my $self = shift;
105              
106 37         967 my $config = _read_config( $self->configfile );
107              
108 37         126 my $repo_count = 1;
109              
110 37 50       1278 my $sort_key = $self->by_path ? 'path' : 'name';
111              
112 37         1375 my @parsed_config;
113              
114 37         284 foreach my $entry ( sort { $a->{$sort_key} cmp $b->{$sort_key} } @$config ) {
  90         304  
115              
116             # a completely empty entry is okay (this will happen when there's no
117             # config at all...)
118 92 100       5054 keys %$entry or next;
119              
120             ### FIXME unnecessarily git specific
121             push @parsed_config , App::GitGot::Repo::Git->new({
122             label => ( $self->by_path ) ? $entry->{path} : $entry->{name} ,
123 84 50       1770 entry => $entry ,
124             count => $repo_count++ ,
125             });
126             }
127              
128 37         3368 return \@parsed_config;
129             }
130              
131             has opt => (
132             is => 'rwp' ,
133             isa => Object ,
134             handles => [ qw/
135             all
136             by_path
137             color_scheme
138             configfile
139             no_color
140             quiet
141             skip_tags
142             tags
143             verbose
144             / ]
145             );
146              
147             has outputter => (
148             is => 'lazy' ,
149             isa => GotOutputter ,
150             handles => [
151             'error' ,
152             'warning' ,
153             'major_change' ,
154             'minor_change' ,
155             ] ,
156             );
157              
158             sub _build_outputter {
159 7     7   704 my $self = shift;
160              
161 7         205 my $scheme = $self->color_scheme;
162              
163 7 50       257 if ( $scheme =~ /^\+/ ) {
164 0         0 $scheme =~ s/^\+//;
165             }
166             else {
167 7         27 $scheme = "App::GitGot::Outputter::$scheme"
168             }
169              
170             try {
171 7     7   1235 eval "use $scheme";
172 7 50       63 die $@ if $@;
173             }
174             catch {
175 0     0   0 say "Failed to load color scheme '$scheme'.\nExitting now.\n";
176 0         0 exit(5);
177 7         130 };
178              
179 7         328 return $scheme->new({ no_color => $self->no_color });
180             }
181              
182             sub execute {
183 42     42 1 200754 my( $self , $opt , $args ) = @_;
184 42         1183 $self->_set_args( $args );
185 42         3248 $self->_set_opt( $opt );
186              
187             # set up colored output if we page thru less
188             # also exit pager immediately if <1 page of output
189 42         1759 $ENV{LESS} = 'RFX';
190              
191             # don't catch any errors here; if this fails we just output stuff like
192             # normal and nobody is the wiser.
193 42 100   15   330 eval 'use IO::Page' if $self->_use_io_page;
  15     6   7707  
  15     1   4237  
  15         368  
  6         82  
  6         21  
  6         113  
  1         8  
  1         2  
  1         18  
194              
195 42         313 $self->_execute($opt,$args);
196             }
197              
198              
199             sub local_repo {
200 12     12 1 28 my $self = shift;
201              
202 12         221 my $dir = $self->_find_repo_root( getcwd() );
203              
204 11     11   321 return first { $_->path eq $dir->absolute } $self->all_repos;
  11         752  
205             }
206              
207              
208             sub max_length_of_an_active_repo_label {
209 18     18 1 47 my( $self ) = @_;
210              
211 18 50       355 my $sort_key = $self->by_path ? 'path' : 'name';
212              
213 18         920 return max ( map { length $_->$sort_key } $self->active_repos);
  72         1699  
214             }
215              
216              
217             sub prompt_yn {
218 0     0 1 0 my( $self , $message ) = @_;
219 0         0 printf '%s [y/N]: ' , $message;
220 0         0 chomp( my $response = );
221 0         0 return lc($response) eq 'y';
222             }
223              
224              
225             sub search_repos {
226 0     0 1 0 my $self = shift;
227              
228 0         0 return App::GitGot::Repositories->new(
229             repos => [ $self->all_repos ]
230             );
231             }
232              
233              
234             sub write_config {
235 14     14 1 67 my ($self) = @_;
236              
237             DumpFile(
238             $self->configfile,
239             [
240 6         43 sort { $a->{name} cmp $b->{name} }
241 14         411 map { $_->in_writable_format } $self->all_repos
  19         1545  
242             ] ,
243             );
244             }
245              
246             sub _expand_arg_list {
247 4     4   12 my $args = shift;
248              
249             ## no critic
250              
251             return [
252             map {
253 4         14 s!/$!!;
  4         16  
254 4 50       18 if (/^(\d+)-(\d+)?$/) { ( $1 .. $2 ) }
  0         0  
255 4         21 else { ($_) }
256             } @$args
257             ];
258              
259             ## use critic
260             }
261              
262             sub _fetch {
263 1     1   250 my( $self , @repos ) = @_;
264              
265 1         9 my $max_len = $self->max_length_of_an_active_repo_label;
266              
267 1         4 REPO: for my $repo ( @repos ) {
268 4 100       118 next REPO unless $repo->repo;
269              
270 2         9 my $name = $repo->name;
271              
272 2         25 my $msg = sprintf "%3d) %-${max_len}s : ", $repo->number, $repo->label;
273              
274 2         6 my ( $status, $fxn );
275              
276 2         7 my $repo_type = $repo->type;
277              
278 2 50       7 if ( $repo_type eq 'git' ) { $fxn = '_git_fetch' }
  2         5  
279             ### FIXME elsif( $repo_type eq 'svn' ) { $fxn = 'svn_update' }
280 0         0 else { $status = $self->error("ERROR: repo type '$_' not supported") }
281              
282 2 50       14 $status = $self->$fxn($repo) if ($fxn);
283              
284 2 50 33     55 next REPO if $self->quiet and !$status;
285              
286 2         76 say "$msg$status";
287             }
288             }
289              
290             sub _find_repo_root {
291 12     12   42 my( $self , $path ) = @_;
292              
293 12         40 my $dir = path( $path );
294              
295             # find repo root
296 12 100       403 while ( ! grep { -d and $_->basename eq '.git' } $dir->children ) {
  71         5592  
297 3 100       141 die "$path doesn't seem to be in a git directory\n" if $dir eq $dir->parent;
298 2         187 $dir = $dir->parent;
299             }
300              
301 11         605 return $dir
302             }
303              
304             sub _git_clone_or_callback {
305 6 50   6   29 my( $self , $entry , $callback ) = @_
306             or die "Need entry and callback";
307              
308 6         35 my $msg = '';
309              
310 6         47 my $path = $entry->path;
311              
312 6 100       266 if ( !-d $path ) {
    50          
313 3         28 path($path)->mkpath;
314              
315             try {
316 3     3   270 $entry->clone( $entry->repo , './' );
317 3         76 $msg .= $self->major_change('Checked out');
318             }
319 3     0   1240 catch { $msg .= $self->error('ERROR') . "\n$_" };
  0         0  
320             }
321             elsif ( -d "$path/.git" ) {
322             try {
323 3     3   191 $msg .= $callback->($msg , $entry);
324             }
325 3     0   61 catch { $msg .= $self->error('ERROR') . "\n$_" };
  0         0  
326             }
327              
328 6         134 return $msg;
329              
330             }
331              
332             sub _git_fetch {
333 2 50   2   7 my ( $self, $entry ) = @_
334             or die "Need entry";
335              
336             $self->_git_clone_or_callback( $entry ,
337             sub {
338 1     1   3 my( $msg , $entry ) = @_;
339              
340 1         38 my @o = $entry->fetch;
341              
342             # "git fetch" doesn't output anything to STDOUT only STDERR
343 1         3 my @err = @{ $entry->_wrapper->ERR };
  1         39  
344              
345             # If something was updated then STDERR should contain something
346             # similar to:
347             #
348             # From git://example.com/link-to-repo
349             # SHA1___..SHA1___ master -> origin/master
350             #
351             # So search for /^From / in STDERR to see if anything was outputed
352 1 50       8 if ( grep { /^From / } @err ) {
  0 50       0  
353 0         0 $msg .= $self->major_change('Updated');
354 0 0       0 $msg .= "\n" . join("\n",@err) unless $self->quiet;
355             }
356             elsif ( scalar @err == 0) {
357             # No messages to STDERR means repo was already updated
358 1 50       26 $msg .= $self->minor_change('Up to date') unless $self->quiet;
359             }
360             else {
361             # Something else occured (possibly a warning)
362             # Print STDERR and move on
363 0         0 $msg .= $self->warning('Problem during fetch');
364 0 0       0 $msg .= "\n" . join("\n",@err) unless $self->quiet;
365             }
366              
367 1         4 return $msg;
368             }
369 2         32 );
370             }
371              
372             sub _git_status {
373 6 50   6   22 my ( $self, $entry ) = @_
374             or die "Need entry";
375              
376 6         25 my( $msg , $verbose_msg ) = $self->_run_git_status( $entry );
377              
378 6 50       34 $msg .= $self->_run_git_cherry( $entry )
379             if $entry->current_remote_branch;
380 6 100 66     38 if ($self->opt->show_branch and defined $entry->current_branch) {
381 2         7 $msg .= '[' . $entry->current_branch . ']';
382             }
383              
384 6 100       138 return ( $self->verbose ) ? "$msg$verbose_msg" : $msg;
385             }
386              
387             sub _git_update {
388 2 50   2   10 my ( $self, $entry ) = @_
389             or die "Need entry";
390              
391             $self->_git_clone_or_callback( $entry ,
392             sub {
393 1     1   4 my( $msg , $entry ) = @_;
394              
395 1         37 my @o = $entry->pull;
396 1 50       7 if ( $o[0] =~ /^Already up.to.date\./ ) {
397 0 0       0 $msg .= $self->minor_change('Up to date') unless $self->quiet;
398             }
399             else {
400 1         51 $msg .= $self->major_change('Updated');
401 1 50       29 $msg .= "\n" . join("\n",@o) unless $self->quiet;
402             }
403              
404 1         47 return $msg;
405             }
406 2         21 );
407             }
408              
409             sub _path_is_managed {
410 0     0   0 my( $self , $path ) = @_;
411              
412 0 0       0 return unless $path;
413              
414 0         0 my $dir = $self->_find_repo_root( $path );
415 0         0 my $max_len = $self->max_length_of_an_active_repo_label;
416              
417 0         0 for my $repo ( $self->active_repos ) {
418 0 0       0 next unless $repo->path eq $dir->absolute;
419              
420 0 0 0     0 my $repo_remote = ( $repo->repo and -d $repo->path ) ? $repo->repo
    0          
    0          
421             : ( $repo->repo ) ? $repo->repo . ' (Not checked out)'
422             : ( -d $repo->path ) ? 'NO REMOTE'
423             : 'ERROR: No remote and no repo?!';
424              
425 0         0 printf "%3d) ", $repo->number;
426              
427 0 0       0 if ( $self->quiet ) { say $repo->label }
  0         0  
428             else {
429 0         0 printf "%-${max_len}s %-4s %s\n",
430             $repo->label, $repo->type, $repo_remote;
431 0 0       0 if ( $self->verbose ) {
432 0 0       0 printf " tags: %s\n" , $repo->tags if $repo->tags;
433             }
434             }
435              
436 0         0 return 1;
437             }
438              
439 0         0 say "repository not in Got list";
440 0         0 return;
441             }
442              
443             sub _read_config {
444 37     37   1527 my $file = shift;
445              
446 37         94 my $config;
447              
448 37 100       798 if ( -e $file ) {
449 29     29   1927 try { $config = LoadFile( $file ) }
450 29     0   671 catch { say "Failed to parse config..."; exit };
  0         0  
  0         0  
451             }
452              
453             # if the config is completely empty, bootstrap _something_
454 37   100     307947 return $config // [ {} ];
455             }
456              
457             sub _run_git_cherry {
458 0     0   0 my( $self , $entry ) = @_;
459              
460 0         0 my $msg = '';
461              
462             try {
463 0 0   0   0 if ( $entry->remote ) {
464 0         0 my $cherry = $entry->cherry;
465 0 0       0 if ( $cherry > 0 ) {
466 0         0 $msg = $self->major_change("Ahead by $cherry");
467             }
468             }
469             }
470 0     0   0 catch { $msg = $self->error('ERROR') . "\n$_" };
  0         0  
471              
472 0         0 return $msg
473             }
474              
475             sub _run_git_status {
476 6     6   11 my( $self , $entry ) = @_;
477              
478 6         34 my %types = (
479             indexed => 'Changes to be committed' ,
480             changed => 'Changed but not updated' ,
481             unknown => 'Untracked files' ,
482             conflict => 'Files with conflicts' ,
483             );
484              
485 6         17 my( $msg , $verbose_msg ) = ('','');
486              
487             try {
488 6     6   414 my $status = $entry->status;
489 6 50       26 if ( keys %$status ) {
490 0         0 $msg .= $self->warning('Dirty') . ' ';
491             } else {
492 6 50       126 $msg .= $self->minor_change('OK ') unless $self->quiet;
493             }
494              
495 6 100       116 if ( $self->verbose ) {
496 2         58 TYPE: for my $type ( keys %types ) {
497 8 50       18 my @states = $status->get( $type ) or next TYPE;
498 0         0 $verbose_msg .= "\n** $types{$type}:\n";
499 0         0 for ( @states ) {
500 0         0 $verbose_msg .= sprintf ' %-12s %s' , $_->mode , $_->from;
501 0 0       0 $verbose_msg .= sprintf ' -> %s' , $_->to if $_->mode eq 'renamed';
502 0         0 $verbose_msg .= "\n";
503             }
504             }
505 2 50       10 $verbose_msg = "\n$verbose_msg" if $verbose_msg;
506             }
507             }
508 6     0   67 catch { $msg .= $self->error('ERROR') . "\n$_" };
  0         0  
509              
510 6         263 return( $msg , $verbose_msg );
511             }
512              
513             sub _status {
514 3     3   526 my( $self , @repos ) = @_;
515              
516 3         16 my $max_len = $self->max_length_of_an_active_repo_label;
517              
518 3         16 REPO: for my $repo ( @repos ) {
519 12         194 my $label = $repo->label;
520              
521 12         87 my $msg = sprintf "%3d) %-${max_len}s : ", $repo->number, $label;
522              
523 12         25 my ( $status, $fxn );
524              
525 12 100       251 if ( -d $repo->path ) {
    100          
526 6         27 my $repo_type = $repo->type;
527 6 50       16 if ( $repo_type eq 'git' ) { $fxn = '_git_status' }
  6         16  
528             ### FIXME elsif( $repo_type eq 'svn' ) { $fxn = 'svn_status' }
529 0         0 else { $status = $self->error("ERROR: repo type '$repo_type' not supported") }
530              
531 6 50       37 $status = $self->$fxn($repo) if ($fxn);
532              
533 6 50 33     265 next REPO if $self->quiet and !$status;
534             }
535 3         13 elsif ( $repo->repo ) { $status = 'Not checked out' }
536 3         89 else { $status = $self->error("ERROR: repo '$label' does not exist") }
537              
538 12         276 say "$msg$status";
539             }
540             }
541              
542             sub _update {
543 1     1   279 my( $self , @repos ) = @_;
544              
545 1         7 my $max_len = $self->max_length_of_an_active_repo_label;
546              
547 1         10 REPO: for my $repo ( @repos ) {
548 4 100       119 next REPO unless $repo->repo;
549              
550 2         11 my $name = $repo->name;
551              
552 2         30 my $msg = sprintf "%3d) %-${max_len}s : ", $repo->number, $repo->label;
553              
554 2         5 my ( $status, $fxn );
555              
556 2         9 my $repo_type = $repo->type;
557              
558 2 50       10 if ( $repo_type eq 'git' ) { $fxn = '_git_update' }
  2         6  
559             ### FIXME elsif( $repo_type eq 'svn' ) { $fxn = 'svn_update' }
560 0         0 else { $status = $self->error("ERROR: repo type '$_' not supported") }
561              
562 2 50       16 $status = $self->$fxn($repo) if ($fxn);
563              
564 2 50 33     53 next REPO if $self->quiet and !$status;
565              
566 2         83 say "$msg$status";
567             }
568             }
569              
570             # override this in commands that shouldn't use IO::Page -- i.e., ones that
571             # need to do incremental output
572 25     25   2954 sub _use_io_page { 1 }
573              
574              
575             1;
576              
577             __END__