File Coverage

File:Dpkg/Changelog.pm
Coverage:68.7%

linestmtbrancondsubpodtimecode
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 - 30
=head1 NAME

Dpkg::Changelog - base class to implement a changelog parser

=head1 DESCRIPTION

Dpkg::Changelog is a class representing a changelog file
as an array of changelog entries (Dpkg::Changelog::Entry).
By deriving this class and implementing its parse method, you
add the ability to fill this object with changelog entries.

=cut
31
32package Dpkg::Changelog;
33
34
6
6
6
13
3
67
use strict;
35
6
6
6
9
3
122
use warnings;
36
37our $VERSION = '2.00';
38
39
6
6
6
9
4
128
use Carp;
40
41
6
6
6
13
2
130
use Dpkg::Gettext;
42
6
6
6
13
14
384
use Dpkg::ErrorHandling qw(:DEFAULT report REPORT_WARN);
43
6
6
6
719
5
179
use Dpkg::Control;
44
6
6
6
664
4
94
use Dpkg::Control::Changelog;
45
6
6
6
11
4
183
use Dpkg::Control::Fields;
46
6
6
6
739
5
79
use Dpkg::Index;
47
6
6
6
753
5
191
use Dpkg::Version;
48
6
6
6
13
4
100
use Dpkg::Vendor qw(run_vendor_hook);
49
50
6
6
6
8
5
11
use parent qw(Dpkg::Interface::Storable);
51
52use overload
53
6
6
6
36
206
6
12
59
    '@{}' => sub { return $_[0]->{data} };
54
55 - 63
=head1 METHODS

=over 4

=item $c = Dpkg::Changelog->new(%options)

Creates a new changelog object.

=cut
64
65sub new {
66
28
1
36
    my ($this, %opts) = @_;
67
28
63
    my $class = ref($this) || $this;
68
28
40
    my $self = {
69        verbose => 1,
70        parse_errors => []
71    };
72
28
27
    bless $self, $class;
73
28
46
    $self->set_options(%opts);
74
28
31
    return $self;
75}
76
77 - 86
=item $c->set_options(%opts)

Change the value of some options. "verbose" (defaults to 1) defines
whether parse errors are displayed as warnings by default. "reportfile"
is a string to use instead of the name of the file parsed, in particular
in error messages. "range" defines the range of entries that we want to
parse, the parser will stop as soon as it has parsed enough data to
satisfy $c->get_range($opts{range}).

=cut
87
88sub set_options {
89
28
1
29
    my ($self, %opts) = @_;
90
28
79
    $self->{$_} = $opts{$_} foreach keys %opts;
91}
92
93 - 114
=item $count = $c->parse($fh, $description)

Read the filehandle and parse a changelog in it. The data in the object is
reset before parsing new data.

Returns the number of changelog entries that have been parsed with success.

This method needs to be implemented by one of the specialized changelog
format subclasses.

=item $count = $c->load($filename)

Parse $filename contents for a changelog.

Returns the number of changelog entries that have been parsed with success.

=item $c->reset_parse_errors()

Can be used to delete all information about errors occurred during
previous L<parse> runs.

=cut
115
116sub reset_parse_errors {
117
28
1
17
    my $self = shift;
118
28
33
    $self->{parse_errors} = [];
119}
120
121 - 126
=item $c->parse_error($file, $line_nr, $error, [$line])

Record a new parse error in $file at line $line_nr. The error message is
specified with $error and a copy of the line can be recorded in $line.

=cut
127
128sub parse_error {
129
9
1
14
    my ($self, $file, $line_nr, $error, $line) = @_;
130
131
9
9
7
12
    push @{$self->{parse_errors}}, [ $file, $line_nr, $error, $line ];
132
133
9
15
    if ($self->{verbose}) {
134
3
2
        if ($line) {
135
3
5
            warning("%20s(l$line_nr): $error\nLINE: $line", $file);
136        } else {
137
0
0
            warning("%20s(l$line_nr): $error", $file);
138        }
139    }
140}
141
142 - 170
=item $c->get_parse_errors()

Returns all error messages from the last L<parse> run.
If called in scalar context returns a human readable
string representation. If called in list context returns
an array of arrays. Each of these arrays contains

=over 4

=item 1.

