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 50     50   1024 use 5.010;
  50         202  
4 50     50   332 use utf8;
  50         125  
  50         303  
5 50     50   1222 use namespace::autoclean;
  50         111  
  50         330  
6 50     50   3606 use Encode;
  50         148  
  50         4579  
7 50     50   367 use Moo;
  50         150  
  50         362  
8 50     50   20358 use App::Sqitch::Types qw(Str Bool Maybe Change Tag Depend UserEmail DateTime ArrayRef);
  50         223  
  50         558  
9 50     50   106418 use App::Sqitch::Plan::Depend;
  50         221  
  50         1953  
10 50     50   406 use Locale::TextDomain qw(App-Sqitch);
  50         133  
  50         382  
11             extends 'App::Sqitch::Plan::Line';
12              
13             our $VERSION = 'v1.4.0'; # 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 8594 sub requires { @{ shift->_requires } }
  1945         6760  
23              
24             has _conflicts => (
25             is => 'ro',
26             isa => ArrayRef[Depend],
27             init_arg => 'conflicts',
28             default => sub { [] },
29             );
30              
31 1145     1145 1 5724 sub conflicts { @{ shift->_conflicts } }
  1145         4355  
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 167     167 1 653 sub rework_tags { @{ shift->_rework_tags } }
  167         2802  
58             sub add_rework_tags { push @{ shift->_rework_tags } => @_ }
59 1     1 1 667 sub clear_rework_tags { @{ shift->_rework_tags } = () }
  1         26  
60 719     719 1 3161 sub is_reworked { @{ shift->_rework_tags } > 0 }
  719         11580  
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 1649     1649 1 17427 sub tags { @{ shift->_tags } }
  1649         29176  
76 582     582 1 12598 sub add_tag { push @{ shift->_tags } => @_ }
  582         11470  
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 662     662 1 48104 sub path_segments { @{ shift->_path_segments } }
  662         10865  
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 1105 my $self = shift;
173 1         4 return $self->requires, $self->conflicts;
174             }
175              
176             sub deploy_dir {
177 282     282 1 3297 my $self = shift;
178 282         5264 my $target = $self->target;
179 282 100       8049 return $self->is_reworked
180             ? $target->reworked_deploy_dir
181             : $target->deploy_dir;
182             }
183              
184             sub deploy_file {
185 244     244 1 25211 my $self = shift;
186 244         645 $self->deploy_dir->file( $self->path_segments );
187             }
188              
189             sub _deploy_hash {
190 3     3   1059 my $path = shift->deploy_file;
191 3 100       505 return undef unless -f $path;
192 2         183 require Digest::SHA;
193 2         30 my $sha = Digest::SHA->new(1);
194 2         52 $sha->add( $path->slurp(iomode => '<:raw') );
195 2         687 return $sha->hexdigest;
196             }
197              
198             sub revert_dir {
199 212     212 1 2327 my $self = shift;
200 212         4542 my $target = $self->target;
201 212 100       6124 return $self->is_reworked
202             ? $target->reworked_revert_dir
203             : $target->revert_dir;
204             }
205              
206             sub revert_file {
207 210     210 1 13415 my $self = shift;
208 210         491 $self->revert_dir->file( $self->path_segments );
209             }
210              
211             sub verify_dir {
212 147     147 1 2114 my $self = shift;
213 147         3303 my $target = $self->target;
214 147 100       4282 return $self->is_reworked
215             ? $target->reworked_verify_dir
216             : $target->verify_dir;
217             }
218              
219             sub verify_file {
220 145     145 1 42033 my $self = shift;
221 145         365 $self->verify_dir->file( $self->path_segments );
222             }
223              
224             sub script_file {
225 42     42 1 405 my ($self, $name) = @_;
226 42 100       310 if ( my $meth = $self->can("$name\_file") ) {
227 40         140 return $self->$meth;
228             }
229 2         45 return $self->target->top_dir->subdir($name)->cleanup->file(
230             $self->path_segments
231             );
232             }
233              
234             sub is_revert {
235 10     10 1 46 shift->operator eq '-';
236             }
237              
238             sub is_deploy {
239 32     32 1 8714 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 379     379 1 78277 my $self = shift;
248 379         1061 return join ' ', $self->format_name, map { $_->format_name } $self->tags;
  183         1750  
249             }
250              
251             sub format_tag_qualified_name {
252 12     12 1 1486 my $self = shift;
253 12         94 my ($tag) = $self->tags;
254 12 100       153 unless ($tag) {
255 6 100       26 ($tag) = $self->rework_tags or return $self->format_name . '@HEAD';
256             }
257 7         44 return join '', $self->format_name, $tag->format_name;
258             }
259              
260             sub format_dependencies {
261 183     183 1 335 my $self = shift;
262             my $deps = join(
263             ' ',
264 183 100       474 map { $_->as_plan_string } $self->requires, $self->conflicts
  104         360  
265             ) or return '';
266 65         459 return "[$deps]";
267             }
268              
269             sub format_name_with_dependencies {
270 34     34 1 68 my $self = shift;
271 34 100       77 my $dep = $self->format_dependencies or return $self->format_name;
272 16         74 return $self->format_name . $self->pspace . $dep;
273             }
274              
275             sub format_op_name_dependencies {
276 32     32 1 909 my $self = shift;
277 32         146 return $self->format_operator . $self->format_name_with_dependencies;
278             }
279              
280             sub format_planner {
281 1047     1047 1 12011 my $self = shift;
282 1047         8180 return join ' ', $self->planner_name, '<' . $self->planner_email . '>';
283             }
284              
285             sub deploy_handle {
286 1     1 1 673 my $self = shift;
287 1         12 $self->plan->open_script($self->deploy_file);
288             }
289              
290             sub revert_handle {
291 1     1 1 365 my $self = shift;
292 1         92 $self->plan->open_script($self->revert_file);
293             }
294              
295             sub verify_handle {
296 1     1 1 386 my $self = shift;
297 1         8 $self->plan->open_script($self->verify_file);
298             }
299              
300             sub format_content {
301 147     147 1 279 my $self = shift;
302 147   66     404 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 1828 my $self = shift;
312 1         5 my $plan = $self->plan;
313 1         4 return map { $plan->find( $_->key_name ) } $self->requires;
  2         8  
314             }
315              
316             sub conflicts_changes {
317 1     1 1 4 my $self = shift;
318 1         4 my $plan = $self->plan;
319 1         5 return map { $plan->find( $_->key_name ) } $self->conflicts;
  1         8  
320             }
321              
322             sub note_prompt {
323 2     2 1 167 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         13 join "\n ", '', @{ $p{scripts} },
  2         11  
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-2023 iovation Inc., David E. Wheeler
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