]> git.decadent.org.uk Git - dak.git/blob - lisa
New.
[dak.git] / lisa
1 #!/usr/bin/env python
2
3 # Handles NEW and BYHAND packages
4 # Copyright (C) 2001  James Troup <james@nocrew.org>
5 # $Id: lisa,v 1.1 2002-02-12 23:08:07 troup Exp $
6
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 ################################################################################
22
23 # 23:12|<aj> I will not hush!
24 # 23:12|<elmo> :>
25 # 23:12|<aj> Where there is injustice in the world, I shall be there!
26 # 23:13|<aj> I shall not be silenced!
27 # 23:13|<aj> The world shall know!
28 # 23:13|<aj> The world *must* know!
29 # 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
30 # 23:13|<aj> yay powerpuff girls!!
31 # 23:13|<aj> buttercup's my favourite, who's yours?
32 # 23:14|<aj> you're backing away from the keyboard right now aren't you?
33 # 23:14|<aj> *AREN'T YOU*?!
34 # 23:15|<aj> I will not be treated like this.
35 # 23:15|<aj> I shall have my revenge.
36 # 23:15|<aj> I SHALL!!!
37
38 ################################################################################
39
40 # TODO
41 # ----
42
43 # We don't check error codes very thoroughly; the old 'trust jennifer'
44 # chess nut... db_access calls in particular
45
46 # Possible TODO
47 # -------------
48
49 # Handle multiple different section/priorities (?)
50 # hardcoded debianness (debian-installer, source priority etc.) (?)
51 # Slang/ncurses interface (?)
52 # write changed sections/priority back to katie for later processing (?)
53
54 ################################################################################
55
56 import errno, os, readline, string, stat, sys, tempfile;
57 import apt_pkg, apt_inst;
58 import db_access, fernanda, katie, logging, utils;
59
60 # Globals
61 lisa_version = "$Revision: 1.1 $";
62
63 Cnf = None;
64 Options = None;
65 Katie = None;
66 projectB = None;
67 Logger = None;
68
69 Priorities = None;
70 Sections = None;
71
72 ################################################################################
73 ################################################################################
74 ################################################################################
75
76 def determine_new (changes, files):
77     new = {};
78
79     # Build up a list of potentially new things
80     for file in files.keys():
81         f = files[file];
82         # Skip byhand elements
83         if f["type"] == "byhand":
84             continue;
85         pkg = f["package"];
86         priority = f["priority"];
87         section = f["section"];
88         type = get_type(f);
89         component = f["component"];
90
91         if type == "dsc":
92             priority = "source";
93         if not new.has_key(pkg):
94             new[pkg] = {};
95             new[pkg]["priority"] = priority;
96             new[pkg]["section"] = section;
97             new[pkg]["type"] = type;
98             new[pkg]["component"] = component;
99             new[pkg]["files"] = [];
100         else:
101             old_type = new[pkg]["type"];
102             if old_type != type:
103                 # source gets trumped by deb or udeb
104                 if old_type == "dsc":
105                     new[pkg]["priority"] = priority;
106                     new[pkg]["section"] = section;
107                     new[pkg]["type"] = type;
108                     new[pkg]["component"] = component;
109         new[pkg]["files"].append(file);
110
111     for suite in changes["distribution"].keys():
112         suite_id = db_access.get_suite_id(suite);
113         for pkg in new.keys():
114             component_id = db_access.get_component_id(new[pkg]["component"]);
115             type_id = db_access.get_override_type_id(new[pkg]["type"]);
116             q = projectB.query("SELECT package FROM override WHERE package = '%s' AND suite = %s AND component = %s AND type = %s" % (pkg, suite_id, component_id, type_id));
117             ql = q.getresult();
118             if ql:
119                 for file in new[pkg]["files"]:
120                     if files[file].has_key("new"):
121                         del files[file]["new"];
122                 del new[pkg];
123
124     return new;
125
126 ################################################################################
127
128 # Sort by 'have source', by ctime, by source name, by source version number, by filename
129
130 def changes_compare_by_time (a, b):
131     try:
132         a_changes = utils.parse_changes(a, 0)
133     except:
134         return -1;
135
136     try:
137         b_changes = utils.parse_changes(b, 0)
138     except:
139         return 1;
140
141     utils.cc_fix_changes (a_changes);
142     utils.cc_fix_changes (b_changes);
143
144     # Sort by 'have source'
145
146     a_has_source = a_changes["architecture"].get("source")
147     b_has_source = b_changes["architecture"].get("source")
148     if a_has_source and not b_has_source:
149         return -1;
150     elif b_has_source and not a_has_source:
151         return 1;
152
153     # Sort by ctime
154     a_ctime = os.stat(a)[stat.ST_CTIME];
155     b_ctime = os.stat(b)[stat.ST_CTIME];
156     q = cmp (a_ctime, b_ctime);
157     if q:
158         return q;
159
160     # Sort by source name
161
162     a_source = a_changes.get("source");
163     b_source = b_changes.get("source");
164     q = cmp (a_source, b_source);
165     if q:
166         return q;
167
168     # Sort by source version
169
170     a_version = a_changes.get("version");
171     b_version = b_changes.get("version");
172     q = apt_pkg.VersionCompare(a_version, b_version);
173     if q:
174         return q
175
176     # Fall back to sort by filename
177
178     return cmp(a, b);
179
180 ################################################################################
181
182 class Section_Completer:
183     def __init__ (self):
184         self.sections = [];
185         q = projectB.query("SELECT section FROM section");
186         for i in q.getresult():
187             self.sections.append(i[0]);
188
189     def complete(self, text, state):
190         if state == 0:
191             self.matches = [];
192             n = len(text);
193             for word in self.sections:
194                 if word[:n] == text:
195                     self.matches.append(word);
196         try:
197             return self.matches[state]
198         except IndexError:
199             return None
200
201 ############################################################
202
203 class Priority_Completer:
204     def __init__ (self):
205         self.priorities = [];
206         q = projectB.query("SELECT priority FROM priority");
207         for i in q.getresult():
208             self.priorities.append(i[0]);
209
210     def complete(self, text, state):
211         if state == 0:
212             self.matches = [];
213             n = len(text);
214             for word in self.priorities:
215                 if word[:n] == text:
216                     self.matches.append(word);
217         try:
218             return self.matches[state]
219         except IndexError:
220             return None
221
222 ################################################################################
223
224 def check_valid (new):
225     for pkg in new.keys():
226         section = new[pkg]["section"];
227         priority = new[pkg]["priority"];
228         type = new[pkg]["type"];
229         new[pkg]["section id"] = db_access.get_section_id(section);
230         new[pkg]["priority id"] = db_access.get_priority_id(new[pkg]["priority"]);
231         # Sanity checks
232         if (section == "debian-installer" and type != "udeb") or \
233            (section != "debian-installer" and type == "udeb"):
234             new[pkg]["section id"] = -1;
235         if (priority == "source" and type != "dsc") or \
236            (priority != "source" and type == "dsc"):
237             new[pkg]["priority id"] = -1;
238
239 ################################################################################
240
241 def print_new (new, indexed, file=sys.stdout):
242     check_valid(new);
243     ret_code = 0;
244     index = 0;
245     for pkg in new.keys():
246         index = index + 1;
247         section = new[pkg]["section"];
248         priority = new[pkg]["priority"];
249         if new[pkg]["section id"] == -1:
250             section = section + "[!]";
251             ret_code = 1;
252         if new[pkg]["priority id"] == -1:
253             priority = priority + "[!]";
254             ret_code = 1;
255         if indexed:
256             line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section);
257         else:
258             line = "%-20s %-20s %-20s" % (pkg, priority, section);
259         line = string.strip(line)+'\n';
260         file.write(line);
261     return ret_code;
262
263 ################################################################################
264
265 def get_type (f):
266     # Determine the type
267     if f.has_key("dbtype"):
268         type = f["dbtype"];
269     elif f["type"] == "orig.tar.gz" or f["type"] == "tar.gz" or f["type"] == "diff.gz" or f["type"] == "dsc":
270         type = "dsc";
271     else:
272         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (type));
273
274     # Validate the override type
275     type_id = db_access.get_override_type_id(type);
276     if type_id == -1:
277         utils.fubar("invalid type (%s) for new.  Say wha?" % (type));
278
279     return type;
280
281 ################################################################################
282
283 def index_range (index):
284     if index == 1:
285         return "1";
286     else:
287         return "1-%s" % (index);
288
289 ################################################################################
290 ################################################################################
291
292 def spawn_editor (new):
293     # Write the current data to a temporary file
294     temp_filename = tempfile.mktemp();
295     fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
296     os.close(fd);
297     temp_file = utils.open_file(temp_filename, 'w');
298     print_new (new, 0, temp_file);
299     temp_file.close();
300     # Spawn an editor on that file
301     editor = os.environ.get("EDITOR","vi")
302     result = os.system("%s %s" % (editor, temp_filename))
303     if result != 0:
304         utils.fubar ("vi invocation failed for `%s'!" % (temp_filename), result)
305     # Read the (edited) data back in
306     file = utils.open_file(temp_filename);
307     for line in file.readlines():
308         line = string.strip(line[:-1]);
309         if line == "":
310             continue;
311         s = string.split(line);
312         # Pad the list if necessary
313         s[len(s):3] = [None] * (3-len(s));
314         (pkg, priority, section) = s[:3];
315         if not new.has_key(pkg):
316             utils.warn("Ignoring unknown package '%s'" % (pkg));
317         else:
318             # Strip off any invalid markers, print_new will readd them.
319             if section[-3:] == "[!]":
320                 section = section[:-3];
321             if priority[-3:] == "[!]":
322                 priority = priority[:-3];
323             for file in new[pkg]["files"]:
324                 Katie.pkg.files[file]["section"] = section;
325                 Katie.pkg.files[file]["priority"] = priority;
326             new[pkg]["section"] = section;
327             new[pkg]["priority"] = priority;
328     os.unlink(temp_filename);
329
330 ################################################################################
331
332 def edit_index (new, index):
333     priority = new[index]["priority"]
334     section = new[index]["section"]
335     type = new[index]["type"];
336     done = 0
337     while not done:
338         print string.join([index, priority, section], '\t');
339
340         answer = "XXX";
341         if type != "dsc":
342             prompt = "[B]oth, Priority, Section, Done ? ";
343         else:
344             prompt = "[S]ection, Done ? ";
345         edit_priority = edit_section = 0;
346
347         while string.find(prompt, answer) == -1:
348             answer = raw_input(prompt);
349             m = katie.re_default_answer.match(prompt)
350             if answer == "":
351                 answer = m.group(1)
352             answer = string.upper(answer[:1])
353
354         if answer == 'P':
355             edit_priority = 1;
356         elif answer == 'S':
357             edit_section = 1;
358         elif answer == 'B':
359             edit_priority = edit_section = 1;
360         elif answer == 'D':
361             done = 1;
362
363         # Edit the priority
364         if edit_priority:
365             readline.set_completer(Priorities.complete);
366             got_priority = 0;
367             while not got_priority:
368                 new_priority = string.strip(raw_input("New priority: "));
369                 if Priorities.priorities.count(new_priority) == 0:
370                     print "E: '%s' is not a valid priority, try again." % (new_priority);
371                 else:
372                     got_priority = 1;
373                     priority = new_priority;
374
375         # Edit the section
376         if edit_section:
377             readline.set_completer(Sections.complete);
378             got_section = 0;
379             while not got_section:
380                 new_section = string.strip(raw_input("New section: "));
381                 if Sections.sections.count(new_section) == 0:
382                     print "E: '%s' is not a valid section, try again." % (new_section);
383                 else:
384                     got_section = 1;
385                     section = new_section;
386
387         # Reset the readline completer
388         readline.set_completer(None);
389
390     for file in new[index]["files"]:
391         Katie.pkg.files[file]["section"] = section;
392         Katie.pkg.files[file]["priority"] = priority;
393     new[index]["priority"] = priority;
394     new[index]["section"] = section;
395     return new;
396
397 ################################################################################
398
399 def edit_overrides (new):
400     print;
401     done = 0
402     while not done:
403         print_new (new, 1);
404         new_index = {};
405         index = 0;
406         for i in new.keys():
407             index = index + 1;
408             new_index[index] = i;
409
410         prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index));
411
412         got_answer = 0
413         while not got_answer:
414             answer = raw_input(prompt)
415             answer = string.upper(answer[:1])
416             if answer == "E" or answer == "D":
417                 got_answer = 1;
418             elif katie.re_isanum.match (answer):
419                 answer = int(answer);
420                 if (answer < 1) or (answer > index):
421                     print "%s is not a valid index (%s).  Please retry." % (index_range(index), answer);
422                 else:
423                     got_answer = 1;
424
425         if answer == 'E':
426             spawn_editor(new);
427         elif answer == 'D':
428             done = 1;
429         else:
430             edit_index (new, new_index[answer]);
431
432     return new;
433
434 ################################################################################
435
436 def check_pkg ():
437     try:
438         less_fd = os.popen("less -", 'w', 0);
439         stdout_fd = sys.stdout;
440         try:
441             sys.stdout = less_fd;
442             fernanda.display_changes(Katie.pkg.changes_file);
443             files = Katie.pkg.files;
444             for file in files.keys():
445                 if files[file].has_key("new"):
446                     type = files[file]["type"];
447                     if type == "deb":
448                         fernanda.check_deb(file);
449                     elif type == "dsc":
450                         fernanda.check_dsc(file);
451         finally:
452             sys.stdout = stdout_fd;
453     except IOError, e:
454         if errno.errorcode[e.errno] == 'EPIPE':
455             utils.warn("[fernanda] Caught EPIPE; skipping.");
456             pass;
457         else:
458             raise;
459     except KeyboardInterrupt:
460         utils.warn("[fernanda] Caught C-c; skipping.");
461         pass;
462
463 ################################################################################
464
465 ## FIXME: horribly Debian specific
466
467 def do_bxa_notification():
468     files = Katie.pkg.files;
469     summary = "";
470     for file in files.keys():
471         if files[file]["type"] == "deb":
472             control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)));
473             summary = summary + "\n";
474             summary = summary + "Package: %s\n" % (control.Find("Package"));
475             summary = summary + "Description: %s\n" % (control.Find("Description"));
476     Katie.Subst["__BINARY_DESCRIPTIONS__"] = summary;
477     bxa_mail = utils.TemplateSubst(Katie.Subst,open(Cnf["Dir::TemplatesDir"]+"/lisa.bxa_notification","r").read());
478     utils.send_mail(bxa_mail,"");
479
480 ################################################################################
481
482 def add_overrides (new):
483     changes = Katie.pkg.changes;
484     files = Katie.pkg.files;
485
486     projectB.query("BEGIN WORK");
487     for suite in changes["distribution"].keys():
488         suite_id = db_access.get_suite_id(suite);
489         for pkg in new.keys():
490             component_id = db_access.get_component_id(new[pkg]["component"]);
491             type_id = db_access.get_override_type_id(new[pkg]["type"]);
492             priority_id = new[pkg]["priority id"];
493             section_id = new[pkg]["section id"];
494             projectB.query("INSERT INTO override (suite, component, type, package, priority, section) VALUES (%s, %s, %s, '%s', %s, %s)" % (suite_id, component_id, type_id, pkg, priority_id, section_id));
495             for file in new[pkg]["files"]:
496                 if files[file].has_key("new"):
497                     del files[file]["new"];
498             del new[pkg];
499
500     projectB.query("COMMIT WORK");
501
502     if Cnf.FindI("Dinstall::BXANotify"):
503         do_bxa_notification();
504
505 ################################################################################
506
507 def do_new():
508     print "NEW\n";
509     files = Katie.pkg.files;
510     changes = Katie.pkg.changes;
511
512     # Fix up the list of target suites
513     for suite in changes["distribution"].keys():
514         override = Cnf.Find("Suite::%s::OverrideSuite" % (suite));
515         if override:
516             del changes["distribution"][suite];
517             changes["distribution"][override] = 1;
518     # Validate suites
519     for suite in changes["distribution"].keys():
520         suite_id = db_access.get_suite_id(suite);
521         if suite_id == -1:
522             utils.fubar("%s has invalid suite '%s' (possibly overriden).  say wha?" % (changes, suite));
523
524     # The main NEW processing loop
525     done = 0;
526     while not done:
527         # Find out what's new
528         new = determine_new(changes, files);
529
530         if not new:
531             break;
532
533         answer = "XXX";
534         if Options["No-Action"] or Options["Automatic"]:
535             answer = 'S'
536         if Options["Automatic"]:
537             answer = 'A';
538
539         broken = print_new(new, 0);
540         prompt = "";
541         if not broken:
542             prompt = "[A]dd overrides, ";
543         else:
544             print "W: [!] marked entries must be fixed before package can be processed.";
545             if answer == 'A':
546                 answer = 'E';
547         prompt = prompt + "Edit overrides, Check, Manual reject, Skip, Quit ?";
548
549         while string.find(prompt, answer) == -1:
550             answer = raw_input(prompt)
551             m = katie.re_default_answer.match(prompt)
552             if answer == "":
553                 answer = m.group(1)
554             answer = string.upper(answer[:1])
555
556         if answer == 'A':
557             done = add_overrides (new);
558         elif answer == 'C':
559             check_pkg();
560         elif answer == 'E':
561             new = edit_overrides (new);
562         elif answer == 'M':
563             Katie.do_reject(1, Options["Manual-Reject"]);
564             os.unlink(Katie.pkg.changes_file[:-8]+".katie");
565             done = 1;
566         elif answer == 'S':
567             done = 1;
568         elif answer == 'Q':
569             sys.exit(0)
570
571 ################################################################################
572 ################################################################################
573 ################################################################################
574
575 def usage (exit_code=0):
576     print """Usage: lisa [OPTION]... [CHANGES]...
577   -a, --automatic           automatic run
578   -h, --help                show this help and exit.
579   -m, --manual-reject=MSG   manual reject with `msg'
580   -n, --no-action           don't do anything
581   -s, --sort=TYPE           sort type ('time' or 'normal')
582   -V, --version             display the version number and exit"""
583     sys.exit(exit_code)
584
585 ################################################################################
586
587 def init():
588     global Cnf, Options, Logger, Katie, projectB, Sections, Priorities;
589
590     Cnf = utils.get_conf();
591
592     Arguments = [('a',"automatic","Lisa::Options::Automatic"),
593                  ('h',"help","Lisa::Options::Help"),
594                  ('m',"manual-reject","Lisa::Options::Manual-Reject", "HasArg"),
595                  ('n',"no-action","Lisa::Options::No-Action"),
596                  ('s',"sort","Lisa::Options::Sort","HasArg"),
597                  ('V',"version","Lisa::Options::Version")];
598
599     for i in ["automatic", "help", "manual-reject", "no-action", "version"]:
600         if not Cnf.has_key("Lisa::Options::%s" % (i)):
601             Cnf["Lisa::Options::%s" % (i)] = "";
602     if not Cnf.has_key("Lisa::Options::Sort"):
603         Cnf["Lisa::Options::Sort"] = "time";
604
605     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
606     Options = Cnf.SubTree("Lisa::Options")
607
608     if Options["Help"]:
609         usage();
610
611     if Options["Version"]:
612         print "lisa %s" % (lisa_version);
613         sys.exit(0);
614
615     if Options["Sort"] != "time" and Options["Sort"] != "normal":
616         utils.fubar("Unrecognised sort type '%s'. (Recognised sort types are: time and normal)" % (Options["Sort"]));
617
618     Katie = katie.Katie(Cnf);
619
620     if not Options["No-Action"]:
621         Logger = Katie.Logger = logging.Logger(Cnf, "lisa");
622
623     projectB = Katie.projectB;
624
625     Sections = Section_Completer();
626     Priorities = Priority_Completer();
627     readline.parse_and_bind("tab: complete");
628
629     return changes_files;
630
631 ################################################################################
632
633 def do_byhand():
634     done = 0;
635     while not done:
636         files = Katie.pkg.files;
637         will_install = 1;
638         byhand = [];
639
640         for file in files.keys():
641             if files[file]["type"] == "byhand":
642                 if os.path.exists(file):
643                     print "W: %s still present; please process byhand components and try again." % (file);
644                     will_install = 0;
645                 else:
646                     byhand.append(file);
647
648         answer = "XXXX";
649         if Options["No-Action"]:
650             answer = "S";
651         if will_install:
652             if Options["Automatic"] and not Options["No-Action"]:
653                 answer = 'A';
654             prompt = "[A]ccept, Manual reject, Skip, Quit ?";
655         else:
656             prompt = "Manual reject, [S]kip, Quit ?";
657
658         while string.find(prompt, answer) == -1:
659             answer = raw_input(prompt)
660             m = katie.re_default_answer.match(prompt)
661             if answer == "":
662                 answer = m.group(1)
663             answer = string.upper(answer[:1])
664
665         if answer == 'A':
666             done = 1;
667             for file in byhand:
668                 del files[file];
669         elif answer == 'M':
670             Katie.do_reject(1, Options["Manual-Reject"]);
671             os.unlink(Katie.pkg.changes_file[:-8]+".katie");
672             done = 1;
673         elif answer == 'S':
674             done = 1;
675         elif answer == 'Q':
676             sys.exit(0);
677
678 ################################################################################
679
680 def do_accept():
681     print "ACCEPT";
682     if not Options["No-Action"]:
683         (summary, short_summary) = Katie.build_summaries();
684         Katie.accept(summary, short_summary);
685         os.unlink(Katie.pkg.changes_file[:-8]+".katie");
686
687 def check_status(files):
688     new = byhand = 0;
689     for file in files.keys():
690         if files[file]["type"] == "byhand":
691             byhand = 1;
692         elif files[file].has_key("new"):
693             new = 1;
694     return (new, byhand);
695
696 def do_pkg(changes_file):
697     Katie.pkg.changes_file = changes_file;
698     Katie.init_vars();
699     Katie.update_vars();
700     Katie.update_subst();
701     files = Katie.pkg.files;
702
703     (new, byhand) = check_status(files);
704     if new or byhand:
705         if new:
706             do_new();
707         if byhand:
708             do_byhand();
709         (new, byhand) = check_status(files);
710
711     if not new and not byhand:
712         do_accept();
713
714 ################################################################################
715
716 def end():
717     accept_count = Katie.accept_count;
718     accept_bytes = Katie.accept_bytes;
719
720     if accept_count:
721         sets = "set"
722         if accept_count > 1:
723             sets = "sets"
724         sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))));
725         Logger.log(["total",accept_count,accept_bytes]);
726
727     if not Options["No-Action"]:
728         Logger.close();
729
730 ################################################################################
731
732 def main():
733     changes_files = init();
734
735     # Sort the changes files
736     if Options["Sort"] == "time":
737         changes_files.sort(changes_compare_by_time);
738     else:
739         changes_files.sort(utils.changes_compare);
740
741     # Kill me now? **FIXME**
742     Cnf["Dinstall::Options::No-Mail"] = "";
743     bcc = "X-Lisa: %s" % (lisa_version);
744     if Cnf.has_key("Dinstall::Bcc"):
745         Katie.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
746     else:
747         Katie.Subst["__BCC__"] = bcc;
748
749     for changes_file in changes_files:
750         print "\n" + changes_file;
751         do_pkg (changes_file);
752
753     end();
754
755 ################################################################################
756
757 if __name__ == '__main__':
758     main()