a string describing the origin of the data (a filename usually). If the
reportfile configuration option was given, its value will be used instead.

=item 2.

the line number where the error occurred

=item 3.

an error description

=item 4.

the original line

=back

=cut
171
172sub get_parse_errors {
173
16
1
13
    my $self = shift;
174
175
16
15
    if (wantarray) {
176
4
4
4
4
        return @{$self->{parse_errors}};
177    } else {
178
12
7
        my $res = '';
179
12
12
9
14
        foreach my $e (@{$self->{parse_errors}}) {
180
0
0
            if ($e->[3]) {
181
0
0
                $res .= report(REPORT_WARN, g_("%s(l%s): %s\nLINE: %s"), @$e);
182            } else {
183
0
0
                $res .= report(REPORT_WARN, g_('%s(l%s): %s'), @$e);
184            }
185        }
186
12
12
        return $res;
187    }
188}
189
190 - 200
=item $c->set_unparsed_tail($tail)

Add a string representing unparsed lines after the changelog entries.
Use undef as $tail to remove the unparsed lines currently set.

=item $c->get_unparsed_tail()

Return a string representing the unparsed lines after the changelog
entries. Returns undef if there's no such thing.

=cut
201
202sub set_unparsed_tail {
203
38
1
33
    my ($self, $tail) = @_;
204
38
37
    $self->{unparsed_tail} = $tail;
205}
206
207sub get_unparsed_tail {
208
26
1
21
    my $self = shift;
209
26
25
    return $self->{unparsed_tail};
210}
211
212 - 224
=item @{$c}

Returns all the Dpkg::Changelog::Entry objects contained in this changelog
in the order in which they have been parsed.

=item $c->get_range($range)

Returns an array (if called in list context) or a reference to an array of
Dpkg::Changelog::Entry objects which each represent one entry of the
changelog. $range is a hash reference describing the range of entries
to return. See section L<"RANGE SELECTION">.

