File Coverage

blib/lib/App/Sqitch/Plan/ChangeList.pm
Criterion Covered Total %
statement 114 114 100.0
branch 46 48 95.8
condition 9 12 75.0
subroutine 26 26 100.0
pod 16 16 100.0
total 211 216 97.6


line stmt bran cond sub pod time code
1              
2             use 5.010;
3 50     50   948 use utf8;
  50         161  
4 50     50   247 use strict;
  50         85  
  50         261  
5 50     50   1099 use List::Util;
  50         135  
  50         974  
6 50     50   255 use Locale::TextDomain qw(App-Sqitch);
  50         2468  
  50         2886  
7 50     50   323 use App::Sqitch::X qw(hurl);
  50         112  
  50         421  
8 50     50   8580  
  50         1893  
  50         493  
9             our $VERSION = 'v1.3.0'; # VERSION
10              
11             my $class = shift;
12             my $self = bless {
13 245     245 1 13313 list => [],
14 245         1127 lookup => {},
15             last_tagged_at => undef,
16             } => $class;
17             $self->append(@_) if @_;
18             return $self;
19 245 100       1391 }
20 245         3847  
21              
22             # Like [:punct:], but excluding _. Copied from perlrecharclass.
23 125     125 1 905 my $punct = q{-!"#$%&'()*+,./:;<=>?@[\\]^`{|}~};
  125         475  
24 41     41 1 515  
  41         192  
25 11     11 1 112 # Look for symbolic references.
  32         181  
  11         34  
26 1     1 1 3 if ( $_[0] =~ s{(?<![$punct])([~^])(?:(\1)|(\d+))?\z}{} ) {
  1         5  
27 599     599 1 7622 my $offset = $3 // ($2 ? 2 : 1);
28 21     21 1 403 $offset *= -1 if $1 eq '^';
29             return $offset;
30             } else {
31             return 0;
32             }
33             }
34              
35 889 100   889   5682 my ( $self, $key ) = @_;
36 70 100 66     354  
37 70 100       175 # Look for non-deployed symbolic references.
38 70         192 if ( my $offset = _offset $key ) {
39             my $idx = $self->_index_of( $key ) // return undef;
40 819         2205 $idx += $offset;
41             return $idx < 0 ? undef : $idx > $#{ $self->{list} } ? undef : $idx;
42             } else {
43             return $self->_index_of( $key );
44             }
45 577     577 1 8772 }
46              
47             my ( $self, $key ) = @_;
48 577 100       1131  
49 43   50     91 my ( $change, $tag ) = split /@/ => $key, 2;
50 43         63  
51 43 100       74 if ($change eq '') {
  40 100       209  
52             # Just want the change with the associated tag.
53 534         1253 my $idx = $self->{lookup}{'@' . $tag} or return undef;
54             return $idx->[0];
55             }
56              
57             my $idx = $self->{lookup}{$change} or return undef;
58 577     577   949 if (defined $tag) {
59             # Wanted for a change as of a specific tag.
60 577         1771 my $tag_idx = $self->{lookup}{'@' . $tag} or hurl plan => __x(
61             'Unknown tag "{tag}"',
62 577 100       1387 tag => '@' . $tag,
63             );
64 158 100       608 $tag_idx = $tag_idx->[0];
65 119         502 for my $i (reverse @{ $idx }) {
66             return $i if $i <= $tag_idx;
67             }
68 419 100       1545 return undef;
69 345 100       710 };
70              
71 130 100       457 # Just want index for a change name. Return if we have 0 or 1.
72             return $idx->[0] if @{ $idx } < 2;
73              
74              
75 128         204 # Too many changes found. Give the user some options and die.
76 128         165 App::Sqitch->vent(__x(
  128         284  
77 164 100       717 'Change "{change}" is ambiguous. Please specify a tag-qualified change:',
78             change => $key,
79 3         16 ));
80              
81             my $list = $self->{list};
82             App::Sqitch->vent( ' * ', $list->[$_]->format_tag_qualified_name )
83 215 100       294 for reverse @{ $idx };
  215         929  
84              
85             hurl plan => __ 'Change lookup failed';
86             }
87 4         25  
88             my ( $self, $key, $since ) = @_;
89              
90             # Look for non-deployed symbolic references.
91             if ( my $offset = _offset $key ) {
92 4         816 my $idx = $self->_first_index_of( $key, $since ) // return undef;
93             $idx += $offset;
94 4         8 return $idx < 0 ? undef : $idx > $#{ $self->{list} } ? undef : $idx;
  4         25  
95             } else {
96 4         35 return $self->_first_index_of( $key, $since );
97             }
98             }
99              
100 286     286 1 1040 my ( $self, $change, $since ) = @_;
101              
102             # Just return the first index if no tag.
103 286 100       525 my $idx = $self->{lookup}{$change} or return undef;
104 24   50     54 return $idx->[0] unless defined $since;
105 24         37  
106 24 50       44 # Find the tag index.
  24 50       113  
