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.336';
4             # ABSTRACT: Base class for App::GitGot commands
5 20     20   9899 use 5.014;
  20         59  
6              
7 20     19   139 use App::Cmd::Setup -command;
  19         58  
  19         114  
8 19     16   2491 use Cwd;
  16         33  
  16         779  
9 16     15   5873 use File::HomeDir;
  15         64007  
  15         655  
10 15     15   93 use List::Util qw/ max first /;
  15         25  
  15         1190  
11 15     15   84 use Path::Tiny;
  15         38  
  15         556  
12 15     15   72 use Try::Tiny;
  15         25  
  15         617  
13 15     15   6480 use Types::Standard -types;
  15         861800  
  15         136  
14 15     15   56974 use YAML qw/ DumpFile LoadFile /;
  15         29  
  15         657  
15              
16 15     15   5252 use App::GitGot::Repo::Git;
  15         41  
  15         477  
17 15     15   5547 use App::GitGot::Repositories;
  15         46  
  15         506  
18 15     15   92 use App::GitGot::Types -all;
  15         28  
  15         88  
19              
20 15     15   24872 use Moo;
  15         28  
  15         61  
21 15     15   5340 use MooX::HandlesVia;
  15         27  
  15         72  
22 15     15   1302 use namespace::autoclean;
  15         34  
  15         76  