=cut
225
226sub _sanitize_range {
227
70
45
    my ($self, $r) = @_;
228
70
55
    my $data = $self->{data};
229
230
70
94
    if (defined($r->{offset}) and not defined($r->{count})) {
231
0
0
        warning(g_("'offset' without 'count' has no effect")) if $self->{verbose};
232
0
0
        delete $r->{offset};
233    }
234
235    ## no critic (ControlStructures::ProhibitUntilBlocks)
236
70
231
    if ((defined($r->{count}) || defined($r->{offset})) &&
237        (defined($r->{from}) || defined($r->{since}) ||
238         defined($r->{to}) || defined($r->{until})))
239    {
240        warning(g_("you can't combine 'count' or 'offset' with any other " .
241
0
0
                   'range option')) if $self->{verbose};
242
0
0
        delete $r->{from};
243
0
0
        delete $r->{since};
244
0
0
        delete $r->{to};
245
0
0
        delete $r->{until};
246    }
247
70
72
    if (defined($r->{from}) && defined($r->{since})) {
248        warning(g_("you can only specify one of 'from' and 'since', using " .
249
0
0
                   "'since'")) if $self->{verbose};
250
0
0
        delete $r->{from};
251    }
252
70
69
    if (defined($r->{to}) && defined($r->{until})) {
253        warning(g_("you can only specify one of 'to' and 'until', using " .
254
0
0
                   "'until'")) if $self->{verbose};
255
0
0
        delete $r->{to};
256    }
257
258    # Handle non-existing versions
259
70
40
    my (%versions, @versions);
260
70
70
44
55
    foreach my $entry (@{$data}) {
261
818
533
        my $version = $entry->get_version();
262
818
518
        next unless defined $version;
263
818
532
        $versions{$version->as_string()} = 1;
264
818
540
        push @versions, $version->as_string();
265    }
266
70
81
    if ((defined($r->{since}) and not exists $versions{$r->{since}})) {
267
2
3
        warning(g_("'%s' option specifies non-existing version '%s'"), 'since', $r->{since});
268
2
2
        warning(g_('use newest entry that is earlier than the one specified'));
269
2
3
        foreach my $v (@versions) {
270
14
15
            if (version_compare_relation($v, REL_LT, $r->{since})) {
271
0
0
                $r->{since} = $v;
272
0
0
                last;
273            }
274        }
275
2
4
        if (not exists $versions{$r->{since}}) {
276            # No version was earlier, include all
277
2
2
            warning(g_('none found, starting from the oldest entry'));
278
2
2
            delete $r->{since};
279
2
2
            $r->{from} = $versions[-1];
280        }
281    }
282
70
72
    if ((defined($r->{from}) and not exists $versions{$r->{from}})) {
283
0
0
        warning(g_("'%s' option specifies non-existing version '%s'"), 'from', $r->{from});
284
0
0
        warning(g_('use oldest entry that is later than the one specified'));
285
0
0
        my $oldest;
286
0
0
        foreach my $v (@versions) {
287
0
0
            if (version_compare_relation($v, REL_GT, $r->{from})) {
288
0
0
                $oldest = $v;
289            }
290        }
291
0
0
        if (defined($oldest)) {
292
0
0
            $r->{from} = $oldest;
293        } else {
294
0
0
            warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'from', $r->{from});
295
0
0
            delete $r->{from}; # No version was oldest
296        }
297    }
298
70
64
    if (defined($r->{until}) and not exists $versions{$r->{until}}) {
299
0
0
        warning(g_("'%s' option specifies non-existing version '%s'"), 'until', $r->{until});
300
0
0
        warning(g_('use oldest entry that is later than the one specified'));
301
0
0
        my $oldest;
302
0
0
        foreach my $v (@versions) {
303
0
0
            if (version_compare_relation($v, REL_GT, $r->{until})) {
304
0
0
                $oldest = $v;
305            }
306        }
307
0
0
        if (defined($oldest)) {
308
0
0
            $r->{until} = $oldest;
309        } else {
310
0
0
            warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'until', $r->{until});
311
0
0
            delete $r->{until}; # No version was oldest
312        }
313    }
314
70
66
    if (defined($r->{to}) and not exists $versions{$r->{to}}) {
315
0
0
        warning(g_("'%s' option specifies non-existing version '%s'"), 'to', $r->{to});
316
0
0
        warning(g_('use newest entry that is earlier than the one specified'));
317
0
0
        foreach my $v (@versions) {
318
0
0
            if (version_compare_relation($v, REL_LT, $r->{to})) {
319
0
0
                $r->{to} = $v;
320
0
0
                last;
321            }
322        }
323
0
0
        if (not exists $versions{$r->{to}}) {
324            # No version was earlier
325
0
0
            warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'to', $r->{to});
326
0
0
            delete $r->{to};
327        }
328    }
329
330
70
57
    if (defined($r->{since}) and $data->[0]->get_version() eq $r->{since}) {
331
0
0
        warning(g_("'since' option specifies most recent version '%s', ignoring"), $r->{since});
332
0
0
        delete $r->{since};
333    }
334
70
160
    if (defined($r->{until}) and $data->[-1]->get_version() eq $r->{until}) {
335
0
0
        warning(g_("'until' option specifies oldest version '%s', ignoring"), $r->{until});
336
0
0
        delete $r->{until};
337    }
338    ## use critic
339}
340
341sub get_range {
342
84
1
66
    my ($self, $range) = @_;
343
84
84
    $range //= {};
344
84
81
    my $res = $self->_data_range($range);
345
84
58
    return unless defined $res;
346
84
121
    if (wantarray) {
347
84
2
60
4
        return reverse @{$res} if $range->{reverse};
348
82
82
37
189
        return @{$res};
349    } else {
350
0
0
        return $res;
351    }
352}
353
354sub _is_full_range {
355
84
46
    my ($self, $range) = @_;
356
357
84
68
    return 1 if $range->{all};
358
359    # If no range delimiter is specified, we want everything.
360
82
63
    foreach my $delim (qw(since until from to count offset)) {
361
314
252
        return 0 if exists $range->{$delim};
362    }
363
364
12
22
    return 1;
365}
366
367sub _data_range {
368
84
53
    my ($self, $range) = @_;
369
370
84
89
    my $data = $self->{data} or return;
371
372
84
65
    return [ @$data ] if $self->_is_full_range($range);
373
374
70
76
    $self->_sanitize_range($range);
375
376
70
42
    my ($start, $end);
377
70
66
    if (defined($range->{count})) {
378
40
61
        my $offset = $range->{offset} // 0;
379
40
24
        my $count = $range->{count};
380        # Convert count/offset in start/end
381
40
40
        if ($offset > 0) {
382
12
9
            $offset -= ($count < 0);
383        } elsif ($offset < 0) {
384
8
5
            $offset = $#$data + ($count > 0) + $offset;
385        } else {
386
20
18
            $offset = $#$data if $count < 0;
387        }
388
40
24
        $start = $end = $offset;
389
40
35
        $start += $count+1 if $count < 0;
390
40
34
        $end += $count-1 if $count > 0;
391        # Check limits
392
40
33
        $start = 0 if $start < 0;
393
40
31
        return if $start > $#$data;
394
40
33
        $end = $#$data if $end > $#$data;
395
40
28
        return if $end < 0;
396
40
29
        $end = $start if $end < $start;
397
40
40
29
55
        return [ @{$data}[$start .. $end] ];
398    }
399
400    ## no critic (ControlStructures::ProhibitUntilBlocks)
401
30
17
    my @result;
402
30
17
    my $include = 1;
403
30
159
    $include = 0 if defined($range->{to}) or defined($range->{until});
404
30
30
21
21
    foreach my $entry (@{$data}) {
405
538
318
        my $v = $entry->get_version();
406
538
378
        $include = 1 if defined($range->{to}) and $v eq $range->{to};
407
538
362
        last if defined($range->{since}) and $v eq $range->{since};
408
409
536
326
        push @result, $entry if $include;
410
411
536
351
        $include = 1 if defined($range->{until}) and $v eq $range->{until};
412
536
401
        last if defined($range->{from}) and $v eq $range->{from};
413    }
414    ## use critic
415
416
30
38
    return \@result if scalar(@result);
417
0
0
    return;
418}
419
420 - 425
=item $c->abort_early()

