File Coverage

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


line stmt bran cond sub pod time code
1             package App::Sqitch::Command::add;
2              
3 3     3   8324 use 5.010;
  3         10  
4 3     3   13 use strict;
  3         4  
  3         60  
5 3     3   10 use warnings;
  3         5  
  3         108  
6 3     3   10 use utf8;
  3         5  
  3         18  
7 3     3   86 use Locale::TextDomain qw(App-Sqitch);
  3         6  
  3         23  
8 3     3   516 use App::Sqitch::X qw(hurl);
  3         6  
  3         41  
9 3     3   795 use Moo;
  3         6  
  3         24  
10 3     3   1148 use App::Sqitch::Types qw(Str Int ArrayRef HashRef Dir Bool Maybe);
  3         26  
  3         30  
11 3     3   6787 use Path::Class;
  3         5  
  3         148  
12 3     3   14 use Try::Tiny;
  3         3  
  3         132  
13 3     3   30 use Clone qw(clone);
  3         4  
  3         147  
14 3     3   35 use List::Util qw(first);
  3         3  
  3         146  
15 3     3   12 use namespace::autoclean;
  3         3  
  3         25  
16              
17             extends 'App::Sqitch::Command';
18             with 'App::Sqitch::Role::ContextCommand';
19              
20             our $VERSION = 'v1.6.1'; # 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   4951 my $file = file shift;
100              
101 11 100       785 hurl add => __x(
102             'Template {template} does not exist',
103             template => $file,
104             ) unless -e $file;
105              
106 10 100       389 hurl add => __x(
107             'Template {template} is not a file',
108             template => $file,
109             ) unless -f $file;
110              
111 9         262 return $file;
112             }
113              
114             sub _config_templates {
115 28     28   592 my ($self, $config) = @_;
116 28         184 my $tmpl = $config->get_section( section => 'add.templates' );
117 28         50 $_ = _check_script $_ for values %{ $tmpl };
  28         95  
118 28         361 return $tmpl;
119             }
120              
121             sub all_templates {
122 23     23 1 5741 my ($self, $name) = @_;
123 23         770 my $config = $self->sqitch->config;
124 23         189 my $tmpl = { %{ $self->templates } };
  23         424  
125              
126             # Read all the template directories.
127 23         595 for my $dir (
128             $self->template_directory,
129             $config->user_dir->subdir('templates'),
130             $config->system_dir->subdir('templates'),
131             ) {
132 69 100 100     4057 next unless $dir && -d $dir;
133 24         1334 for my $subdir($dir->children) {
134 70 50       20560 next unless $subdir->is_dir;
135 70 100       383 next if $tmpl->{my $script = $subdir->basename};
136 60         400 my $file = $subdir->file("$name.tmpl");
137 60 50       4070 $tmpl->{$script} = $file if -f $file
138             }
139             }
140              
141             # Make sure we have core templates.
142 23         617 my $with = $self->with_scripts;
143 23         58 for my $script (qw(deploy revert verify)) {
144             hurl add => __x(
145             'Cannot find {script} template',
146             script => $script,
147 66 100 66     296 ) if !$tmpl->{$script} && ($with->{$script} || !exists $with->{$script});
      66        
148             }
149              
150 20         113 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   1287 my ( $class, $args ) = @_;
172              
173 7         15 my (%opts, %vars);
174 7         42 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   4067 my ($opt, $key, $val) = @_;
180 4 100       14 if (exists $vars{$key}) {
181 1 50       7 $vars{$key} = [$vars{$key}] unless ref $vars{$key};
182 1         3 push @{ $vars{$key} } => $val;
  1         25  
183             } else {
184 3         36 $vars{$key} = $val;
185             }
186             }
187 7 50       745 ) or $class->usage;
188 7 100       8726 $opts{set} = \%vars if %vars;
189              
190             # Convert dashes to underscores.
191 7         25 for my $k (keys %opts) {
192 7 100       32 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         47 ( map { $_ => 1 } qw(deploy revert verify) ),
199 1 100       4 ( map { $_ => 1 } @{ delete $opts{with} || [] } ),
  7         43  
200 7 100       84 ( map { $_ => 0 } @{ delete $opts{without} || [] } ),
  1         8  
  7         42  
201             };
202 7         135 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 28893 my $self = shift;
268 16 100 100     93 $self->usage unless @_ || $self->change_name;
269              
270 14         177 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       78 unless (defined $name) {
279 2 100   2   9 if (my $target = first { my $n = $_->name; first { $_ eq $n } @_ } @{ $targets }) {
  2         6  
  2         10  
  2         15  
  2         6  
280             # Name conflicts with a target.
281 1         3 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         23 my $note = join "\n\n", => @{ $self->note };
  10         63  
290 10         22 my ($first_change, %added, @files, %seen);
291              
292 10         20 for my $target (@{ $targets }) {
  10         43  
293 14         466 my $plan = $target->plan;
294 14         1791 my $with = $self->with_scripts;
295 14   33     363 my $tmpl = $self->all_templates($self->template_name || $target->engine_key);
296 14         499 my $file = $plan->file;
297 14   100     4671 my $spec = $added{$file} ||= { scripts => [], seen => {} };
298 14         466 my $change = $spec->{change};
299 14 100       40 if ($change) {
300             # Need a dupe for *this* target so script names are right.
301 1         22 $change = ref($change)->new(
302             plan => $plan,
303             name => $change->name,
304             );
305             } else {
306 13         169 $change = $spec->{change} = $plan->add(
307             name => $name,
308             requires => $self->requires,
309             conflicts => $self->conflicts,
310             note => $note,
311             );
312 13   66     72 $first_change ||= $change;
313             }
314              
315             # Suss out the files we'll need to write.
316 14         40 push @{ $spec->{scripts} } => map {
317 39 100       1984 push @files => $_->[1] unless $seen{$_->[1]}++;
318 39         1836 [ $_->[1], $tmpl->{ $_->[0] }, $target->engine_key, $plan->project ];
319             } grep {
320 39         1905 !$spec->{seen}{ $_->[1] }++;
321             } map {
322 39         2417 [$_ => $change->script_file($_)];
323             } grep {
324 43 100       162 !exists $with->{$_} || $with->{$_}
325 14         380 } sort keys %{ $tmpl };
  14         78  
326             }
327              
328             # Make sure we have a note.
329 10         712 $note = $first_change->request_note(
330             for => __ 'add',
331             scripts => \@files,
332             );
333              
334             # Time to write everything out.
335 10         69 for my $target (@{ $targets }) {
  10         29  
336 14         1440 my $plan = $target->plan;
337 14         400 my $file = $plan->file;
338 14 100       174 my $spec = delete $added{$file} or next;
339              
340             # Write out the scripts.
341 13         426 $self->_add($name, @{ $_ }) for @{ $spec->{scripts} };
  13         45  
  39         4180  
342              
343             # We good. Set the note on all changes and write out the plan files.
344 13         2303 my $change = $spec->{change};
345 13         355 $change->note($note);
346 13         326 $plan->write_to( $plan->file );
347             $self->info(__x(
348             'Added "{change}" to {file}',
349             change => $spec->{change}->format_op_name_dependencies,
350 13         87 file => $plan->file,
351             ));
352             }
353              
354             # Let 'em at it.
355 10 100       2127 if ($self->open_editor) {
356 1         11 my $sqitch = $self->sqitch;
357 1         24 $sqitch->shell( $sqitch->editor . ' ' . $sqitch->quote_shell(@files) );
358             }
359              
360 10         2455 return $self;
361             }
362              
363             sub _add {
364 46     46   6233 my ( $self, $name, $file, $tmpl, $engine, $project ) = @_;
365 46 100       197 if (-e $file) {
366 4         279 $self->info(__x(
367             'Skipped {file}: already exists',
368             file => $file,
369             ));
370 4         919 return $self;
371             }
372              
373             # Create the directory for the file, if it does not exist.
374 42         2642 $self->_mkpath($file->dir->stringify);
375              
376             my $vars = clone {
377 42         116 %{ $self->variables },
  42         968  
378             change => $name,
379             engine => $engine,
380             project => $project,
381             requires => $self->requires,
382             conflicts => $self->conflicts,
383             };
384              
385 42 50       9792 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   656 if (eval 'use Template; 1') {
  1     2   15288  
  1     2   18  
  2     2   26  
  1     1   3  
  1     1   16  
  2     1   26  
  1     1   1  
  1     1   13  
  2     1   18  
  2     1   6  
  2     1   49  
  1     1   7  
  1     1   2  
  1     1   14  
  1     1   8  
  1     1   9  
  1     1   19  
  1     1   7  
  1     1   2  
  1     1   13  
  1     1   6  
  1     1   3  
  1     1   14  
  1     1   5  
  1     1   2  
  1     1   14  
  1     1   5  
  1     1   2  
  1     1   13  
  1     1   10  
  1     1   3  
  1     1   21  
  1     1   6  
  1     1   2  
  1     1   15  
  1     1   6  
  1     1   2  
  1         13  
  1         6  
  1         1  
  1         14  
  1         6  
  1         1  
  1         15  
  1         6  
  1         1  
  1         14  
  1         5  
  1         2  
  1         13  
  1         10  
  1         3  
  1         25  
  1         9  
  1         3  
  1         20  
  1         8  
  1         3  
  1         19  
  1         8  
  1         3  
  1         20  
  1         10  
  1         3  
  1         20  
  1         9  
  1         3  
  1         19  
  1         6  
  1         3  
  1         13  
  1         6  
  1         2  
  1         14  
  1         5  
  1         3  
  1         26  
  1         9  
  1         3  
  1         23  
  1         6  
  1         2  
  1         14  
  1         5  
  1         3  
  1         13  
  1         5  
  1         3  
  1         17  
  1         6  
  1         2  
  1         15  
  1         7  
  1         2  
  1         17  
  1         6  
  1         2  
  1         35  
  1         6  
  1         2  
  1         13  
  1         10  
  1         3  
  1         21  
  1         8  
  1         3  
  1         23  
  1         10  
  1         3  
  1         20  
  1         9  
  1         2  
  1         20  
  42         15055  
392 39         216 my $tt = Template->new;
393 39 100       66097 $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   9 eval 'use Template::Tiny 0.11; 1' or die $@;
  1     1   2154  
  1     1   24  
  1         4  
  1         15  
  1         12  
  1         3  
  1         15  
  1         13  
  3         1075  
400 3         8 my $output = '';
401 3         12 Template::Tiny->new->process( $self->_slurp($tmpl), $vars, \$output );
402 3         2443 print $fh $output;
403             }
404              
405 41 50       217184 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       531 if ($file =~ m/\.(\w+)\.\1+$/) {
413 2         156 my $ext = $1;
414 2         13 $self->warn(__x(
415             'File {file} has a double extension of {ext}',
416             file => $file,
417             ext => $ext,
418             ));
419             }
420            
421 41         2400 $self->info(__x 'Created {file}', file => $file);
422             }
423              
424             sub _slurp {
425 42     42   3489 my ( $self, $tmpl ) = @_;
426 1 50   1   79 open my $fh, "<:utf8_strict", $tmpl or hurl add => __x(
  1         3  
  1         12  
  42         496  
427             'Cannot open {file}: {error}',
428             file => $tmpl,
429             error => $!
430             );
431 42         8769 local $/;
432 42         1940 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-2026 David E. Wheeler, 2012-2021 iovation Inc.
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