File Coverage

blib/lib/App/Sqitch/Plan/Change.pm
Criterion Covered Total %
statement 107 107 100.0
branch 20 20 100.0
condition 2 3 66.6
subroutine 41 41 100.0
pod 32 32 100.0
total 202 203 99.5


line stmt bran cond sub pod time code
1             package App::Sqitch::Plan::Change;
2              
3 51     51   1083 use 5.010;
  51         383  
4 51     51   316 use utf8;
  51         107  
  51         2954  
5 51     51   1886 use namespace::autoclean;
  51         123  
  51         2864  
6 51     51   4882 use Encode;
  51         183  
  51         8483  
7 51     51   2130 use Moo;
  51         182  
  51         476  
8 51     51   28169 use App::Sqitch::Types qw(Str Bool Maybe Change Tag Depend UserEmail DateTime ArrayRef);
  51         155  
  51         581  
9 51     51   229278 use App::Sqitch::Plan::Depend;
  51         221  
  51         2862  
10 51     51   475 use Locale::TextDomain qw(App-Sqitch);
  51         103  
  51         372  
11             extends 'App::Sqitch::Plan::Line';
12              
13             our $VERSION = 'v1.6.1'; # VERSION
14              
15             has _requires => (
16             is => 'ro',
17             isa => ArrayRef[Depend],
18             init_arg => 'requires',
19             default => sub { [] },
20             );
21              
22 1945     1945 1 7725 sub requires { @{ shift->_requires } }
  1945         7695  
23              
24             has _conflicts => (
25             is => 'ro',
26             isa => ArrayRef[Depend],
27             init_arg => 'conflicts',
28             default => sub { [] },
29             );
30              
31 1145     1145 1 5584 sub conflicts { @{ shift->_conflicts } }
  1145         9292  
32              
33             has pspace => (
34             is => 'ro',
35             isa => Str,
36             default => ' ',
37             );
38              
39             has since_tag => (
40             is => 'ro',
41             isa => Tag,
42             );
43              
44             has parent => (
45             is => 'ro',
46             isa => Change,
47             );
48              
49             has _rework_tags => (
50             is => 'ro',
51             isa => ArrayRef[Tag],
52             init_arg => 'rework_tags',
53             lazy => 1,
54             default => sub { [] },
55             );
56              
57 170     170 1 757 sub rework_tags { @{ shift->_rework_tags } }
  170         3188  
58             sub add_rework_tags { push @{ shift->_rework_tags } => @_ }
59 1     1 1 917 sub clear_rework_tags { @{ shift->_rework_tags } = () }
  1         36  
60 749     749 1 4000 sub is_reworked { @{ shift->_rework_tags } > 0 }
  749         14029  
61              
62             after add_rework_tags => sub {
63             my $self = shift;
64             # Need to reset the file name if a new value is passed.
65             $self->_clear_path_segments(undef);
66             };
67              
68             has _tags => (
69             is => 'ro',
70             isa => ArrayRef[Tag],
71             lazy => 1,
72             default => sub { [] },
73             );
74              
75 1644     1644 1 21244 sub tags { @{ shift->_tags } }
  1644         35171  
76 582     582 1 14884 sub add_tag { push @{ shift->_tags } => @_ }
  582         13898  
77              
78             has _path_segments => (
79             is => 'ro',
80             isa => ArrayRef[Str],
81             lazy => 1,
82             clearer => 1, # Creates _clear_path_segments().
83             default => sub {
84             my $self = shift;
85             my @path = split m{/} => $self->name;
86             my $ext = '.' . $self->target->extension;
87             if (my @rework_tags = $self->rework_tags) {
88             # Determine suffix based on the first one found in the deploy dir.
89             my $dir = $self->deploy_dir;
90             my $bn = pop @path;
91             my $first;
92             for my $tag (@rework_tags) {
93             my $fn = join '', $bn, $tag->format_name, $ext;
94             $first //= $fn;
95             if ( -e $dir->file(@path, $fn) ) {
96             push @path => $fn;
97             $first = undef;
98             last;
99             }
100             }
101             push @path => $first if defined $first;
102             } else {
103             $path[-1] .= $ext;
104             }
105             return \@path;
106             },
107             );
108              
109 692     692 1 54565 sub path_segments { @{ shift->_path_segments } }
  692         12661  
