File Coverage

blib/lib/App/Sqitch/Plan.pm
Criterion Covered Total %
statement 378 378 100.0
branch 174 188 92.5
condition 57 68 83.8
subroutine 61 61 100.0
pod 29 29 100.0
total 699 724 96.5


line stmt bran cond sub pod time code
1             package App::Sqitch::Plan;
2              
3 50     50   1063 use 5.010;
  50         191  
4 50     50   291 use utf8;
  50         111  
  50         309  
5 50     50   23565 use App::Sqitch::Plan::Tag;
  50         211  
  50         1979  
6 50     50   26557 use App::Sqitch::Plan::Change;
  50         236  
  50         2095  
7 50     50   24576 use App::Sqitch::Plan::Blank;
  50         232  
  50         1842  
8 50     50   22467 use App::Sqitch::Plan::Pragma;
  50         195  
  50         1724  
9 50     50   407 use App::Sqitch::Plan::Depend;
  50         124  
  50         1381  
10 50     50   679 use Path::Class;
  50         137  
  50         3175  
11 50     50   24113 use App::Sqitch::Plan::ChangeList;
  50         163  
  50         1837  
12 50     50   22216 use App::Sqitch::Plan::LineList;
  50         144  
  50         1884  
13 50     50   373 use Locale::TextDomain qw(App-Sqitch);
  50         125  
  50         337  
14 50     50   8085 use App::Sqitch::X qw(hurl);
  50         120  
  50         401  
15 50     50   12434 use List::MoreUtils qw(uniq any);
  50         116  
  50         533  
16 50     50   61978 use namespace::autoclean;
  50         128  
  50         336  
17 50     50   3393 use Moo;
  50         138  
  50         302  
18 50     50   20114 use App::Sqitch::Types qw(Str Int HashRef ChangeList LineList Maybe Sqitch URI File Target);
  50         145  
  50         398  
19 50     50   94751 use constant SYNTAX_VERSION => '1.0.0';
  50         121  
  50         326639  
