X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=Debbugs%2FStatus.pm;h=6e21f99cff7ee3de8ec61aa5513cfc2222db1dd6;hb=9415ae826d9efc4e70727e22b2ca29060d1a6a15;hp=360cf38cfe2a3e1ac01dcc12af885408991449c0;hpb=adad9c647a843eb1d491aaa8e4493d847b5b98fe;p=debbugs.git diff --git a/Debbugs/Status.pm b/Debbugs/Status.pm index 360cf38..6e21f99 100644 --- a/Debbugs/Status.pm +++ b/Debbugs/Status.pm @@ -5,7 +5,7 @@ # # [Other people have contributed to this file; their copyrights should # go here too.] -# Copyright 2007 by Don Armstrong . +# Copyright 2007-9 by Don Armstrong . package Debbugs::Status; @@ -32,6 +32,7 @@ status of a particular bug use warnings; use strict; + use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT); use base qw(Exporter); @@ -39,13 +40,15 @@ use Params::Validate qw(validate_with :types); use Debbugs::Common qw(:util :lock :quit :misc); use Debbugs::Config qw(:config); use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522); -use Debbugs::Packages qw(makesourceversions getversions get_versions binarytosource); +use Debbugs::Packages qw(makesourceversions make_source_versions getversions get_versions binarytosource); use Debbugs::Versions; use Debbugs::Versions::Dpkg; use POSIX qw(ceil); +use Storable qw(dclone); use List::Util qw(min max); +use Carp qw(croak); BEGIN{ $VERSION = 1.00; @@ -53,17 +56,20 @@ BEGIN{ @EXPORT = (); %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable), - qw(isstrongseverity bug_presence), + qw(isstrongseverity bug_presence split_status_fields), + ], + read => [qw(readbug read_bug lockreadbug lockreadbugmerge), + qw(lock_read_all_merged_bugs), ], - read => [qw(readbug read_bug lockreadbug lockreadbugmerge)], write => [qw(writebug makestatus unlockwritebug)], versions => [qw(addfoundversions addfixedversions), qw(removefoundversions removefixedversions) ], hook => [qw(bughook bughook_archive)], + fields => [qw(%fields)], ); @EXPORT_OK = (); - Exporter::export_ok_tags(qw(status read write versions hook)); + Exporter::export_ok_tags(qw(status read write versions hook fields)); $EXPORT_TAGS{all} = [@EXPORT_OK]; } @@ -78,8 +84,9 @@ location. Valid locations are those understood by L =cut - -my %fields = (originator => 'submitter', +# these probably shouldn't be imported by most people, but +# Debbugs::Control needs them, so they're now exportable +our %fields = (originator => 'submitter', date => 'date', subject => 'subject', msgid => 'message-id', @@ -97,8 +104,11 @@ my %fields = (originator => 'submitter', blocks => 'blocks', blockedby => 'blocked-by', unarchived => 'unarchived', + summary => 'summary', + affects => 'affects', ); + # Fields which need to be RFC1522-decoded in format versions earlier than 3. my @rfc1522_fields = qw(originator subject done forwarded owner); @@ -129,6 +139,10 @@ path to the summary file instead of the bug number and/or location. =item summary -- complete path to the .summary file which will be read +=item lock -- whether to obtain a lock for the bug to prevent +something modifying it while the bug has been read. You B call +C if something not undef is returned from read_bug. + =back One of C or C must be passed. This function will return @@ -154,6 +168,9 @@ sub read_bug{ summary => {type => SCALAR, optional => 1, }, + lock => {type => BOOLEAN, + optional => 1, + }, }, ); die "One of bug or summary must be passed to read_bug" @@ -171,6 +188,7 @@ sub read_bug{ $status = getbugcomponent($lref, 'summary', $location); $log = getbugcomponent($lref, 'log' , $location); return undef unless defined $status; + return undef if not -e $status; } else { $status = $param{summary}; @@ -178,8 +196,17 @@ sub read_bug{ $log =~ s/\.summary$/.log/; ($location) = $status =~ m/(db-h|db|archive)/; } - my $status_fh = new IO::File $status, 'r' or - warn "Unable to open $status for reading: $!" and return undef; + if ($param{lock}) { + filelock("$config{spool_dir}/lock/$param{bug}"); + } + my $status_fh = IO::File->new($status, 'r'); + if (not defined $status_fh) { + warn "Unable to open $status for reading: $!"; + if ($param{lock}) { + unfilelock(); + } + return undef; + } my %data; my @lines; @@ -193,7 +220,13 @@ sub read_bug{ } # Version 3 is the latest format version currently supported. - return undef if $version > 3; + if ($version > 3) { + warn "Unsupported status version '$version'"; + if ($param{lock}) { + unfilelock(); + } + return undef; + } my %namemap = reverse %fields; for my $line (@lines) { @@ -223,13 +256,116 @@ sub read_bug{ $data{$field} = decode_rfc1522($data{$field}); } } + my $status_modified = (stat($status))[9]; # Add log last modified time $data{log_modified} = (stat($log))[9]; + $data{last_modified} = max($status_modified,$data{log_modified}); $data{location} = $location; + $data{archived} = (defined($location) and ($location eq 'archive'))?1:0; + $data{bug_num} = $param{bug}; return \%data; } +=head2 split_status_fields + + my @data = split_status_fields(@data); + +Splits splittable status fields (like package, tags, blocks, +blockedby, etc.) into arrayrefs (use make_list on these). Keeps the +passed @data intact using dclone. + +In scalar context, returns only the first element of @data. + +=cut + +our $ditch_empty = sub{ + my @t = @_; + my $splitter = shift @t; + return grep {length $_} map {split $splitter} @t; +}; + +my $ditch_empty_space = sub {return &{$ditch_empty}(' ',@_)}; +my %split_fields = + (package => \&splitpackages, + affects => \&splitpackages, + blocks => $ditch_empty_space, + blockedby => $ditch_empty_space, + tags => $ditch_empty_space, + found_versions => $ditch_empty_space, + fixed_versions => $ditch_empty_space, + mergedwith => $ditch_empty_space, + ); + +sub split_status_fields { + my @data = @{dclone(\@_)}; + for my $data (@data) { + next if not defined $data; + croak "Passed an element which is not a hashref to split_status_field".ref($data) if + not (ref($data) and ref($data) eq 'HASH'); + for my $field (keys %{$data}) { + next unless defined $data->{$field}; + if (exists $split_fields{$field}) { + next if ref($data->{$field}); + my @elements; + if (ref($split_fields{$field}) eq 'CODE') { + @elements = &{$split_fields{$field}}($data->{$field}); + } + elsif (not ref($split_fields{$field}) or + UNIVERSAL::isa($split_fields{$field},'Regex') + ) { + @elements = split $split_fields{$field}, $data->{$field}; + } + $data->{$field} = \@elements; + } + } + } + return wantarray?@data:$data[0]; +} + +=head2 join_status_fields + + my @data = join_status_fields(@data); + +Handles joining the splitable status fields. (Basically, the inverse +of split_status_fields. + +Primarily called from makestatus, but may be useful for other +functions after calling split_status_fields (or for legacy functions +if we transition to split fields by default). + +=cut + +sub join_status_fields { + my %join_fields = + (package => ', ', + affects => ', ', + blocks => ' ', + blockedby => ' ', + tags => ' ', + found_versions => ' ', + fixed_versions => ' ', + found_date => ' ', + fixed_date => ' ', + mergedwith => ' ', + ); + my @data = @{dclone(\@_)}; + for my $data (@data) { + next if not defined $data; + croak "Passed an element which is not a hashref to split_status_field: ". + ref($data) + if ref($data) ne 'HASH'; + for my $field (keys %{$data}) { + next unless defined $data->{$field}; + next unless ref($data->{$field}) eq 'ARRAY'; + next unless exists $join_fields{$field}; + $data->{$field} = join($join_fields{$field},@{$data->{$field}}); + } + } + return wantarray?@data:$data[0]; +} + + =head2 lockreadbug lockreadbug($bug_num,$location) @@ -244,10 +380,7 @@ See readbug above for information on what this returns sub lockreadbug { my ($lref, $location) = @_; - &filelock("lock/$lref"); - my $data = read_bug(bug => $lref, location => $location); - &unfilelock unless defined $data; - return $data; + return read_bug(bug => $lref, location => $location, lock => 1); } =head2 lockreadbugmerge @@ -270,7 +403,7 @@ sub lockreadbugmerge { return (1,$data); } unfilelock(); - filelock('lock/merge'); + filelock("$config{spool_dir}/lock/merge"); $data = lockreadbug(@_); if (not defined $data) { unfilelock(); @@ -279,6 +412,71 @@ sub lockreadbugmerge { return (2,$data); } +=head2 lock_read_all_merged_bugs + + my ($locks,@bug_data) = lock_read_all_merged_bugs($bug_num,$location); + +Performs a filelock, then reads the bug passed. If the bug is merged, +locks the merge lock, then reads and locks all of the other merged +bugs. Returns a list of the number of locks and the bug data for all +of the merged bugs. + +Will also return undef if any of the merged bugs failed to be read, +even if all of the others were read properly. + +=cut + +sub lock_read_all_merged_bugs { + my ($bug_num,$location) = @_; + my $locks = 0; + my @data = (lockreadbug(@_)); + if (not @data or not defined $data[0]) { + return ($locks,()); + } + $locks++; + if (not length $data[0]->{mergedwith}) { + return ($locks,@data); + } + unfilelock(); + $locks--; + filelock("$config{spool_dir}/lock/merge"); + $locks++; + @data = (lockreadbug(@_)); + if (not @data or not defined $data[0]) { + unfilelock(); #for merge lock above + $locks--; + return ($locks,()); + } + $locks++; + my @bugs = split / /, $data[0]->{mergedwith}; + for my $bug (@bugs) { + my $newdata = undef; + if ($bug ne $bug_num) { + $newdata = lockreadbug($bug,$location); + if (not defined $newdata) { + for (1..$locks) { + unfilelock(); + } + $locks = 0; + warn "Unable to read bug: $bug while handling merged bug: $bug_num"; + return ($locks,()); + } + $locks++; + push @data,$newdata; + } + # perform a sanity check to make sure that the merged bugs are + # all merged with eachother + my $expectmerge= join(' ',grep {$_ != $bug } sort { $a <=> $b } (@bugs,$bug_num)); + if ($newdata->{mergedwith} ne $expectmerge) { + for (1..$locks) { + unfilelock(); + } + die "Bug $bug_num differs from bug $bug: ($newdata->{bug_num}: $newdata->{mergedwith}) vs. ($expectmerge) (".join(' ',@bugs).")"; + } + } + return ($locks,@data); +} + my @v1fieldorder = qw(originator date subject msgid package keywords done forwarded mergedwith severity); @@ -312,10 +510,7 @@ sub makestatus { [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}]; } } - - for my $field (qw(found_versions fixed_versions found_date fixed_date)) { - $newdata{$field} = join ' ', @{$newdata{$field}||[]}; - } + %newdata = %{join_status_fields(\%newdata)}; if ($version < 3) { for my $field (@rfc1522_fields) { @@ -369,17 +564,17 @@ sub writebug { for my $version (keys %outputs) { next if defined $minversion and $version < $minversion; my $status = getbugcomponent($ref, $outputs{$version}, $location); - &quit("can't find location for $ref") unless defined $status; - open(S,"> $status.new") || &quit("opening $status.new: $!"); + die "can't find location for $ref" unless defined $status; + open(S,"> $status.new") || die "opening $status.new: $!"; print(S makestatus($data, $version)) || - &quit("writing $status.new: $!"); - close(S) || &quit("closing $status.new: $!"); + die "writing $status.new: $!"; + close(S) || die "closing $status.new: $!"; if (-e $status) { $change = 'change'; } else { $change = 'new'; } - rename("$status.new",$status) || &quit("installing new $status: $!"); + rename("$status.new",$status) || die "installing new $status: $!"; } # $disablebughook is a bit of a hack to let format migration scripts use @@ -559,7 +754,7 @@ Split a package string from the status file into a list of package names. sub splitpackages { my $pkgs = shift; return unless defined $pkgs; - return map lc, split /[ \t?,()]+/, $pkgs; + return grep {length $_} map lc, split /[\s,()?]+/, $pkgs; } @@ -624,9 +819,19 @@ sub bug_archiveable{ # Bugs can be archived if they are # 1. Closed if (not defined $status->{done} or not length $status->{done}) { - print STDERR "Cannot archive $param{bug} because it is not done\n"; + print STDERR "Cannot archive $param{bug} because it is not done\n" if $DEBUG; return $cannot_archive } + # Check to make sure that the bug has none of the unremovable tags set + if (@{$config{removal_unremovable_tags}}) { + for my $tag (split ' ', ($status->{tags}||'')) { + if (grep {$tag eq $_} @{$config{removal_unremovable_tags}}) { + print STDERR "Cannot archive $param{bug} because it has an unremovable tag '$tag'\n" if $DEBUG; + return $cannot_archive; + } + } + } + # If we just are checking if the bug can be archived, we'll not even bother # checking the versioning information if the bug has been -done for less than 28 days. my $log_file = getbugcomponent($param{bug},'log'); @@ -725,7 +930,10 @@ sub bug_archiveable{ last if $buggy eq 'found'; $min_fixed_time = min($time_versions{$version},$min_fixed_time); } - $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24))); + $min_archive_days = max($min_archive_days,ceil($config{remove_age} - (time - $min_fixed_time)/(60*60*24))) + # if there are no versions in the archive at all, then + # we can archive if enough days have passed + if @sourceversions; } # If $param{ignore_time}, then we should ignore time. if ($param{ignore_time}) { @@ -773,6 +981,10 @@ currently not correctly implemented. dist, arch, and version. [The entries in this array must be in the "source/version" format.] Eventually this can be used to for caching. +=item indicatesource -- if true, indicate which source packages this +bug could belong to (or does belong to in the case of bugs assigned to +a source package). Defaults to true. + =back Note: Currently the version information is cached; this needs to be @@ -809,6 +1021,9 @@ sub get_bug_status { sourceversions => {type => ARRAYREF, optional => 1, }, + indicatesource => {type => BOOLEAN, + default => 1, + }, }, ); my %status; @@ -838,9 +1053,30 @@ sub get_bug_status { $status{tags} = $status{keywords}; my %tags = map { $_ => 1 } split ' ', $status{tags}; + $status{package} = '' if not defined $status{package}; $status{"package"} =~ s/\s*$//; + # if we aren't supposed to indicate the source, we'll return + # unknown here. + $status{source} = 'unknown'; + if ($param{indicatesource}) { + my @packages = split /\s*,\s*/, $status{package}; + my @source; + for my $package (@packages) { + next if $package eq ''; + if ($package =~ /^src\:$/) { + push @source,$1; + } + else { + push @source, binarytosource($package); + } + } + if (@source) { + $status{source} = join(', ',@source); + } + } + $status{"package"} = 'unknown' if ($status{"package"} eq ''); - $status{"severity"} = 'normal' if ($status{"severity"} eq ''); + $status{"severity"} = 'normal' if (not defined $status{severity} or $status{"severity"} eq ''); $status{"pending"} = 'pending'; $status{"pending"} = 'forwarded' if (length($status{"forwarded"})); @@ -848,7 +1084,8 @@ sub get_bug_status { $status{"pending"} = 'fixed' if ($tags{fixed}); - my $presence = bug_presence(map{(exists $param{$_})?($_,$param{$_}):()} + my $presence = bug_presence(status => \%status, + map{(exists $param{$_})?($_,$param{$_}):()} qw(bug sourceversions arch dist version found fixed package) ); if (defined $presence) { @@ -950,16 +1187,51 @@ sub bug_presence { } } } elsif (defined $param{dist}) { - foreach my $arch (make_list($param{arch})) { - my @versions; + my %affects_distribution_tags; + @affects_distribution_tags{@{$config{affects_distribution_tags}}} = + (1) x @{$config{affects_distribution_tags}}; + my $some_distributions_disallowed = 0; + my %allowed_distributions; + for my $tag (split ' ', ($status{tags}||'')) { + if (exists $config{distribution_aliases}{$tag} and + exists $affects_distribution_tags{$config{distribution_aliases}{$tag}}) { + $some_distributions_disallowed = 1; + $allowed_distributions{$config{distribution_aliases}{$tag}} = 1; + } + elsif (exists $affects_distribution_tags{$tag}) { + $some_distributions_disallowed = 1; + $allowed_distributions{$tag} = 1; + } + } + foreach my $arch (make_list(exists $param{arch}?$param{arch}:undef)) { for my $package (split /\s*,\s*/, $status{package}) { - foreach my $dist (make_list($param{dist})) { - push @versions, getversions($package, $dist, $arch); + my @versions = (); + my $source = 0; + if ($package =~ /^src:(.+)$/) { + $source = 1; + $package = $1; } - my @temp = makesourceversions($package, - $arch, - @versions - ); + foreach my $dist (make_list(exists $param{dist}?$param{dist}:[])) { + # if some distributions are disallowed, + # and this isn't an allowed + # distribution, then we ignore this + # distribution for the purposees of + # finding versions + if ($some_distributions_disallowed and + not exists $allowed_distributions{$dist}) { + next; + } + push @versions, get_versions(package => $package, + dist => $dist, + ($source?(arch => 'source'): + (defined $arch?(arch => $arch):())), + ); + } + next unless @versions; + my @temp = make_source_versions(package => $package, + arch => $arch, + versions => \@versions, + ); @sourceversions{@temp} = (1) x @temp; } } @@ -976,12 +1248,12 @@ sub bug_presence { my $maxbuggy = 'undef'; if (@sourceversions) { $maxbuggy = max_buggy(bug => $param{bug}, - sourceversions => \@sourceversions, - found => $status{found_versions}, - fixed => $status{fixed_versions}, - package => $status{package}, - version_cache => $version_cache, - ); + sourceversions => \@sourceversions, + found => $status{found_versions}, + fixed => $status{fixed_versions}, + package => $status{package}, + version_cache => $version_cache, + ); } elsif (defined $param{dist} and not exists $pseudo_desc->{$status{package}}) { @@ -1217,7 +1489,7 @@ sub update_realtime { sub bughook_archive { my @refs = @_; - &filelock("debbugs.trace.lock"); + &filelock("$config{spool_dir}/debbugs.trace.lock"); &appendfile("debbugs.trace","archive ".join(',',@refs)."\n"); my %bugs = update_realtime("$config{spool_dir}/index.db.realtime", map{($_,'REMOVE')} @refs); @@ -1228,7 +1500,7 @@ sub bughook_archive { sub bughook { my ( $type, %bugs_temp ) = @_; - &filelock("debbugs.trace.lock"); + &filelock("$config{spool_dir}/debbugs.trace.lock"); my %bugs; for my $bug (keys %bugs_temp) {