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