20              
21             our $VERSION = 'v1.4.0'; # VERSION
22              
23             # Like [:punct:], but excluding _. Copied from perlrecharclass.
24             my $punct = q{-!"#$%&'()*+,./:;<=>?@[\\]^`{|}~};
25             my $name_re = qr{
26             (?![$punct]) # first character isn't punctuation
27             (?: # start non-capturing group, repeated once or more ...
28             (?! # negative look ahead for...
29             [~/=%^] # symbolic reference punctuation
30             [[:digit:]]+ # digits
31             (?:$|[[:blank:]]) # eol or blank
32             ) # ...
33             [^[:blank:]:@#\\] # match a valid character
34             )+ # ... end non-capturing group
35             (?<![$punct])\b # last character isn't punctuation
36             }x;
37              
38             my $dir_sep_re = qr{/};
39              
40             my %reserved = map { $_ => undef } qw(ROOT HEAD);
41              
42 461     461 1 5696 sub name_regex { $name_re }
43              
44             has sqitch => (
45             is => 'ro',
46             isa => Sqitch,
47             required => 1,
48             weak_ref => 1,
49             );
50              
51             has target => (
52             is => 'ro',
53             isa => Target,
54             required => 1,
55             weak_ref => 1,
56             );
57              
58             has file => (
59             is => 'ro',
60             isa => File,
61             lazy => 1,
62             default => sub {
63             shift->target->plan_file
64             },
65             );
66              
67             has _plan => (
68             is => 'rw',
69             isa => HashRef,
70             builder => 'load',
71             init_arg => 'plan',
72             lazy => 1,
73             required => 1,
74             );
75              
76             has _changes => (
77             is => 'ro',
78             isa => ChangeList,
79             lazy => 1,
80             default => sub {
81             App::Sqitch::Plan::ChangeList->new(@{ shift->_plan->{changes} }),
82             },
83             );
84              
85             has _lines => (
86             is => 'ro',
87             isa => LineList,
88             lazy => 1,
89             default => sub {
90             App::Sqitch::Plan::LineList->new(@{ shift->_plan->{lines} }),
91             },
92             );
93              
94             has position => (
95             is => 'rw',
96             isa => Int,
97             default => -1,
98             );
99              
100             has project => (
101             is => 'ro',
102             isa => Str,
103             lazy => 1,
104             default => sub {
105             shift->_plan->{pragmas}{project};
106             }
107             );
108              
109             has uri => (
110             is => 'ro',
111             isa => Maybe[URI],
112             lazy => 1,
113             default => sub {
114             my $uri = shift->_plan->{pragmas}{uri} || return;
115             require URI;
116             'URI'->new($uri);
117             }
118             );
119              
120             sub parse {
121 8     8 1 1563 my ( $self, $data ) = @_;
122 8     1   160 open my $fh, '<:utf8_strict', \$data;
  1     1   9  
  1         2  
  1         10  
  1         1104  
  1         3  
  1         11  
123 8         959 $self->_plan( $self->load($fh) );
124 8         307 return $self;
125             }
126              
127             sub load {
128 283     283 1 67777 my $self = shift;
129 283         4943 my $file = $self->file;
130 283   66     41683 my $fh = shift || do {
131             hurl plan => __x('Plan file {file} does not exist', file => $file)
132             unless -e $file;
133             hurl plan => __x('Plan file {file} is not a regular file', file => $file)
134             unless -f $file;
135             $file->open('<:utf8_strict') or hurl io => __x(
136             'Cannot open {file}: {error}',
137             file => $file,
138             error => $!
139             );
140             };
141              
142 274         73893 return $self->_parse($file, $fh);
143             }
144              
145             sub _parse {
146 357     357   922730 my ( $self, $file, $fh ) = @_;
147              
148 357         3178 my @lines; # List of lines.
149             my @changes; # List of changes.
150 357         0 my @curr_changes; # List of changes since last tag.
151 357         0 my %line_no_for; # Maps tags and changes to line numbers.
152 357         0 my %change_named; # Maps change names to change objects.
153 357         0 my %tag_changes; # Maps changes in current tag section to line numbers.
154 357         0 my %pragmas; # Maps pragma names to values.
155 357         0 my $seen_version; # Have we seen a version pragma?
156 357         0 my $prev_tag; # Last seen tag.
157 357         0 my $prev_change; # Last seen change.
158              
159             # Regex to match timestamps.
160 357         2151 my $ts_re = qr/
161             (?<yr>[[:digit:]]{4}) # year
162             - # dash
163             (?<mo>[[:digit:]]{2}) # month
164             - # dash
165             (?<dy>[[:digit:]]{2}) # day
166             T # T
167             (?<hr>[[:digit:]]{2}) # hour
168             : # colon
169             (?<mi>[[:digit:]]{2}) # minute
170             : # colon
171             (?<sc>[[:digit:]]{2}) # second
172             Z # Zulu time
173             /x;
174              
175 357         1209 my $planner_re = qr/
176             (?<planner_name>[^<]+) # name
177             [[:blank:]]+ # blanks
178             <(?<planner_email>[^>]+)> # email
179             /x;
180              
181             # Use for raising syntax error exceptions.
182             my $raise_syntax_error = sub {
183 50     50   4264 hurl parse => __x(
184             'Syntax error in {file} at line {lineno}: {error}',
185             file => $file,
186             lineno => $fh->input_line_number,
187             error => shift
188             );
189 357         2134 };
190              
191             # First, find pragmas.
192 357         9994 HEADER: while ( my $line = $fh->getline ) {
193 841         37751 $line =~ s/\r?\n\z//;
194              
195             # Grab blank lines first.
196 841 100       4167 if ($line =~ /\A(?<lspace>[[:blank:]]*)(?:#[[:blank:]]*(?<note>.+)|$)/) {
197 385         10649 my $line = App::Sqitch::Plan::Blank->new( plan => $self, %+ );
198 385         62392 push @lines => $line;
199 385 100 66     10440 last HEADER if @lines && !$line->note;
200 42         1080 next HEADER;
201             }
202              
203             # Grab inline note.
204 456         4703 $line =~ s/(?<rspace>[[:blank:]]*)(?:[#][[:blank:]]*(?<note>.*))?$//;
205 456         6568 my %params = %+;
206              
207 456 50       11751 $raise_syntax_error->(
208             __ 'Invalid pragma; a blank line must come between pragmas and changes'
209             ) unless $line =~ /
210             \A # Beginning of line
211             (?<lspace>[[:blank:]]*)? # Optional leading space
212             [%] # Required %
213             (?<hspace>[[:blank:]]*)? # Optional space
214             (?<name> # followed by name consisting of...
215             [^$punct] # not punct
216             (?: # followed by...
217             [^[:blank:]=]*? # any number non-blank, non-=
218             [^$punct[:blank:]] # one not blank or punct
219             )? # ... optionally
220             ) # ... required
221             (?: # followed by value consisting of...
222             (?<lopspace>[[:blank:]]*) # Optional blanks
223             (?<operator>=) # Required =
224             (?<ropspace>[[:blank:]]*) # Optional blanks
225             (?<value>.+) # String value
226             )? # ... optionally
227             \z # end of line
228             /x;
229              
230             # XXX Die if the pragma is a dupe?
231              
232 456 100       4787 if ($+{name} eq 'syntax-version') {
    100          
233             # Set explicit version in case we write it out later. In future
234             # releases, may change parsers depending on the version.
235 94         375 $pragmas{syntax_version} = $params{value} = SYNTAX_VERSION;
236             } elsif ($+{name} eq 'project') {
237 355         1680 my $proj = $+{value};
238 355 100       6336 $raise_syntax_error->(__x(
239             qq{invalid project name "{project}": project names must not }
240             . 'begin with punctuation, contain "@", ":", "#", "\\", or blanks, '
241             . 'or end in punctuation or digits following punctuation',
242             project => $proj,
243             )) unless $proj =~ /\A$name_re\z/;
244 343         1358 $pragmas{project} = $proj;
245             } else {
246 7   100     66 $pragmas{ $+{name} } = $+{value} // 1;
247             }
248              
249 444         16542 push @lines => App::Sqitch::Plan::Pragma->new(
250             plan => $self,
251             %+,
252             %params
253             );
254 444         103217 next HEADER;
255             }
256              
257             # We should have a version pragma.
258 345 100       4107 unless ( $pragmas{syntax_version} ) {
259 251         975 unshift @lines => $self->_version_line;
260 251         44470 $pragmas{syntax_version} = SYNTAX_VERSION;
261             }
262              
263             # Should have valid project pragma.
264             hurl parse => __x(
265             'Missing %project pragma in {file}',
266             file => $file,
267 345 100       1358 ) unless $pragmas{project};
268              
269 343         7324 LINE: while ( my $line = $fh->getline ) {
270 1655         50836 $line =~ s/\r?\n\z//;
271              
272             # Grab blank lines first.
273 1655 100       7119 if ($line =~ /\A(?<lspace>[[:blank:]]*)(?:#[[:blank:]]*(?<note>.+)|$)/) {
274 273         6451 my $line = App::Sqitch::Plan::Blank->new( plan => $self, %+ );
275 273         39822 push @lines => $line;
276 273         6295 next LINE;
277             }
278              
279             # Grab inline note.
280 1382         28481 $line =~ s/(?<rspace>[[:blank:]]*)(?:[#][[:blank:]]*(?<note>.*))?$//;
281 1382         14050 my %params = %+;
282              
283             # Is it a tag or a change?
284 1382 100       7514 my $type = $line =~ /^[[:blank:]]*[@]/ ? 'tag' : 'change';
285 1382         31455 $line =~ /
286             ^ # Beginning of line
287             (?<lspace>[[:blank:]]*)? # Optional leading space
288              
289             (?: # followed by...
290             [@] # @ for tag
291             | # ...or...
292             (?<lopspace>[[:blank:]]*) # Optional blanks
293             (?<operator>[+-]) # Required + or -
294             (?<ropspace>[[:blank:]]*) # Optional blanks
295             )? # ... optionally
296              
297             (?<name>$name_re) # followed by name
298             (?<pspace>[[:blank:]]+)? # blanks
299              
300             (?: # followed by...
301             [[](?<dependencies>[^]]+)[]] # dependencies
302             [[:blank:]]* # blanks
303             )? # ... optionally
304              
305             (?: # followed by...
306             $ts_re # timestamp
307             [[:blank:]]* # blanks
308             )? # ... optionally
309              
310             (?: # followed by
311             $planner_re # planner
312             )? # ... optionally
313             $ # end of line
314             /x;
315              
316 1382         43089 %params = ( %params, %+ );
317              
318             # Raise errors for missing data.
319             $raise_syntax_error->(__(
320             qq{Invalid name; names must not begin with punctuation, }
321             . 'contain "@", ":", "#", "\\", or blanks, or end in punctuation or digits following punctuation',
322             )) if !$params{name}
323 1382 100 100     11573 || (!$params{yr} && $line =~ $ts_re);
      100        
324              
325             $raise_syntax_error->(__ 'Missing timestamp and planner name and email')
326 1357 100 100     3469 unless $params{yr} || $params{planner_name};
327 1356 100       2760 $raise_syntax_error->(__ 'Missing timestamp') unless $params{yr};
328              
329             $raise_syntax_error->(__ 'Missing planner name and email')
330 1355 100       2720 unless $params{planner_name};
331              
332             # It must not be a reserved name.
333             $raise_syntax_error->(__x(
334             '"{name}" is a reserved name',
335             name => ($type eq 'tag' ? '@' : '') . $params{name},
336 1354 50       3745 )) if exists $reserved{ $params{name} };
    100          
337              
338             # It must not look like a SHA1 hash.
339             $raise_syntax_error->(__x(
340             '"{name}" is invalid because it could be confused with a SHA1 ID',
341             name => $params{name},
342 1352 100       2918 )) if $params{name} =~ /^[0-9a-f]{40}/;
343              
344             # Assemble the timestamp.
345 1351         17759 require App::Sqitch::DateTime;
346             $params{timestamp} = App::Sqitch::DateTime->new(
347             year => delete $params{yr},
348             month => delete $params{mo},
349             day => delete $params{dy},
350             hour => delete $params{hr},
351             minute => delete $params{mi},
352             second => delete $params{sc},
353 1351         8472 time_zone => 'UTC',
354             );
355              
356 1351 100       504844 if ($type eq 'tag') {
357             # Faile if contains directory separators
358 457 100       5599 if ($params{name} =~ qr/($dir_sep_re)/) {
359             $raise_syntax_error->(__x(
360             'Tag "{tag}" contains illegal character {sep}',
361             tag => $params{name},
362 1         12 sep => $1,
363             ));
364             }
365              
366             # Fail if no changes.
367 456 100       1873 unless ($prev_change) {
368             $raise_syntax_error->(__x(
369             'Tag "{tag}" declared without a preceding change',
370             tag => $params{name},
371 1         7 ));
372             }
373              
374             # Fail on duplicate tag.
375 455         1323 my $key = '@' . $params{name};
376 455 100       1483 if ( my $at = $line_no_for{$key} ) {
377             $raise_syntax_error->(__x(
378             'Tag "{tag}" duplicates earlier declaration on line {line}',
379             tag => $params{name},
380 1         9 line => $at,
381             ));
382             }
383              
384             # Fail on dependencies.
385             $raise_syntax_error->(__x(
386             __ 'Tags may not specify dependencies'
387 454 100       1293 )) if $params{dependencies};
388              
389 453 100       1230 if (@curr_changes) {
390             # Sort all changes up to this tag by their dependencies.
391             push @changes => $self->check_changes(
392             $pragmas{project},
393 383         1504 \%line_no_for,
394             @curr_changes,
395             );
396 383         1081 @curr_changes = ();
397             }
398              
399             # Create the tag and associate it with the previous change.
400 453         11642 $prev_tag = App::Sqitch::Plan::Tag->new(
401             plan => $self,
402             change => $prev_change,
403             %params,
404             );
405              
406             # Keep track of everything and clean up.
407 453         47676 $prev_change->add_tag($prev_tag);
408 453         10303 push @lines => $prev_tag;
409 453         2479 %line_no_for = (%line_no_for, %tag_changes, $key => $fh->input_line_number);
410 453         20296 %tag_changes = ();
411             } else {
412             # Fail on duplicate change since last tag.
413 894 100       3216 if ( my $at = $tag_changes{ $params{name} } ) {
414             $raise_syntax_error->(__x(
415             'Change "{change}" duplicates earlier declaration on line {line}',
416             change => $params{name},
417 1         7 line => $at,
418             ));
419             }
420              
421             # Got dependencies?
422 893 100       2218 if (my $deps = $params{dependencies}) {
423 227         586 my (@req, @con, %seen_dep);
424 227         1374 for my $depstring (split /[[:blank:]]+/, $deps) {
425 326 100       1725 my $dep_params = App::Sqitch::Plan::Depend->parse(
426             $depstring,
427             ) or $raise_syntax_error->(__x(
428             '"{dep}" is not a valid dependency specification',
429             dep => $depstring,
430             ));
431             my $dep = App::Sqitch::Plan::Depend->new(
432             plan => $self,
433 325         1122 %{ $dep_params },
  325         7374  
434             );
435             # Prevent dupes.
436             $raise_syntax_error->(
437             __x( 'Duplicate dependency "{dep}"', dep => $depstring ),
438 325 100       38369 ) if $seen_dep{$depstring}++;
439 324 100       1193 if ($dep->conflicts) {
440 76         399 push @con => $dep;
441             } else {
442 248         934 push @req => $dep;
443             }
444             }
445 225         722 $params{requires} = \@req;
446 225         630 $params{conflicts} = \@con;
447             }
448              
449 891         3275 $tag_changes{ $params{name} } = $fh->input_line_number;
450 891 100       42976 push @curr_changes => $prev_change = App::Sqitch::Plan::Change->new(
    100          
451             plan => $self,
452             ( $prev_tag ? ( since_tag => $prev_tag ) : () ),
453             ( $prev_change ? ( parent => $prev_change ) : () ),
454             %params,
455             );
456 891         112887 push @lines => $prev_change;
457              
458 891 100       3341 if (my $duped = $change_named{ $params{name} }) {
459             # Get rework tags by change in reverse order to reworked change.
460 98         295 my @rework_tags;
461 98         656 for (my $i = $#changes; $changes[$i] ne $duped; $i--) {
462 45         363 push @rework_tags => $changes[$i]->tags;
463             }
464             # Add list of rework tags to the reworked change.
465 98         942 $duped->add_rework_tags(@rework_tags, $duped->tags);
466             }
467 891         24252 $change_named{ $params{name} } = $prev_change;
468             }
469             }
470              
471             # Sort and store any remaining changes.
472             push @changes => $self->check_changes(
473             $pragmas{project},
474 305 100       12881 \%line_no_for,
475             @curr_changes,
476             ) if @curr_changes;
477              
478             return {
479 305         15210 changes => \@changes,
480             lines => \@lines,
481             pragmas => \%pragmas,
482             };
483             }
484              
485             sub _version_line {
486 251     251   4840 App::Sqitch::Plan::Pragma->new(
487             plan => shift,
488             name => 'syntax-version',
489             operator => '=',
490             value => SYNTAX_VERSION,
491             );
492             }
493              
494             sub check_changes {
495 478     478 1 1717 my ( $self, $proj ) = ( shift, shift );
496 478 100       1500 my $seen = ref $_[0] eq 'HASH' ? shift : {};
497              
498 478         862 my %position;
499             my @invalid;
500              
501 478         746 my $i = 0;
502 478         1201 for my $change (@_) {
503 872         1200 my @bad;
504              
505             # XXX Ignoring conflicts for now.
506 872         2634 for my $dep ( $change->requires ) {
507             # Ignore dependencies on other projects.
508 265 100       1531 if ($dep->got_project) {
509             # Skip if parsed project name different from current project.
510 21 100       416 next if $dep->project ne $proj;
511             } else {
512             # Skip if an ID was passed, is it could be internal or external.
513 244 50       660 next if $dep->got_id;
514             }
515 264         846 my $key = $dep->key_name;
516              
517             # Skip it if it's a change from an earlier tag.
518 264 100       1166 if ($key =~ /.@/) {
519             # Need to look it up before the tag.
520 78         471 my ( $change, $tag ) = split /@/ => $key, 2;
521 78 50       408 if ( my $tag_at = $seen->{"\@$tag"} ) {
522 78 50       303 if ( my $change_at = $seen->{$change}) {
523 78 100       306 next if $change_at < $tag_at;
524             }
525             }
526             } else {
527             # Skip it if we've already seen it in the plan.
528 186 100 100     876 next if exists $seen->{$key} || $position{$key};
529             }
530              
531             # Hrm, unknown dependency.
532 12         39 push @bad, $key;
533             }
534 872         3616 $position{$change->name} = ++$i;
535 872 100       2308 push @invalid, [ $change->name => \@bad ] if @bad;
536             }
537              
538              
539             # Nothing bad, then go!
540 478 100       2064 return @_ unless @invalid;
541              
542             # Build up all of the error messages.
543 9         16 my @errors;
544 9         19 for my $bad (@invalid) {
545 10         159 my $change = $bad->[0];
546 10         15 my $max_delta = 0;
547 10         14 for my $dep (@{ $bad->[1] }) {
  10         23  
548 12 100       277 if ($change eq $dep) {
    100          
549 1         6 push @errors => __x(
550             'Change "{change}" cannot require itself',
551             change => $change,
552             );
553             } elsif (my $pos = $position{ $dep }) {
554 8         18 my $delta = $pos - $position{$change};
555 8 100       20 $max_delta = $delta if $delta > $max_delta;
556 8         30 push @errors => __xn(
557             'Change "{change}" planned {num} change before required change "{required}"',
558             'Change "{change}" planned {num} changes before required change "{required}"',
559             $delta,
560             change => $change,
561             required => $dep,
562             num => $delta,
563             );
564             } else {
565 3         16 push @errors => __x(
566             'Unknown change "{required}" required by change "{change}"',
567             required => $dep,
568             change => $change,
569             );
570             }
571             }
572 10 100       1157 if ($max_delta) {
573             # Suggest that the change be moved.
574             # XXX Potentially offer to move it and rewrite the plan.
575 6         174 $errors[-1] .= "\n " . __xn(
576             'HINT: move "{change}" down {num} line in {plan}',
577             'HINT: move "{change}" down {num} lines in {plan}',
578             $max_delta,
579             change => $change,
580             num => $max_delta,
581             plan => $self->file,
582             );
583             }
584             }
585              
586             # Throw the exception with all of the errors.
587 9         874 hurl parse => join(
588             "\n ",
589             __n(
590             'Dependency error detected:',
591             'Dependency errors detected:',
592             @errors
593             ),
594             @errors,
595             );
596             }
597              
598             sub open_script {
599 6     6 1 3898 my ( $self, $file ) = @_;
600             # return has higher precedence than or, so use ||.
601 6   66     22 return $file->open('<:utf8_strict') || hurl io => __x(
602             'Cannot open {file}: {error}',
603             file => $file,
604             error => $!,
605             );
606             }
607              
608 1     1 1 907 sub syntax_version { shift->_plan->{pragmas}{syntax_version} };
609 59     59 1 2899 sub lines { shift->_lines->items }
610 37     37 1 98144 sub changes { shift->_changes->changes }
611 9     9 1 888 sub tags { shift->_changes->tags }
612 62     62 1 28186 sub count { shift->_changes->count }
613 123     123 1 7033 sub index_of { shift->_changes->index_of(shift) }
614 137     137 1 36448 sub get { shift->_changes->get(shift) }
615 150     150 1 99271 sub contains { shift->_changes->contains( shift ) }
616 62     62 1 12862 sub find { shift->_changes->find(shift) }
617 48     48 1 1030 sub first_index_of { shift->_changes->first_index_of(@_) }
618 195     195 1 24341 sub change_at { shift->_changes->change_at(shift) }
619 15     15 1 964 sub last_tagged_change { shift->_changes->last_tagged_change }
620              
621             sub search_changes {
622 19     19 1 17424 my ( $self, %p ) = @_;
623              
624 19         40 my $reverse = 0;
625 19 100       56 if (my $d = delete $p{direction}) {
626 6 100       43 $reverse = $d =~ /^ASC/i ? 0
    100          
627             : $d =~ /^DESC/i ? 1
628             : hurl 'Search direction must be either "ASC" or "DESC"';
629             }
630              
631             # Limit with regular expressions?
632 18         36 my @filters;
633 18 100       43 if (my $regex = delete $p{planner}) {
634 2         30 $regex = qr/$regex/;
635 2     8   18 push @filters => sub { $_[0]->planner_name =~ $regex };
  8         90  
636             }
637 18 100       45 if (my $regex = delete $p{name}) {
638 3         46 $regex = qr/$regex/;
639 3     12   21 push @filters => sub { $_[0]->name =~ $regex };
  12         107  
640             }
641              
642             # Match events?
643 18 100 100     83 if (my $op = lc(delete $p{operation} || '') ) {
644 8     8   28 push @filters => $op eq 'deploy' ? sub { $_[0]->is_deploy }
645 8     8   25 : $op eq 'revert' ? sub { $_[0]->is_revert }
646 5 100       65 : hurl qq{Unknown change operation "$op"};
    100          
647             }
648              
649 17         409 my $changes = $self->_changes;
650 17   100     191 my $offset = delete $p{offset} || 0;
651 17   100     48 my $limit = delete $p{limit} || 0;
652              
653 17 50       39 hurl 'Invalid parameters passed to search_changes(): '
654             . join ', ', sort keys %p if %p;
655              
656             # If no filters, we want to return everything.
657 17 100   20   60 push @filters => sub { 1 } unless @filters;
  20         85  
658              
659 17 100       41 if ($reverse) {
660             # Go backwards.
661 4         13 my $index = $changes->count - ($offset + 1);
662 4 100       20 my $end_at = $limit ? $index - $limit : -1;
663             return sub {
664 13     13   71 while ($index > $end_at) {
665 9 50       33 my $change = $changes->change_at($index--) or return;
666 9 50       37 return $change if any { $_->($change) } @filters;
  9         20  
667             }
668 4         8 return;
669 4         52 };
670             }
671              
672 13         22 my $index = $offset - 1;
673 13 100       49 my $end_at = $limit ? $index + $limit : $changes->count - 1;
674             return sub {
675 40     40   247 while ($index < $end_at) {
676 47 50       139 my $change = $changes->change_at(++$index) or return;
677 47 100       171 return $change if any { $_->($change) } @filters;
  47         97  
678             }
679 13         26 return;
680 13         173 };
681             }
682              
683             sub seek {
684 7     7 1 780 my ( $self, $key ) = @_;
685 7         20 my $index = $self->index_of($key);
686 6 100       30 hurl plan => __x(
687             'Cannot find change "{change}" in plan',
688             change => $key,
689             ) unless defined $index;
690 5         99 $self->position($index);
691 5         183 return $self;
692             }
693              
694             sub reset {
695 31     31 1 70707 my $self = shift;
696 31         668 $self->position(-1);
697 31         973 return $self;
698             }
699              
700             sub next {
701 151     151 1 6841 my $self = shift;
702 151 100       361 if ( my $next = $self->peek ) {
703 125         1967 $self->position( $self->position + 1 );
704 125         4958 return $next;
705             }
706 26 100       125 $self->position( $self->position + 1 ) if defined $self->current;
707 26         1058 return undef;
708             }
709              
710             sub current {
711 92     92 1 78728 my $self = shift;
712 92         1546 my $pos = $self->position;
713 92 100       622 return if $pos < 0;
714 90         1451 $self->_changes->change_at( $pos );
715             }
716              
717             sub peek {
718 155     155 1 882 my $self = shift;
719 155         3102 $self->_changes->change_at( $self->position + 1 );
720             }
721              
722             sub last {
723 9     9 1 2036 shift->_changes->change_at( -1 );
724             }
725              
726             sub do {
727 2     2 1 7 my ( $self, $code ) = @_;
728 2         6 while ( local $_ = $self->next ) {
729 4 50       12 return unless $code->($_);
730             }
731             }
732              
733             sub tag {
734 35     35 1 56532 my ( $self, %p ) = @_;
735 35         157 ( my $name = $p{name} ) =~ s/^@//;
736 35         119 $self->_is_valid(tag => $name);
737              
738 19         422 my $changes = $self->_changes;
739 19         345 my $key = "\@$name";
740              
741 19 100       82 hurl plan => __x(
742             'Tag "{tag}" already exists',
743             tag => $key
744             ) if defined $changes->index_of($key);
745              
746 18         48 my $change;
747 18 100       68 if (my $spec = $p{change}) {
748 3 50       16 $change = $changes->get($spec) or hurl plan => __x(
749             'Unknown change: "{change}"',
750             change => $spec,
751             );
752             } else {
753 15 50       56 $change = $changes->last_change or hurl plan => __x(
754             'Cannot apply tag "{tag}" to a plan with no changes',
755             tag => $key
756             );
757             }
758              
759 18         425 my $tag = App::Sqitch::Plan::Tag->new(
760             %p,
761             plan => $self,
762             name => $name,
763             change => $change,
764             );
765              
766 18         6473 $change->add_tag($tag);
767 18         489 $changes->index_tag( $changes->index_of( $change->id ), $tag );
768              
769             # Add tag to line list, after the change and any preceding tags.
770 18         359 my $lines = $self->_lines;
771 18         405 $lines->insert_at( $tag, $lines->index_of($change) + $change->tags );
772 18         107 return $tag;
773             }
774              
775             sub _parse_deps {
776 47     47   142 my ( $self, $p ) = @_;
777             # Dependencies must be parsed into objects.
778             $p->{requires} = [ map {
779 10   100     74 my $p = App::Sqitch::Plan::Depend->parse($_) // hurl plan => __x(
780             '"{dep}" is not a valid dependency specification',
781             dep => $_,
782             );
783             App::Sqitch::Plan::Depend->new(
784 8         33 %{ $p },
  8         172  
785             plan => $self,
786             conflicts => 0,
787             );
788 47 100       169 } uniq @{ $p->{requires} } ] if $p->{requires};
  34         239  
789              
790             $p->{conflicts} = [ map {
791 7   66     233 my $p = App::Sqitch::Plan::Depend->parse("!$_") // hurl plan => __x(
792             '"{dep}" is not a valid dependency specification',
793             dep => $_,
794             );
795             App::Sqitch::Plan::Depend->new(
796 5         22 %{ $p },
  5         115  
797             plan => $self,
798             conflicts => 1,
799             );
800 45 100       1173 } uniq @{ $p->{conflicts} } ] if $p->{conflicts};
  29         119  
801             }
802              
803             sub add {
804 44     44 1 83857 my ( $self, %p ) = @_;
805 44         204 $self->_is_valid(change => $p{name});
806 29         659 my $changes = $self->_changes;
807              
808 29 100       650 if ( defined( my $idx = $changes->index_of( $p{name} . '@HEAD' ) ) ) {
809 1         5 my $tag_idx = $changes->index_of_last_tagged;
810             hurl plan => __x(
811             qq{Change "{change}" already exists in plan {file}.\n}
812             . 'Use "sqitch rework" to copy and rework it',
813             change => $p{name},
814 1         23 file => $self->file,
815             );
816             }
817              
818 28         157 $self->_parse_deps(\%p);
819 26         817 my $change = App::Sqitch::Plan::Change->new( %p, plan => $self );
820              
821             # Make sure dependencies are valid.
822 26         9843 $self->_check_dependencies( $change, 'add' );
823              
824             # We good. Append a blank line if the previous change has a tag.
825 24 100       158 if ( $changes->count ) {
826 15         60 my $prev = $changes->change_at( $changes->count - 1 );
827 15 100       62 if ( $prev->tags ) {
828 3         113 $self->_lines->append(
829             App::Sqitch::Plan::Blank->new( plan => $self )
830             );
831             }
832             }
833              
834             # Append the change and return.
835 24         238 $changes->append( $change );
836 24         485 $self->_lines->append( $change );
837 24         149 return $change;
838             }
839              
840             sub rework {
841 23     23 1 15249 my ( $self, %p ) = @_;
842 23         615 my $changes = $self->_changes;
843             my $idx = $changes->index_of( $p{name} . '@HEAD') // hurl plan => __x(
844             qq{Change "{change}" does not exist in {file}.\n}
845             . 'Use "sqitch add {change}" to add it to the plan',
846             change => $p{name},
847 23   100     714 file => $self->file,
848             );
849              
850 21         96 my $tag_idx = $changes->index_of_last_tagged;
851             hurl plan => __x(
852             qq{Cannot rework "{change}" without an intervening tag.\n}
853             . 'Use "sqitch tag" to create a tag and try again',
854             change => $p{name},
855 21 100 100     200 ) if !defined $tag_idx || $tag_idx < $idx;
856              
857 19         173 $self->_parse_deps(\%p);
858              
859 17         226 my ($tag) = $changes->change_at($tag_idx)->tags;
860 17   100     564 unshift @{ $p{requires} ||= [] } => App::Sqitch::Plan::Depend->new(
861             plan => $self,
862             change => $p{name},
863 17         217 tag => $tag->name,
864             );
865              
866 17         2865 my $orig = $changes->change_at($idx);
867 17         401 my $new = App::Sqitch::Plan::Change->new( %p, plan => $self );
868              
869             # Make sure dependencies are valid.
870 17         6435 $self->_check_dependencies( $new, 'rework' );
871              
872             # We good.
873 16         429 $orig->add_rework_tags($tag);
874 16         493 $changes->append( $new );
875 16         339 $self->_lines->append( $new );
876 16         123 return $new;
877             }
878              
879             sub _check_dependencies {
880 53     53   6440 my ( $self, $change, $action ) = @_;
881 53         1148 my $changes = $self->_changes;
882 53         1341 my $project = $self->project;
883 53         1058 for my $req ( $change->requires ) {
884 35 100       700 next if $req->project ne $project;
885 32         1392 $req = $req->key_name;
886 32 100       306 next if defined $changes->index_of($req =~ /@/ ? $req : $req . '@HEAD');
    100          
887 6         24 my $name = $change->name;
888 6 100       20 if ($action eq 'add') {
889 2         11 hurl plan => __x(
890             'Cannot add change "{change}": requires unknown change "{req}"',
891             change => $name,
892             req => $req,
893             );
894             } else {
895 4         20 hurl plan => __x(
896             'Cannot rework change "{change}": requires unknown change "{req}"',
897             change => $name,
898             req => $req,
899             );
900             }
901             }
902 47         176 return $self;
903             }
904              
905             sub _is_valid {
906 101     101   12843 my ( $self, $type, $name ) = @_;
907             hurl plan => __x(
908             '"{name}" is a reserved name',
909             name => $name
910 101 100       353 ) if exists $reserved{$name};
911 97 100       293 hurl plan => __x(
912             '"{name}" is invalid because it could be confused with a SHA1 ID',
913             name => $name,
914             ) if $name =~ /^[0-9a-f]{40}/;
915              
916 95 100 100     2647 if ($type eq 'change' && $name !~ /\A$name_re\z/) {
    100 100        
      66        
917 12         57 hurl plan => __x(
918             qq{"{name}" is invalid: changes must not begin with punctuation, }
919             . 'contain "@", ":", "#", "\\", or blanks, or end in punctuation or digits following punctuation',
920             name => $name,
921             );
922             } elsif ($type eq 'tag' && ($name !~ /\A$name_re\z/ || $name =~ $dir_sep_re)) {
923 25         136 hurl plan => __x(
924             qq{"{name}" is invalid: tags must not begin with punctuation, }
925             . 'contain "@", ":", "#", "/", "\\", or blanks, or end in punctuation or digits following punctuation',
926             name => $name,
927             );
928             }
929 58         244 return 1;
930             }
931              
932             sub write_to {
933 52     52 1 5336 my ( $self, $file, $from, $to ) = @_;
934              
935 52         269 my @lines = $self->lines;
936              
937 52 100 100     408 if (defined $from || defined $to) {
938 11         216 my $lines = $self->_lines;
939              
940             # Where are the pragmas?
941 11         110 my $head_ends_at = do {
942 11         25 my $i = 0;
943 11         48 while ( my $line = $lines[$i] ) {
944 37 100 100     515 last if $line->isa('App::Sqitch::Plan::Blank')
945             && !length $line->note;
946 26         94 ++$i;
947             }
948 11         125 $i;
949             };
950              
951             # Where do we start with the changes?
952 11 100       46 my $from_idx = defined $from ? do {
953 9   50     64 my $change = $self->find($from // '@ROOT') // hurl plan => __x(
      33        
954             'Cannot find change {change}',
955             change => $from,
956             );
957 9         41 $lines->index_of($change);
958             } : $head_ends_at + 1;
959              
960             # Where do we end up?
961 11 100       44 my $to_idx = defined $to ? do {
962 6   50     30 my $change = $self->find( $to // '@HEAD' ) // hurl plan => __x(
      33        
963             'Cannot find change {change}',
964             change => $to,
965             );
966              
967             # Include any subsequent tags.
968 6 100       34 if (my @tags = $change->tags) {
969 3         38 $change = $tags[-1];
970             }
971 6         56 $lines->index_of($change);
972             } : $#lines;
973              
974             # Collect the lines to write.
975 11         66 @lines = (
976             @lines[ 0 .. $head_ends_at ],
977             @lines[ $from_idx .. $to_idx ],
978             );
979             }
980              
981 52 50       228 my $fh = $file->open('>:utf8_strict') or hurl io => __x(
982             'Cannot open {file}: {error}',
983             file => $file,
984             error => $!
985             );
986 52         29162 $fh->say($_->as_string) for @lines;
987 52 50       831 $fh->close or hurl io => __x(
988             '"Error closing {file}: {error}',
989             file => $file,
990             error => $!,
991             );
992 52         6375 return $self;
993             }
994              
995             1;
996              
997             __END__
998              
999             =head1 Name
1000              
1001             App::Sqitch::Plan - Sqitch Deployment Plan
1002              
1003             =head1 Synopsis
1004              
1005             my $plan = App::Sqitch::Plan->new( sqitch => $sqitch );
1006             while (my $change = $plan->next) {
1007             say "Deploy ", $change->format_name;
1008             }
1009              
1010             =head1 Description
1011              
1012             App::Sqitch::Plan provides the interface for a Sqitch plan. It parses a plan
1013             file and provides an iteration interface for working with the plan.
1014              
1015             =head1 Interface
1016              
1017             =head2 Constants
1018              
1019             =head3 C<SYNTAX_VERSION>
1020              
1021             Returns the current version of the Sqitch plan syntax. Used for the
1022             C<%sytax-version> pragma.
1023              
1024             =head2 Class Methods
1025              
1026             =head3 C<name_regex>
1027              
1028             die "$this has no name" unless $this =~ App::Sqitch::Plan->name_regex;
1029              
1030             Returns a regular expression that matches names. Note that it is not anchored,
1031             so if you need to make sure that a string is a valid name and nothing else,
1032             you will need to anchor it yourself, like so:
1033              
1034             my $name_re = App::Sqitch::Plan->name_regex;
1035             die "$this is not a valid name" if $this !~ /\A$name_re\z/;
1036              
1037             =head2 Constructors
1038              
1039             =head3 C<new>
1040              
1041             my $plan = App::Sqitch::Plan->new( sqitch => $sqitch );
1042              
1043             Instantiates and returns a App::Sqitch::Plan object. Takes a single parameter:
1044             an L<App::Sqitch> object.
1045              
1046             =head2 Accessors
1047              
1048             =head3 C<sqitch>
1049              
1050             my $sqitch = $plan->sqitch;
1051              
1052             Returns the L<App::Sqitch> object that instantiated the plan.
1053              
1054             =head3 C<target>
1055              
1056             my $target = $plan->target
1057              
1058             Returns the L<App::Sqitch::Target> passed to the constructor.
1059              
1060             =head3 C<file>
1061              
1062             my $file = $plan->file;
1063              
1064             The file name from which to read the plan.
1065              
1066             =head3 C<position>
1067              
1068             Returns the current position of the iterator. This is an integer that's used
1069             as an index into plan. If C<next()> has not been called, or if C<reset()> has
1070             been called, the value will be -1, meaning it is outside of the plan. When
1071             C<next> returns C<undef>, the value will be the last index in the plan plus 1.
1072              
1073             =head3 C<project>
1074              
1075             my $project = $plan->project;
1076              
1077             Returns the name of the project as set via the C<%project> pragma in the plan
1078             file.
1079              
1080             =head3 C<uri>
1081              
1082             my $uri = $plan->uri;
1083              
1084             Returns the URI for the project as set via the C<%uri> pragma, which is
1085             optional. If it is not present, C<undef> will be returned.
1086              
1087             =head3 C<syntax_version>
1088              
1089             my $syntax_version = $plan->syntax_version;
1090              
1091             Returns the plan syntax version, which is always the latest version.
1092              
1093             =head2 Instance Methods
1094              
1095             =head3 C<index_of>
1096              
1097             my $index = $plan->index_of('6c2f28d125aff1deea615f8de774599acf39a7a1');
1098             my $foo_index = $plan->index_of('@foo');
1099             my $bar_index = $plan->index_of('bar');
1100             my $bar1_index = $plan->index_of('bar@alpha')
1101             my $bar2_index = $plan->index_of('bar@HEAD');
1102              
1103             Returns the index of the specified change. Returns C<undef> if no such change
1104             exists. The argument may be any one of:
1105              
1106             =over
1107              
1108             =item * An ID
1109              
1110             my $index = $plan->index_of('6c2f28d125aff1deea615f8de774599acf39a7a1');
1111              
1112             This is the SHA1 hash of a change or tag. Currently, the full 40-character hexed
1113             hash string must be specified.
1114              
1115             =item * A change name
1116              
1117             my $index = $plan->index_of('users_table');
1118              
1119             The name of a change. Will throw an exception if the named change appears more
1120             than once in the list.
1121              
1122             =item * A tag name
1123              
1124             my $index = $plan->index_of('@beta1');
1125              
1126             The name of a tag, including the leading C<@>.
1127              
1128             =item * A tag-qualified change name
1129              
1130             my $index = $plan->index_of('users_table@beta1');
1131              
1132             The named change as it was last seen in the list before the specified tag.
1133              
1134             =back
1135              
1136             =head3 C<contains>
1137              
1138             say 'Yes!' if $plan->contains('6c2f28d125aff1deea615f8de774599acf39a7a1');
1139              
1140             Like C<index_of()>, but never throws an exception, and returns true if the
1141             plan contains the specified change, and false if it does not.
1142              
1143             =head3 C<get>
1144              
1145             my $change = $plan->get('6c2f28d125aff1deea615f8de774599acf39a7a1');
1146             my $foo = $plan->get('@foo');
1147             my $bar = $plan->get('bar');
1148             my $bar1 = $plan->get('bar@alpha')
1149             my $bar2 = $plan->get('bar@HEAD');
1150              
1151             Returns the change corresponding to the specified ID or name. The argument may
1152             be in any of the formats described for C<index_of()>.
1153              
1154             =head3 C<find>
1155              
1156             my $change = $plan->find('6c2f28d125aff1deea615f8de774599acf39a7a1');
1157             my $foo = $plan->find('@foo');
1158             my $bar = $plan->find('bar');
1159             my $bar1 = $plan->find('bar@alpha')
1160             my $bar2 = $plan->find('bar@HEAD');
1161              
1162             Finds the change corresponding to the specified ID or name. The argument may be
1163             in any of the formats described for C<index_of()>. Unlike C<get()>, C<find()>
1164             will not throw an error if more than one change exists with the specified name,
1165             but will return the first instance.
1166              
1167             =head3 C<first_index_of>
1168              
1169             my $index = $plan->first_index_of($change_name);
1170             my $index = $plan->first_index_of($change_name, $change_or_tag_name);
1171              
1172             Returns the index of the first instance of the named change in the plan. If a
1173             second argument is passed, the index of the first instance of the change
1174             I<after> the index of the second argument will be returned. This is useful
1175             for getting the index of a change as it was deployed after a particular tag, for
1176             example, to get the first index of the F<foo> change since the C<@beta> tag, do
1177             this:
1178              
1179             my $index = $plan->first_index_of('foo', '@beta');
1180              
1181             You can also specify the first instance of a change after another change,
1182             including such a change at the point of a tag:
1183              
1184             my $index = $plan->first_index_of('foo', 'users_table@beta1');
1185              
1186             The second argument must unambiguously refer to a single change in the plan. As
1187             such, it should usually be a tag name or tag-qualified change name. Returns
1188             C<undef> if the change does not appear in the plan, or if it does not appear
1189             after the specified second argument change name.
1190              
1191             =head3 C<last_tagged_change>
1192              
1193             my $change = $plan->last_tagged_change;
1194              
1195             Returns the last tagged change object. Returns C<undef> if no changes have
1196             been tagged.
1197              
1198             =head3 C<change_at>
1199              
1200             my $change = $plan->change_at($index);
1201              
1202             Returns the change at the specified index.
1203              
1204             =head3 C<seek>
1205              
1206             $plan->seek('@foo');
1207             $plan->seek('bar');
1208              
1209             Move the plan position to the specified change. Dies if the change cannot be found
1210             in the plan.
1211              
1212             =head3 C<reset>
1213              
1214             $plan->reset;
1215              
1216             Resets iteration. Same as C<< $plan->position(-1) >>, but better.
1217              
1218             =head3 C<next>
1219              
1220             while (my $change = $plan->next) {
1221             say "Deploy ", $change->format_name;
1222             }
1223              
1224             Returns the next L<change|App::Sqitch::Plan::Change> in the plan. Returns C<undef>
1225             if there are no more changes.
1226              
1227             =head3 C<last>
1228              
1229             my $change = $plan->last;
1230              
1231             Returns the last change in the plan. Does not change the current position.
1232              
1233             =head3 C<current>
1234              
1235             my $change = $plan->current;
1236              
1237             Returns the same change as was last returned by C<next()>. Returns C<undef> if
1238             C<next()> has not been called or if the plan has been reset.
1239              
1240             =head3 C<peek>
1241              
1242             my $change = $plan->peek;
1243              
1244             Returns the next change in the plan without incrementing the iterator. Returns
1245             C<undef> if there are no more changes beyond the current change.
1246              
1247             =head3 C<changes>
1248              
1249             my @changes = $plan->changes;
1250              
1251             Returns all of the changes in the plan. This constitutes the entire plan.
1252              
1253             =head3 C<tags>
1254              
1255             my @tags = $plan->tags;
1256              
1257             Returns all of the tags in the plan.
1258              
1259             =head3 C<count>
1260              
1261             my $count = $plan->count;
1262              
1263             Returns the number of changes in the plan.
1264              
1265             =head3 C<lines>
1266              
1267             my @lines = $plan->lines;
1268              
1269             Returns all of the lines in the plan. This includes all the
1270             L<changes|App::Sqitch::Plan::Change>, L<tags|App::Sqitch::Plan::Tag>,
1271             L<pragmas|App::Sqitch::Plan::Pragma>, and L<blank
1272             lines|App::Sqitch::Plan::Blank>.
1273              
1274             =head3 C<do>
1275              
1276             $plan->do(sub { say $_[0]->name; return $_[0]; });
1277             $plan->do(sub { say $_->name; return $_; });
1278              
1279             Pass a code reference to this method to execute it for each change in the plan.
1280             Each change will be stored in C<$_> before executing the code reference, and
1281             will also be passed as the sole argument. If C<next()> has been called prior
1282             to the call to C<do()>, then only the remaining changes in the iterator will
1283             passed to the code reference. Iteration terminates when the code reference
1284             returns false, so be sure to have it return a true value if you want it to
1285             iterate over every change.
1286              
1287             =head3 C<search_changes>
1288              
1289             my $iter = $engine->search_changes( %params );
1290             while (my $change = $iter->()) {
1291             say '* $change->{event}ed $change->{change}";
1292             }
1293              
1294             Searches the changes in the plan returns an iterator code reference with the
1295             results. If no parameters are provided, a list of all changes will be returned
1296             from the iterator in plan order. The supported parameters are:
1297              
1298             =over
1299              
1300             =item C<event>
1301              
1302             An array of the type of event to search for. Allowed values are "deploy" and
1303             "revert".
1304              
1305             =item C<name>
1306              
1307             Limit the results to changes with names matching the specified regular
1308             expression.
1309              
1310             =item C<planner>
1311              
1312             Limit the changes to those added by planners matching the specified regular
1313             expression.
1314              
1315             =item C<limit>
1316              
1317             Limit the number of changes to the specified number.
1318              
1319             =item C<offset>
1320              
1321             Skip the specified number of events.
1322              
1323             =item C<direction>
1324              
1325             Return the results in the specified order, which must be a value matching
1326             C</^(:?a|de)sc/i> for "ascending" or "descending".
1327              
1328             =back
1329              
1330             =head3 C<write_to>
1331              
1332             $plan->write_to($file);
1333             $plan->write_to($file, $from, $to);
1334              
1335             Write the plan to the named file, including notes and white space from the
1336             original plan file. If C<from> and/or C<$to> are provided, the plan will be
1337             written only with the pragmas headers and the lines between those specified
1338             changes.
1339              
1340             =head3 C<open_script>
1341              
1342             my $file_handle = $plan->open_script( $change->deploy_file );
1343              
1344             Opens the script file passed to it and returns a file handle for reading. The
1345             script file must be encoded in UTF-8.
1346              
1347             =head3 C<load>
1348              
1349             my $plan_data = $plan->load;
1350              
1351             Loads the plan data. Called internally, not meant to be called directly, as it
1352             parses the plan file and deploy scripts every time it's called. If you want
1353             the all of the changes, call C<changes()> instead. And if you want to load an
1354             alternate plan, use C<parse()>.
1355              
1356             =head3 C<parse>
1357              
1358             $plan->parse($plan_data);
1359              
1360             Load an alternate plan by passing the complete text of the plan. The text
1361             should be UTF-8 encoded. Useful for loading a plan from a different VCS
1362             branch, for example.
1363              
1364             =head3 C<check_changes>
1365              
1366             @changes = $plan->check_changes( $project, @changes );
1367             @changes = $plan->check_changes( $project, { '@foo' => 1 }, @changes );
1368              
1369             Checks a list of changes to validate their dependencies and returns them. If
1370             the second argument is a hash reference, its keys should be previously-seen
1371             change and tag names that can be assumed to be satisfied requirements for the
1372             succeeding changes.
1373              
1374             =head3 C<tag>
1375              
1376             $plan->tag( name => 'whee' );
1377              
1378             Tags a change in the plan. Exits with a fatal error if the tag already exists
1379             in the plan or if a change cannot be found to tag. The supported parameters
1380             are:
1381              
1382             =over
1383              
1384             =item C<name>
1385              
1386             The tag name to use. Required.
1387              
1388             =item C<change>
1389              
1390             The change to be tagged, specified as a supported change specification as
1391             described in L<sqitchchanges>. Defaults to the last change in the plan.
1392              
1393             =item C<note>
1394              
1395             A brief note about the tag.
1396              
1397             =item C<planner_name>
1398              
1399             The name of the user adding the tag to the plan. Defaults to the value of the
1400             C<user.name> configuration variable.
1401              
1402             =item C<planner_email>
1403              
1404             The email address of the user adding the tag to the plan. Defaults to the
1405             value of the C<user.email> configuration variable.
1406              
1407             =back
1408              
1409             =head3 C<add>
1410              
1411             $plan->add( name => 'whatevs' );
1412             $plan->add(
1413             name => 'widgets',
1414             requires => [qw(foo bar)],
1415             conflicts => [qw(dr_evil)],
1416             );
1417              
1418             Adds a change to the plan. The supported parameters are the same as those
1419             passed to the L<App::Sqitch::Plan::Change> constructor. Exits with a fatal
1420             error if the change already exists, or if the any of the dependencies are
1421             unknown.
1422              
1423             =head3 C<rework>
1424              
1425             $plan->rework( 'whatevs' );
1426             $plan->rework( 'widgets', [qw(foo bar)], [qw(dr_evil)] );
1427              
1428             Reworks an existing change. Said change must already exist in the plan and be
1429             tagged or have a tag following it or an exception will be thrown. The previous
1430             occurrence of the change will have the suffix of the most recent tag added to
1431             it, and a new tag instance will be added to the list.
1432              
1433             =head1 Plan File
1434              
1435             A plan file describes the deployment changes to be run against a database, and
1436             is typically maintained using the L<C<add>|sqitch-add> and
1437             L<C<rework>|sqitch-rework> commands. Its contents must be plain text encoded
1438             as UTF-8. Each line of a plan file may be one of four things:
1439              
1440             =over
1441              
1442             =item *
1443              
1444             A blank line. May include any amount of white space, which will be ignored.
1445              
1446             =item * A Pragma
1447              
1448             Begins with a C<%>, followed by a pragma name, optionally followed by C<=> and
1449             a value. Currently, the only pragma recognized by Sqitch is C<syntax-version>.
1450              
1451             =item * A change.
1452              
1453             A named change change as defined in L<sqitchchanges>. A change may then also
1454             contain a space-delimited list of dependencies, which are the names of other
1455             changes or tags prefixed with a colon (C<:>) for required changes or with an
1456             exclamation point (C<!>) for conflicting changes.
1457              
1458             Changes with a leading C<-> are slated to be reverted, while changes with no
1459             character or a leading C<+> are to be deployed.
1460              
1461             =item * A tag.
1462              
1463             A named deployment tag, generally corresponding to a release name. Begins with
1464             a C<@>, followed by one or more non-blanks characters, excluding "@", ":",
1465             "#", and blanks. The first and last characters must not be punctuation
1466             characters.
1467              
1468             =item * A note.
1469              
1470             Begins with a C<#> and goes to the end of the line. Preceding white space is
1471             ignored. May appear on a line after a pragma, change, or tag.
1472              
1473             =back
1474              
1475             Here's an example of a plan file with a single deploy change and tag:
1476              
1477             %syntax-version=1.0.0
1478             +users_table
1479             @alpha
1480              
1481             There may, of course, be any number of tags and changes. Here's an expansion:
1482              
1483             %syntax-version=1.0.0
1484             +users_table
1485             +insert_user
1486             +update_user
1487             +delete_user
1488             @root
1489             @alpha
1490              
1491             Here we have four changes -- "users_table", "insert_user", "update_user", and
1492             "delete_user" -- followed by two tags: "@root" and "@alpha".
1493              
1494             Most plans will have many changes and tags. Here's a longer example with three
1495             tagged deployment points, as well as a change that is deployed and later
1496             reverted:
1497              
1498             %syntax-version=1.0.0
1499             +users_table
1500             +insert_user
1501             +update_user
1502             +delete_user
1503             +dr_evil
1504             @root
1505             @alpha
1506              
1507             +widgets_table
1508             +list_widgets
1509             @beta
1510              
1511             -dr_evil
1512             +ftw
1513             @gamma
1514              
1515             Using this plan, to deploy to the "beta" tag, all of the changes up to the
1516             "@root" and "@alpha" tags must be deployed, as must changes listed before the
1517             "@beta" tag. To then deploy to the "@gamma" tag, the "dr_evil" change must be
1518             reverted and the "ftw" change must be deployed. If you then choose to revert
1519             to "@alpha", then the "ftw" change will be reverted, the "dr_evil" change
1520             re-deployed, and the "@gamma" tag removed; then "list_widgets" must be
1521             reverted and the associated "@beta" tag removed, then the "widgets_table"
1522             change must be reverted.
1523              
1524             Changes can only be repeated if one or more tags intervene. This allows Sqitch
1525             to distinguish between them. An example:
1526              
1527             %syntax-version=1.0.0
1528             +users_table
1529             @alpha
1530              
1531             +add_widget
1532             +widgets_table
1533             @beta
1534              
1535             +add_user
1536             @gamma
1537              
1538             +widgets_created_at
1539             @delta
1540              
1541             +add_widget
1542              
1543             Note that the "add_widget" change is repeated after the "@beta" tag, and at
1544             the end. Sqitch will notice the repetition when it parses this file, and allow
1545             it, because at least one tag "@beta" appears between the instances of
1546             "add_widget". When deploying, Sqitch will fetch the instance of the deploy
1547             script as of the "@delta" tag and apply it as the first change, and then, when
1548             it gets to the last change, retrieve the current instance of the deploy
1549             script. How does it find such files? The first instances files will either be
1550             named F<add_widget@delta.sql> or (soon) findable in the VCS history as of a
1551             VCS "delta" tag.
1552              
1553             =head2 Grammar
1554              
1555             Here is the EBNF Grammar for the plan file:
1556              
1557             plan-file = { <pragma> | <change-line> | <tag-line> | <note-line> | <blank-line> }* ;
1558              
1559             blank-line = [ <blanks> ] <eol>;
1560             note-line = <note> ;
1561             change-line = <name> [ "[" { <requires> | <conflicts> } "]" ] ( <eol> | <note> ) ;
1562             tag-line = <tag> ( <eol> | <note> ) ;
1563             pragma = "%" [ <blanks> ] <name> [ <blanks> ] = [ <blanks> ] <value> ( <eol> | <note> ) ;
1564              
1565             tag = "@" <name> ;
1566             requires = <name> ;
1567             conflicts = "!" <name> ;
1568             name = <non-punct> [ [ ? non-blank and not "@", ":", or "#" characters ? ] <non-punct> ] ;
1569             non-punct = ? non-punctuation, non-blank character ? ;
1570             value = ? non-EOL or "#" characters ?
1571              
1572             note = [ <blanks> ] "#" [ <string> ] <EOL> ;
1573             eol = [ <blanks> ] <EOL> ;
1574              
1575             blanks = ? blank characters ? ;
1576             string = ? non-EOL characters ? ;
1577              
1578             And written as regular expressions:
1579              
1580             my $eol = qr/[[:blank:]]*$/
1581             my $note = qr/(?:[[:blank:]]+)?[#].+$/;
1582             my $punct = q{-!"#$%&'()*+,./:;<=>?@[\\]^`{|}~};
1583             my $name = qr/[^$punct[:blank:]](?:(?:[^[:space:]:#@]+)?[^$punct[:blank:]])?/;
1584             my $tag = qr/[@]$name/;
1585             my $requires = qr/$name/;
1586             my conflicts = qr/[!]$name/;
1587             my $tag_line = qr/^$tag(?:$note|$eol)/;
1588             my $change_line = qr/^$name(?:[[](?:$requires|$conflicts)+[]])?(?:$note|$eol)/;
1589             my $note_line = qr/^$note/;
1590             my $pragma = qr/^][[:blank:]]*[%][[:blank:]]*$name[[:blank:]]*=[[:blank:]].+?(?:$note|$eol)$/;
1591             my $blank_line = qr/^$eol/;
1592             my $plan = qr/(?:$pragma|$change_line|$tag_line|$note_line|$blank_line)+/ms;
1593              
1594             =head1 See Also
1595              
1596             =over
1597              
1598             =item L<sqitch>
1599              
1600             The Sqitch command-line client.
1601              
1602             =back
1603              
1604             =head1 Author
1605              
1606             David E. Wheeler <david@justatheory.com>
1607              
1608             =head1 License
1609              
1610             Copyright (c) 2012-2023 iovation Inc., David E. Wheeler
1611              
1612             Permission is hereby granted, free of charge, to any person obtaining a copy
1613             of this software and associated documentation files (the "Software"), to deal
1614             in the Software without restriction, including without limitation the rights
1615             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1616             copies of the Software, and to permit persons to whom the Software is
1617             furnished to do so, subject to the following conditions:
1618              
1619             The above copyright notice and this permission notice shall be included in all
1620             copies or substantial portions of the Software.
1621              
1622             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1623             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1624             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1625             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1626             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1627             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1628             SOFTWARE.
1629              
1630             =cut