File Coverage

File:Dpkg/Changelog.pm
Coverage:69.1%

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 (L<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 2.00;
33
34
18
18
18
67
34
403
use strict;
35
18
18
18
39
10
488
use warnings;
36
37
18
18
18
39
9
521
use Carp;
38
39
18
18
18
35
11
501
use Dpkg::Gettext;
40
18
18
18
32
12
1680
use Dpkg::ErrorHandling qw(:DEFAULT report REPORT_WARN);
41
18
18
18
3138
25
922
use Dpkg::Control;
42
18
18
18
2516
22
383
use Dpkg::Control::Changelog;
43
18
18
18
36
12
674
use Dpkg::Control::Fields;
44
18
18
18
2597
23
338
use Dpkg::Index;
45
18
18
18
2947
26
987
use Dpkg::Version;
46
18
18
18
65
16
400
use Dpkg::Vendor qw(run_vendor_hook);
47
48
18
18
18
37
12
49
use parent qw(Dpkg::Interface::Storable);
49
50use overload
51
18
18
18
108
737
14
54
256
    '@{}' => sub { return $_[0]->{data} };
52
53 - 61
=head1 METHODS

=over 4

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

Creates a new changelog object.

=cut
62
63sub new {
64
84
1
157
    my ($this, %opts) = @_;
65
84
319
    my $class = ref($this) || $this;
66
84
178
    my $self = {
67        verbose => 1,
68        parse_errors => []
69    };
70
84
116
    bless $self, $class;
71
84
222
    $self->set_options(%opts);
72
84
136
    return $self;
73}
74
75 - 84
=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
85
86sub set_options {
87
84
1
99
    my ($self, %opts) = @_;
88
84
338
    $self->{$_} = $opts{$_} foreach keys %opts;
89}
90
91 - 112
=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 parse() runs.

=cut
113
114sub reset_parse_errors {
115
84
1
63
    my $self = shift;
116
84
132
    $self->{parse_errors} = [];
117}
118
119 - 124
=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
125
126sub parse_error {
127
27
1
55
    my ($self, $file, $line_nr, $error, $line) = @_;
128
129
27
27
28
55
    push @{$self->{parse_errors}}, [ $file, $line_nr, $error, $line ];
130
131
27
54
    if ($self->{verbose}) {
132
9
6
        if ($line) {
133
9
21
            warning("%20s(l$line_nr): $error\nLINE: $line", $file);
134        } else {
135
0
0
            warning("%20s(l$line_nr): $error", $file);
136        }
137    }
138}
139
140 - 168
=item $c->get_parse_errors()

Returns all error messages from the last 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
169
170sub get_parse_errors {
171
48
1
74
    my $self = shift;
172
173
48
77
    if (wantarray) {
174
12
12
14
32
        return @{$self->{parse_errors}};
175    } else {
176
36
33
        my $res = '';
177
36
36
37
61
        foreach my $e (@{$self->{parse_errors}}) {
178
0
0
            if ($e->[3]) {
179
0
0
                $res .= report(REPORT_WARN, g_("%s(l%s): %s\nLINE: %s"), @$e);
180            } else {
181
0
0
                $res .= report(REPORT_WARN, g_('%s(l%s): %s'), @$e);
182            }
183        }
184
36
52
        return $res;
185    }
186}
187
188 - 198
=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
199
200sub set_unparsed_tail {
201
114
1
118
    my ($self, $tail) = @_;
202
114
154
    $self->{unparsed_tail} = $tail;
203}
204
205sub get_unparsed_tail {
206
78
1
62
    my $self = shift;
207
78
107
    return $self->{unparsed_tail};
208}
209
210 - 222
=item @{$c}

Returns all the L<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
L<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
223
224sub _sanitize_range {
225
210
200
    my ($self, $r) = @_;
226
210
192
    my $data = $self->{data};
227
228
210
439
    if (defined($r->{offset}) and not defined($r->{count})) {
229
0
0
        warning(g_("'offset' without 'count' has no effect")) if $self->{verbose};
230
0
0
        delete $r->{offset};
231    }
232
233    ## no critic (ControlStructures::ProhibitUntilBlocks)
234
210
1154
    if ((defined($r->{count}) || defined($r->{offset})) &&
235        (defined($r->{from}) || defined($r->{since}) ||
236         defined($r->{to}) || defined($r->{until})))
237    {
238        warning(g_("you can't combine 'count' or 'offset' with any other " .
239
0
0
                   'range option')) if $self->{verbose};
240
0
0
        delete $r->{from};
241
0
0
        delete $r->{since};
242
0
0
        delete $r->{to};
243
0
0
        delete $r->{until};
244    }
245
210
303
    if (defined($r->{from}) && defined($r->{since})) {
246        warning(g_("you can only specify one of 'from' and 'since', using " .
247
0
0
                   "'since'")) if $self->{verbose};
248
0
0
        delete $r->{from};
249    }
250
210
278
    if (defined($r->{to}) && defined($r->{until})) {
251        warning(g_("you can only specify one of 'to' and 'until', using " .
252
0
0
                   "'until'")) if $self->{verbose};
253
0
0
        delete $r->{to};
254    }
255
256    # Handle non-existing versions
257
210
173
    my (%versions, @versions);
258
210
210
174
247
    foreach my $entry (@{$data}) {
259
2454
2291
        my $version = $entry->get_version();
260
2454
1851
        next unless defined $version;
261
2454
2060
        $versions{$version->as_string()} = 1;
262
2454
2086
        push @versions, $version->as_string();
263    }
264
210
368
    if ((defined($r->{since}) and not exists $versions{$r->{since}})) {
265
6
16
        warning(g_("'%s' option specifies non-existing version '%s'"), 'since', $r->{since});
266
6
10
        warning(g_('use newest entry that is earlier than the one specified'));
267
6
7
        foreach my $v (@versions) {
268
42
52
            if (version_compare_relation($v, REL_LT, $r->{since})) {
269
0
0
                $r->{since} = $v;
270
0
0
                last;
271            }
272        }
273
6
14
        if (not exists $versions{$r->{since}}) {
274            # No version was earlier, include all
275
6
12
            warning(g_('none found, starting from the oldest entry'));
276
6
8
            delete $r->{since};
277
6
8
            $r->{from} = $versions[-1];
278        }
279    }
280
210
338
    if ((defined($r->{from}) and not exists $versions{$r->{from}})) {
281
0
0
        warning(g_("'%s' option specifies non-existing version '%s'"), 'from', $r->{from});
282
0
0
        warning(g_('use oldest entry that is later than the one specified'));
283
0
0
        my $oldest;
284
0
0
        foreach my $v (@versions) {
285
0
0
            if (version_compare_relation($v, REL_GT, $r->{from})) {
286
0
0
                $oldest = $v;
287            }
288        }
289
0
0
        if (defined($oldest)) {
290
0
0
            $r->{from} = $oldest;
291        } else {
292
0
0
            warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'from', $r->{from});
293
0
0
            delete $r->{from}; # No version was oldest
294        }
295    }
296
210
305
    if (defined($r->{until}) and not exists $versions{$r->{until}}) {
297
0
0
        warning(g_("'%s' option specifies non-existing version '%s'"), 'until', $r->{until});
298
0
0
        warning(g_('use oldest entry that is later than the one specified'));
299
0
0
        my $oldest;
300
0
0
        foreach my $v (@versions) {
301
0
0
            if (version_compare_relation($v, REL_GT, $r->{until})) {
302
0
0
                $oldest = $v;
303            }
304        }
305
0
0
        if (defined($oldest)) {
306
0
0
            $r->{until} = $oldest;
307        } else {
308
0
0
            warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'until', $r->{until});
309
0
0
            delete $r->{until}; # No version was oldest
310        }
311    }
312
210
278
    if (defined($r->{to}) and not exists $versions{$r->{to}}) {
313
0
0
        warning(g_("'%s' option specifies non-existing version '%s'"), 'to', $r->{to});
314
0
0
        warning(g_('use newest entry that is earlier than the one specified'));
315
0
0
        foreach my $v (@versions) {
316
0
0
            if (version_compare_relation($v, REL_LT, $r->{to})) {
317
0
0
                $r->{to} = $v;
318
0
0
                last;
319            }
320        }
321
0
0
        if (not exists $versions{$r->{to}}) {
322            # No version was earlier
323
0
0
            warning(g_("no such entry found, ignoring '%s' parameter '%s'"), 'to', $r->{to});
324
0
0
            delete $r->{to};
325        }
326    }
327
328
210
283
    if (defined($r->{since}) and $data->[0]->get_version() eq $r->{since}) {
329
0
0
        warning(g_("'since' option specifies most recent version '%s', ignoring"), $r->{since});
330
0
0
        delete $r->{since};
331    }
332
210
771
    if (defined($r->{until}) and $data->[-1]->get_version() eq $r->{until}) {
333
0
0
        warning(g_("'until' option specifies oldest version '%s', ignoring"), $r->{until});
334
0
0
        delete $r->{until};
335    }
336    ## use critic
337}
338
339sub get_range {
340
252
1
278
    my ($self, $range) = @_;
341
252
399
    $range //= {};
342
252
362
    my $res = $self->_data_range($range);
343
252
285
    return unless defined $res;
344
252
242
    if (wantarray) {
345
252
6
282
16
        return reverse @{$res} if $range->{reverse};
346
246
246
215
653
        return @{$res};
347    } else {
348
0
0
        return $res;
349    }
350}
351
352sub _is_full_range {
353
252
227
    my ($self, $range) = @_;
354
355
252
325
    return 1 if $range->{all};
356
357    # If no range delimiter is specified, we want everything.
358
246
270
    foreach my $delim (qw(since until from to count offset)) {
359
942
1026
        return 0 if exists $range->{$delim};
360    }
361
362
36
121
    return 1;
363}
364
365sub _data_range {
366
252
226
    my ($self, $range) = @_;
367
368
252
433
    my $data = $self->{data} or return;
369
370
252
332
    return [ @$data ] if $self->_is_full_range($range);
371
372
210
312
    $self->_sanitize_range($range);
373
374
210
191
    my ($start, $end);
375
210
268
    if (defined($range->{count})) {
376
120
185
        my $offset = $range->{offset} // 0;
377
120
86
        my $count = $range->{count};
378        # Convert count/offset in start/end
379
120
155
        if ($offset > 0) {
380
36
33
            $offset -= ($count < 0);
381        } elsif ($offset < 0) {
382
24
26
            $offset = $#$data + ($count > 0) + $offset;
383        } else {
384
60
67
            $offset = $#$data if $count < 0;
385        }
386
120
93
        $start = $end = $offset;
387
120
125
        $start += $count+1 if $count < 0;
388
120
109
        $end += $count-1 if $count > 0;
389        # Check limits
390
120
110
        $start = 0 if $start < 0;
391
120
143
        return if $start > $#$data;
392
120
121
        $end = $#$data if $end > $#$data;
393
120
120
        return if $end < 0;
394
120
95
        $end = $start if $end < $start;
395
120
120
119
208
        return [ @{$data}[$start .. $end] ];
396    }
397
398    ## no critic (ControlStructures::ProhibitUntilBlocks)
399
90
95
    my @result;
400
90
85
    my $include = 1;
401
90
305
    $include = 0 if defined($range->{to}) or defined($range->{until});
402
90
90
87
106
    foreach my $entry (@{$data}) {
403
1614
1421
        my $v = $entry->get_version();
404
1614
1515
        $include = 1 if defined($range->{to}) and $v eq $range->{to};
405
1614
1445
        last if defined($range->{since}) and $v eq $range->{since};
406
407
1608
1308
        push @result, $entry if $include;
408
409
1608
1369
        $include = 1 if defined($range->{until}) and $v eq $range->{until};
410
1608
1736
        last if defined($range->{from}) and $v eq $range->{from};
411    }
412    ## use critic
413
414
90
225
    return \@result if scalar(@result);
415
0
0
    return;
416}
417
418 - 423
=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
424
425sub abort_early {
426
873
1
625
    my $self = shift;
427
428
873
1078
    my $data = $self->{data} or return;
429
873
1391
    my $r = $self->{range} or return;
430
0
0
    my $count = $r->{count} // 0;
431
0
0
    my $offset = $r->{offset} // 0;
432
433
0
0
    return if $self->_is_full_range($r);
434
0
0
    return if $offset < 0 or $count < 0;
435
0
0
    if (defined($r->{count})) {
436
0
0
        if ($offset > 0) {
437
0
0
            $offset -= ($count < 0);
438        }
439
0
0
        my $start = my $end = $offset;
440
0
0
        $end += $count-1 if $count > 0;
441
0
0
0
0
        return $start < @{$data} > $end;
442    }
443
444
0
0
    return unless defined($r->{since}) or defined($r->{from});
445
0
0
0
0
    foreach my $entry (@{$data}) {
446
0
0
        my $v = $entry->get_version();
447
0
0
        return 1 if defined($r->{since}) and $v eq $r->{since};
448
0
0
        return 1 if defined($r->{from}) and $v eq $r->{from};
449    }
450
451
0
0
    return;
452}
453
454 - 465
=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
466
467sub output {
468
36
1
41
    my ($self, $fh) = @_;
469
36
27
    my $str = '';
470
36
36
29
58
    foreach my $entry (@{$self}) {
471
726
632
        my $text = $entry->output();
472
726
0
526
0
        print { $fh } $text if defined $fh;
473
726
1054
        $str .= $text if defined wantarray;
474    }
475
36
74
    my $text = $self->get_unparsed_tail();
476
36
53
    if (defined $text) {
477
12
0
22
0
        print { $fh } $text if defined $fh;
478
12
20
        $str .= $text if defined wantarray;
479    }
480
36
384
    return $str;
481}
482
483 - 487
=item $c->save($filename)

