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