File Coverage

blib/lib/App/Sqitch/Command/add.pm
Criterion Covered Total %
statement 283 283 100.0
branch 52 60 86.6
condition 15 20 75.0
subroutine 64 64 100.0
pod 2 2 100.0
total 416 429 96.9


line stmt bran cond sub pod time code
1             package App::Sqitch::Command::add;
2              
3 3     3   9200 use 5.010;
  3         17  
4 3     3   22 use strict;
  3         8  
  3         86  
5 3     3   17 use warnings;
  3         6  
  3         155  
6 3     3   39 use utf8;
  3         8  
  3         17  
7 3     3   108 use Locale::TextDomain qw(App-Sqitch);
  3         6  
  3         26  
8 3     3   754 use App::Sqitch::X qw(hurl);
  3         9  
  3         43  
9 3     3   1016 use Moo;
  3         7  
  3         21  
10 3     3   1556 use App::Sqitch::Types qw(Str Int ArrayRef HashRef Dir Bool Maybe);
  3         8  
  3         52  
11 3     3   5788 use Path::Class;
  3         19  
  3         200  
12 3     3   23 use Try::Tiny;
  3         8  
  3         192  
13 3     3   18 use Clone qw(clone);
  3         7  
  3         201  
14 3     3   18 use List::Util qw(first);
  3         17  
  3         201  
15 3     3   22 use namespace::autoclean;
  3         6  
  3         39  
