File Coverage

blib/lib/App/Sqitch/Command/add.pm
Criterion Covered Total %
statement 271 271 100.0
branch 50 58 86.2
condition 15 20 75.0
subroutine 61 61 100.0
pod 2 2 100.0
total 399 412 96.8


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