Save the changelog in the given file.

=cut
488
489our ( @URGENCIES, %URGENCIES );
490BEGIN {
491
18
19110
    @URGENCIES = qw(
492        low
493        medium
494        high
495        critical
496        emergency
497    );
498
18
18
    my $i = 1;
499
18
90
25
9411
    %URGENCIES = map { $_ => $i++ } @URGENCIES;
500}
501
502sub _format_dpkg {
503
48
61
    my ($self, $range) = @_;
504
505
48
114
    my @data = $self->get_range($range) or return;
506
48
54
    my $src = shift @data;
507
508
48
212
    my $c = Dpkg::Control::Changelog->new();
509
48
114
    $c->{Urgency} = $src->get_urgency() || 'unknown';
510
48
107
    $c->{Source} = $src->get_source() || 'unknown';
511
48
92
    $c->{Version} = $src->get_version() // 'unknown';
512
48
104
    $c->{Distribution} = join ' ', $src->get_distributions();
513
48
93
    $c->{Maintainer} = $src->get_maintainer() // '';
514
48
91
    $c->{Date} = $src->get_timestamp() // '';
515
48
94
    $c->{Timestamp} = $src->get_timepiece && $src->get_timepiece->epoch // '';
516
48
130
    $c->{Changes} = $src->get_dpkg_changes();
517
518    # handle optional fields
519
48
79
    my $opts = $src->get_optional_fields();
520
48
43
    my %closes;
521
48
48
38
59
    foreach my $f (keys %{$opts}) {
522
78
119
        if ($f eq 'Urgency') {
523            # Already handled.
524        } elsif ($f eq 'Closes') {
525
30
38
            $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
526        } else {
527
0
0
            field_transfer_single($opts, $c, $f);
528        }
529    }
530
531
48
77
    foreach my $bin (@data) {
532
714
608
        my $oldurg = $c->{Urgency} // '';
533
714
717
        my $oldurgn = $URGENCIES{$c->{Urgency}} // -1;
534
714
883
        my $newurg = $bin->get_urgency() // '';
535
714
786
        my $newurgn = $URGENCIES{$newurg} // -1;
536
714
743
        $c->{Urgency} = ($newurgn > $oldurgn) ? $newurg : $oldurg;
537
714
653
        $c->{Changes} .= "\n" . $bin->get_dpkg_changes();
538
539        # handle optional fields
540
714
2348
        $opts = $bin->get_optional_fields();
541
714
714
423
615
        foreach my $f (keys %{$opts}) {
542
1248
1203
            if ($f eq 'Closes') {
543
504
398
                $closes{$_} = 1 foreach (split(/\s+/, $opts->{Closes}));
544            } elsif (not exists $c->{$f}) {
545                # Don't overwrite an existing field
546
30
50
                field_transfer_single($opts, $c, $f);
547            }
548        }
549    }
550
551
48
74
    if (scalar keys %closes) {
552
30
25571
376
13180
        $c->{Closes} = join ' ', sort { $a <=> $b } keys %closes;
553    }
554
48
222
    run_vendor_hook('post-process-changelog-entry', $c);
555
556
48
551
    return $c;
557}
558
559sub _format_rfc822 {
560
66
82
    my ($self, $range) = @_;
561
562
66
136
    my @data = $self->get_range($range) or return;
563
66
66
    my @ctrl;
564
565
66
62
    foreach my $entry (@data) {
566
1452
2103
        my $c = Dpkg::Control::Changelog->new();
567
1452
1982
        $c->{Urgency} = $entry->get_urgency() || 'unknown';
568
1452
2219
        $c->{Source} = $entry->get_source() || 'unknown';
569
1452
1947
        $c->{Version} = $entry->get_version() // 'unknown';
570
1452
2200
        $c->{Distribution} = join ' ', $entry->get_distributions();
571
1452
1923
        $c->{Maintainer} = $entry->get_maintainer() // '';
572
1452
1790
        $c->{Date} = $entry->get_timestamp() // '';
573
1452
1772
        $c->{Timestamp} = $entry->get_timepiece && $entry->get_timepiece->epoch // '';
574
1452
2287
        $c->{Changes} = $entry->get_dpkg_changes();
575
576        # handle optional fields
577
1452
1906
        my $opts = $entry->get_optional_fields();
578
1452
1452
962
1267
        foreach my $f (keys %{$opts}) {
579
2502
2098
            field_transfer_single($opts, $c, $f) unless exists $c->{$f};
580        }
581
582
1452
2675
        run_vendor_hook('post-process-changelog-entry', $c);
583
584
1452
1702
        push @ctrl, $c;
585    }
586
587
66
423
    return @ctrl;
588}
589
590 - 651
=item $control = $c->format_range($format, $range)

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

With format B<dpkg> the returned L<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 L<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
114
1
166
    my ($self, $format, $range) = @_;
655
656
114
102
    my @ctrl;
657
658
114
205
    if ($format eq 'dpkg') {
659
48
101
        @ctrl = $self->_format_dpkg($range);
660    } elsif ($format eq 'rfc822') {
661
66
135
        @ctrl = $self->_format_rfc822($range);
662    } else {
663
0
0
        croak "unknown changelog output format $format";
664    }
665
666
114
154
    if (wantarray) {
667
6
12
        return @ctrl;
668    } else {
669
108
321
        my $index = Dpkg::Index->new(type => CTRL_CHANGELOG);
670
671
108
105
        foreach my $c (@ctrl) {
672
1494
1352
            $index->add($c);
673        }
674
675
108
569
        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;