File Coverage

blib/lib/App/Sqitch/Command/status.pm
Criterion Covered Total %
statement 116 116 100.0
branch 18 22 81.8
condition 8 10 80.0
subroutine 20 20 100.0
pod 5 5 100.0
total 167 173 96.5


line stmt bran cond sub pod time code
1             package App::Sqitch::Command::status;
2              
3 2     2   2348 use 5.010;
  2         9  
4 2     2   12 use strict;
  2         6  
  2         64  
5 2     2   14 use warnings;
  2         11  
  2         74  
6 2     2   15 use utf8;
  2         6  
  2         22  
7 2     2   70 use Locale::TextDomain qw(App-Sqitch);
  2         9  
  2         18  
8 2     2   436 use App::Sqitch::X qw(hurl);
  2         5  
  2         20  
9 2     2   633 use Moo;
  2         9  
  2         15  
10 2     2   846 use App::Sqitch::Types qw(Str Bool Target);
  2         6  
  2         26  
11 2     2   2289 use List::Util qw(max);
  2         6  
  2         127  
12 2     2   12 use Try::Tiny;
  2         7  
  2         107  
13 2     2   17 use namespace::autoclean;
  2         5  
  2         16  
14              
15             extends 'App::Sqitch::Command';
16             with 'App::Sqitch::Role::ContextCommand';
17             with 'App::Sqitch::Role::ConnectingCommand';
18              
19             our $VERSION = 'v1.4.0'; # VERSION
20              
21             has target_name => (
22             is => 'ro',
23             isa => Str,
24             );
25              
26             has target => (
27             is => 'rw',
28             isa => Target,
29             handles => [qw(engine plan plan_file)],
30             );
31              
32             has show_changes => (
33             is => 'ro',
34             isa => Bool,
35             lazy => 1,
36             default => sub {
37             shift->sqitch->config->get(
38             key => "status.show_changes",
39             as => 'bool',
40             ) // 0;
41             }
42             );
43              
44             has show_tags => (
45             is => 'ro',
46             isa => Bool,
47             lazy => 1,
48             default => sub {
49             shift->sqitch->config->get(
50             key => "status.show_tags",
51             as => 'bool',
52             ) // 0;
53             }
54             );
55              
56             has date_format => (
57             is => 'ro',
58             lazy => 1,
59             isa => Str,
60             default => sub {
61             shift->sqitch->config->get( key => 'status.date_format' ) || 'iso'
62             }
63             );
64              
65             has project => (
66             is => 'ro',
67             isa => Str,
68             lazy => 1,
69             default => sub {
70             my $self = shift;
71             try { $self->plan->project } catch {
72             # Just die on parse and I/O errors.
73             die $_ if try { $_->ident eq 'parse' || $_->ident eq 'io' };
74              
75             # Try to extract a project name from the registry.
76             my $engine = $self->engine;
77             hurl status => __ 'Database not initialized for Sqitch'
78             unless $engine->initialized;
79             my @projs = $engine->registered_projects
80             or hurl status => __ 'No projects registered';
81             hurl status => __x(
82             'Use --project to select which project to query: {projects}',
83             projects => join __ ', ', @projs,
84             ) if @projs > 1;
85             return $projs[0];
86             };
87             },
88             );
89              
90             sub options {
91             return qw(
92             project=s
93             target|t=s
94             show-tags
95             show-changes
96             date-format|date=s
97             );
98             }
99              
100             sub execute {
101 9     9 1 11762 my $self = shift;
102 9         64 my ($targets) = $self->parse_args(
103             target => $self->target_name,
104             args => \@_,
105             );
106              
107             # Warn on multiple targets.
108 9         19 my $target = shift @{ $targets };
  9         19  
109             $self->warn(__x(
110             'Too many targets specified; connecting to {target}',
111             target => $target->name,
112 9 100       17 )) if @{ $targets };
  9         36  
113              
114             # Good to go.
115 9         381 $self->target($target);
116 9         729 my $engine = $target->engine;
117              
118             # Where are we?
119 9         1682 $self->comment( __x 'On database {db}', db => $engine->destination );
120              
121             # Exit with status 1 on no state, probably not expected.
122             my $state = try {
123 9     9   593 $engine->current_state( $self->project )
124             } catch {
125             # Just die on parse and I/O errors.
126 1 0   1   31 die $_ if try { $_->ident eq 'parse' || $_->ident eq 'io' };
  1 50       35  
127              
128             # Hrm. Maybe not initialized?
129 1 50       19 die $_ if $engine->initialized;
130 1         9 hurl status => __x(
131             'Database {db} has not been initialized for Sqitch',
132             db => $engine->registry_destination
133             );
134 9         1514 };
135              
136 8 100       676 hurl {
137             ident => 'status',
138             message => __ 'No changes deployed',
139             exitval => 1,
140             } unless $state;
141              
142             # Emit the state basics.
143 7         35 $self->emit_state($state);
144              
145             # Emit changes and tags, if required.
146 7         42 $self->emit_changes;
147 7         926 $self->emit_tags;
148              
149 7     7   888 my $plan_proj = try { $target->plan->project };
  7         333  
150 7 100 66     518 if (defined $plan_proj && $self->project eq $plan_proj ) {
151 5         99 $self->emit_status($state);
152             } else {
153             # If we have no access to the project plan, we can't emit the status.
154 2         63 $self->comment('');
155 2         93 $self->emit(__x(
156             'Status unknown. Use --plan-file to assess "{project}" status',
157             project => $self->project,
158             ));
159             }
160              
161 7         320 return $self;
162             }
163              
164             sub configure {
165             my ( $class, $config, $opt ) = @_;
166              
167             # Make sure the date format is valid.
168             if (my $format = $opt->{date_format}
169             || $config->get(key => 'status.date_format')
170             ) {
171             require App::Sqitch::DateTime;
172             App::Sqitch::DateTime->validate_as_string_format($format);
173             }
174              
175             # Set boolean options from config.
176             for my $key (qw(show_changes show_tags)) {
177             next if exists $opt->{$key};
178             my $val = $config->get(key => "status.$key", as => 'bool') // next;
179             $opt->{$key} = $val;
180             }
181              
182             my $ret = $class->SUPER::configure( $config, $opt );
183             $ret->{target_name} = delete $ret->{target} if exists $ret->{target};
184             return $ret;
185             }
186              
187             sub emit_state {
188 10     10 1 4273 my ( $self, $state ) = @_;
189             $self->comment(__x(
190             'Project: {project}',
191             project => $state->{project},
192 10         47 ));
193             $self->comment(__x(
194             'Change: {change_id}',
195             change_id => $state->{change_id},
196 10         1377 ));
197             $self->comment(__x(
198             'Name: {change}',
199             change => $state->{change},
200 10         1199 ));
201 10 100       1178 if (my @tags = @{ $state->{tags}} ) {
  10         63  
202 9         49 $self->comment(__nx(
203             'Tag: {tags}',
204             'Tags: {tags}',
205             @tags,
206             tags => join(__ ', ', @tags),
207             ));
208             }
209              
210             $self->comment(__x(
211             'Deployed: {date}',
212             date => $state->{committed_at}->as_string(
213 10         1593 format => $self->date_format
214             ),
215             ));
216             $self->comment(__x(
217             'By: {name} <{email}>',
218             name => $state->{committer_name},
219             email=> $state->{committer_email},
220 10         3363 ));
221 10         1462 return $self;
222             }
223              
224             sub _all {
225 5     5   657 my $iter = shift;
226 5         11 my @res;
227 5         12 while (my $row = $iter->()) {
228 8         36 push @res => $row;
229             }
230 5         23 return \@res;
231             }
232              
233             sub emit_changes {
234 10     10 1 5987 my $self = shift;
235 10 100       185 return $self unless $self->show_changes;
236              
237             # Emit the header.
238 2         50 my $changes = _all $self->engine->current_changes( $self->project );
239 2         42 $self->comment('');
240 2         63 $self->comment(__n 'Change:', 'Changes:', @{ $changes });
  2         11  
241              
242             # Find the longest change name.
243 2         168 my $len = max map { length $_->{change} } @{ $changes };
  4         16  
  2         6  
244 2         35 my $format = $self->date_format;
245              
246             # Emit each change.
247             $self->comment(sprintf(
248             ' %s%s - %s - %s <%s>',
249             $_->{change},
250             ((' ') x ($len - length $_->{change})) || '',
251             $_->{committed_at}->as_string( format => $format ),
252             $_->{committer_name},
253             $_->{committer_email},
254 2   100     172 )) for @{ $changes };
  2         29  
255              
256 2         485 return $self;
257             }
258              
259             sub emit_tags {
260 11     11 1 9365 my $self = shift;
261 11 100       210 return $self unless $self->show_tags;
262              
263             # Emit the header.
264 3         76 my $tags = _all $self->engine->current_tags( $self->project );
265 3         62 $self->comment('');
266              
267             # If no tags, say so and return.
268 3 100       100 unless (@{ $tags }) {
  3         15  
269 1         8 $self->comment(__ 'Tags: None.');
270 1         104 return $self;
271             }
272              
273 2         9 $self->comment(__n 'Tag:', 'Tags:', @{ $tags });
  2         9  
274              
275             # Find the longest tag name.
276 2         182 my $len = max map { length $_->{tag} } @{ $tags };
  4         14  
  2         7  
277 2         38 my $format = $self->date_format;
278              
279             # Emit each tag.
280             $self->comment(sprintf(
281             ' %s%s - %s - %s <%s>',
282             $_->{tag},
283             ((' ') x ($len - length $_->{tag})) || '',
284             $_->{committed_at}->as_string( format => $format ),
285             $_->{committer_name},
286             $_->{committer_email},
287 2   100     183 )) for @{ $tags };
  2         33  
288              
289 2         494 return $self;
290             }
291              
292             sub emit_status {
293 9     9 1 2012 my ( $self, $state ) = @_;
294 9         166 my $plan = $self->plan;
295 9         569 $self->comment('');
296              
297 9   66     339 my $idx = $plan->index_of( $state->{change_id} ) // do {
298 1         33 $self->vent(__x(
299             'Cannot find this change in {file}',
300             file => $self->plan_file
301             ));
302 1         319 hurl status => __ 'Make sure you are connected to the proper '
303             . 'database for this project.';
304             };
305              
306             # Say something about our current state.
307 8 100       34 if ( $idx == $plan->count - 1 ) {
308 1         8 $self->emit( __ 'Nothing to deploy (up-to-date)' );
309             } else {
310 7         25 $self->emit(__n(
311             'Undeployed change:',
312             'Undeployed changes:',
313             $plan->count - ( $idx + 1 )
314             ));
315 7         810 $plan->position($idx);
316 7         282 while ( my $change = $plan->next ) {
317 13         51 $self->emit( ' * ', $change->format_name_with_tags );
318             }
319             }
320 8         166 return $self;
321             }
322              
323             1;
324              
325             __END__
326              
327             =head1 Name
328              
329             App::Sqitch::Command::status - Display status information about Sqitch
330              
331             =head1 Synopsis
332              
333             my $cmd = App::Sqitch::Command::status->new(%params);
334             $cmd->execute;
335              
336             =head1 Description
337              
338             If you want to know how to use the C<status> command, you probably want to be
339             reading C<sqitch-status>. But if you really want to know how the C<status> command
340             works, read on.
341              
342             =head1 Interface
343              
344             =head2 Attributes
345              
346             =head3 C<target_name>
347              
348             The name or URI of the database target as specified by the C<--target> option.
349              
350             =head3 C<target>
351              
352             An L<App::Sqitch::Target> object from which to read the status. Must be
353             instantiated by C<execute()>.
354              
355             =head2 Instance Methods
356              
357             =head3 C<execute>
358              
359             $status->execute;
360              
361             Executes the status command. The current state of the target database will be
362             compared to the plan in order to show where things stand.
363              
364             =head3 C<emit_changes>
365              
366             $status->emit_changes;
367              
368             Emits a list of deployed changes if C<show_changes> is true.
369              
370             =head3 C<emit_tags>
371              
372             $status->emit_tags;
373              
374             Emits a list of deployed tags if C<show_tags> is true.
375              
376             =head3 C<emit_state>
377              
378             $status->emit_state($state);
379              
380             Emits the current state of the target database. Pass in a state hash as
381             returned by L<App::Sqitch::Engine> C<current_state()>.
382              
383             =head3 C<emit_status>
384              
385             $status->emit_state($state);
386              
387             Emits information about the current status of the target database compared to
388             the plan. Pass in a state hash as returned by L<App::Sqitch::Engine>
389             C<current_state()>. Throws an exception if the current state's change cannot
390             be found in the plan.
391              
392             =head1 See Also
393              
394             =over
395              
396             =item L<sqitch-status>
397              
398             Documentation for the C<status> command to the Sqitch command-line client.
399              
400             =item L<sqitch>
401              
402             The Sqitch command-line client.
403              
404             =back
405              
406             =head1 Author
407              
408             David E. Wheeler <david@justatheory.com>
409              
410             =head1 License
411              
412             Copyright (c) 2012-2023 iovation Inc., David E. Wheeler
413              
414             Permission is hereby granted, free of charge, to any person obtaining a copy
415             of this software and associated documentation files (the "Software"), to deal
416             in the Software without restriction, including without limitation the rights
417             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
418             copies of the Software, and to permit persons to whom the Software is
419             furnished to do so, subject to the following conditions:
420              
421             The above copyright notice and this permission notice shall be included in all
422             copies or substantial portions of the Software.
423              
424             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
425             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
426             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
427             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
428             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
429             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
430             SOFTWARE.
431              
432             =cut