107             my $since_index = $self->index_of($since) // hurl plan => __x(
108 262         629 'Unknown change: "{change}"',
109             change => $since,
110             );
111              
112             # Return the first change after the tag.
113 286     286   518 return List::Util::first { $_ > $since_index } @{ $idx };
114             }
115              
116 286 100       1642 shift->{last_tagged_at};
117 154 100       599 }
118              
119             my $self = shift;
120 37   100     81 return defined $self->{last_tagged_at}
121             ? $self->{list}[ $self->{last_tagged_at} ]
122             : undef;
123             }
124              
125             my $self = shift;
126 34     42   119 my $idx = $self->index_of(@_) // return undef;
  42         183  
  34         100  
127             return $self->{list}[ $idx ];
128             }
129              
130 29     29 1 862 my ( $self, $name ) = @_;
131             return defined ( $name =~ /@/
132             ? $self->index_of($name)
133             : $self->first_index_of($name)
134 22     22 1 1179 );
135             }
136 22 100       113  
137             my ( $self, $name ) = @_;
138             my $idx = $name =~ /@/
139             ? $self->index_of($name)
140             : $self->first_index_of($name);
141 177     177 1 3839 return defined $idx ? $self->change_at($idx) : undef;
142 177   100     507 }
143 146         1041  
144             my $self = shift;
145             my $list = $self->{list};
146             my $lookup = $self->{lookup};
147 175     175 1 2967  
148 175 100       717 for my $change (@_) {
149             push @{ $list } => $change;
150             push @{ $lookup->{ $change->format_name } } => $#$list;
151             $lookup->{ $change->id } = my $pos = [$#$list];
152              
153             # Index on the tags, too.
154             for my $tag ($change->tags) {
155 89     89 1 739 $lookup->{ $tag->format_name } = $pos;
156 89 100       364 $lookup->{ $tag->id } = $pos;
157             $self->{last_tagged_at} = $#$list;
158             }
159 89 100       289 }
160              
161             $lookup->{'HEAD'} = $lookup->{'@HEAD'} = [$#$list];
162             $lookup->{'ROOT'} = $lookup->{'@ROOT'} = [0];
163 274     274 1 1262  
164 274         478 return $self;
165 274         425 }
166              
167 274         628 my ( $self, $index, $tag ) = @_;
168 777         5978 my $list = $self->{list};
  777         1335  
169 777         1042 my $lookup = $self->{lookup};
  777         2340  
170 777         12471 my $pos = [$index];
171             $lookup->{ $tag->id } = $pos;
172             $lookup->{ $tag->format_name } = $pos;
173 777         39071 $self->{last_tagged_at} = $index if $index == $#{ $self->{list} };
174 393         3280 return $self;
175 393         5712 }
176 393         18655  
177             1;
178              
179              
180 274         3587 =head1 Name
181 274         871  
182             App::Sqitch::Plan::ChangeList - Sqitch deployment plan change list
183 274         534  
184             =head1 Synopsis
185              
186             my $list = App::Sqitch::Plan::ChangeList->new(
187 19     19 1 52 $add_roles,
188 19         31 $add_users,
189 19         31 $insert_user,
190 19         38 $insert_user2,
191 19         311 );
192 19         1023  
193 19 100       29 my @changes = $list->changes;
  19         65  