Returns true if enough data have been parsed to be able to return all
entries selected by the range set at creation (or with set_options).

=cut
426
427sub abort_early {
428
291
1
178
    my $self = shift;
429
430
291
263
    my $data = $self->{data} or return;
431
291
370
    my $r = $self->{range} or return;
432
0
0
    my $count = $r->{count} // 0;
433
0
0
    my $offset = $r->{offset} // 0;
434
435
0
0
    return if $self->_is_full_range($r);
436
0
0
    return if $offset < 0 or $count < 0;
437
0
0
    if (defined($r->{count})) {
438
0
0
        if ($offset > 0) {
439
0
0
            $offset -= ($count < 0);
440        }
441
0
0
        my $start = my $end = $offset;
442
0
0
        $end += $count-1 if $count > 0;
443
0
0
        return ($start < @$data and $end < @$data);
444    }
445
446
0
0
    return unless defined($r->{since}) or defined($r->{from});
447
0
0
0
0
    foreach my $entry (@{$data}) {
448
0
0
        my $v = $entry->get_version();
449
0
0
        return 1 if defined($r->{since}) and $v eq $r->{since};
450
0
0
        return 1 if defined($r->{from}) and $v eq $r->{from};
451    }
452
453
0
0
    return;
454}
455
456 - 467
=item $str = $c->output()

=item "$c"

Returns a string representation of the changelog (it's a concatenation of
the string representation of the individual changelog entries).

=item $c->output($fh)

Output the changelog to the given filehandle.

=cut
468
469sub output {
470
12
1
24
    my ($self, $fh) = @_;
471
12
14
    my $str = '';
472
12
12
6
16
    foreach my $entry (@{$self}) {
473
242
177
        my $text = $entry->output();
474
242
0
144
0
        print { $fh } $text if defined $fh;
475
242
312
        $str .= $text if defined wantarray;
476    }
477
12
11
    my $text = $self->get_unparsed_tail();
478
12
11
    if (defined $text) {
479
4
0
6
0
        print { $fh } $text if defined $fh;
480
4
6
        $str .= $text if defined wantarray;
481    }
482
12
107
    return $str;
483}
484
485 - 489
=item $c->save($filename)

Save the changelog in the given file.

