OLD | NEW |
| (Empty) |
1 # Copyright © 2005, 2007 Frank Lichtenheld <frank@lichtenheld.de> | |
2 # Copyright © 2009 Raphaël Hertzog <hertzog@debian.org> | |
3 # | |
4 # This program is free software; you can redistribute it and/or modify | |
5 # it under the terms of the GNU General Public License as published by | |
6 # the Free Software Foundation; either version 2 of the License, or | |
7 # (at your option) any later version. | |
8 # | |
9 # This program is distributed in the hope that it will be useful, | |
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 # GNU General Public License for more details. | |
13 # | |
14 # You should have received a copy of the GNU General Public License | |
15 # along with this program. If not, see <https://www.gnu.org/licenses/>. | |
16 | |
17 =encoding utf8 | |
18 | |
19 =head1 NAME | |
20 | |
21 Dpkg::Changelog - base class to implement a changelog parser | |
22 | |
23 =head1 DESCRIPTION | |
24 | |
25 Dpkg::Changelog is a class representing a changelog file | |
26 as an array of changelog entries (Dpkg::Changelog::Entry). | |
27 By deriving this object and implementing its parse method, you | |
28 add the ability to fill this object with changelog entries. | |
29 | |
30 =head2 FUNCTIONS | |
31 | |
32 =cut | |
33 | |
34 package Dpkg::Changelog; | |
35 | |
36 use strict; | |
37 use warnings; | |
38 | |
39 our $VERSION = '1.00'; | |
40 | |
41 use Dpkg; | |
42 use Dpkg::Gettext; | |
43 use Dpkg::ErrorHandling qw(:DEFAULT report); | |
44 use Dpkg::Control; | |
45 use Dpkg::Control::Changelog; | |
46 use Dpkg::Control::Fields; | |
47 use Dpkg::Index; | |
48 use Dpkg::Version; | |
49 use Dpkg::Vendor qw(run_vendor_hook); | |
50 | |
51 use parent qw(Dpkg::Interface::Storable); | |
52 | |
53 use overload | |
54 '@{}' => sub { return $_[0]->{data} }; | |
55 | |
56 =over 4 | |
57 | |
58 =item my $c = Dpkg::Changelog->new(%options) | |
59 | |
60 Creates a new changelog object. | |
61 | |
62 =cut | |
63 | |
64 sub new { | |
65 my ($this, %opts) = @_; | |
66 my $class = ref($this) || $this; | |
67 my $self = { | |
68 verbose => 1, | |
69 parse_errors => [] | |
70 }; | |
71 bless $self, $class; | |
72 $self->set_options(%opts); | |
73 return $self; | |
74 } | |
75 | |
76 =item $c->load($filename) | |
77 | |
78 Parse $filename as a changelog. | |
79 | |
80 =cut | |
81 | |
82 =item $c->set_options(%opts) | |
83 | |
84 Change the value of some options. "verbose" (defaults to 1) defines | |
85 whether parse errors are displayed as warnings by default. "reportfile" | |
86 is a string to use instead of the name of the file parsed, in particular | |
87 in error messages. "range" defines the range of entries that we want to | |
88 parse, the parser will stop as soon as it has parsed enough data to | |
89 satisfy $c->get_range($opts{range}). | |
90 | |
91 =cut | |
92 | |
93 sub set_options { | |
94 my ($self, %opts) = @_; | |
95 $self->{$_} = $opts{$_} foreach keys %opts; | |
96 } | |
97 | |
98 =item $c->reset_parse_errors() | |
99 | |
100 Can be used to delete all information about errors occurred during | |
101 previous L<parse> runs. | |
102 | |
103 =cut | |
104 | |
105 sub reset_parse_errors { | |
106 my ($self) = @_; | |
107 $self->{parse_errors} = []; | |
108 } | |
109 | |
110 =item $c->parse_error($file, $line_nr, $error, [$line]) | |
111 | |
112 Record a new parse error in $file at line $line_nr. The error message is | |
113 specified with $error and a copy of the line can be recorded in $line. | |
114 | |
115 =cut | |
116 | |
117 sub parse_error { | |
118 my ($self, $file, $line_nr, $error, $line) = @_; | |
119 shift; | |
120 | |
121 push @{$self->{parse_errors}}, [ @_ ]; | |
122 | |
123 if ($self->{verbose}) { | |
124 if ($line) { | |
125 warning("%20s(l$line_nr): $error\nLINE: $line", $file); | |
126 } else { | |
127 warning("%20s(l$line_nr): $error", $file); | |
128 } | |
129 } | |
130 } | |
131 | |
132 =item $c->get_parse_errors() | |
133 | |
134 Returns all error messages from the last L<parse> run. | |
135 If called in scalar context returns a human readable | |
136 string representation. If called in list context returns | |
137 an array of arrays. Each of these arrays contains | |
138 | |
139 =over 4 | |
140 | |
141 =item 1. | |
142 | |
143 a string describing the origin of the data (a filename usually). If the | |
144 reportfile configuration option was given, its value will be used instead. | |
145 | |
146 =item 2. | |
147 | |
148 the line number where the error occurred | |
149 | |
150 =item 3. | |
151 | |
152 an error description | |
153 | |
154 =item 4. | |
155 | |
156 the original line | |
157 | |
158 =back | |
159 | |
160 =cut | |
161 | |
162 sub get_parse_errors { | |
163 my ($self) = @_; | |
164 | |
165 if (wantarray) { | |
166 return @{$self->{parse_errors}}; | |
167 } else { | |
168 my $res = ''; | |
169 foreach my $e (@{$self->{parse_errors}}) { | |
170 if ($e->[3]) { | |
171 $res .= report(_g('warning'),_g("%s(l%s): %s\nLINE: %s"), @$e ); | |
172 } else { | |
173 $res .= report(_g('warning'), _g('%s(l%s): %s'), @$e); | |
174 } | |
175 } | |
176 return $res; | |
177 } | |
178 } | |
179 | |
180 =item $c->set_unparsed_tail($tail) | |
181 | |
182 Add a string representing unparsed lines after the changelog entries. | |
183 Use undef as $tail to remove the unparsed lines currently set. | |
184 | |
185 =item $c->get_unparsed_tail() | |
186 | |
187 Return a string representing the unparsed lines after the changelog | |
188 entries. Returns undef if there's no such thing. | |
189 | |
190 =cut | |
191 | |
192 sub set_unparsed_tail { | |
193 my ($self, $tail) = @_; | |
194 $self->{unparsed_tail} = $tail; | |
195 } | |
196 | |
197 sub get_unparsed_tail { | |
198 my ($self) = @_; | |
199 return $self->{unparsed_tail}; | |
200 } | |
201 | |
202 =item @{$c} | |
203 | |
204 Returns all the Dpkg::Changelog::Entry objects contained in this changelog | |
205 in the order in which they have been parsed. | |
206 | |
207 =item $c->get_range($range) | |
208 | |
209 Returns an array (if called in list context) or a reference to an array of | |
210 Dpkg::Changelog::Entry objects which each represent one entry of the | |
211 changelog. $range is a hash reference describing the range of entries | |
212 to return. See section L<"RANGE SELECTION">. | |
213 | |
214 =cut | |
215 | |
216 sub __sanity_check_range { | |
217 my ($self, $r) = @_; | |
218 my $data = $self->{data}; | |
219 | |
220 if (defined($r->{offset}) and not defined($r->{count})) { | |
221 warning(_g("'offset' without 'count' has no effect")) if $self->{verbose
}; | |
222 delete $r->{offset}; | |
223 } | |
224 | |
225 ## no critic (ControlStructures::ProhibitUntilBlocks) | |
226 if ((defined($r->{count}) || defined($r->{offset})) && | |
227 (defined($r->{from}) || defined($r->{since}) || | |
228 defined($r->{to}) || defined($r->{until}))) | |
229 { | |
230 warning(_g("you can't combine 'count' or 'offset' with any other " . | |
231 'range option')) if $self->{verbose}; | |
232 delete $r->{from}; | |
233 delete $r->{since}; | |
234 delete $r->{to}; | |
235 delete $r->{until}; | |
236 } | |
237 if (defined($r->{from}) && defined($r->{since})) { | |
238 warning(_g("you can only specify one of 'from' and 'since', using " . | |
239 "'since'")) if $self->{verbose}; | |
240 delete $r->{from}; | |
241 } | |
242 if (defined($r->{to}) && defined($r->{until})) { | |
243 warning(_g("you can only specify one of 'to' and 'until', using " . | |
244 "'until'")) if $self->{verbose}; | |
245 delete $r->{to}; | |
246 } | |
247 | |
248 # Handle non-existing versions | |
249 my (%versions, @versions); | |
250 foreach my $entry (@{$data}) { | |
251 $versions{$entry->get_version()->as_string()} = 1; | |
252 push @versions, $entry->get_version()->as_string(); | |
253 } | |
254 if ((defined($r->{since}) and not exists $versions{$r->{since}})) { | |
255 warning(_g("'%s' option specifies non-existing version"), 'since'); | |
256 warning(_g('use newest entry that is earlier than the one specified')); | |
257 foreach my $v (@versions) { | |
258 if (version_compare_relation($v, REL_LT, $r->{since})) { | |
259 $r->{since} = $v; | |
260 last; | |
261 } | |
262 } | |
263 if (not exists $versions{$r->{since}}) { | |
264 # No version was earlier, include all | |
265 warning(_g('none found, starting from the oldest entry')); | |
266 delete $r->{since}; | |
267 $r->{from} = $versions[-1]; | |
268 } | |
269 } | |
270 if ((defined($r->{from}) and not exists $versions{$r->{from}})) { | |
271 warning(_g("'%s' option specifies non-existing version"), 'from'); | |
272 warning(_g('use oldest entry that is later than the one specified')); | |
273 my $oldest; | |
274 foreach my $v (@versions) { | |
275 if (version_compare_relation($v, REL_GT, $r->{from})) { | |
276 $oldest = $v; | |
277 } | |
278 } | |
279 if (defined($oldest)) { | |
280 $r->{from} = $oldest; | |
281 } else { | |
282 warning(_g("no such entry found, ignoring '%s' parameter"), 'from'); | |
283 delete $r->{from}; # No version was oldest | |
284 } | |
285 } | |
286 if (defined($r->{until}) and not exists $versions{$r->{until}}) { | |
287 warning(_g("'%s' option specifies non-existing version"), 'until'); | |
288 warning(_g('use oldest entry that is later than the one specified')); | |
289 my $oldest; | |
290 foreach my $v (@versions) { | |
291 if (version_compare_relation($v, REL_GT, $r->{until})) { | |
292 $oldest = $v; | |
293 } | |
294 } | |
295 if (defined($oldest)) { | |
296 $r->{until} = $oldest; | |
297 } else { | |
298 warning(_g("no such entry found, ignoring '%s' parameter"), 'until')
; | |
299 delete $r->{until}; # No version was oldest | |
300 } | |
301 } | |
302 if (defined($r->{to}) and not exists $versions{$r->{to}}) { | |
303 warning(_g("'%s' option specifies non-existing version"), 'to'); | |
304 warning(_g('use newest entry that is earlier than the one specified')); | |
305 foreach my $v (@versions) { | |
306 if (version_compare_relation($v, REL_LT, $r->{to})) { | |
307 $r->{to} = $v; | |
308 last; | |
309 } | |
310 } | |
311 if (not exists $versions{$r->{to}}) { | |
312 # No version was earlier | |
313 warning(_g("no such entry found, ignoring '%s' parameter"), 'to'); | |
314 delete $r->{to}; | |
315 } | |
316 } | |
317 | |
318 if (defined($r->{since}) and $data->[0]->get_version() eq $r->{since}) { | |
319 warning(_g("'since' option specifies most recent version, ignoring")); | |
320 delete $r->{since}; | |
321 } | |
322 if (defined($r->{until}) and $data->[-1]->get_version() eq $r->{until}) { | |
323 warning(_g("'until' option specifies oldest version, ignoring")); | |
324 delete $r->{until}; | |
325 } | |
326 ## use critic | |
327 } | |
328 | |
329 sub get_range { | |
330 my ($self, $range) = @_; | |
331 $range //= {}; | |
332 my $res = $self->_data_range($range); | |
333 if (defined $res) { | |
334 return @$res if wantarray; | |
335 return $res; | |
336 } else { | |
337 return; | |
338 } | |
339 } | |
340 | |
341 sub _is_full_range { | |
342 my ($self, $range) = @_; | |
343 | |
344 return 1 if $range->{all}; | |
345 | |
346 # If no range delimiter is specified, we want everything. | |
347 foreach (qw(since until from to count offset)) { | |
348 return 0 if exists $range->{$_}; | |
349 } | |
350 | |
351 return 1; | |
352 } | |
353 | |
354 sub _data_range { | |
355 my ($self, $range) = @_; | |
356 | |
357 my $data = $self->{data} or return; | |
358 | |
359 return [ @$data ] if $self->_is_full_range($range); | |
360 | |
361 $self->__sanity_check_range($range); | |
362 | |
363 my ($start, $end); | |
364 if (defined($range->{count})) { | |
365 my $offset = $range->{offset} || 0; | |
366 my $count = $range->{count}; | |
367 # Convert count/offset in start/end | |
368 if ($offset > 0) { | |
369 $offset -= ($count < 0); | |
370 } elsif ($offset < 0) { | |
371 $offset = $#$data + ($count > 0) + $offset; | |
372 } else { | |
373 $offset = $#$data if $count < 0; | |
374 } | |
375 $start = $end = $offset; | |
376 $start += $count+1 if $count < 0; | |
377 $end += $count-1 if $count > 0; | |
378 # Check limits | |
379 $start = 0 if $start < 0; | |
380 return if $start > $#$data; | |
381 $end = $#$data if $end > $#$data; | |
382 return if $end < 0; | |
383 $end = $start if $end < $start; | |
384 return [ @{$data}[$start .. $end] ]; | |
385 } | |
386 | |
387 ## no critic (ControlStructures::ProhibitUntilBlocks) | |
388 my @result; | |
389 my $include = 1; | |
390 $include = 0 if defined($range->{to}) or defined($range->{until}); | |
391 foreach (@$data) { | |
392 my $v = $_->get_version(); | |
393 $include = 1 if defined($range->{to}) and $v eq $range->{to}; | |
394 last if defined($range->{since}) and $v eq $range->{since}; | |
395 | |
396 push @result, $_ if $include; | |
397 | |
398 $include = 1 if defined($range->{until}) and $v eq $range->{until}; | |
399 last if defined($range->{from}) and $v eq $range->{from}; | |
400 } | |
401 ## use critic | |
402 | |
403 return \@result if scalar(@result); | |
404 return; | |
405 } | |
406 | |
407 =item $c->abort_early() | |
408 | |
409 Returns true if enough data have been parsed to be able to return all | |
410 entries selected by the range set at creation (or with set_options). | |
411 | |
412 =cut | |
413 | |
414 sub abort_early { | |
415 my ($self) = @_; | |
416 | |
417 my $data = $self->{data} or return; | |
418 my $r = $self->{range} or return; | |
419 my $count = $r->{count} || 0; | |
420 my $offset = $r->{offset} || 0; | |
421 | |
422 return if $self->_is_full_range($r); | |
423 return if $offset < 0 or $count < 0; | |
424 if (defined($r->{count})) { | |
425 if ($offset > 0) { | |
426 $offset -= ($count < 0); | |
427 } | |
428 my $start = my $end = $offset; | |
429 $end += $count-1 if $count > 0; | |
430 return ($start < @$data and $end < @$data); | |
431 } | |
432 | |
433 return unless defined($r->{since}) or defined($r->{from}); | |
434 foreach (@$data) { | |
435 my $v = $_->get_version(); | |
436 return 1 if defined($r->{since}) and $v eq $r->{since}; | |
437 return 1 if defined($r->{from}) and $v eq $r->{from}; | |
438 } | |
439 | |
440 return; | |
441 } | |
442 | |
443 =item $c->save($filename) | |
444 | |
445 Save the changelog in the given file. | |
446 | |
447 =item $c->output() | |
448 | |
449 =item "$c" | |
450 | |
451 Returns a string representation of the changelog (it's a concatenation of | |
452 the string representation of the individual changelog entries). | |
453 | |
454 =item $c->output($fh) | |
455 | |
456 Output the changelog to the given filehandle. | |
457 | |
458 =cut | |
459 | |
460 sub output { | |
461 my ($self, $fh) = @_; | |
462 my $str = ''; | |
463 foreach my $entry (@{$self}) { | |
464 my $text = $entry->output(); | |
465 print { $fh } $text if defined $fh; | |
466 $str .= $text if defined wantarray; | |
467 } | |
468 my $text = $self->get_unparsed_tail(); | |
469 if (defined $text) { | |
470 print { $fh } $text if defined $fh; | |
471 $str .= $text if defined wantarray; | |
472 } | |
473 return $str; | |
474 } | |
475 | |
476 =item my $control = $c->dpkg($range) | |
477 | |
478 Returns a Dpkg::Control::Changelog object representing the entries selected | |
479 by the optional range specifier (see L<"RANGE SELECTION"> for details). | |
480 Returns undef in no entries are matched. | |
481 | |
482 The following fields are contained in the object: | |
483 | |
484 =over 4 | |
485 | |
486 =item Source | |
487 | |
488 package name (in the first entry) | |
489 | |
490 =item Version | |
491 | |
492 packages' version (from first entry) | |
493 | |
494 =item Distribution | |
495 | |
496 target distribution (from first entry) | |
497 | |
498 =item Urgency | |
499 | |
500 urgency (highest of all printed entries) | |
501 | |
502 =item Maintainer | |
503 | |
504 person that created the (first) entry | |
505 | |
506 =item Date | |
507 | |
508 date of the (first) entry | |
509 | |
510 =item Closes | |
511 | |
512 bugs closed by the entry/entries, sorted by bug number | |
513 | |
514 =item Changes | |
515 | |
516 content of the the entry/entries | |
517 | |
518 =back | |
519 | |
520 =cut | |
521 | |
522 our ( @URGENCIES, %URGENCIES ); | |
523 BEGIN { | |
524 @URGENCIES = qw(low medium high critical emergency); | |
525 my $i = 1; | |
526 %URGENCIES = map { $_ => $i++ } @URGENCIES; | |
527 } | |
528 | |
529 sub dpkg { | |
530 my ($self, $range) = @_; | |
531 | |
532 my @data = $self->get_range($range) or return; | |
533 my $src = shift @data; | |
534 | |
535 my $f = Dpkg::Control::Changelog->new(); | |
536 $f->{Urgency} = $src->get_urgency() || 'unknown'; | |
537 $f->{Source} = $src->get_source() || 'unknown'; | |
538 $f->{Version} = $src->get_version() // 'unknown'; | |
539 $f->{Distribution} = join(' ', $src->get_distributions()); | |
540 $f->{Maintainer} = $src->get_maintainer() || ''; | |
541 $f->{Date} = $src->get_timestamp() || ''; | |
542 $f->{Changes} = $src->get_dpkg_changes(); | |
543 | |
544 # handle optional fields | |
545 my $opts = $src->get_optional_fields(); | |
546 my %closes; | |
547 foreach (keys %$opts) { | |
548 if (/^Urgency$/i) { # Already dealt | |
549 } elsif (/^Closes$/i) { | |
550 $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes})); | |
551 } else { | |
552 field_transfer_single($opts, $f); | |
553 } | |
554 } | |
555 | |
556 foreach my $bin (@data) { | |
557 my $oldurg = $f->{Urgency} || ''; | |
558 my $oldurgn = $URGENCIES{$f->{Urgency}} || -1; | |
559 my $newurg = $bin->get_urgency() || ''; | |
560 my $newurgn = $URGENCIES{$newurg} || -1; | |
561 $f->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg; | |
562 $f->{Changes} .= "\n" . $bin->get_dpkg_changes(); | |
563 | |
564 # handle optional fields | |
565 $opts = $bin->get_optional_fields(); | |
566 foreach (keys %$opts) { | |
567 if (/^Closes$/i) { | |
568 $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes})); | |
569 } elsif (not exists $f->{$_}) { # Don't overwrite an existing field | |
570 field_transfer_single($opts, $f); | |
571 } | |
572 } | |
573 } | |
574 | |
575 if (scalar keys %closes) { | |
576 $f->{Closes} = join ' ', sort { $a <=> $b } keys %closes; | |
577 } | |
578 run_vendor_hook('post-process-changelog-entry', $f); | |
579 | |
580 return $f; | |
581 } | |
582 | |
583 =item my @controls = $c->rfc822($range) | |
584 | |
585 Returns a Dpkg::Index containing Dpkg::Control::Changelog objects where | |
586 each object represents one entry in the changelog that is part of the | |
587 range requested (see L<"RANGE SELECTION"> for details). For the format of | |
588 such an object see the description of the L<"dpkg"> method (while ignoring | |
589 the remarks about which values are taken from the first entry). | |
590 | |
591 =cut | |
592 | |
593 sub rfc822 { | |
594 my ($self, $range) = @_; | |
595 | |
596 my @data = $self->get_range($range) or return; | |
597 my $index = Dpkg::Index->new(type => CTRL_CHANGELOG); | |
598 | |
599 foreach my $entry (@data) { | |
600 my $f = Dpkg::Control::Changelog->new(); | |
601 $f->{Urgency} = $entry->get_urgency() || 'unknown'; | |
602 $f->{Source} = $entry->get_source() || 'unknown'; | |
603 $f->{Version} = $entry->get_version() // 'unknown'; | |
604 $f->{Distribution} = join(' ', $entry->get_distributions()); | |
605 $f->{Maintainer} = $entry->get_maintainer() || ''; | |
606 $f->{Date} = $entry->get_timestamp() || ''; | |
607 $f->{Changes} = $entry->get_dpkg_changes(); | |
608 | |
609 # handle optional fields | |
610 my $opts = $entry->get_optional_fields(); | |
611 foreach (keys %$opts) { | |
612 field_transfer_single($opts, $f) unless exists $f->{$_}; | |
613 } | |
614 | |
615 run_vendor_hook('post-process-changelog-entry', $f); | |
616 | |
617 $index->add($f); | |
618 } | |
619 return $index; | |
620 } | |
621 | |
622 =back | |
623 | |
624 =head1 RANGE SELECTION | |
625 | |
626 A range selection is described by a hash reference where | |
627 the allowed keys and values are described below. | |
628 | |
629 The following options take a version number as value. | |
630 | |
631 =over 4 | |
632 | |
633 =item since | |
634 | |
635 Causes changelog information from all versions strictly | |
636 later than B<version> to be used. | |
637 | |
638 =item until | |
639 | |
640 Causes changelog information from all versions strictly | |
641 earlier than B<version> to be used. | |
642 | |
643 =item from | |
644 | |
645 Similar to C<since> but also includes the information for the | |
646 specified B<version> itself. | |
647 | |
648 =item to | |
649 | |
650 Similar to C<until> but also includes the information for the | |
651 specified B<version> itself. | |
652 | |
653 =back | |
654 | |
655 The following options don't take version numbers as values: | |
656 | |
657 =over 4 | |
658 | |
659 =item all | |
660 | |
661 If set to a true value, all entries of the changelog are returned, | |
662 this overrides all other options. | |
663 | |
664 =item count | |
665 | |
666 Expects a signed integer as value. Returns C<value> entries from the | |
667 top of the changelog if set to a positive integer, and C<abs(value)> | |
668 entries from the tail if set to a negative integer. | |
669 | |
670 =item offset | |
671 | |
672 Expects a signed integer as value. Changes the starting point for | |
673 C<count>, either counted from the top (positive integer) or from | |
674 the tail (negative integer). C<offset> has no effect if C<count> | |
675 wasn't given as well. | |
676 | |
677 =back | |
678 | |
679 Some examples for the above options. Imagine an example changelog with | |
680 entries for the versions 1.2, 1.3, 2.0, 2.1, 2.2, 3.0 and 3.1. | |
681 | |
682 Range Included entries | |
683 C<{ since =E<gt> '2.0' }> 3.1, 3.0, 2.2 | |
684 C<{ until =E<gt> '2.0' }> 1.3, 1.2 | |
685 C<{ from =E<gt> '2.0' }> 3.1, 3.0, 2.2, 2.1, 2.0 | |
686 C<{ to =E<gt> '2.0' }> 2.0, 1.3, 1.2 | |
687 C<{ count =E<gt> 2 }> 3.1, 3.0 | |
688 C<{ count =E<gt> -2 }> 1.3, 1.2 | |
689 C<{ count =E<gt> 3, offset=E<gt> 2 }> 2.2, 2.1, 2.0 | |
690 C<{ count =E<gt> 2, offset=E<gt> -3 }> 2.0, 1.3 | |
691 C<{ count =E<gt> -2, offset=E<gt> 3 }> 3.0, 2.2 | |
692 C<{ count =E<gt> -2, offset=E<gt> -3 }> 2.2, 2.1 | |
693 | |
694 Any combination of one option of C<since> and C<from> and one of | |
695 C<until> and C<to> returns the intersection of the two results | |
696 with only one of the options specified. | |
697 | |
698 =head1 AUTHOR | |
699 | |
700 Frank Lichtenheld, E<lt>frank@lichtenheld.deE<gt> | |
701 Raphaël Hertzog, E<lt>hertzog@debian.orgE<gt> | |
702 | |
703 =cut | |
704 1; | |
OLD | NEW |