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   8007 use strict;
  3         11  
4 3     3   18 use warnings;
  3         5  
  3         76  
5 3     3   15 use utf8;
  3         6  
  3         88  
6 3     3   15 use Locale::TextDomain qw(App-Sqitch);
  3         13  
  3         19  
7 3     3   94 use App::Sqitch::X qw(hurl);
  3         8  
  3         20  
8 3     3   571 use Moo;
  3         12  
  3         26  
9 3     3   817 use App::Sqitch::Types qw(Str Int ArrayRef HashRef Dir Bool Maybe);
  3         7  
  3         17  
10 3     3   1083 use Path::Class;
  3         7  
  3         32  
11 3     3   4125 use Try::Tiny;
  3         7  
  3         142  
12 3     3   18 use Clone qw(clone);
  3         7  
  3         164  
13 3     3   19 use List::Util qw(first);
  3         7  
  3         137  
14 3     3   26 use namespace::autoclean;
  3         7  
  3         143  
15 3     3   19  
  3         5  
  3         21  
16             extends 'App::Sqitch::Command';
17             with 'App::Sqitch::Role::ContextCommand';
18              
19             our $VERSION = 'v1.3.1'; # 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   5251 hurl add => __x(
100             'Template {template} does not exist',
101 11 100       891 template => $file,
102             ) unless -e $file;
103              
104             hurl add => __x(
105             'Template {template} is not a file',
106 10 100       414 template => $file,
107             ) unless -f $file;
108              
109             return $file;
110             }
111 9         337  
112             my ($self, $config) = @_;
113             my $tmpl = $config->get_section( section => 'add.templates' );
114             $_ = _check_script $_ for values %{ $tmpl };
115 28     28   528 return $tmpl;
116 28         127 }
117 28         50  
  28         90  
118 28         298 my ($self, $name) = @_;
119             my $config = $self->sqitch->config;
120             my $tmpl = $self->templates;
121              
122 22     22 1 3769 # Read all the template directories.
123 22         466 for my $dir (
124 22         502 $self->template_directory,
125             $config->user_dir->subdir('templates'),
126             $config->system_dir->subdir('templates'),
127 22         538 ) {
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     3424 my $file = $subdir->file("$name.tmpl");
133 23         1012 $tmpl->{$script} = $file if -f $file
134 67 50       16711 }
135 67 100       279 }
136 45         293  
137 45 50       2770 # 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         700 script => $script,
143 22         52 ) 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         114 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   1062 $class->options,
172             'set|s=s%' => sub {
173 7         12 my ($opt, $key, $val) = @_;
174 7         41 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   2248 }
180 4 100       11 }
181 1 50       6 ) or $class->usage;
182 1         2 $opts{set} = \%vars if %vars;
  1         4  
183              
184 3         11 # Convert dashes to underscores.
185             for my $k (keys %opts) {
186             next unless ( my $nk = $k ) =~ s/-/_/g;
187 7 50       481 $opts{$nk} = delete $opts{$k};
188 7 100       5739 }
189              
190             # Merge with and without.
191 7         25 $opts{with_scripts} = {
192 7 100       24 ( map { $_ => 1 } qw(deploy revert verify) ),
193 1         3 ( map { $_ => 1 } @{ delete $opts{with} || [] } ),
194             ( map { $_ => 0 } @{ delete $opts{without} || [] } ),
195             };
196             return \%opts;
197             }
198 21         38  
199 1 100       6 my ( $class, $config, $opt ) = @_;
  7         31  
200 7 100       17  
  1         5  
  7         36  
201             my %params = (
202 7         87 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 20570 );
268 16 100 100     245  
269             # Check for missing name.
270 14         156 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       90 $self->usage;
279 2 100   2   10 }
  2         7  
  2         10  
  2         9  
  2         9  
280              
281 1         3 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         7 my $with = $self->with_scripts;
287             my $tmpl = $self->all_templates($self->template_name || $target->engine_key);
288             my $file = $plan->file;
289 10         19 my $spec = $added{$file} ||= { scripts => [], seen => {} };
  10         44  
290 10         31 my $change = $spec->{change};
291             if ($change) {
292 10         21 # Need a dupe for *this* target so script names are right.
  10         33  
293 14         395 $change = ref($change)->new(
294 14         1347 plan => $plan,
295 14   33     280 name => $change->name,
296 14         347 );
297 14   100     4078 } else {
298 14         381 $change = $spec->{change} = $plan->add(
299 14 100       33 name => $name,
300             requires => $self->requires,
301 1         21 conflicts => $self->conflicts,
302             note => $note,
303             );
304             $first_change ||= $change;
305             }
306 13         107  
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     54 !$spec->{seen}{ $_->[1] }++;
313             } map {
314             [$_ => $change->script_file($_)];
315             } grep {
316 14         34 !exists $with->{$_} || $with->{$_}
317 39 100       1562 } sort keys %{ $tmpl };
318 39         1540 }
319              
320 39         1704 # Make sure we have a note.
321             $note = $first_change->request_note(
322 39         2059 for => __ 'add',
323             scripts => \@files,
324 43 100       142 );
325 14         293  
  14         70  