=cut
490
491our ( @URGENCIES, %URGENCIES );
492BEGIN {
493
6
5313
    @URGENCIES = qw(
494        low
495        medium
496        high
497        critical
498        emergency
499    );
500
6
4
    my $i = 1;
501
6
30
6
2360
    %URGENCIES = map { $_ => $i++ } @URGENCIES;
502}
503
504sub _format_dpkg {
505
16
13
    my ($self, $range) = @_;
506
507
16
17
    my @data = $self->get_range($range) or return;
508
16
11
    my $src = shift @data;
509
510
16
36
    my $f = Dpkg::Control::Changelog->new();
511
16
21
    $f->{Urgency} = $src->get_urgency() || 'unknown';
512
16
24
    $f->{Source} = $src->get_source() || 'unknown';
513
16
15
    $f->{Version} = $src->get_version() // 'unknown';
514
16
17
    $f->{Distribution} = join(' ', $src->get_distributions());
515
16
20
    $f->{Maintainer} = $src->get_maintainer() // '';
516
16
18
    $f->{Date} = $src->get_timestamp() // '';
517
16
15
    $f->{Timestamp} = $src->get_timepiece && $src->get_timepiece->epoch // '';
518
16
24
    $f->{Changes} = $src->get_dpkg_changes();
519
520    # handle optional fields
521
16
16
    my $opts = $src->get_optional_fields();
522
16
12
    my %closes;
523
16
11
    foreach (keys %$opts) {
524
26
41
        if (/^Urgency$/i) { # Already dealt
525        } elsif (/^Closes$/i) {
526
10
7
            $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
527        } else {
528
0
0
            field_transfer_single($opts, $f);
529        }
530    }
531
532
16
13
    foreach my $bin (@data) {
533
238
167
        my $oldurg = $f->{Urgency} // '';
534
238
193
        my $oldurgn = $URGENCIES{$f->{Urgency}} // -1;
535
238
214
        my $newurg = $bin->get_urgency() // '';
536
238
197
        my $newurgn = $URGENCIES{$newurg} // -1;
537
238
193
        $f->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg;
538
238
174
        $f->{Changes} .= "\n" . $bin->get_dpkg_changes();
539
540        # handle optional fields
541
238
527
        $opts = $bin->get_optional_fields();
542
238
177
        foreach (keys %$opts) {
543
416
388
            if (/^Closes$/i) {
544
168
123
                $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
545            } elsif (not exists $f->{$_}) { # Don't overwrite an existing field
546
10
9
                field_transfer_single($opts, $f);
547            }
548        }
549    }
550
551
16
15
    if (scalar keys %closes) {
552
10
8521
65
3732
        $f->{Closes} = join ' ', sort { $a <=> $b } keys %closes;
553    }
554
16
38
    run_vendor_hook('post-process-changelog-entry', $f);
555
556
16
85
    return $f;
557}
558
559sub _format_rfc822 {
560
22
14
    my ($self, $range) = @_;
561
562
22
25
    my @data = $self->get_range($range) or return;
563
22
16
    my @ctrl;
564
565
22
12
    foreach my $entry (@data) {
566
484
432
        my $f = Dpkg::Control::Changelog->new();
567
484
423
        $f->{Urgency} = $entry->get_urgency() || 'unknown';
568
484
502
        $f->{Source} = $entry->get_source() || 'unknown';
569
484
431
        $f->{Version} = $entry->get_version() // 'unknown';
570
484
509
        $f->{Distribution} = join(' ', $entry->get_distributions());
571
484
447
        $f->{Maintainer} = $entry->get_maintainer() // '';
572
484
431
        $f->{Date} = $entry->get_timestamp() // '';
573
484
439
        $f->{Timestamp} = $entry->get_timepiece && $entry->get_timepiece->epoch // '';
574
484
519
        $f->{Changes} = $entry->get_dpkg_changes();
575
576        # handle optional fields
577
484
440
        my $opts = $entry->get_optional_fields();
578
484
360
        foreach (keys %$opts) {
579
834
543
            field_transfer_single($opts, $f) unless exists $f->{$_};
580        }
581
582
484
574
        run_vendor_hook('post-process-changelog-entry', $f);
583
584
484
365
        push @ctrl, $f;
585    }
586
587
22
58
    return @ctrl;
588}
589
590 - 651
=item $control = $c->format_range($format, $range)

Formats the changelog into Dpkg::Control::Changelog objects representing the
entries selected by the optional range specifier (see L<"RANGE SELECTION">
for details). In scalar context returns a Dpkg::Index object containing the
selected entries, in list context returns an array of Dpkg::Control::Changelog
objects.

