2 # debbugs-loadsql is part of debbugs, and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2012 by Don Armstrong <don@donarmstrong.com>.
11 use Getopt::Long qw(:config no_ignore_case);
16 debbugs-loadsql -- load debbugs sql database
20 debbugs-loadsql [options] [subcommand]
23 bugs help versions configuration
24 suites logs packages debinfo
26 --quick, -q only load changed things
27 --progress Show progress bar
28 --service, -s service name
29 --sysconfdir, -c postgresql service config dir
30 --spool-dir debbugs spool directory
31 --debug, -d debugging level (Default 0)
32 --help, -h display this help
33 --man, -m display manual
43 Add bugs (subject, number, etc) to the database
45 --preload create all bugs first, then add information
49 Add version descendant information (which version is based on which version) to
54 Add source maintainers to the BTS
58 Add debbugs configuration information (tags, severity, etc)
62 Add suite information from ftp distribution
64 --ftpdists location of FTP mirror
72 Add package information from the ftp archive
74 --ftpdists location of FTP mirror
75 --suites Suite to operate on
79 Add package information from a debinfo file
81 --null -0 names of debinfo files are null separated
89 Only load changed bugs
93 Show progress bar (requires Term::ProgressBar)
97 Postgreql service to use; defaults to debbugs
99 =item B<--sysconfdir,-c>
101 System configuration directory to use; if not set, defaults to the
102 postgresql default. [Operates by setting PGSYSCONFDIR]
106 Debbugs spool directory; defaults to the value configured in the
107 debbugs configuration file.
111 Output more information about what is happening. Probably not useful
112 if you also set --progress.
120 Display brief useage information.
134 use Debbugs::Common (qw(checkpid lockpid get_hashname getparsedaddrs getbugcomponent make_list getsourcemaintainers),
135 qw(hash_slice open_compressed_file),);
136 use Debbugs::Config qw(:config);
137 use Debbugs::Status qw(read_bug split_status_fields);
140 use Debbugs::DB::Load qw(:load_bug :load_package :load_suite);
148 use IO::Uncompress::AnyUncompress;
149 use Encode qw(decode_utf8);
150 use List::MoreUtils qw(natatime);
159 service => $config{debbugs_db},
163 Getopt::Long::Configure('pass_through');
164 GetOptions(\%options,
170 'spool_dir|spool-dir=s',
173 'debug|d+','help|h|?','man|m');
174 Getopt::Long::Configure('default');
176 pod2usage() if $options{help};
177 pod2usage({verbose=>2}) if $options{man};
179 $DEBUG = $options{debug};
182 ('bugs' => {function => \&add_bugs,
183 arguments => {'preload' => 0},
185 'versions' => {function => \&add_versions,
187 'debinfo' => {function => \&add_debinfo,
188 arguments => {'0|null' => 0,
189 'debinfo_dir|debinfo-dir=s' => 0,
192 'maintainers' => {function => \&add_maintainers,
194 'configuration' => {function => \&add_configuration,
196 'suites' => {function => \&add_suite,
197 arguments => {'ftpdists=s' => 1,
200 'logs' => {function => \&add_logs,
202 'bugs_and_logs' => {function => \&add_bugs_and_logs,
204 'packages' => {function => \&add_packages,
205 arguments => {'ftpdists=s' => 1,
209 'help' => {function => sub {pod2usage({verbose => 2});}}
213 $options{verbose} = $options{verbose} - $options{quiet};
215 if ($options{progress}) {
216 eval "use Term::ProgressBar";
217 push @USAGE_ERRORS, "You asked for a progress bar, but Term::ProgressBar isn't installed" if $@;
221 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
223 if (exists $options{sysconfdir}) {
224 if (not defined $options{sysconfdir} or not length $options{sysconfdir}) {
225 delete $ENV{PGSYSCONFDIR};
227 $ENV{PGSYSCONFDIR} = $options{sysconfdir};
231 if (exists $options{spool_dir} and defined $options{spool_dir}) {
232 $config{spool_dir} = $options{spool_dir};
236 if ($options{progress}) {
237 $prog_bar = eval "Term::ProgressBar->new({count => 1,ETA=>q(linear)})";
238 warn "Unable to initialize progress bar: $@" if not $prog_bar;
242 my ($subcommand) = shift @ARGV;
243 if (not defined $subcommand) {
244 $subcommand = 'help';
245 print STDERR "You must provide a subcommand; displaying usage.\n";
247 } elsif (not exists $subcommands{$subcommand}) {
248 print STDERR "$subcommand is not a valid subcommand; displaying usage.\n";
252 binmode(STDOUT,':encoding(UTF-8)');
253 binmode(STDERR,':encoding(UTF-8)');
256 handle_subcommand_arguments(\@ARGV,$subcommands{$subcommand}{arguments});
257 $subcommands{$subcommand}{function}->(\%options,$opts,$prog_bar,\%config,\@ARGV);
260 my ($options,$opts,$p,$config,$argv) = @_;
261 chdir($config->{spool_dir}) or
262 die "chdir $config->{spool_dir} failed: $!";
264 my $verbose = $options->{debug};
266 my $initialdir = "db-h";
268 if (defined $argv->[0] and $argv->[0] eq "archive") {
269 $initialdir = "archive";
271 my $s = db_connect($options);
278 if ($opts->{preload}) {
280 walk_bugs([(@{$argv}?@{$argv} : $initialdir)],
289 $s->resultset('Bug')->quick_insert_bugs(@bugs);
291 walk_bugs([(@{$argv}?@{$argv} : $initialdir)],
298 if ($options{quick}) {
300 bugs_to_update($s,$initialdir,@bugs);
302 @bugs_to_update = @bugs;
306 for my $bug (@bugs_to_update) {
310 severities => \%severities,
316 die "failure while trying to load bug: $@";
321 handle_load_bug_queue(db => $s,
326 my ($options,$opts,$p,$config,$argv) = @_;
328 my $s = db_connect($options);
330 my @files = @{$argv};
331 $p->target(scalar @files) if $p;
332 for my $file (@files) {
333 my $fh = IO::File->new($file,'r') or
334 die "Unable to open $file for reading: $!";
339 next unless length $_;
340 if (/(\w[-+0-9a-z.]+) \(([^\(\) \t]+)\)/) {
341 push @versions, [$1,$2];
346 for my $i (reverse 0..($#versions)) {
348 if (not defined $src_pkgs{$versions[$i][0]}) {
349 $src_pkgs{$versions[$i][0]} =
350 $s->resultset('SrcPkg')->
351 get_src_pkg_id($versions[$i][0]);
353 $sp = $src_pkgs{$versions[$i][0]};
354 # There's probably something wrong if the source package
355 # doesn't exist, but we'll skip it for now
356 last if not defined $sp;
357 my $sv = $s->resultset('SrcVer')->find({src_pkg=>$sp,
358 ver => $versions[$i][1],
360 last if not defined $sv;
361 if (defined $ancestor_sv and defined $sv and not defined $sv->based_on()) {
362 $sv->update({based_on => $ancestor_sv})
364 $ancestor_sv = $sv->id();
372 my ($options,$opts,$p,$config,$argv) = @_;
374 my @files = @{$argv};
375 if (exists $opts->{debinfo_dir} and not @files) {
377 if (-f $_ and /\.debinfo$/) {
378 push @files, $File::Find::name;
387 local $/ = "\0" if $opts->{0};
389 s/\n$// unless $opts->{0};
390 s/\0$// if $opts->{0};
395 return unless @files;
396 my $s = db_connect($options);
397 $p->target(scalar @files) if $p;
398 my $it = natatime 100, @files;
399 while (my @v = $it->()) {
403 my $fh = IO::File->new($file,'r') or
404 die "Unable to open $file for reading: $!";
405 my $f_stat = stat($file);
406 my $ct_date = DateTime->from_epoch(epoch => $f_stat->ctime);
409 next unless length $_;
410 my ($binname, $binver, $binarch, $srcname, $srcver) = split;
411 # if $srcver is not defined, this is probably a broken
412 # .debinfo file [they were causing #686106, see commit
413 # 49c85ab8 in dak.] Basically, $binarch didn't get put into
414 # the file, so we'll fudge it from the filename.
415 if (not defined $srcver) {
416 ($srcname,$srcver) = ($binarch,$srcname);
417 ($binarch) = $file =~ /_([^\.]+)\.debinfo/;
419 if (not defined $srcver) {
420 print STDERR "malformed debinfo (no srcver): $file\n";
424 [$binname,$binver,$binarch,$srcname,$srcver,$ct_date];
429 for my $di (@debinfos) {
430 Debbugs::DB::Load::load_debinfo($s,@{$di}[0..5],\%cache);
433 $p->update($p->last_update()+@v) if $p;
438 sub add_maintainers {
439 my ($options,$opts,$p,$config,$argv) = @_;
441 my $s = db_connect($options);
442 my $maintainers = getsourcemaintainers() // {};
444 ## get all of the maintainers, and add the missing ones
445 my $maints = $s->resultset('Maintainer')->
446 get_maintainers(values %{$maintainers});
448 my @svs = $s->resultset('SrcVer')->
449 search({maintainer => undef
452 group_by => 'me.src_pkg, src_pkg.pkg',
453 result_class => 'DBIx::Class::ResultClass::HashRefInflator',
454 columns => [qw(me.src_pkg src_pkg.pkg)],
457 $p->target(2+@svs) if $p;
460 if (exists $maintainers->{$sv->{src_pkg}{pkg}}) {
461 my $pkg = $sv->{src_pkg}{pkg};
462 my $maint = $maints->
463 {$maintainers->{$pkg}};
464 $s->txn_do(sub {$s->resultset('SrcVer')->
465 search({maintainer => undef,
466 'src_pkg.pkg' => $pkg
469 )->update({maintainer => $maint})
477 sub add_configuration {
478 my ($options,$opts,$p,$config,$argv) = @_;
480 my $s = db_connect($options);
485 for my $tag (@{$config{tags}}) {
487 $s->resultset('Tag')->find_or_create({tag => $tag});
490 for my $tag ($s->resultset('Tag')->search_rs()->all()) {
491 next if exists $tags{$tag->tag};
499 for my $sev_name (($config{default_severity},@{$config{severity_list}})) {
500 # add all severitites
501 my $sev = $s->resultset('Severity')->find_or_create({severity => $sev_name});
502 # mark strong severities
503 if (grep {$_ eq $sev_name} @{$config{strong_severities}}) {
506 $sev->ordering($order);
509 $sev_names{$sev_name} = 1;
511 # mark obsolete severities
512 for my $sev ($s->resultset('Severity')->search_rs()->all()) {
513 next if exists $sev_names{$sev->severity()};
520 my ($options,$opts,$p,$config,$argv) = @_;
523 my $s = db_connect($options);
524 my $dist_dir = IO::Dir->new($opts->{ftpdists});
526 grep { $_ !~ /^\./ and
527 -d $opts->{ftpdists}.'/'.$_ and
528 not -l $opts->{ftpdists}.'/'.$_
530 while (my $dist = shift @dist_names) {
531 my $dist_dir = $opts->{ftpdists}.'/'.$dist;
532 my ($dist_info,$package_files) =
533 read_release_file($dist_dir.'/Release');
534 load_suite($s,$dist_info);
539 my ($options,$opts,$p,$config,$argv) = @_;
541 chdir($config->{spool_dir}) or
542 die "chdir $config->{spool_dir} failed: $!";
544 my $verbose = $options->{debug};
546 my $initialdir = "db-h";
548 if (defined $argv->[0] and $argv->[0] eq "archive") {
549 $initialdir = "archive";
551 my $s = db_connect($options);
553 walk_bugs([(@{$argv}?@{$argv} : $initialdir)],
559 my $stat = stat(getbugcomponent($bug,'log',$initialdir));
560 if (not defined $stat) {
561 print STDERR "Unable to stat $bug $!\n";
564 if ($options{quick}) {
565 my $rs = $s->resultset('Bug')->
566 search({id=>$bug})->single();
567 return if defined $rs and
568 $stat->mtime <= $rs->last_modified()->epoch();
571 load_bug_log(db => $s,
575 die "failure while trying to load bug log $bug\n$@";
580 sub add_bugs_and_logs {
581 my ($options,$opts,$p,$config,$argv) = @_;
583 chdir($config->{spool_dir}) or
584 die "chdir $config->{spool_dir} failed: $!";
586 my $verbose = $options->{debug};
588 my $initialdir = "db-h";
590 if (defined $argv->[0] and $argv->[0] eq "archive") {
591 $initialdir = "archive";
593 my $s = db_connect($options);
599 walk_bugs([(@{$argv}?@{$argv} : $initialdir)],
606 if ($options{quick}) {
608 bugs_to_update($s,$initialdir,@bugs);
610 @bugs_to_update = @bugs;
614 for my $bug (@bugs_to_update) {
618 severities => \%severities,
624 die "failure while trying to load bug: $@";
626 for my $bug (@bugs) {
627 my $stat = stat(getbugcomponent($bug,'log',$initialdir));
628 if (not defined $stat) {
629 print STDERR "Unable to stat $bug $!\n";
632 if ($options{quick}) {
633 my $rs = $s->resultset('Bug')->
634 search({id=>$bug})->single();
635 return if defined $rs and
636 $stat->mtime <= $rs->last_modified()->epoch();
639 load_bug_log(db => $s,
643 die "failure while trying to load bug log $bug\n$@";
649 handle_load_bug_queue(db=>$s,
656 my ($options,$opts,$p,$config,$argv) = @_;
658 my $dist_dir = IO::Dir->new($opts->{ftpdists});
660 grep { $_ !~ /^\./ and
661 -d $opts->{ftpdists}.'/'.$_ and
662 not -l $opts->{ftpdists}.'/'.$_
665 while (my $dist = shift @dist_names) {
666 my $dist_dir = $opts->{ftpdists}.'/'.$dist;
667 my ($dist_info,$package_files) =
668 read_release_file($dist_dir.'/Release');
669 $s_p{$dist_info->{Codename}} = $package_files;
672 for my $suite (keys %s_p) {
673 for my $component (keys %{$s_p{$suite}}) {
674 $tot += scalar keys %{$s_p{$suite}{$component}};
677 $p->target($tot) if $p;
680 my $tot_suites = scalar keys %s_p;
682 my $completed_pkgs=0;
683 # parse packages files
684 for my $suite (keys %s_p) {
686 for my $component (keys %{$s_p{$suite}}) {
687 my @archs = keys %{$s_p{$suite}{$component}};
688 if (grep {$_ eq 'source'} @archs) {
689 @archs = ('source',grep {$_ ne 'source'} @archs);
691 for my $arch (@archs) {
692 my $pfh = open_compressed_file($s_p{$suite}{$component}{$arch}) or
693 die "Unable to open $s_p{$suite}{$component}{$arch} for reading: $!";
695 local $/ = ''; # paragraph mode
698 for my $field (qw(Package Maintainer Version Source)) {
699 /^\Q$field\E: (.*)/m;
702 next unless defined $pkg{Package} and
703 defined $pkg{Version};
704 push @pkgs,[$arch,$component,\%pkg];
708 my $s = db_connect($options);
712 $p->target($avg_pkgs*($tot_suites-$done_suites-1)+
713 $completed_pkgs+@pkgs) if $p;
718 $avg_pkgs=($avg_pkgs*$done_suites + @pkgs)/($done_suites+1);
719 $completed_pkgs += @pkgs;
725 sub handle_subcommand_arguments {
726 my ($argv,$args) = @_;
728 Getopt::Long::GetOptionsFromArray($argv,
733 for my $arg (keys %{$args}) {
734 next unless $args->{$arg};
735 my $r_arg = $arg; # real argument name
736 $r_arg =~ s/[=\|].+//g;
737 if (not defined $subopt->{$r_arg}) {
738 push @usage_errors, "You must give a $r_arg option";
741 pod2usage(join("\n",@usage_errors)) if @usage_errors;
746 my ($subcommand,$config,$options) = @_;
747 if (not lockpid($config->{spool_dir}.'/lock/debbugs-loadsql-$subcommand')) {
748 if ($options->{quick}) {
749 # If this is a quick run, just exit
750 print STDERR "Another debbugs-loadsql is running; stopping\n" if $options->{verbose};
753 print STDERR "Another debbugs-loadsql is running; stopping\n";
760 # connect to the database; figure out how to handle errors
762 my $s = Debbugs::DB->connect($options->{dsn} //
763 $options->{service}) or
764 die "Unable to connect to database: ";
767 sub read_release_file {
770 my $rfh = open_compressed_file($file) or
771 die "Unable to open $file for reading: $!";
777 if (s/^(\S+):\s*//) {
778 if ($1 eq 'SHA1'or $1 eq 'SHA256') {
785 my ($sha,$size,$f) = split /\s+/,$_;
786 next unless $f =~ /(?:Packages|Sources)(?:\.gz|\.xz)$/;
787 next unless $f =~ m{^([^/]+)/([^/]+)/([^/]+)$};
788 my ($component,$arch,$package_source) = ($1,$2,$3);
789 $arch =~ s/binary-//;
790 next if exists $p_f{$component}{$arch};
791 $p_f{$component}{$arch} = File::Spec->catfile(dirname($file),$f);
794 return (\%dist_info,\%p_f);
798 my ($dirs,$p,$what,$verbose,$sub,$n) = @_;
800 my $tot_dirs = @dirs;
802 my $avg_subfiles = 0;
803 my $completed_files = 0;
805 while (my $dir = shift @dirs) {
806 printf "Doing dir %s ...\n", $dir if $verbose;
808 opendir(DIR, "$dir/.") or die "opendir $dir: $!";
809 my @subdirs = readdir(DIR);
812 my @list = map { m/^(\d+)\.$what$/?($1):() } @subdirs;
814 push @dirs, map { m/^(\d+)$/ && -d "$dir/$1"?("$dir/$1"):() } @subdirs;
816 if ($avg_subfiles == 0) {
817 $avg_subfiles = @list;
820 $p->target($avg_subfiles*($tot_dirs-$done_dirs)+$completed_files+@list) if $p;
821 $avg_subfiles = ($avg_subfiles * $done_dirs + @list) / ($done_dirs+1);
824 my $it = natatime $n,@list;
825 while (my @bugs = $it->()) {
827 $completed_files += scalar @bugs;
828 $p->update($completed_files) if $p;
829 print "Up to $completed_files bugs...\n"
830 if ($completed_files % 100 == 0 && $verbose);
838 my ($s,$initialdir,@bugs) = @_;
840 for my $bug (@bugs) {
841 my $stat = stat(getbugcomponent($bug,'summary',$initialdir));
842 if (not defined $stat) {
843 print STDERR "Unable to stat $bug $!\n";
846 my $rs = $s->resultset('Bug')->search({id=>$bug})->single();
847 next if defined $rs and $stat->mtime <= $rs->last_modified()->epoch();
848 push @bugs_to_update, $bug;