110              
111             has info => (
112             is => 'ro',
113             isa => Str,
114             lazy => 1,
115             default => sub {
116             my $self = shift;
117             my $reqs = join "\n + ", map { $_->as_string } $self->requires;
118             my $confs = join "\n - ", map { $_->as_string } $self->conflicts;
119             return join "\n", (
120             'project ' . $self->project,
121             ( $self->uri ? ( 'uri ' . $self->uri->canonical ) : () ),
122             'change ' . $self->format_name,
123             ( $self->parent ? ( 'parent ' . $self->parent->id ) : () ),
124             'planner ' . $self->format_planner,
125             'date ' . $self->timestamp->as_string,
126             ( $reqs ? "requires\n + $reqs" : ()),
127             ( $confs ? "conflicts\n - $confs" : ()),
128             ( $self->note ? ('', $self->note) : ()),
129             );
130             }
131             );
132              
133             has id => (
134             is => 'ro',
135             isa => Str,
136             lazy => 1,
137             default => sub {
138             my $content = encode_utf8 shift->info;
139             require Digest::SHA;
140             return Digest::SHA->new(1)->add(
141             'change ' . length($content) . "\0" . $content
142             )->hexdigest;
143             }
144             );
145              
146             has timestamp => (
147             is => 'ro',
148             isa => DateTime,
149             default => sub { require App::Sqitch::DateTime && App::Sqitch::DateTime->now },
150             );
151              
152             has planner_name => (
153             is => 'ro',
154             isa => Str,
155             default => sub { shift->sqitch->user_name },
156             );
157              
158             has planner_email => (
159             is => 'ro',
160             isa => UserEmail,
161             default => sub { shift->sqitch->user_email },
162             );
163              
164             has script_hash => (
165             is => 'ro',
166             isa => Maybe[Str],
167             lazy => 1,
168             builder => '_deploy_hash'
169             );
170              
171             sub dependencies {
172 1     1 1 1494 my $self = shift;
173 1         6 return $self->requires, $self->conflicts;
174             }
175              
176             sub deploy_dir {
177 300     300 1 3805 my $self = shift;
178 300         7632 my $target = $self->target;
179 300 100       9628 return $self->is_reworked
180             ? $target->reworked_deploy_dir
181             : $target->deploy_dir;
182             }
183              
184             sub deploy_file {
185 262     262 1 39465 my $self = shift;
186 262         737 $self->deploy_dir->file( $self->path_segments );
187             }
188              
189             sub _deploy_hash {
190 3     3   1509 my $path = shift->deploy_file;
191 3 100       450 return undef unless -f $path;
192 2         178 require Digest::SHA;
193 2         25 my $sha = Digest::SHA->new(1);
194 2         50 $sha->add( $path->slurp(iomode => '<:raw') );
195 2         711 return $sha->hexdigest;
196             }
197              
198             sub revert_dir {
199 224     224 1 2655 my $self = shift;
200 224         6005 my $target = $self->target;
201 224 100       7828 return $self->is_reworked
202             ? $target->reworked_revert_dir
203             : $target->revert_dir;
204             }
205              
206             sub revert_file {
207 222     222 1 14902 my $self = shift;
208 222         604 $self->revert_dir->file( $self->path_segments );
209             }
210              
211             sub verify_dir {
212 147     147 1 2416 my $self = shift;
213 147         4892 my $target = $self->target;
214 147 100       4641 return $self->is_reworked
215             ? $target->reworked_verify_dir
216             : $target->verify_dir;
217             }
218              
219             sub verify_file {
220 145     145 1 47296 my $self = shift;
221 145         415 $self->verify_dir->file( $self->path_segments );
222             }
223              
224             sub script_file {
225 42     42 1 424 my ($self, $name) = @_;
226 42 100       374 if ( my $meth = $self->can("$name\_file") ) {
227 40         145 return $self->$meth;
228             }
229 2         47 return $self->target->top_dir->subdir($name)->cleanup->file(
230             $self->path_segments
231             );
232             }
233              
234             sub is_revert {
235 10     10 1 54 shift->operator eq '-';
236             }
237              
238             sub is_deploy {
239 32     32 1 8988 shift->operator ne '-';
240             }
241              
242             sub action {
243 2 100   2 1 8 shift->is_deploy ? 'deploy' : 'revert';
244             }
245              
246             sub format_name_with_tags {
247 373     373 1 96244 my $self = shift;
248 373         1499 return join ' ', $self->format_name, map { $_->format_name } $self->tags;
  177         1991  
249             }
250              
251             sub format_tag_qualified_name {
252 12     12 1 2036 my $self = shift;
253 12         37 my ($tag) = $self->tags;
254 12 100       166 unless ($tag) {
255 6 100       26 ($tag) = $self->rework_tags or return $self->format_name . '@HEAD';
256             }
257 7         37 return join '', $self->format_name, $tag->format_name;
258             }
259              
260             sub format_dependencies {
261 183     183 1 311 my $self = shift;
262             my $deps = join(
263             ' ',
264 183 100       558 map { $_->as_plan_string } $self->requires, $self->conflicts
  104         348  
265             ) or return '';
266 65         546 return "[$deps]";
267             }
268              
269             sub format_name_with_dependencies {
270 34     34 1 69 my $self = shift;
271 34 100       86 my $dep = $self->format_dependencies or return $self->format_name;
272 16         93 return $self->format_name . $self->pspace . $dep;
273             }
274              
275             sub format_op_name_dependencies {
276 32     32 1 1079 my $self = shift;
277 32         152 return $self->format_operator . $self->format_name_with_dependencies;
278             }
279              
280             sub format_planner {
281 1047     1047 1 13342 my $self = shift;
282 1047         9931 return join ' ', $self->planner_name, '<' . $self->planner_email . '>';
283             }
284              
285             sub deploy_handle {
286 1     1 1 580 my $self = shift;
287 1         6 $self->plan->open_script($self->deploy_file);
288             }
289              
290             sub revert_handle {
291 1     1 1 413 my $self = shift;
292 1         5 $self->plan->open_script($self->revert_file);
293             }
294              
295             sub verify_handle {
296 1     1 1 403 my $self = shift;
297 1         5 $self->plan->open_script($self->verify_file);
298             }
299              
300             sub format_content {
301 147     147 1 292 my $self = shift;
302 147   66     471 return $self->SUPER::format_content . $self->pspace . join (
303             ' ',
304             ($self->format_dependencies || ()),
305             $self->timestamp->as_string,
306             $self->format_planner
307             );
308             }
309              
310             sub requires_changes {
311 1     1 1 2477 my $self = shift;
312 1         8 my $plan = $self->plan;
313 1         5 return map { $plan->find( $_->key_name ) } $self->requires;
  2         14  
314             }
315              
316             sub conflicts_changes {
317 1     1 1 5 my $self = shift;
318 1         5 my $plan = $self->plan;
319 1         4 return map { $plan->find( $_->key_name ) } $self->conflicts;
  1         6  
320             }
321              
322             sub note_prompt {
323 2     2 1 221 my ( $self, %p ) = @_;
324              
325             return join(
326             '',
327             __x(
328             "Please enter a note for your change. Lines starting with '#' will\n" .
329             "be ignored, and an empty message aborts the {command}.",
330             command => $p{for},
331             ),
332             "\n",
333             __x('Change to {command}:', command => $p{for}),
334             "\n\n",
335             ' ', $self->format_op_name_dependencies,
336 2         15 join "\n ", '', @{ $p{scripts} },
  2         16  
337             "\n",
338             );
339             }
340              
341             1;
342              
343             __END__
344              
345             =head1 Name
346              
347             App::Sqitch::Plan::Change - Sqitch deployment plan tag
348              
349             =head1 Synopsis
350              
351             my $plan = App::Sqitch::Plan->new( sqitch => $sqitch );
352             for my $line ($plan->lines) {
353             say $line->as_string;
354             }
355              
356             =head1 Description
357              
358             A App::Sqitch::Plan::Change represents a change as parsed from a plan file. In
359             addition to the interface inherited from L<App::Sqitch::Plan::Line>, it offers
360             interfaces for parsing dependencies from the deploy script, as well as for
361             opening the deploy, revert, and verify scripts.
362              
363             =head1 Interface
364              
365             See L<App::Sqitch::Plan::Line> for the basics.
366              
367             =head2 Accessors
368              
369             =head3 C<since_tag>
370              
371             An L<App::Sqitch::Plan::Tag> object representing the last tag to appear in the
372             plan B<before> the change. May be C<undef>.
373              
374             =head3 C<pspace>
375              
376             Blank space separating the change name from the dependencies, timestamp, and
377             planner in the file.
378              
379             =head3 C<is_reworked>
380              
381             Boolean indicting whether or not the change has been reworked.
382              
383             =head3 C<info>
384              
385             Information about the change, returned as a string. Includes the change ID,
386             the name and email address of the user who added the change to the plan, and
387             the timestamp for when the change was added to the plan.
388              
389             =head3 C<id>
390              
391             A SHA1 hash of the data returned by C<info()>, which can be used as a
392             globally-unique identifier for the change.
393              
394             =head3 C<timestamp>
395              
396             Returns the an L<App::Sqitch::DateTime> object representing the time at which
397             the change was added to the plan.
398              
399             =head3 C<planner_name>
400              
401             Returns the name of the user who added the change to the plan.
402              
403             =head3 C<planner_email>
404              
405             Returns the email address of the user who added the change to the plan.
406              
407             =head3 C<parent>
408              
409             Parent change object.
410              
411             =head3 C<tags>
412              
413             A list of tag objects associated with the change.
414              
415             =head2 Instance Methods
416              
417             =head3 C<path_segments>
418              
419             my @segments = $change->path_segments;
420              
421             Returns the path segment for the change. For example, if the change is named
422             "foo", C<('foo.sql')> is returned. If the change is named "functions/bar>
423             C<('functions', 'bar.sql')> is returned. Internally, this data is used to
424             create the deploy, revert, and verify file names.
425              
426             =head3 C<deploy_dir>
427              
428             my $file = $change->deploy_dir;
429              
430             Returns the path to the deploy directory for the change.
431              
432             =head3 C<deploy_file>
433              
434             my $file = $change->deploy_file;
435              
436             Returns the path to the deploy script file for the change.
437              
438             =head3 C<revert_dir>
439              
440             my $file = $change->revert_dir;
441              
442             Returns the path to the revert directory for the change.
443              
444             =head3 C<revert_file>
445              
446             my $file = $change->revert_file;
447              
448             Returns the path to the revert script file for the change.
449              
450             =head3 C<verify_dir>
451              
452             my $file = $change->verify_dir;
453              
454             Returns the path to the verify directory for the change.
455              
456             =head3 C<verify_file>
457              
458             my $file = $change->verify_file;
459              
460             Returns the path to the verify script file for the change.
461              
462             =head3 C<script_file>
463              
464             my $file = $sqitch->script_file($script_name);
465              
466             Returns the path to a script, for the change.
467              
468             =head3 C<script_hash>
469              
470             my $hash = $change->script_hash;
471              
472             Returns the hex digest of the SHA-1 hash for the deploy script.
473              
474             =head3 C<rework_tags>
475              
476             my @tags = $change->rework_tags;
477              
478             Returns a list of tags that occur between a change and its next reworking.
479             Returns an empty list if the change is not reworked.
480              
481             =head3 C<add_tag>
482              
483             $change->add_tag($tag);
484              
485             Adds a tag object to the change.
486              
487             =head3 C<add_rework_tags>
488              
489             $change->add_rework_tags(@tags);
490              
491             Adds tags to the list of rework tags.
492              
493             =head3 C<clear_rework_tags>
494              
495             $change->clear_rework_tags(@tags);
496              
497             Clears the list of rework tags.
498              
499             =head3 C<requires>
500              
501             my @requires = $change->requires;
502              
503             Returns a list of L<App::Sqitch::Plan::Depend> objects representing changes
504             required by this change.
505              
506             =head3 C<requires_changes>
507              
508             my @requires_changes = $change->requires_changes;
509              
510             Returns a list of the C<App::Sqitch::Plan::Change> objects representing
511             changes required by this change.
512              
513             =head3 C<conflicts>
514              
515             my @conflicts = $change->conflicts;
516              
517             Returns a list of L<App::Sqitch::Plan::Depend> objects representing changes
518             with which this change conflicts.
519              
520             =head3 C<conflicts_changes>
521              
522             my @conflicts_changes = $change->conflicts_changes;
523              
524             Returns a list of the C<App::Sqitch::Plan::Change> objects representing
525             changes with which this change conflicts.
526              
527             =head3 C<dependencies>
528              
529             my @dependencies = $change->dependencies;
530              
531             Returns a list of L<App::Sqitch::Plan::Depend> objects representing all
532             dependencies, required and conflicting.
533              
534             =head3 C<is_deploy>
535              
536             Returns true if the change is intended to be deployed, and false if it should be
537             reverted.
538              
539             =head3 C<is_revert>
540              
541             Returns true if the change is intended to be reverted, and false if it should be
542             deployed.
543              
544             =head3 C<action>
545              
546             Returns "deploy" if the change should be deployed, or "revert" if it should be
547             reverted.
548              
549             =head3 C<format_tag_qualified_name>
550              
551             my $tag_qualified_name = $change->format_tag_qualified_name;
552              
553             Returns a string with the change name followed by the next tag in the plan.
554             Useful for displaying unambiguous change specifications for reworked changes.
555             If there is no tag appearing in the file after the change, the C<@HEAD> will
556             be used.
557              
558             =head3 C<format_name_with_tags>
559              
560             my $name_with_tags = $change->format_name_with_tags;
561              
562             Returns a string formatted with the change name followed by the list of tags, if
563             any, associated with the change. Used to display a change as it is deployed.
564              
565             =head3 C<format_dependencies>
566              
567             my $dependencies = $change->format_dependencies;
568              
569             Returns a string containing a bracketed list of dependencies. If there are no
570             dependencies, an empty string will be returned.
571              
572             =head3 C<format_name_with_dependencies>
573              
574             my $name_with_dependencies = $change->format_name_with_dependencies;
575              
576             Returns a string formatted with the change name followed by a bracketed list
577             of dependencies, if any, associated with the change. Used to display a change
578             when added to a plan.
579              
580             =head3 C<format_op_name_dependencies>
581              
582             my $op_name_dependencies = $change->format_op_name_dependencies;
583              
584             Like C<format_name_with_dependencies>, but includes the operator, if present.
585              
586             =head3 C<format_planner>
587              
588             my $planner = $change->format_planner;
589              
590             Returns a string formatted with the name and email address of the user who
591             added the change to the plan.
592              
593             =head3 C<deploy_handle>
594              
595             my $fh = $change->deploy_handle;
596              
597             Returns an L<IO::File> file handle, opened for reading, for the deploy script
598             for the change.
599              
600             =head3 C<revert_handle>
601              
602             my $fh = $change->revert_handle;
603              
604             Returns an L<IO::File> file handle, opened for reading, for the revert script
605             for the change.
606              
607             =head3 C<verify_handle>
608              
609             my $fh = $change->verify_handle;
610              
611             Returns an L<IO::File> file handle, opened for reading, for the verify script
612             for the change.
613              
614             =head3 C<note_prompt>
615              
616             my $prompt = $change->note_prompt(
617             for => 'rework',
618             scripts => [$change->deploy_file, $change->revert_file],
619             );
620              
621             Overrides the implementation from C<App::Sqitch::Plan::Line> to add the
622             C<files> parameter. This is a list of the files to be created for the command.
623             These will usually be the deploy, revert, and verify files, but the caller
624             might not be creating all of them, so it needs to pass the list.
625              
626             =head1 See Also
627              
628             =over
629              
630             =item L<App::Sqitch::Plan>
631              
632             Class representing a plan.
633              
634             =item L<App::Sqitch::Plan::Line>
635              
636             Base class from which App::Sqitch::Plan::Change inherits.
637              
638             =item L<sqitch>
639              
640             The Sqitch command-line client.
641              
642             =back
643              
644             =head1 Author
645              
646             David E. Wheeler <david@justatheory.com>
647              
648             =head1 License
649              
650             Copyright (c) 2012-2026 David E. Wheeler, 2012-2021 iovation Inc.
651              
652             Permission is hereby granted, free of charge, to any person obtaining a copy
653             of this software and associated documentation files (the "Software"), to deal
654             in the Software without restriction, including without limitation the rights
655             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
656             copies of the Software, and to permit persons to whom the Software is
657             furnished to do so, subject to the following conditions:
658              
659             The above copyright notice and this permission notice shall be included in all
660             copies or substantial portions of the Software.
661              
662             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
663             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
664             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
665             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
666             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
667             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
668             SOFTWARE.
669              
670             =cut