With format B<dpkg> the returned Dpkg::Control::Changelog object is coalesced
from the entries in the changelog that are part of the range requested,
with the fields described below, but considering that "selected entry"
means the first entry of the selected range.

With format B<rfc822> each returned Dpkg::Control::Changelog objects
represents one entry in the changelog that is part of the range requested,
with the fields described below, but considering that "selected entry"
means for each entry.

The different formats return undef if no entries are matched. The following
fields are contained in the object(s) returned:

=over 4

=item Source

package name (selected entry)

=item Version

packages' version (selected entry)

=item Distribution

target distribution (selected entry)

=item Urgency

urgency (highest of all entries in range)

=item Maintainer

person that created the (selected) entry

=item Date

date of the (selected) entry

=item Timestamp

date of the (selected) entry as a timestamp in seconds since the epoch

=item Closes

bugs closed by the (selected) entry/entries, sorted by bug number

=item Changes

content of the (selected) entry/entries

=back

=cut
652
653sub format_range {
654
38
1
30
    my ($self, $format, $range) = @_;
655
656
38
26
    my @ctrl;
657
658
38
42
    if ($format eq 'dpkg') {
659
16
18
        @ctrl = $self->_format_dpkg($range);
660    } elsif ($format eq 'rfc822') {
661
22
21
        @ctrl = $self->_format_rfc822($range);
662    } else {
663
0
0
        croak "unknown changelog output format $format";
664    }
665
666
38
27
    if (wantarray) {
667
2
3
        return @ctrl;
668    } else {
669
36
64
        my $index = Dpkg::Index->new(type => CTRL_CHANGELOG);
670
671
36
25
        foreach my $f (@ctrl) {
672
498
334
            $index->add($f);
673        }
674
675
36
67
        return $index;
676    }
677}
678
679=back
680
681 - 774
=head1 RANGE SELECTION

A range selection is described by a hash reference where
the allowed keys and values are described below.

The following options take a version number as value.

=over 4

=item since

Causes changelog information from all versions strictly
later than B<version> to be used.

=item until

Causes changelog information from all versions strictly
earlier than B<version> to be used.

=item from

Similar to C<since> but also includes the information for the
specified B<version> itself.

=item to

Similar to C<until> but also includes the information for the
specified B<version> itself.

=back

The following options don't take version numbers as values:

=over 4

=item all

If set to a true value, all entries of the changelog are returned,
this overrides all other options.

=item count

Expects a signed integer as value. Returns C<value> entries from the
top of the changelog if set to a positive integer, and C<abs(value)>
entries from the tail if set to a negative integer.

=item offset

Expects a signed integer as value. Changes the starting point for
C<count>, either counted from the top (positive integer) or from
the tail (negative integer). C<offset> has no effect if C<count>
wasn't given as well.

=back

Some examples for the above options. Imagine an example changelog with
entries for the versions 1.2, 1.3, 2.0, 2.1, 2.2, 3.0 and 3.1.

  Range                        Included entries
  -----                        ----------------
  since => '2.0'               3.1, 3.0, 2.2
  until => '2.0'               1.3, 1.2
  from  => '2.0'               3.1, 3.0, 2.2, 2.1, 2.0
  to    => '2.0'               2.0, 1.3, 1.2
  count =>  2                  3.1, 3.0
  count => -2                  1.3, 1.2
  count =>  3, offset => 2     2.2, 2.1, 2.0
  count =>  2, offset => -3    2.0, 1.3
  count => -2, offset => 3     3.0, 2.2
  count => -2, offset => -3    2.2, 2.1

Any combination of one option of C<since> and C<from> and one of
C<until> and C<to> returns the intersection of the two results
with only one of the options specified.

=head1 CHANGES

=head2 Version 2.00 (dpkg 1.20.0)

Remove methods: $c->dpkg(), $c->rfc822().

=head2 Version 1.01 (dpkg 1.18.8)

New method: $c->format_range().

Deprecated methods: $c->dpkg(), $c->rfc822().

New field Timestamp in output formats.

=head2 Version 1.00 (dpkg 1.15.6)

Mark the module as public.

=cut
7751;