File Coverage

blib/lib/App/Multigit/Repo.pm
Criterion Covered Total %
statement 26 70 37.1
branch 0 26 0.0
condition 0 5 0.0
subroutine 9 16 56.2
pod 3 3 100.0
total 38 120 31.6


line stmt bran cond sub pod time code
1             package App::Multigit::Repo;
2              
3 1     1   499 use App::Multigit::Loop qw(loop);
  1         3  
  1         69  
4 1     1   470 use IO::Async::Process;
  1         5021  
  1         6  
5 1     1   20 use Future;
  1         2  
  1         3  
6 1     1   472 use Moo;
  1         9264  
  1         4  
7 1     1   1068 use Cwd 'getcwd';
  1         1  
  1         38  
8 1     1   5 use Try::Tiny;
  1         1  
  1         39  
9              
10 1     1   14 use 5.014;
  1         3  
11              
12             our $VERSION = '0.18';
13              
14             =encoding utf8
15              
16             =head1 NAME
17              
18             App::Multigit::Repo - Moo class to represent a repo
19              
20             =head1 DESCRIPTION
21              
22             Holds the name and config for a repo, to make future chaining code cleaner.
23              
24             You can curry objects is what I mean.
25              
26             =head1 PROPERTIES
27              
28             =head2 name
29              
30             Name as in the key from the mgconfig file that defines this repo. As in, the
31             URL.
32              
33             It's called name because it doesn't have to be the URL, but is by default.
34              
35             =cut
36              
37             has name => (
38             is => 'ro',
39             );
40              
41             =head2 config
42              
43             The config from the mgconfig file for this repo.
44              
45             This is given a C key if the config does not already specify one.
46              
47             =cut
48              
49             has config => (
50             is => 'ro',
51             );
52              
53             =head1 METHODS
54              
55             =head2 run($command, [%data])
56              
57             Run a command, in one of two ways:
58              
59             If the command is a CODE ref, it is run with this Repo object, and the entirety
60             of C<%data>. The CODE reference should use normal print/say/warn/die behaviour.
61             Its return value is discarded. If the subref returns at all, it is considered to
62             have succeeded.
63              
64             If it is an ARRAY ref, it is run with IO::Async::Process, with C sent
65             to the process's STDIN.
66              
67             A Future object is returned. When the command finishes, the Future is completed
68             with a hash-shaped list identical to the one C accepts.
69              
70             If an error occurs I running the command (i.e. if IO::Async throws the
71             error), it will behave as though an error occurred within the command, and
72             C will be set to 255.
73              
74             =head3 data
75              
76             C accepts a hash of data. If C or C are provided here, the
77             Future will have these values in C and C, and
78             C and C will get populated with the I STDOUT and STDERR
79             from the provided C<$command>.
80              
81             =over
82              
83             =item C - The STDOUT from the operation. Will be set to the empty string
84             if undef.
85              
86             =item C - The STDERR from the operation. Will be set to the empty string
87             if undef.
88              
89             =item C - The C<$?> equivalent as produced by IO::Async::Process.
90              
91             =item C - The STDOUT from the prior command
92              
93             =item C - The STDERR from the prior command
94              
95             =back
96              
97             C and C are never used; they are provided for you to
98             write any procedure you may require to concatenate new output with old. See
99             C.
100              
101             =head3 IO::Async::Process
102              
103             The special key C to the C<%data> hash will be removed from the hash
104             and used as configuration for the L object that powers the
105             whole system.
106              
107             It currently supports the C option, to prevent attempting to C
108             into the repo's directory.
109              
110             $repo->run($subref, ia_config => { no_cd => 1 });
111              
112             =cut
113              
114             sub run {
115 0     0 1   my ($self, $command, %data) = @_;
116 0           my $future = loop->new_future;
117              
118 0           bless $future, 'App::Multigit::Future';
119              
120 0   0       $data{stdout} //= '';
121 0           my $ia_config = delete $data{ia_config};
122              
123 0           my $ignore_stdout = $App::Multigit::BEHAVIOUR{ignore_stdout};
124 0           my $ignore_stderr = $App::Multigit::BEHAVIOUR{ignore_stderr};
125              
126             my $finish_code = sub {
127 0     0     my (undef, $exitcode, $stdout, $stderr) = @_;
128             my %details = (
129             stdout => $ignore_stdout ? '' : $stdout,
130             stderr => $ignore_stderr ? '' : $stderr,
131             exitcode => $exitcode,
132             past_stdout => $ignore_stdout ? '' : $data{stdout},
133             past_stderr => $ignore_stderr ? '' : $data{stderr},
134 0 0         );
    0          
    0          
    0          
135              
136 0 0         if ($exitcode == 0) {
137 0           $future->done(%details);
138             }
139             else {
140 0           $future->fail(
141             "Child process exited with nonzero exit status",
142             exit_nonzero => %details);
143             }
144 0           };
145              
146             try
147             {
148 0     0     my $setup = [];
149 0 0         unless($ia_config->{no_cd}) {
150             $setup = [
151             chdir => $self->config->{dir}
152 0           ];
153             }
154 0 0         if (ref $command eq 'CODE') {
155             loop->run_child(
156             code => sub {
157 0           $command->($self, %data); 0;
  0            
158             },
159 0           setup => $setup,
160             on_finish => $finish_code,
161             );
162             }
163             else {
164             loop->run_child(
165             command => $command,
166             setup => $setup,
167             stdin => $data{stdout},
168 0           on_finish => $finish_code,
169             )
170             }
171             }
172             catch
173             {
174             # make failures coming from the Async code come out as an error
175             # relating to the repo as they probably are.
176             # rather than crashing the whole program hard.
177             # the common error case is the subdirectory for the module not existing.
178 0     0     $finish_code->(undef, 255, '', "Error running\n" . $_);
179 0           };
180 0           return $future;
181             }
182              
183             =head2 gather(%data)
184              
185             Intended for currying. This goes between Cs and ensures output is not lost.
186              
187             Concatenates the STDOUT and STDERR from the command with the respective STDOUT
188             or STDERR of the previous command and continues the chain.
189              
190             $repo->run([qw/git command/])
191             ->then($repo->curry::run([qw/another git command/]))
192             ->then($repo->curry::gather)
193             ->then(App::Multigit::report($repo))
194              
195             See C for the shape of the data
196              
197             =cut
198              
199             sub gather {
200 0     0 1   my ($self, %data) = @_;
201              
202 1     1   4 no warnings 'uninitialized';
  1         1  
  1         153  
203 0           my $stdout = join "\n", grep { $_ } delete $data{past_stdout}, $data{stdout};
  0            
204 0           my $stderr = join "\n", grep { $_ } delete $data{past_stderr}, $data{stderr};
  0            
205 0 0         $data{stdout} = $stdout unless $App::Multigit::BEHAVIOUR{ignore_stdout};
206 0 0         $data{stderr} = $stderr unless $App::Multigit::BEHAVIOUR{ignore_stderr};
207              
208 0           Future->done(%data);
209             }
210              
211             =head2 report(%data)
212              
213             Intended for currying, and accepts a hash-shaped list à la C.
214              
215             Returns a Future that yields a two-element list of the directory - from the
216             config - and the STDOUT from the command, indented with tabs.
217              
218             Use C to collect STDOUT/STDERR from previous commands too.
219              
220             The yielded list is intended for use as a hash constructor.
221              
222              
223             my $future = App::Multigit::each(sub {
224             my $repo = shift;
225             $repo->run([qw/git command/])
226             ->then($repo->curry::run([qw/another git command/]))
227             ->then($repo->curry::gather)
228             ->then($repo->curry::report)
229             ;
230             });
231              
232             my %report = $future->get;
233              
234             for my $dir (sort keys %report) { ... }
235              
236             =cut
237              
238             sub report {
239 0     0 1   my $self = shift;
240 0           my %data = @_;
241              
242 0           my $dir = $self->config->{dir};
243              
244 0 0         $data{stdout} = '' if $App::Multigit::BEHAVIOUR{ignore_stdout};
245 0 0         $data{stderr} = '' if $App::Multigit::BEHAVIOUR{ignore_stderr};
246              
247 0           my $output = do {
248 1     1   3 no warnings 'uninitialized';
  1         4  
  1         130  
249 0           _indent($data{stdout}, 1) . _indent($data{stderr}, 1);
250             };
251              
252             return Future->done unless $App::Multigit::BEHAVIOUR{report_on_no_output}
253 0 0 0       or $output =~ s/\s//gr;
254              
255 0           return Future->done(
256             $dir => $output
257             );
258             }
259              
260              
261             =head2 _indent
262              
263             Returns a copy of the first argument indented by the number of tabs in the
264             second argument. Not really a method on this class but it's here if you want it.
265              
266             =cut
267              
268             sub _indent {
269 0 0   0     return if not defined $_[0];
270 0           $_[0] =~ s/^/"\t" x $_[1]/germ
  0            
271             }
272             1;
273              
274             __END__