194 19         46 my $add_users = $list->change_at(1);
195             my $add_users = $list->get('add_users');
196              
197             my $insert_user1 = $list->get('insert_user@alpha');
198             my $insert_user2 = $list->get('insert_user');
199              
200             =head1 Description
201              
202             This module is used internally by L<App::Sqitch::Plan> to manage plan changes.
203             It's modeled on L<Array::AsHash> and L<Hash::MultiValue>, but makes allowances
204             for finding changes relative to tags.
205              
206             =head1 Interface
207              
208             =head2 Constructors
209              
210             =head3 C<new>
211              
212             my $plan = App::Sqitch::Plan::ChangeList->new( @changes );
213              
214             Instantiates and returns a App::Sqitch::Plan::ChangeList object with the list of
215             changes. Each change should be a L<App::Sqitch::Plan::Change> object. Order will be
216             preserved but the location of each change will be indexed by its name and ID, as
217             well as the names and IDs of any associated tags.
218              
219             =head2 Instance Methods
220              
221             =head3 C<count>
222              
223             my $count = $changelist->count;
224              
225             Returns the number of changes in the list.
226              
227             =head3 C<changes>
228              
229             my @changes = $changelist->changes;
230              
231             Returns all of the changes in the list.
232              
233             =head3 C<tags>
234              
235             my @tags = $changelist->tags;
236              
237             Returns all of the tags associated with changes in the list.
238              
239             =head3 C<items>
240              
241             my @changes = $changelist->items;
242              
243             An alias for C<changes>.
244              
245             =head3 C<change_at>
246              
247             my $change = $change_list->change_at(10);
248              
249             Returns the change at the specified index.
250              
251             =head3 C<index_of>
252              
253             my $index = $changelist->index_of($change_id);
254             my $index = $changelist->index_of($change_name);
255              
256             Returns the index of the change with the specified ID or name. The value passed
257             may be one of these forms:
258              
259             =over
260              
261             =item * An ID
262              
263             my $index = $changelist->index_of('6c2f28d125aff1deea615f8de774599acf39a7a1');
264              
265             This is the SHA1 ID of a change or tag. Currently, the full 40-character hexed
266             hash string must be specified.
267              
268             =item * A change name
269              
270             my $index = $changelist->index_of('users_table');
271              
272             The name of a change. Will throw an exception if the more than one change in the
273             list goes by that name.
274              
275             =item * A tag name
276              
277             my $index = $changelist->index_of('@beta1');
278              
279             The name of a tag, including the leading C<@>.
280              
281             =item * A tag-qualified change name
282              
283             my $index = $changelist->index_of('users_table@beta1');
284              
285             The named change as it was last seen in the list before the specified tag.
286              
287             =back
288              
289             =head3 C<first_index_of>
290              
291             my $index = $changelist->first_index_of($change_name);
292             my $index = $changelist->first_index_of($change_name, $name);
293              
294             Returns the index of the first instance of the named change in the list. If a
295             second argument is passed, the index of the first instance of the change
296             I<after> the index of the second argument will be returned. This is useful
297             for getting the index of a change as it was deployed after a particular tag, for
298             example:
299              
300             my $index = $changelist->first_index_of('foo', '@beta');
301             my $index = $changelist->first_index_of('foo', 'users_table@beta1');
302              
303             The second argument must unambiguously refer to a single change in the list. As
304             such, it should usually be a tag name or tag-qualified change name. Returns
305             C<undef> if the change does not appear in the list, or if it does not appear
306             after the specified second argument change name.
307              
308             =head3 C<last_change>
309              
310             my $change = $changelist->last_change;
311              
312             Returns the last change to be appear in the list. Returns C<undef> if the list
313             contains no changes.
314              
315             =head3 C<last_tagged_change>
316              
317             my $change = $changelist->last_tagged_change;
318              
319             Returns the last tagged change in the list. Returns C<undef> if the list
320             contains no tagged changes.
321              
322             =head3 C<index_of_last_tagged>
323              
324             my $index = $changelist->index_of_last_tagged;
325              
326             Returns the index of the last tagged change in the list. Returns C<undef> if the
327             list contains no tags.
328              
329             =head3 C<get>
330              
331             my $change = $changelist->get($id);
332             my $change = $changelist->get($change_name);
333             my $change = $changelist->get($tag_name);
334              
335             Returns the change for the specified ID or name. The name may be specified as
336             described for C<index_of()>. An exception will be thrown if more than one change
337             goes by a specified name. As such, it is best to specify it as unambiguously
338             as possible: as a tag name, a tag-qualified change name, or an ID.
339              
340             =head3 C<contains>
341              
342             say 'Yes!' if $plan->contains('6c2f28d125aff1deea615f8de774599acf39a7a1');
343              
344             Like C<index_of()>, but never throws an exception, and returns true if the
345             plan contains the specified change, and false if it does not.
346              
347             =head3 C<find>
348              
349             my $change = $changelist->find($id);
350             my $change = $changelist->find($change_name);
351             my $change = $changelist->find($tag_name);
352             my $change = $changelist->find("$change_name\@$tag_name");
353              
354             Tries to find and return a change based on the argument. If no tag is specified,
355             finds and returns the first instance of the named change. Otherwise, it returns
356             the change as of the specified tag. Unlike C<get()>, it will not throw an error
357             if more than one change exists with the specified name, but will return the
358             first instance.
359              
360             =head3 C<append>
361              
362             $changelist->append(@changes);
363              
364             Append one or more changes to the list. Does not check for duplicates, so
365             use with care.
366              
367             =head3 C<index_tag>
368              
369             $changelist->index_tag($index, $tag);
370              
371             Index the tag at the specified index. That is, the tag is assumed to be
372             associated with the change at the specified index, and so the internal look up
373             table is updated so that the change at that index can be found via the tag's
374             name and ID.
375              
376             =head1 See Also
377              
378             =over
379              
380             =item L<App::Sqitch::Plan>
381              
382             The Sqitch plan.
383              
384             =back
385              
386             =head1 Author
387              
388             David E. Wheeler <david@justatheory.com>
389              
390             =head1 License
391              
392             Copyright (c) 2012-2022 iovation Inc., David E. Wheeler
393              
394             Permission is hereby granted, free of charge, to any person obtaining a copy
395             of this software and associated documentation files (the "Software"), to deal
396             in the Software without restriction, including without limitation the rights
397             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
398             copies of the Software, and to permit persons to whom the Software is
399             furnished to do so, subject to the following conditions:
400              
401             The above copyright notice and this permission notice shall be included in all
402             copies or substantial portions of the Software.
403              
404             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
405             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
406             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
407             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
408             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
409             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
410             SOFTWARE.
411              
412             =cut