23              
24             sub opt_spec {
25 42     42 1 273338 my( $class , $app ) = @_;
26              
27             return (
28 42         722 [ '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->my_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   696 my ( $self ) = @_;
54              
55             return $self->full_repo_list
56 21 100 66     413 if $self->all or ! $self->tags and ! $self->skip_tags and ! @{ $self->args };
      100        
57              
58 4         20 my $list = _expand_arg_list( $self->args );
59              
60 4         8 my @repos;
61 4         89 REPO: foreach my $repo ( $self->all_repos ) {
62 12 100       418 if ( grep { $_ eq $repo->number or $_ eq $repo->name } @$list ) {
  12 100       100  
63 4         11 push @repos, $repo;
64 4         8 next REPO;
65             }
66              
67 8 50       119 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       266 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     253 push @repos, $repo unless $self->tags or @$list;
82             }
83              
84 4         120 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   983 my $self = shift;
105              
106 37         841 my $config = _read_config( $self->configfile );
107              
108 37         88 my $repo_count = 1;
109              
110 37 50       937 my $sort_key = $self->by_path ? 'path' : 'name';
111              
112 37         1009 my @parsed_config;
113              
114 37         229 foreach my $entry ( sort { $a->{$sort_key} cmp $b->{$sort_key} } @$config ) {
  90         236  
115              
116             # a completely empty entry is okay (this will happen when there's no
117             # config at all...)
118 92 100       3957 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       1344 entry => $entry ,
124             count => $repo_count++ ,
125             });
126             }
127              
128 37         2644 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   506 my $self = shift;
160              
161 7         120 my $scheme = $self->color_scheme;
162              
163 7 50       187 if ( $scheme =~ /^\+/ ) {
164 0         0 $scheme =~ s/^\+//;
165             }
166             else {
167 7         26 $scheme = "App::GitGot::Outputter::$scheme"
168             }
169              
170             try {
171 7     7   859 eval "use $scheme";
172 7 50       54 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         84 };
178              
179 7         244 return $scheme->new({ no_color => $self->no_color });
180             }
181              
182             sub execute {
183 42     42 1 180144 my( $self , $opt , $args ) = @_;
184 42         914 $self->_set_args( $args );
185 42         2210 $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         1300 $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   256 eval 'use IO::Page' if $self->_use_io_page;
  15     6   5518  
  15     1   3194  
  15         279  
  6         49  
  6         18  
  6         98  
  1         6  
  1         2  
  1         13  
194              
195 42         229 $self->_execute($opt,$args);
196             }
197              
198              
199             sub local_repo {
200 12     12 1 19 my $self = shift;
201              
202 12         132 my $dir = $self->_find_repo_root( getcwd() );
203              
204 11     11   243 return first { $_->path eq $dir->absolute } $self->all_repos;
  11         565  
205             }
206              
207              
208             sub max_length_of_an_active_repo_label {
209 18     18 1 39 my( $self ) = @_;
210              
211 18 50       290 my $sort_key = $self->by_path ? 'path' : 'name';
212              
213 18         724 return max ( map { length $_->$sort_key } $self->active_repos);
  72         1354  
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 47 my ($self) = @_;
236              
237             DumpFile(
238             $self->configfile,
239             [
240 6         33 sort { $a->{name} cmp $b->{name} }
241 14         445 map { $_->in_writable_format } $self->all_repos
  19         1592  
242             ] ,
243             );
244             }
245              
246             sub _expand_arg_list {
247 4     4   9 my $args = shift;
248              
249             ## no critic
250              
251             return [
252             map {
253 4         11 s!/$!!;
  4         8  
254 4 50       12 if (/^(\d+)-(\d+)?$/) { ( $1 .. $2 ) }
  0         0  
255 4         15 else { ($_) }
256             } @$args
257             ];
258              
259             ## use critic
260             }
261              
262             sub _fetch {
263 1     1   169 my( $self , @repos ) = @_;
264              
265 1         6 my $max_len = $self->max_length_of_an_active_repo_label;
266              
267 1         7 REPO: for my $repo ( @repos ) {
268 4 100       87 next REPO unless $repo->repo;
269              
270 2         5 my $name = $repo->name;
271              
272 2         25 my $msg = sprintf "%3d) %-${max_len}s : ", $repo->number, $repo->label;
273              
274 2         4 my ( $status, $fxn );
275              
276 2         7 my $repo_type = $repo->type;
277              
278 2 50       5 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     44 next REPO if $self->quiet and !$status;
285              
286 2         57 say "$msg$status";
287             }
288             }
289              
290             sub _find_repo_root {
291 12     12   27 my( $self , $path ) = @_;
292              
293 12         31 my $dir = path( $path );
294              
295             # find repo root
296 12 100       310 while ( ! grep { -d and $_->basename eq '.git' } $dir->children ) {
  71         4218  
297 3 100       108 die "$path doesn't seem to be in a git directory\n" if $dir eq $dir->parent;
298 2         135 $dir = $dir->parent;
299             }
300              
301 11         438 return $dir
302             }
303              
304             sub _git_clone_or_callback {
305 6 50   6   24 my( $self , $entry , $callback ) = @_
306             or die "Need entry and callback";
307              
308 6         24 my $msg = '';
309              
310 6         26 my $path = $entry->path;
311              
312 6 100       172 if ( !-d $path ) {
    50          
313 3         22 path($path)->mkpath;
314              
315             try {
316 3     3   213 $entry->clone( $entry->repo , './' );
317 3         61 $msg .= $self->major_change('Checked out');
318             }
319 3     0   907 catch { $msg .= $self->error('ERROR') . "\n$_" };
  0         0  
320             }
321             elsif ( -d "$path/.git" ) {
322             try {
323 3     3   137 $msg .= $callback->($msg , $entry);
324             }
325 3     0   37 catch { $msg .= $self->error('ERROR') . "\n$_" };
  0         0  
326             }
327              
328 6         98 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   2 my( $msg , $entry ) = @_;
339              
340 1         27 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         19  
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       5 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       20 $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         21 );
370             }
371              
372             sub _git_status {
373 6 50   6   17 my ( $self, $entry ) = @_
374             or die "Need entry";
375              
376 6         19 my( $msg , $verbose_msg ) = $self->_run_git_status( $entry );
377              
378 6 50       16 $msg .= $self->_run_git_cherry( $entry )
379             if $entry->current_remote_branch;
380 6 100 66     34 if ($self->opt->show_branch and defined $entry->current_branch) {
381 2         6 $msg .= '[' . $entry->current_branch . ']';
382             }
383              
384 6 100       107 return ( $self->verbose ) ? "$msg$verbose_msg" : $msg;
385             }
386              
387             sub _git_update {
388 2 50   2   8 my ( $self, $entry ) = @_
389             or die "Need entry";
390              
391             $self->_git_clone_or_callback( $entry ,
392             sub {
393 1     1   3 my( $msg , $entry ) = @_;
394              
395 1         32 my @o = $entry->pull;
396 1 50       4 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         31 $msg .= $self->major_change('Updated');
401 1 50       22 $msg .= "\n" . join("\n",@o) unless $self->quiet;
402             }
403              
404 1         43 return $msg;
405             }
406 2         23 );
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   1189 my $file = shift;
445              
446 37         59 my $config;
447              
448 37 100       552 if ( -e $file ) {
449 29     29   1519 try { $config = LoadFile( $file ) }
450 29     0   498 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     230554 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         35 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         10 my( $msg , $verbose_msg ) = ('','');
486              
487             try {
488 6     6   337 my $status = $entry->status;
489 6 50       28 if ( keys %$status ) {
490 0         0 $msg .= $self->warning('Dirty') . ' ';
491             } else {
492 6 50       111 $msg .= $self->minor_change('OK ') unless $self->quiet;
493             }
494              
495 6 100       92 if ( $self->verbose ) {
496 2         50 TYPE: for my $type ( keys %types ) {
497 8 50       11 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       7 $verbose_msg = "\n$verbose_msg" if $verbose_msg;
506             }
507             }
508 6     0   60 catch { $msg .= $self->error('ERROR') . "\n$_" };
  0         0  
509              
510 6         217 return( $msg , $verbose_msg );
511             }
512              
513             sub _status {
514 3     3   393 my( $self , @repos ) = @_;
515              
516 3         12 my $max_len = $self->max_length_of_an_active_repo_label;
517              
518 3         12 REPO: for my $repo ( @repos ) {
519 12         181 my $label = $repo->label;
520              
521 12         98 my $msg = sprintf "%3d) %-${max_len}s : ", $repo->number, $label;
522              
523 12         24 my ( $status, $fxn );
524              
525 12 100       190 if ( -d $repo->path ) {
    100          
526 6         24 my $repo_type = $repo->type;
527 6 50       10 if ( $repo_type eq 'git' ) { $fxn = '_git_status' }
  6         14  
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       30 $status = $self->$fxn($repo) if ($fxn);
532              
533 6 50 33     221 next REPO if $self->quiet and !$status;
534             }
535 3         14 elsif ( $repo->repo ) { $status = 'Not checked out' }
536 3         77 else { $status = $self->error("ERROR: repo '$label' does not exist") }
537              
538 12         182 say "$msg$status";
539             }
540             }
541              
542             sub _update {
543 1     1   205 my( $self , @repos ) = @_;
544              
545 1         7 my $max_len = $self->max_length_of_an_active_repo_label;
546              
547 1         6 REPO: for my $repo ( @repos ) {
548 4 100       137 next REPO unless $repo->repo;
549              
550 2         9 my $name = $repo->name;
551              
552 2         28 my $msg = sprintf "%3d) %-${max_len}s : ", $repo->number, $repo->label;
553              
554 2         5 my ( $status, $fxn );
555              
556 2         7 my $repo_type = $repo->type;
557              
558 2 50       5 if ( $repo_type eq 'git' ) { $fxn = '_git_update' }
  2         5  
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       13 $status = $self->$fxn($repo) if ($fxn);
563              
564 2 50 33     40 next REPO if $self->quiet and !$status;
565              
566 2         58 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   2115 sub _use_io_page { 1 }
573              
574              
575             1;
576              
577             __END__