]> git.donarmstrong.com Git - debbugs.git/blob - bin/debbugs-loadsql
add load_suite
[debbugs.git] / bin / debbugs-loadsql
1 #! /usr/bin/perl
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>.
6
7
8 use warnings;
9 use strict;
10
11 use Getopt::Long qw(:config no_ignore_case);
12 use Pod::Usage;
13
14 =head1 NAME
15
16 debbugs-loadsql -- load debbugs sql database
17
18 =head1 SYNOPSIS
19
20 debbugs-loadsql [options]
21
22  Options:
23   --quick, -q only load changed bugs
24   --progress Show progress bar
25   --service, -s service name
26   --sysconfdir, -c postgresql service config dir
27   --spool-dir debbugs spool directory
28   --debug, -d debugging level (Default 0)
29   --help, -h display this help
30   --man, -m display manual
31
32 =head1 SUBCOMMANDS
33
34 =head2 help
35
36 Display this manual
37
38 =head2 bugs
39
40 Add bugs
41
42 =head2 versions
43
44 Add versions
45
46 =head2 maintainers
47
48 Add source maintainers
49
50 =head1 OPTIONS
51
52 =over
53
54 =item B<--quick, -q>
55
56 Only load changed bugs
57
58 =item B<--progress>
59
60 Show progress bar (requires Term::ProgressBar)
61
62 =item B<--service,-s>
63
64 Postgreql service to use; defaults to debbugs
65
66 =item B<--sysconfdir,-c>
67
68 System configuration directory to use; if not set, defaults to the
69 postgresql default. [Operates by setting PGSYSCONFDIR]
70
71 =item B<--spool-dir>
72
73 Debbugs spool directory; defaults to the value configured in the
74 debbugs configuration file.
75
76 =item B<--verbose>
77
78 Output more information about what is happening. Probably not useful
79 if you also set --progress.
80
81 =item B<--debug, -d>
82
83 Debug verbosity.
84
85 =item B<--help, -h>
86
87 Display brief useage information.
88
89 =item B<--man, -m>
90
91 Display this manual.
92
93 =back
94
95
96 =cut
97
98
99 use vars qw($DEBUG);
100
101 use Debbugs::Common qw(checkpid lockpid get_hashname getparsedaddrs getbugcomponent make_list getsourcemaintainers);
102 use Debbugs::Config qw(:config);
103 use Debbugs::Status qw(read_bug split_status_fields);
104 use Debbugs::Log;
105 use Debbugs::DB;
106 use Debbugs::DB::Load qw(load_bug handle_load_bug_queue :load_package :load_suite);
107 use DateTime;
108 use File::stat;
109 use IO::Dir;
110 use IO::Uncompress::AnyUncompress;
111
112 my %options =
113     (debug           => 0,
114      help            => 0,
115      man             => 0,
116      verbose         => 0,
117      quiet           => 0,
118      quick           => 0,
119      service         => $config{debbugs_db},
120      progress        => 0,
121     );
122
123 Getopt::Long::Configure('pass_through');
124 GetOptions(\%options,
125            'quick|q',
126            'service|s=s',
127            'sysconfdir|c=s',
128            'progress!',
129            'spool_dir|spool-dir=s',
130            'verbose|v+',
131            'quiet+',
132            'debug|d+','help|h|?','man|m');
133 Getopt::Long::Configure('default');
134
135 pod2usage() if $options{help};
136 pod2usage({verbose=>2}) if $options{man};
137
138 $DEBUG = $options{debug};
139
140 my %subcommands =
141     ('bugs' => {function => \&add_bugs,
142                },
143      'versions' => {function => \&add_versions,
144                    },
145      'debinfo' => {function => \&add_debinfo,
146                    arguments => {'0|null' => 0},
147                   },
148      'maintainers' => {function => \&add_maintainers,
149                       },
150      'configuration' => {function => \&add_configuration,
151                         },
152      'suites' => {function => \&add_suite,
153                   arguments => {'ftpdists=s' => 1,
154                                },
155                  },
156      'logs' => {function => \&add_logs,
157                },
158      'packages' => {function => \&add_packages,
159                     arguments => {'ftpdists=s' => 1,
160                                  },
161                    },
162      'help' => {function => sub {pod2usage({verbose => 2});}}
163     );
164
165 my @USAGE_ERRORS;
166 $options{verbose} = $options{verbose} - $options{quiet};
167
168 if ($options{progress}) {
169     eval "use Term::ProgressBar";
170     push @USAGE_ERRORS, "You asked for a progress bar, but Term::ProgressBar isn't installed" if $@;
171 }
172
173
174 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
175
176 if (exists $options{sysconfdir}) {
177     if (not defined $options{sysconfdir} or not length $options{sysconfdir}) {
178         delete $ENV{PGSYSCONFDIR};
179     } else {
180         $ENV{PGSYSCONFDIR} = $options{sysconfdir};
181     }
182 }
183
184 if (exists $options{spool_dir} and defined $options{spool_dir}) {
185     $config{spool_dir} = $options{spool_dir};
186 }
187
188 my $prog_bar;
189 if ($options{progress}) {
190     $prog_bar = eval "Term::ProgressBar->new({count => 1,ETA=>q(linear)})";
191     warn "Unable to initialize progress bar: $@" if not $prog_bar;
192 }
193
194
195 my ($subcommand) = shift @ARGV;
196 if (not defined $subcommand) {
197     $subcommand = 'help';
198     print STDERR "You must provide a subcommand; displaying usage.\n";
199     pod2usage();
200 } elsif (not exists $subcommands{$subcommand}) {
201     print STDERR "$subcommand is not a valid subcommand; displaying usage.\n";
202     pod2usage();
203 }
204
205 my $opts =
206     handle_subcommand_arguments(\@ARGV,$subcommands{$subcommand}{arguments});
207 $subcommands{$subcommand}{function}->(\%options,$opts,$prog_bar,\%config,\@ARGV);
208
209 sub add_bugs {
210     my ($options,$opts,$p,$config,$argv) = @_;
211     chdir($config->{spool_dir}) or
212         die "chdir $config->{spool_dir} failed: $!";
213
214     my $verbose = $options->{debug};
215
216     my $initialdir = "db-h";
217
218     if (defined $argv->[0] and $argv->[0] eq "archive") {
219         $initialdir = "archive";
220     }
221     my $s = db_connect($options);
222
223
224     my $time = 0;
225     my $start_time = time;
226     my %tags;
227     my %severities;
228     my %queue;
229
230     walk_bugs([(@{$argv}?@{$argv} : $initialdir)],
231               $p,
232               'summary',
233               $verbose,
234               sub {
235                   my $bug = shift;
236                   my $stat = stat(getbugcomponent($bug,'summary',$initialdir));
237                   if (not defined $stat) {
238                       print STDERR "Unable to stat $bug $!\n";
239                       next;
240                   }
241                   if ($options{quick}) {
242                       my $rs = $s->resultset('Bug')->search({bug=>$bug})->single();
243                       next if defined $rs and $stat->mtime < $rs->last_modified()->epoch();
244                   }
245                   my $data = read_bug(bug => $bug,
246                                       location => $initialdir);
247                   eval {
248                       load_bug(db => $s,
249                                data => split_status_fields($data),
250                                tags => \%tags,
251                                severities => \%severities,
252                                queue => \%queue);
253                   };
254                   if ($@) {
255                       use Data::Dumper;
256                       print STDERR Dumper($data) if $DEBUG;
257                       die "failure while trying to load bug $bug\n$@";
258                   }
259               }
260              );
261     handle_load_bug_queue(db => $s,
262                           queue => \%queue);
263 }
264
265 sub add_versions {
266     my ($options,$opts,$p,$config,$argv) = @_;
267
268     my $s = db_connect($options);
269
270     my @files = @{$argv};
271     $p->target(scalar @files) if $p;
272     for my $file (@files) {
273         my $fh = IO::File->new($file,'r') or
274             die "Unable to open $file for reading: $!";
275         my @versions;
276         my %src_pkgs;
277         while (<$fh>) {
278             chomp;
279             next unless length $_;
280             if (/(\w[-+0-9a-z.]+) \(([^\(\) \t]+)\)/) {
281                 push @versions, [$1,$2];
282             }
283         }
284         close($fh);
285         my $ancestor_sv;
286         for my $i (reverse 0..($#versions)) {
287             my $sp;
288             if (not defined $src_pkgs{$versions[$i][0]}) {
289                 $src_pkgs{$versions[$i][0]} =
290                     $s->resultset('SrcPkg')->find_or_create({pkg => $versions[$i][0]});
291             }
292             $sp = $src_pkgs{$versions[$i][0]};
293             # There's probably something wrong if the source package
294             # doesn't exist, but we'll skip it for now
295             next unless defined $sp;
296             my $sv = $s->resultset('SrcVer')->find({src_pkg=>$sp->id(),
297                                                     ver => $versions[$i][1],
298                                                    });
299             if (defined $ancestor_sv and defined $sv and not defined $sv->based_on()) {
300                 $sv->update({based_on => $ancestor_sv->id()})
301             }
302             $ancestor_sv = $sv;
303         }
304         $p->update() if $p;
305     }
306     $p->remove() if $p;
307 }
308
309 sub add_debinfo {
310     my ($options,$opts,$p,$config,$argv) = @_;
311
312     my @files = @{$argv};
313     if (not @files) {
314        {
315            if ($opts->{0}) {
316                local $/ = "\0";
317            }
318            while (<STDIN>) {
319                push @files, $_;
320            }
321        }
322     }
323     return unless @files;
324     my $s = db_connect($options);
325     my %arch;
326     $p->target(scalar @files) if $p;
327     for my $file (@files) {
328         my $fh = IO::File->new($file,'r') or
329             die "Unable to open $file for reading: $!";
330         my $f_stat = stat($file);
331         while (<$fh>) {
332             chomp;
333             next unless length $_;
334             my ($binname, $binver, $binarch, $srcname, $srcver) = split;
335             # if $srcver is not defined, this is probably a broken
336             # .debinfo file [they were causing #686106, see commit
337             # 49c85ab8 in dak.] Basically, $binarch didn't get put into
338             # the file, so we'll fudge it from the filename.
339             if (not defined $srcver) {
340                 ($srcname,$srcver) = ($binarch,$srcname);
341                 ($binarch) = $file =~ /_([^\.]+)\.debinfo/;
342             }
343             my $sp = $s->resultset('SrcPkg')->find_or_create({pkg => $srcname});
344             # update the creation date if the data we have is earlier
345             my $ct_date = DateTime->from_epoch(epoch => $f_stat->ctime);
346             if ($ct_date < $sp->creation) {
347                 $sp->creation($ct_date);
348                 $sp->last_modified(DateTime->now);
349                 $sp->update;
350             }
351             my $sv = $s->resultset('SrcVer')->find_or_create({src_pkg =>$sp->id(),
352                                                               ver => $srcver});
353             if (not defined $sv->upload_date() or $ct_date < $sv->upload_date()) {
354                 $sv->upload_date($ct_date);
355                 $sv->update;
356             }
357             my $arch;
358             if (defined $arch{$binarch}) {
359                 $arch = $arch{$binarch};
360             } else {
361                 $arch = $s->resultset('Arch')->find_or_create({arch => $binarch});
362                 $arch{$binarch} = $arch;
363             }
364             my $bp = $s->resultset('BinPkg')->find_or_create({pkg => $binname});
365             $s->resultset('BinVer')->find_or_create({bin_pkg => $bp->id(),
366                                                      src_ver => $sv->id(),
367                                                      arch    => $arch->id(),
368                                                      ver        => $binver,
369                                                     });
370         }
371         $p->update() if $p;
372     }
373     $p->remove() if $p;
374 }
375
376 sub add_maintainers {
377     my ($options,$opts,$p,$config,$argv) = @_;
378
379     my $s = db_connect($options);
380     my $maintainers = getsourcemaintainers();
381     $p->target(scalar keys %{$maintainers}) if $p;
382     for my $pkg (keys %{$maintainers}) {
383         my $maint = $maintainers->{$pkg};
384         # see if a maintainer already exists; if so, we don't do
385         # anything here
386         my $maint_r = $s->resultset('Maintainer')->
387             find({name => $maint});
388         if (not defined $maint_r) {
389             # get e-mail address of maintainer
390             my $addr = getparsedaddrs($maint);
391             my $e_mail = $addr->address();
392             my $full_name = $addr->phrase();
393             $full_name =~ s/^\"|\"$//g;
394             $full_name =~ s/^\s+|\s+$//g;
395             # find correspondent
396             my $correspondent = $s->resultset('Correspondent')->
397                 find_or_create({addr => $e_mail});
398             if (length $full_name) {
399                 my $c_full_name = $correspondent->find_or_create_related('correspondent_full_names',
400                                                                         {full_name => $full_name}) if length $full_name;
401                 $c_full_name->update({last_seen => 'NOW()'});
402             }
403             $maint_r =
404                 $s->resultset('Maintainer')->
405                 find_or_create({name => $maint,
406                                 correspondent => $correspondent,
407                                });
408         }
409         # add the maintainer to the source package for packages with
410         # no maintainer
411         $s->txn_do(sub {
412                       $s->resultset('SrcPkg')->search({pkg => $pkg})->
413                           search_related_rs('src_vers',{ maintainer => undef})->
414                           update_all({maintainer => $maint_r->id()});
415                   });
416         $p->update() if $p;
417     }
418     $p->remove() if $p;
419 }
420
421 sub add_configuration {
422     my ($options,$opts,$p,$config,$argv) = @_;
423
424     my $s = db_connect($options);
425
426     # tags
427     # add all tags
428     # mark obsolete tags
429
430     # severities
431     my %sev_names;
432     my $order = 0;
433     for my $sev_name (@{$config{severities}}) {
434         # add all severitites
435         my $sev = $s->resultset('Severity')->find_or_create({severity => $sev_name});
436         # mark strong severities
437         if (grep {$_ eq $sev_name} @{$config{strong_severities}}) {
438             $sev->strong(1);
439         }
440         $sev->order($order);
441         $sev->update();
442         $order++;
443         $sev_names{$sev_name} = 1;
444     }
445     # mark obsolete severities
446     for my $sev ($s->resultset('Severity')->find()) {
447         next if exists $sev_names{$sev->severity()};
448         $sev->obsolete(1);
449         $sev->update();
450     }
451 }
452
453 sub add_suite {
454     my ($options,$opts,$p,$config,$argv) = @_;
455     # suites
456
457     my $s = db_connect($options);
458     my $dist_dir = IO::Dir->new($opts->{ftpdists});
459     my @dist_names =
460         grep { $_ !~ /^\./ and
461                -d $opts->{ftpdists}.'/'.$_ and
462                not -l $opts->{ftpdists}.'/'.$_
463            } $dist_dir->read;
464     while (my $dist = shift @dist_names) {
465         my $dist_dir = $opts->{ftpdists}.'/'.$dist;
466         my ($dist_info,$package_files) =
467             read_release_file($dist_dir.'/Release');
468         load_suite($s,$dist_info);
469     }
470 }
471
472 sub add_logs {
473     my ($options,$opts,$p,$config,$argv) = @_;
474
475     chdir($config->{spool_dir}) or
476         die "chdir $config->{spool_dir} failed: $!";
477
478     my $verbose = $options->{debug};
479
480     my $initialdir = "db-h";
481
482     if (defined $argv->[0] and $argv->[0] eq "archive") {
483         $initialdir = "archive";
484     }
485     my $s = db_connect($options);
486
487
488     my $time = 0;
489     my $start_time = time;
490
491     walk_bugs([(@{$argv}?@{$argv} : $initialdir)],
492               $p,
493               'log',
494               $verbose,
495               sub {
496                   my $bug = shift;
497                   eval { 
498                       load_bug_log(db => $s,
499                                    bug => $bug);
500                   };
501                   if ($@) {
502                       die "failure while trying to load bug log $bug\n$@";
503                   }
504               });
505 }
506
507 sub add_packages {
508     my ($options,$opts,$p,$config,$argv) = @_;
509
510     my $s = db_connect($options);
511
512     my $dist_dir = IO::Dir->new($opts->{ftpdists});
513     my @dist_names =
514         grep { $_ !~ /^\./ and
515                -d $opts->{ftpdists}.'/'.$_ and
516                not -l $opts->{ftpdists}.'/'.$_
517            } $dist_dir->read;
518     my %s_p;
519     my %s_info;
520     while (my $dist = shift @dist_names) {
521         my $dist_dir = $opts->{ftpdists}.'/'.$dist;
522         # parse release
523         my $rfh =  IO::Uncompress::AnyUncompress->new($dist_dir.'/Release');
524         my %dist_info;
525         my $in_sha1;
526         my %p_f;
527         while (<$rfh>) {
528             chomp;
529             if (s/^(\S+):\s*//) {
530                 if ($1 eq 'SHA1'or $1 eq 'SHA256') {
531                     $in_sha1 = 1;
532                     next;
533                 }
534                 $dist_info{$1} = $_;
535             } elsif ($in_sha1) {
536                 s/^\s//;
537                 my ($sha,$size,$file) = split /\s+/,$_;
538                 next unless $file =~ /(?:Packages|Sources)(?:\.gz|\.xz)$/;
539                 next unless $file =~ m{^([^/]+)/([^/]+)/([^/]+)$};
540                 my ($component,$arch,$package_source) = ($1,$2,$3);
541                 $arch =~ s/binary-//;
542                 next if exists $p_f{$component}{$arch};
543                 $p_f{$component}{$arch} = $dist_dir.'/'.$file;
544             }
545         }
546         $s_p{$dist_info{Suite}} = \%p_f;
547         $s_info{$dist_info{Suite}} = \%s_info;
548     }
549     # parse packages files
550     for my $suite (keys %s_p) {
551         for my $component (keys %{$s_p{$suite}}) {
552             for my $arch (keys %{$s_p{$suite}{$component}}) {
553                 my $pfh =  IO::Uncompress::AnyUncompress->new($s_p{$suite}{$component}{$arch}) or
554                     die "Unable to open $s_p{$suite}{$component}{$arch} for reading: $!";
555                 my $lastkey;
556                 my %pkg;
557                 while (<$pfh>) {
558                     if (/^$/) {
559                         load_package($s,$suite,$component,$arch,\%pkg);
560                         %pkg = ();
561                         next;
562                     }
563                     if (my ($key, $value) = m/^(\S+): (.*)/) {
564                         $pkg{$key} = $value;
565                         $lastkey=$key;
566                     }
567                     else {
568                         s/ //;
569                         s/^\.$//;
570                         chomp;
571                         $pkg{$lastkey} .= "\n" . $_;
572                     }
573                 }
574                 if (keys %pkg) {
575                     load_package($s,$suite,$component,$arch,\%pkg);
576                 }
577             }
578         }
579     }
580     use Data::Printer;
581     p %s_p;
582 }
583
584 sub handle_subcommand_arguments {
585     my ($argv,$args) = @_;
586     my $subopt = {};
587     Getopt::Long::GetOptionsFromArray($argv,
588                               $subopt,
589                               keys %{$args},
590                              );
591     my @usage_errors;
592     for my $arg  (keys %{$args}) {
593         next unless $args->{$arg};
594         my $r_arg = $arg; # real argument name
595         $r_arg =~ s/[=\|].+//g;
596         if (not defined $subopt->{$r_arg}) {
597             push @usage_errors, "You must give a $r_arg option";
598         }
599     }
600     pod2usage(join("\n",@usage_errors)) if @usage_errors;
601     return $subopt;
602 }
603
604 sub get_lock{
605     my ($subcommand,$config,$options) = @_;
606     if (not lockpid($config->{spool_dir}.'/lock/debbugs-loadsql-$subcommand')) {
607         if ($options->{quick}) {
608             # If this is a quick run, just exit
609             print STDERR "Another debbugs-loadsql is running; stopping\n" if $options->{verbose};
610             exit 0;
611         }
612         print STDERR "Another debbugs-loadsql is running; stopping\n";
613         exit 1;
614     }
615 }
616
617 sub db_connect {
618     my ($options) = @_;
619     # connect to the database; figure out how to handle errors
620     # properly here.
621     my $s = Debbugs::DB->connect($options->{service}) or
622         die "Unable to connect to database: ";
623 }
624
625 sub walk_bugs {
626     my ($dirs,$p,$what,$verbose,$sub) = @_;
627     my @dirs = @{$dirs};
628     my $tot_dirs = @dirs;
629     my $done_dirs = 0;
630     my $avg_subfiles = 0;
631     my $completed_files = 0;
632     while (my $dir = shift @dirs) {
633         printf "Doing dir %s ...\n", $dir if $verbose;
634
635         opendir(DIR, "$dir/.") or die "opendir $dir: $!";
636         my @subdirs = readdir(DIR);
637         closedir(DIR);
638
639         my @list = map { m/^(\d+)\.$what$/?($1):() } @subdirs;
640         $tot_dirs -= @dirs;
641         push @dirs, map { m/^(\d+)$/ && -d "$dir/$1"?("$dir/$1"):() } @subdirs;
642         $tot_dirs += @dirs;
643         if ($avg_subfiles == 0) {
644             $avg_subfiles = @list;
645         }
646
647         $p->target($avg_subfiles*($tot_dirs-$done_dirs)+$completed_files+@list) if $p;
648         $avg_subfiles = ($avg_subfiles * $done_dirs + @list) / ($done_dirs+1);
649         $done_dirs += 1;
650
651         for my $bug (@list) {
652             $completed_files++;
653             $p->update($completed_files) if $p;
654             print "Up to $completed_files bugs...\n" if ($completed_files % 100 == 0 && $verbose);
655             $sub->($bug);
656         }
657     }
658     $p->remove() if $p;
659 }
660
661
662
663 __END__