326             # Time to write everything out.
327             for my $target (@{ $targets }) {
328             my $plan = $target->plan;
329 10         575 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         71 # We good. Set the note on all changes and write out the plan files.
  10         24  
336 14         816 my $change = $spec->{change};
337 14         264 $change->note($note);
338 14 100       116 $plan->write_to( $plan->file );
339             $self->info(__x(
340             'Added "{change}" to {file}',
341 13         340 change => $spec->{change}->format_op_name_dependencies,
  13         34  
  39         3948  
342             file => $plan->file,
343             ));
344 13         2058 }
345 13         272  
346 13         241 # 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         77 }
351              
352             return $self;
353             }
354              
355 10 100       1762 my ( $self, $name, $file, $tmpl, $engine, $project ) = @_;
356 1         29 if (-e $file) {
357 1         28 $self->info(__x(
358             'Skipped {file}: already exists',
359             file => $file,
360 10         1877 ));
361             return $self;
362             }
363              
364 44     44   2139 # Create the directory for the file, if it does not exist.
365 44 100       212 $self->_mkpath($file->dir->stringify);
366 4         169  
367             my $vars = clone {
368             %{ $self->variables },
369             change => $name,
370 4         562 engine => $engine,
371             project => $project,
372             requires => $self->requires,
373             conflicts => $self->conflicts,
374 40         1933 };
375              
376             my $fh = $file->open('>:utf8_strict') or hurl add => __x(
377 40         79 'Cannot open {file}: {error}',
  40         748  
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       1421 '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   594 my $output = '';
  1     2   17639  
  1     2   24  
  2     2   24  
  1     1   2  
  1     1   13  
  2     1   16  
  2     1   7  
  2     1   26  
  2     1   16  
  2     1   4  
  2     1   34  
  1     1   6  
  1     1   3  
  1     1   17  
  1     1   7  
  1     1   2  
  1     1   19  
  1     1   6  
  1     1   2  
  1     1   14  
  1     1   7  
  1     1   2  
  1     1   14  
  1     1   7  
  1     1   2  
  1     1   15  
  1     1   9  
  1     1   2  
  1     1   15  
  1     1   8  
  1     1   3  
  1     1   12  
  1     1   8  
  1     1   2  
  1     1   12  
  1         15  
  1         2  
  1         21  
  1         6  
  1         2  
  1         13  
  1         13  
  1         3  
  1         13  
  1         7  
  1         2  
  1         16  
  1         6  
  1         2  
  1         12  
  1         7  
  1         2  
  1         12  
  1         7  
  1         2  
  1         12  
  1         6  
  1         1  
  1         13  
  1         8  
  1         4  
  1         13  
  1         14  
  1         2  
  1         14  
  1         7  
  1         6  
  1         15  
  1         8  
  1         2  
  1         12  
  1         14  
  1         3  
  1         14  
  1         6  
  1         2  
  1         13  
  1         7  
  1         2  
  1         13  
  1         8  
  1         2  
  1         13  
  1         8  
  1         2  
  1         12  
  1         8  
  1         3  
  1         14  
  1         6  
  1         3  
  1         12  
  1         17  
  1         3  
  1         13  
  1         13  
  1         2  
  1         14  
  1         8  
  1         2  
  1         15  
  1         9  
  1         2  
  1         13  
  1         6  
  1         2  
  1         13  
  40         9804  
392 38         166 Template::Tiny->new->process( $self->_slurp($tmpl), $vars, \$output );
393 38 100       63158 print $fh $output;
394             }
395              
396             close $fh or hurl add => __x(
397             'Error closing {file}: {error}',
398             file => $file,
399 1 50   1   12 error => $!
  1     1   1713  
  1         17  
  1         7  
  1         15  
  1         12  
  2         873  
400 2         7 );
401 2         5 $self->info(__x 'Created {file}', file => $file);
402 2         1009 }
403              
404             my ( $self, $tmpl ) = @_;
405 39 50       197984 open my $fh, "<:utf8_strict", $tmpl or hurl add => __x(
406             'Cannot open {file}: {error}',
407             file => $tmpl,
408             error => $!
409             );
410 39         247 local $/;
411             return \<$fh>;
412             }
413              
414 40     40   2809 1;
415 1 50   1   86  
  1         3  
  1         22  
  40         405  
416              
417             =head1 Name
418              
419             App::Sqitch::Command::add - Add a new change to Sqitch plans
420 40         4977  
421 40         2014 =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