16              
17             extends 'App::Sqitch::Command';
18             with 'App::Sqitch::Role::ContextCommand';
19              
20             our $VERSION = 'v1.4.0'; # VERSION
21              
22             has change_name => (
23             is => 'ro',
24             isa => Maybe[Str],
25             );
26              
27             has requires => (
28             is => 'ro',
29             isa => ArrayRef[Str],
30             default => sub { [] },
31             );
32              
33             has conflicts => (
34             is => 'ro',
35             isa => ArrayRef[Str],
36             default => sub { [] },
37             );
38              
39             has all => (
40             is => 'ro',
41             isa => Bool,
42             default => 0
43             );
44              
45             has note => (
46             is => 'ro',
47             isa => ArrayRef[Str],
48             default => sub { [] },
49             );
50              
51             has variables => (
52             is => 'ro',
53             isa => HashRef,
54             lazy => 1,
55             default => sub {
56             shift->sqitch->config->get_section( section => 'add.variables' );
57             },
58             );
59              
60             has template_directory => (
61             is => 'ro',
62             isa => Maybe[Dir],
63             );
64              
65             has template_name => (
66             is => 'ro',
67             isa => Maybe[Str],
68             );
69              
70             has with_scripts => (
71             is => 'ro',
72             isa => HashRef,
73             default => sub { {} },
74             );
75              
76             has templates => (
77             is => 'ro',
78             isa => HashRef,
79             lazy => 1,
80             default => sub {
81             my $self = shift;
82             $self->_config_templates($self->sqitch->config);
83             },
84             );
85              
86             has open_editor => (
87             is => 'ro',
88             isa => Bool,
89             lazy => 1,
90             default => sub {
91             shift->sqitch->config->get(
92             key => 'add.open_editor',
93             as => 'bool',
94             ) // 0;
95             },
96             );
97              
98             sub _check_script($) {
99 11     11   6705 my $file = file shift;
100              
101 11 100       1189 hurl add => __x(
102             'Template {template} does not exist',
103             template => $file,
104             ) unless -e $file;
105              
106 10 100       547 hurl add => __x(
107             'Template {template} is not a file',
108             template => $file,
109             ) unless -f $file;
110              
111 9         404 return $file;
112             }
113              
114             sub _config_templates {
115 28     28   718 my ($self, $config) = @_;
116 28         184 my $tmpl = $config->get_section( section => 'add.templates' );
117 28         77 $_ = _check_script $_ for values %{ $tmpl };
  28         118  
118 28         358 return $tmpl;
119             }
120              
121             sub all_templates {
122 22     22 1 4717 my ($self, $name) = @_;
123 22         510 my $config = $self->sqitch->config;
124 22         614 my $tmpl = $self->templates;
125              
126             # Read all the template directories.
127 22         631 for my $dir (
128             $self->template_directory,
129             $config->user_dir->subdir('templates'),
130             $config->system_dir->subdir('templates'),
131             ) {
132 66 100 100     4060 next unless $dir && -d $dir;
133 23         1235 for my $subdir($dir->children) {
134 67 50       19562 next unless $subdir->is_dir;
135 67 100       357 next if $tmpl->{my $script = $subdir->basename};
136 45         328 my $file = $subdir->file("$name.tmpl");
137 45 50       3309 $tmpl->{$script} = $file if -f $file
138             }
139             }
140              
141             # Make sure we have core templates.
142 22         898 my $with = $self->with_scripts;
143 22         67 for my $script (qw(deploy revert verify)) {
144             hurl add => __x(
145             'Cannot find {script} template',
146             script => $script,
147 63 100 66     382 ) if !$tmpl->{$script} && ($with->{$script} || !exists $with->{$script});
      66        
148             }
149              
150 19         151 return $tmpl;
151             }
152              
153             sub options {
154             return qw(
155             change-name|change|c=s
156             requires|r=s@
157             conflicts|x=s@
158             note|n|m=s@
159             all|a!
160             template-name|template|t=s
161             template-directory=s
162             with=s@
163             without=s@
164             use=s%
165             open-editor|edit|e!
166             );
167             }
168              
169             # Override to convert multiple vars to an array.
170             sub _parse_opts {
171 7     7   1305 my ( $class, $args ) = @_;
172              
173 7         14 my (%opts, %vars);
174 7         125 Getopt::Long::Configure(qw(bundling no_pass_through));
175             Getopt::Long::GetOptionsFromArray(
176             $args, \%opts,
177             $class->options,
178             'set|s=s%' => sub {
179 4     4   2678 my ($opt, $key, $val) = @_;
180 4 100       12 if (exists $vars{$key}) {
181 1 50       5 $vars{$key} = [$vars{$key}] unless ref $vars{$key};
182 1         3 push @{ $vars{$key} } => $val;
  1         4  
183             } else {
184 3         12 $vars{$key} = $val;
185             }
186             }
187 7 50       574 ) or $class->usage;
188 7 100       6343 $opts{set} = \%vars if %vars;
189              
190             # Convert dashes to underscores.
191 7         25 for my $k (keys %opts) {
192 7 100       28 next unless ( my $nk = $k ) =~ s/-/_/g;
193 1         4 $opts{$nk} = delete $opts{$k};
194             }
195              
196             # Merge with and without.
197             $opts{with_scripts} = {
198 21         50 ( map { $_ => 1 } qw(deploy revert verify) ),
199 1 100       3 ( map { $_ => 1 } @{ delete $opts{with} || [] } ),
  7         37  
200 7 100       19 ( map { $_ => 0 } @{ delete $opts{without} || [] } ),
  1         7  
  7         44  
201             };
202 7         100 return \%opts;
203             }
204              
205             sub configure {
206             my ( $class, $config, $opt ) = @_;
207              
208             my %params = (
209             requires => $opt->{requires} || [],
210             conflicts => $opt->{conflicts} || [],
211             note => $opt->{note} || [],
212             );
213              
214             for my $key (qw(with_scripts change_name)) {
215             $params{$key} = $opt->{$key} if $opt->{$key};
216             }
217              
218             if (
219             my $dir = $opt->{template_directory}
220             || $config->get( key => 'add.template_directory' )
221             ) {
222             $dir = $params{template_directory} = dir $dir;
223             hurl add => __x(
224             'Directory "{dir}" does not exist',
225             dir => $dir,
226             ) unless -e $dir;
227              
228             hurl add => __x(
229             '"{dir}" is not a directory',
230             dir => $dir,
231             ) unless -d $dir;
232             }
233              
234             if (
235             my $name = $opt->{template_name}
236             || $config->get( key => 'add.template_name' )
237             ) {
238             $params{template_name} = $name;
239             }
240              
241             # Merge variables.
242             if ( my $vars = $opt->{set} ) {
243             $params{variables} = {
244             %{ $config->get_section( section => 'add.variables' ) },
245             %{ $vars },
246             };
247             }
248              
249             # Merge template info.
250             my $tmpl = $class->_config_templates($config);
251             if ( my $use = delete $opt->{use} ) {
252             while (my ($k, $v) = each %{ $use }) {
253             $tmpl->{$k} = _check_script $v;
254             }
255             }
256             $params{templates} = $tmpl if %{ $tmpl };
257              
258             # Copy other options.
259             for my $key (qw(all open_editor)) {
260             $params{$key} = $opt->{$key} if exists $opt->{$key};
261             }
262              
263             return \%params;
264             }
265              
266             sub execute {
267 16     16 1 24740 my $self = shift;
268 16 100 100     234 $self->usage unless @_ || $self->change_name;
269              
270 14         194 my ($name, $targets) = $self->parse_args(
271             names => [$self->change_name],
272             all => $self->all,
273             args => \@_,
274             no_changes => 1,
275             );
276              
277             # Check for missing name.
278 12 100       83 unless (defined $name) {
279 2 100   2   21 if (my $target = first { my $n = $_->name; first { $_ eq $n } @_ } @{ $targets }) {
  2         8  
  2         17  
  2         14  
  2         13  
280             # Name conflicts with a target.
281 1         8 hurl add => __x(
282             'Name "{name}" identifies a target; use "--change {name}" to use it for the change name',
283             name => $target->name,
284             );
285             }
286 1         7 $self->usage;
287             }
288              
289 10         34 my $note = join "\n\n", => @{ $self->note };
  10         68  
290 10         34 my ($first_change, %added, @files, %seen);
291              
292 10         20 for my $target (@{ $targets }) {
  10         40  
293 14         529 my $plan = $target->plan;
294 14         1795 my $with = $self->with_scripts;
295 14   33     305 my $tmpl = $self->all_templates($self->template_name || $target->engine_key);
296 14         370 my $file = $plan->file;
297 14   100     5149 my $spec = $added{$file} ||= { scripts => [], seen => {} };
298 14         491 my $change = $spec->{change};
299 14 100       45 if ($change) {
300             # Need a dupe for *this* target so script names are right.
301 1         26 $change = ref($change)->new(
302             plan => $plan,
303             name => $change->name,
304             );
305             } else {
306 13         130 $change = $spec->{change} = $plan->add(
307             name => $name,
308             requires => $self->requires,
309             conflicts => $self->conflicts,
310             note => $note,
311             );
312 13   66     71 $first_change ||= $change;
313             }
314              
315             # Suss out the files we'll need to write.
316 14         43 push @{ $spec->{scripts} } => map {
317 39 100       2036 push @files => $_->[1] unless $seen{$_->[1]}++;
318 39         1884 [ $_->[1], $tmpl->{ $_->[0] }, $target->engine_key, $plan->project ];
319             } grep {
320 39         2093 !$spec->{seen}{ $_->[1] }++;
321             } map {
322 39         2616 [$_ => $change->script_file($_)];
323             } grep {
324 43 100       175 !exists $with->{$_} || $with->{$_}
325 14         355 } sort keys %{ $tmpl };
  14         99  
326             }
327              
328             # Make sure we have a note.
329 10         725 $note = $first_change->request_note(
330             for => __ 'add',
331             scripts => \@files,
332             );
333              
334             # Time to write everything out.
335 10         104 for my $target (@{ $targets }) {
  10         46  
336 14         994 my $plan = $target->plan;
337 14         328 my $file = $plan->file;
338 14 100       158 my $spec = delete $added{$file} or next;
339              
340             # Write out the scripts.
341 13         412 $self->_add($name, @{ $_ }) for @{ $spec->{scripts} };
  13         63  
  39         4293  
342              
343             # We good. Set the note on all changes and write out the plan files.
344 13         2075 my $change = $spec->{change};
345 13         303 $change->note($note);
346 13         365 $plan->write_to( $plan->file );
347             $self->info(__x(
348             'Added "{change}" to {file}',
349             change => $spec->{change}->format_op_name_dependencies,
350 13         102 file => $plan->file,
351             ));
352             }
353              
354             # Let 'em at it.
355 10 100       2089 if ($self->open_editor) {
356 1         31 my $sqitch = $self->sqitch;
357 1         35 $sqitch->shell( $sqitch->editor . ' ' . $sqitch->quote_shell(@files) );
358             }
359              
360 10         2327 return $self;
361             }
362              
363             sub _add {
364 46     46   6866 my ( $self, $name, $file, $tmpl, $engine, $project ) = @_;
365 46 100       235 if (-e $file) {
366 4         199 $self->info(__x(
367             'Skipped {file}: already exists',
368             file => $file,
369             ));
370 4         685 return $self;
371             }
372              
373             # Create the directory for the file, if it does not exist.
374 42         2463 $self->_mkpath($file->dir->stringify);
375              
376             my $vars = clone {
377 42         111 %{ $self->variables },
  42         922  
378             change => $name,
379             engine => $engine,
380             project => $project,
381             requires => $self->requires,
382             conflicts => $self->conflicts,
383             };
384              
385 42 50       1903 my $fh = $file->open('>:utf8_strict') or hurl add => __x(
386             'Cannot open {file}: {error}',
387             file => $file,
388             error => $!
389             );
390              
391 2 100   2   597 if (eval 'use Template; 1') {
  1     2   19602  
  1     2   26  
  2     2   45  
  1     1   4  
  1     1   14  
  2     1   31  
  1     1   14  
  1     1   16  
  2     1   34  
  2     1   5  
  2     1   33  
  1     1   14  
  1     1   10  
  1     1   26  
  1     1   23  
  1     1   3  
  1     1   25  
  1     1   12  
  1     1   3  
  1     1   15  
  1     1   9  
  1     1   2  
  1     1   23  
  1     1   7  
  1     1   5  
  1     1   16  
  1     1   11  
  1     1   3  
  1     1   15  
  1     1   8  
  1     1   2  
  1     1   17  
  1     1   8  
  1     1   3  
  1     1   18  
  1     1   7  
  1     1   13  
  1         16  
  1         7  
  1         4  
  1         14  
  1         11  
  1         3  
  1         20  
  1         8  
  1         3  
  1         15  
  1         9  
  1         2  
  1         15  
  1         20  
  1         2  
  1         24  
  1         7  
  1         21  
  1         24  
  1         8  
  1         3  
  1         16  
  1         13  
  1         3  
  1         16  
  1         17  
  1         7  
  1         18  
  1         29  
  1         5  
  1         14  
  1         7  
  1         8  
  1         15  
  1         8  
  1         3  
  1         14  
  1         9  
  1         3  
  1         15  
  1         8  
  1         2  
  1         18  
  1         8  
  1         2  
  1         16  
  1         10  
  1         4  
  1         15  
  1         7  
  1         3  
  1         19  
  1         9  
  1         3  
  1         16  
  1         7  
  1         3  
  1         14  
  1         9  
  1         2  
  1         15  
  1         8  
  1         2  
  1         15  
  1         8  
  1         3  
  1         16  
  1         11  
  1         2  
  1         19  
  1         8  
  1         3  
  1         15  
  1         8  
  1         3  
  1         16  
  42         12552  
392 39         226 my $tt = Template->new;
393 39 100       67482 $tt->process( $self->_slurp($tmpl), $vars, $fh ) or hurl add => __x(
394             'Error executing {template}: {error}',
395             template => $tmpl,
396             error => $tt->error,
397             );
398             } else {
399 1 50   1   21 eval 'use Template::Tiny 0.11; 1' or die $@;
  1     1   2191  
  1     1   31  
  1         5  
  1         19  
  1         15  
  1         6  
  1         18  
  1         14  
  3         1068  
400 3         15 my $output = '';
401 3         13 Template::Tiny->new->process( $self->_slurp($tmpl), $vars, \$output );
402 3         1769 print $fh $output;
403             }
404              
405 41 50       240751 close $fh or hurl add => __x(
406             'Error closing {file}: {error}',
407             file => $file,
408             error => $!
409             );
410            
411             # Warn if the file name has a double extension
412 41 100       356 if ($file =~ m/\.(\w+)\.\1+$/) {
413 2         100 my $ext = $1;
414 2         14 $self->warn(__x(
415             'File {file} has a double extension of {ext}',
416             file => $file,
417             ext => $ext,
418             ));
419             }
420            
421 41         2384 $self->info(__x 'Created {file}', file => $file);
422             }
423              
424             sub _slurp {
425 42     42   3375 my ( $self, $tmpl ) = @_;
426 1 50   1   113 open my $fh, "<:utf8_strict", $tmpl or hurl add => __x(
  1         11  
  1         31  
  42         465  
427             'Cannot open {file}: {error}',
428             file => $tmpl,
429             error => $!
430             );
431 42         6531 local $/;
432 42         2549 return \<$fh>;
433             }
434              
435             1;
436              
437             __END__
438              
439             =head1 Name
440              
441             App::Sqitch::Command::add - Add a new change to Sqitch plans
442              
443             =head1 Synopsis
444              
445             my $cmd = App::Sqitch::Command::add->new(%params);
446             $cmd->execute;
447              
448             =head1 Description
449              
450             Adds a new deployment change. This will result in the creation of a scripts in
451             the deploy, revert, and verify directories. The scripts are based on
452             L<Template::Tiny> templates in F<~/.sqitch/templates/> or
453             C<$(prefix)/etc/sqitch/templates> (call C<sqitch --etc-path> to find out
454             where, exactly (e.g., C<$(sqitch --etc-path)/sqitch.conf>).
455              
456             =head1 Interface
457              
458             =head2 Class Methods
459              
460             =head3 C<options>
461              
462             my @opts = App::Sqitch::Command::add->options;
463              
464             Returns a list of L<Getopt::Long> option specifications for the command-line
465             options for the C<add> command.
466              
467             =head3 C<configure>
468              
469             my $params = App::Sqitch::Command::add->configure(
470             $config,
471             $options,
472             );
473              
474             Processes the configuration and command options and returns a hash suitable
475             for the constructor.
476              
477             =head2 Attributes
478              
479             =head3 C<change_name>
480              
481             The name of the change to be added.
482              
483             =head3 C<note>
484              
485             Text of the change note.
486              
487             =head3 C<requires>
488              
489             List of required changes.
490              
491             =head3 C<conflicts>
492              
493             List of conflicting changes.
494              
495             =head3 C<all>
496              
497             Boolean indicating whether or not to run the command against all plans in the
498             project.
499              
500             =head3 C<template_name>
501              
502             The name of the templates to use when generating scripts. Defaults to the
503             engine for which the scripts are being generated.
504              
505             =head3 C<template_directory>
506              
507             Directory in which to find the change script templates.
508              
509             =head3 C<with_scripts>
510              
511             Hash reference indicating which scripts to create.
512              
513             =head2 Instance Methods
514              
515             =head3 C<execute>
516              
517             $add->execute($command);
518              
519             Executes the C<add> command.
520              
521             =head3 C<all_templates>
522              
523             Returns a hash reference of script names mapped to template files for all
524             scripts that should be generated for the new change.
525              
526             =head1 See Also
527              
528             =over
529              
530             =item L<sqitch-add>
531              
532             Documentation for the C<add> command to the Sqitch command-line client.
533              
534             =item L<sqitch>
535              
536             The Sqitch command-line client.
537              
538             =back
539              
540             =head1 Author
541              
542             David E. Wheeler <david@justatheory.com>
543              
544             =head1 License
545              
546             Copyright (c) 2012-2023 iovation Inc., David E. Wheeler
547              
548             Permission is hereby granted, free of charge, to any person obtaining a copy
549             of this software and associated documentation files (the "Software"), to deal
550             in the Software without restriction, including without limitation the rights
551             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
552             copies of the Software, and to permit persons to whom the Software is
553             furnished to do so, subject to the following conditions:
554              
555             The above copyright notice and this permission notice shall be included in all
556             copies or substantial portions of the Software.
557              
558             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
559             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
560             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
561             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
562             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
563             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
564             SOFTWARE.
565              
566             =cut