]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Versions.pm
switch to compatibility level 12
[debbugs.git] / Debbugs / Versions.pm
1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
5 #
6 # [Other people have contributed to this file; their copyrights should
7 # go here too.]
8
9 package Debbugs::Versions;
10
11 use warnings;
12
13 use strict;
14
15 =head1 NAME
16
17 Debbugs::Versions - debbugs version information processing
18
19 =head1 DESCRIPTION
20
21 The Debbugs::Versions module provides generic support functions for the
22 implementation of version tracking in debbugs.
23
24 Complex organizations, such as Debian, require the tracking of bugs in
25 multiple versions of packages. The versioning scheme is frequently branched:
26 for example, a security update announced by an upstream developer will be
27 packaged as-is for the unstable distribution while a minimal backport is
28 made to the stable distribution. In order to report properly on the bugs
29 open in each distribution, debbugs must be aware of the structure of the
30 version tree for each package.
31
32 Gathering the version data is beyond the scope of this module: in the case
33 of Debian it is carried out by mechanical analysis of package changelogs.
34 Debbugs::Versions takes version data for a package generated by this or any
35 other means, merges it into a tree structure, and allows the user to perform
36 queries based on supplied data about the versions in which bugs have been
37 found and the versions in which they have been fixed.
38
39 =head1 DATA FORMAT
40
41 The data format looks like this (backslashes are not actually there, and
42 indicate continuation lines):
43
44   1.5.4 1.5.0 1.5-iwj.0.4 1.5-iwj.0.3 1.5-iwj.0.2 1.5-iwj.0.1 1.4.0 1.3.14 \
45         1.3.13 1.3.12 1.3.11 1.3.10 ...
46   1.4.1.6 1.4.1.5 1.4.1.4 1.4.1.3 1.4.1.2 1.4.1.1 1.4.1 1.4.0.31 1.4.0.30 \
47         1.4.0.29 1.4.0.28 1.4.0.27 1.4.0.26.0.1 1.4.0.26 1.4.0.25 1.4.0.24 \
48         1.4.0.23.2 1.4.0.23.1 1.4.0.23 1.4.0.22 1.4.0.21 1.4.0.20 1.4.0.19 \
49         1.4.0.18 1.4.0.17 1.4.0.16 1.4.0.15 1.4.0.14 1.4.0.13 1.4.0.12 \
50         1.4.0.11 1.4.0.10 1.4.0.9 1.4.0.8 1.4.0.7 1.4.0.6 1.4.0.5 1.4.0.4 \
51         1.4.0.3 1.4.0.2 1.4.0.1 1.4.0 \
52   1.4.0.35 1.4.0.34 1.4.0.33 1.4.0.32 1.4.0.31
53
54 =head1 METHODS
55
56 =over 8
57
58 =item new
59
60 Constructs a Debbugs::Versions object. The argument is a reference to a
61 version comparison function, which must be usable by Perl's built-in C<sort>
62 function.
63
64 =cut
65
66 sub new
67 {
68     my $this = shift;
69     my $class = ref($this) || $this;
70     my $vercmp = shift;
71     my $self = { parent => {}, vercmp => $vercmp };
72     return bless $self, $class;
73 }
74
75 =item isancestor
76
77 Takes two arguments, C<ancestor> and C<descendant>. Returns true if and only
78 if C<ancestor> is a version on which C<descendant> is based according to the
79 version data supplied to this object. (As a degenerate case, this relation
80 is reflexive: a version is considered to be an ancestor of itself.)
81
82 This method is expected mainly to be used internally by the C<merge> method.
83
84 =cut
85
86 sub isancestor
87 {
88     my $self = shift;
89     my $ancestor = shift;
90     my $descendant = shift;
91
92     my $parent = $self->{parent};
93     for (my $node = $descendant; defined $node; $node = $parent->{$node}) {
94         return 1 if $node eq $ancestor;
95     }
96
97     return 0;
98 }
99
100 =item leaves
101
102 Find the leaves of the version tree, i.e. those versions with no
103 descendants.
104
105 This method is mainly for internal use.
106
107 =cut
108
109 sub leaves
110 {
111     my $self = shift;
112
113     my $parent = $self->{parent};
114     my @vers = keys %$parent;
115     my %leaf;
116     @leaf{@vers} = (1) x @vers;
117     for my $v (@vers) {
118         delete $leaf{$parent->{$v}} if defined $parent->{$v};
119     }
120     return keys %leaf;
121 }
122
123 =item merge
124
125 Merges one branch of version data into this object. This branch takes the
126 form of a list of versions, each of which is to be considered as based on
127 the next in the list.
128
129 =cut
130
131 sub merge
132 {
133     my $self = shift;
134     return unless @_;
135     my $last = $_[0];
136     for my $i (1 .. $#_) {
137         # Detect loops.
138         next if $self->isancestor($last, $_[$i]);
139
140         # If it's already an ancestor version, don't add it again. This
141         # keeps the tree correct when we get several partial branches, such
142         # as '1.4.0 1.3.14 1.3.13 1.3.12' followed by '1.4.0 1.3.12 1.3.10'.
143         unless ($self->isancestor($_[$i], $last)) {
144             $self->{parent}{$last} = $_[$i];
145         }
146
147         $last = $_[$i];
148     }
149     # Insert undef for the last version so that we can tell a known version
150     # by seeing if it exists in $self->{parent}.
151     $self->{parent}{$_[$#_]} = undef unless exists $self->{parent}{$_[$#_]};
152 }
153
154 =item load
155
156 Loads version data from the filehandle passed as the argument. Each line of
157 input is expected to represent one branch, with versions separated by
158 whitespace.
159
160 =cut
161
162 sub load
163 {
164     my $self = shift;
165     my $fh = shift;
166     local $_;
167     while (<$fh>) {
168         $self->merge(split);
169     }
170 }
171
172 =item save
173
174 Outputs the version tree represented by this object to the filehandle passed
175 as the argument. The format is the same as that expected by the C<load>
176 method.
177
178 =cut
179
180 sub save
181 {
182     my $self = shift;
183     my $fh = shift;
184     local $_;
185     my $parent = $self->{parent};
186
187     # TODO: breaks with tcp-wrappers/1.0-1 tcpd/2.0-1 case
188     my @leaves = reverse sort {
189         my ($x, $y) = ($a, $b);
190         $x =~ s{.*/}{};
191         $y =~ s{.*/}{};
192         $self->{vercmp}->($x, $y);
193     } $self->leaves();
194
195     my %seen;
196     for my $lf (@leaves) {
197         print $fh $lf;
198         $seen{$lf} = 1;
199         for (my $node = $parent->{$lf}; defined $node;
200              $node = $parent->{$node}) {
201             print $fh " $node";
202             last if exists $seen{$node};
203             $seen{$node} = 1;
204         }
205         print $fh "\n";
206     }
207 }
208
209 =item buggy
210
211 Takes three arguments, C<version>, C<found>, and C<fixed>. Returns true if
212 and only if C<version> is based on or equal to a version in the list
213 referenced by C<found>, and not based on or equal to one referenced by
214 C<fixed>.
215
216 C<buggy> attempts to cope with found and fixed versions not in the version
217 tree by simply checking whether any fixed versions are recorded in the event
218 that nothing is known about any of the found versions.
219
220 =cut
221
222 sub buggy
223 {
224     my $self = shift;
225     my $version = shift;
226     my $found = shift;
227     my $fixed = shift;
228
229     my %found = map { $_ => 1 } @$found;
230     my %fixed = map { $_ => 1 } @$fixed;
231     my $parent = $self->{parent};
232     for (my $node = $version; defined $node; $node = $parent->{$node}) {
233         # The found and fixed tests are this way round because the most
234         # likely scenario is that somebody thought they'd fixed a bug and
235         # then it was reopened because it turned out not to have been fixed
236         # after all. However, tools that build found and fixed lists should
237         # generally know the order of events and make sure that the two
238         # lists have no common entries.
239         return 'found' if $found{$node};
240         return 'fixed' if $fixed{$node};
241     }
242
243     unless (@$found) {
244         # We don't know when it was found. Was it fixed in a descendant of
245         # this version? If so, this one should be considered buggy.
246         for my $f (@$fixed) {
247             for (my $node = $f; defined $node; $node = $parent->{$node}) {
248                 return 'found' if $node eq $version;
249             }
250         }
251     }
252
253     # Nothing in the requested version's ancestor chain can be confirmed as
254     # a version in which the bug was found or fixed. If it was only found or
255     # fixed on some other branch, then this one isn't buggy.
256     for my $f (@$found, @$fixed) {
257         return 'absent' if exists $parent->{$f};
258     }
259
260     # Otherwise, we degenerate to checking whether any fixed versions at all
261     # are recorded.
262     return 'fixed' if @$fixed;
263     return 'found';
264 }
265
266 =item allstates
267
268 Takes two arguments, C<found> and C<fixed>, which are interpreted as in
269 L</buggy>. Efficiently returns the state of the bug at every known version,
270 in the form of a hash from versions to states (as returned by L</buggy>). If
271 you pass a third argument, C<interested>, this method will stop after
272 determining the state of the bug at all the versions listed therein.
273
274 Whether this is faster than calling L</buggy> for each version you're
275 interested in is not altogether clear, and depends rather strongly on the
276 number of known and interested versions.
277
278 =cut
279
280 sub allstates
281 {
282     my $self = shift;
283     my $found = shift;
284     my $fixed = shift;
285     my $interested = shift;
286
287     my %found = map { $_ => 1 } @$found;
288     my %fixed = map { $_ => 1 } @$fixed;
289     my %interested;
290     if (defined $interested) {
291         %interested = map { $_ => 1 } @$interested;
292     }
293     my $parent = $self->{parent};
294     my @leaves = $self->leaves();
295
296     # Are any of the found or fixed versions known? We'll need this later.
297     my $known = 0;
298     for my $f (@$found, @$fixed) {
299         if (exists $parent->{$f}) {
300             $known = 1;
301             last;
302         }
303     }
304
305     # Start at each leaf in turn, working our way up and remembering the
306     # list of versions in the branch.
307     my %state;
308     LEAF: for my $lf (@leaves) {
309         my @branch;
310         my $fixeddesc = 0;
311
312         for (my $node = $lf; defined $node; $node = $parent->{$node}) {
313             # If we're about to start a new branch, check whether we know
314             # the state of every version in which we're interested. If so,
315             # we can stop now.
316             if (defined $interested and not @branch) {
317                 my @remove;
318                 for my $interest (keys %interested) {
319                     if (exists $state{$interest}) {
320                         push @remove, $interest;
321                     }
322                 }
323                 delete @interested{@remove};
324                 last LEAF unless keys %interested;
325             }
326
327             # We encounter a version whose state we already know. Record the
328             # branch with the same state as that version, and go on to the
329             # next leaf.
330             if (exists $state{$node}) {
331                 $state{$_} = $state{$node} foreach @branch;
332                 last;
333             }
334
335             push @branch, $node;
336
337             # We encounter a version in the found list. Record the branch as
338             # 'found', and start a new branch.
339             if ($found{$node}) {
340                 $state{$_} = 'found' foreach @branch;
341                 @branch = ();
342             }
343
344             # We encounter a version in the fixed list. Record the branch as
345             # 'fixed', and start a new branch, remembering that we have a
346             # fixed descendant.
347             elsif ($fixed{$node}) {
348                 $state{$_} = 'fixed' foreach @branch;
349                 @branch = ();
350                 $fixeddesc = 1;
351             }
352
353             # We encounter a root.
354             elsif (not defined $parent->{$node}) {
355                 # If the found list is empty and we have a fixed descendant,
356                 # record the branch as 'found' (since they probably just
357                 # forgot to report a version when opening the bug).
358                 if (not @$found and $fixeddesc) {
359                     $state{$_} = 'found' foreach @branch;
360                 }
361
362                 # If any of the found or fixed versions are known, record
363                 # the branch as 'absent' (since all the activity must have
364                 # happened on some other branch).
365                 elsif ($known) {
366                     $state{$_} = 'absent' foreach @branch;
367                 }
368
369                 # If there are any fixed versions at all (but they're
370                 # unknown), then who knows, but we guess at recording the
371                 # branch as 'fixed'.
372                 elsif (@$fixed) {
373                     $state{$_} = 'fixed' foreach @branch;
374                 }
375
376                 # Otherwise, fall back to recording the branch as 'found'.
377                 else {
378                     $state{$_} = 'found' foreach @branch;
379                 }
380
381                 # In any case, we're done.
382                 last;
383             }
384         }
385     }
386
387     return %state;
388 }
389
390 =back
391
392 =cut
393
394 1;