]> git.decadent.org.uk Git - dak.git/blob - lisa
Add new top level directories
[dak.git] / lisa
1 #!/usr/bin/env python
2
3 # Handles NEW and BYHAND packages
4 # Copyright (C) 2001, 2002, 2003, 2004, 2005  James Troup <james@nocrew.org>
5 # $Id: lisa,v 1.31 2005-11-15 09:50:32 ajt 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 import copy, errno, os, readline, stat, sys, time;
41 import apt_pkg, apt_inst;
42 import db_access, fernanda, katie, logging, utils;
43
44 # Globals
45 lisa_version = "$Revision: 1.31 $";
46
47 Cnf = None;
48 Options = None;
49 Katie = None;
50 projectB = None;
51 Logger = None;
52
53 Priorities = None;
54 Sections = None;
55
56 reject_message = "";
57
58 ################################################################################
59 ################################################################################
60 ################################################################################
61
62 def reject (str, prefix="Rejected: "):
63     global reject_message;
64     if str:
65         reject_message += prefix + str + "\n";
66
67 def recheck():
68     global reject_message;
69     files = Katie.pkg.files;
70     reject_message = "";
71
72     for file in files.keys():
73         # The .orig.tar.gz can disappear out from under us is it's a
74         # duplicate of one in the archive.
75         if not files.has_key(file):
76             continue;
77         # Check that the source still exists
78         if files[file]["type"] == "deb":
79             source_version = files[file]["source version"];
80             source_package = files[file]["source package"];
81             if not Katie.pkg.changes["architecture"].has_key("source") \
82                and not Katie.source_exists(source_package, source_version, Katie.pkg.changes["distribution"].keys()):
83                 source_epochless_version = utils.re_no_epoch.sub('', source_version);
84                 dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version);
85                 if not os.path.exists(Cnf["Dir::Queue::Accepted"] + '/' + dsc_filename):
86                     reject("no source found for %s %s (%s)." % (source_package, source_version, file));
87
88         # Version and file overwrite checks
89         if files[file]["type"] == "deb":
90             reject(Katie.check_binary_against_db(file));
91         elif files[file]["type"] == "dsc":
92             reject(Katie.check_source_against_db(file));
93             (reject_msg, is_in_incoming) = Katie.check_dsc_against_db(file);
94             reject(reject_msg);
95
96     if reject_message:
97         answer = "XXX";
98         if Options["No-Action"] or Options["Automatic"]:
99             answer = 'S'
100
101         print "REJECT\n" + reject_message,;
102         prompt = "[R]eject, Skip, Quit ?";
103
104         while prompt.find(answer) == -1:
105             answer = utils.our_raw_input(prompt);
106             m = katie.re_default_answer.match(prompt);
107             if answer == "":
108                 answer = m.group(1);
109             answer = answer[:1].upper();
110
111         if answer == 'R':
112             Katie.do_reject(0, reject_message);
113             os.unlink(Katie.pkg.changes_file[:-8]+".katie");
114             return 0;
115         elif answer == 'S':
116             return 0;
117         elif answer == 'Q':
118             sys.exit(0);
119
120     return 1;
121
122 ################################################################################
123
124 def determine_new (changes, files):
125     new = {};
126
127     # Build up a list of potentially new things
128     for file in files.keys():
129         f = files[file];
130         # Skip byhand elements
131         if f["type"] == "byhand":
132             continue;
133         pkg = f["package"];
134         priority = f["priority"];
135         section = f["section"];
136         # FIXME: unhardcode
137         if section == "non-US/main":
138             section = "non-US";
139         type = get_type(f);
140         component = f["component"];
141
142         if type == "dsc":
143             priority = "source";
144         if not new.has_key(pkg):
145             new[pkg] = {};
146             new[pkg]["priority"] = priority;
147             new[pkg]["section"] = section;
148             new[pkg]["type"] = type;
149             new[pkg]["component"] = component;
150             new[pkg]["files"] = [];
151         else:
152             old_type = new[pkg]["type"];
153             if old_type != type:
154                 # source gets trumped by deb or udeb
155                 if old_type == "dsc":
156                     new[pkg]["priority"] = priority;
157                     new[pkg]["section"] = section;
158                     new[pkg]["type"] = type;
159                     new[pkg]["component"] = component;
160         new[pkg]["files"].append(file);
161         if f.has_key("othercomponents"):
162             new[pkg]["othercomponents"] = f["othercomponents"];
163
164     for suite in changes["suite"].keys():
165         suite_id = db_access.get_suite_id(suite);
166         for pkg in new.keys():
167             component_id = db_access.get_component_id(new[pkg]["component"]);
168             type_id = db_access.get_override_type_id(new[pkg]["type"]);
169             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));
170             ql = q.getresult();
171             if ql:
172                 for file in new[pkg]["files"]:
173                     if files[file].has_key("new"):
174                         del files[file]["new"];
175                 del new[pkg];
176
177     if changes["suite"].has_key("stable"):
178         print "WARNING: overrides will be added for stable!";
179     if changes["suite"].has_key("oldstable"):
180         print "WARNING: overrides will be added for OLDstable!";
181     for pkg in new.keys():
182         if new[pkg].has_key("othercomponents"):
183             print "WARNING: %s already present in %s distribution." % (pkg, new[pkg]["othercomponents"]);
184
185     return new;
186
187 ################################################################################
188
189 def indiv_sg_compare (a, b):
190     """Sort by source name, source, version, 'have source', and
191        finally by filename."""
192     # Sort by source version
193     q = apt_pkg.VersionCompare(a["version"], b["version"]);
194     if q:
195         return -q;
196
197     # Sort by 'have source'
198     a_has_source = a["architecture"].get("source");
199     b_has_source = b["architecture"].get("source");
200     if a_has_source and not b_has_source:
201         return -1;
202     elif b_has_source and not a_has_source:
203         return 1;
204
205     return cmp(a["filename"], b["filename"]);
206
207 ############################################################
208
209 def sg_compare (a, b):
210     a = a[1];
211     b = b[1];
212     """Sort by have note, time of oldest upload."""
213     # Sort by have note
214     a_note_state = a["note_state"];
215     b_note_state = b["note_state"];
216     if a_note_state < b_note_state:
217         return -1;
218     elif a_note_state > b_note_state:
219         return 1;
220
221     # Sort by time of oldest upload
222     return cmp(a["oldest"], b["oldest"]);
223
224 def sort_changes(changes_files):
225     """Sort into source groups, then sort each source group by version,
226     have source, filename.  Finally, sort the source groups by have
227     note, time of oldest upload of each source upload."""
228     if len(changes_files) == 1:
229         return changes_files;
230
231     sorted_list = [];
232     cache = {};
233     # Read in all the .changes files
234     for filename in changes_files:
235         try:
236             Katie.pkg.changes_file = filename;
237             Katie.init_vars();
238             Katie.update_vars();
239             cache[filename] = copy.copy(Katie.pkg.changes);
240             cache[filename]["filename"] = filename;
241         except:
242             sorted_list.append(filename);
243             break;
244     # Divide the .changes into per-source groups
245     per_source = {};
246     for filename in cache.keys():
247         source = cache[filename]["source"];
248         if not per_source.has_key(source):
249             per_source[source] = {};
250             per_source[source]["list"] = [];
251         per_source[source]["list"].append(cache[filename]);
252     # Determine oldest time and have note status for each source group
253     for source in per_source.keys():
254         source_list = per_source[source]["list"];
255         first = source_list[0];
256         oldest = os.stat(first["filename"])[stat.ST_MTIME];
257         have_note = 0;
258         for d in per_source[source]["list"]:
259             mtime = os.stat(d["filename"])[stat.ST_MTIME];
260             if mtime < oldest:
261                 oldest = mtime;
262             have_note += (d.has_key("lisa note"));
263         per_source[source]["oldest"] = oldest;
264         if not have_note:
265             per_source[source]["note_state"] = 0; # none
266         elif have_note < len(source_list):
267             per_source[source]["note_state"] = 1; # some
268         else:
269             per_source[source]["note_state"] = 2; # all
270         per_source[source]["list"].sort(indiv_sg_compare);
271     per_source_items = per_source.items();
272     per_source_items.sort(sg_compare);
273     for i in per_source_items:
274         for j in i[1]["list"]:
275             sorted_list.append(j["filename"]);
276     return sorted_list;
277
278 ################################################################################
279
280 class Section_Completer:
281     def __init__ (self):
282         self.sections = [];
283         q = projectB.query("SELECT section FROM section");
284         for i in q.getresult():
285             self.sections.append(i[0]);
286
287     def complete(self, text, state):
288         if state == 0:
289             self.matches = [];
290             n = len(text);
291             for word in self.sections:
292                 if word[:n] == text:
293                     self.matches.append(word);
294         try:
295             return self.matches[state]
296         except IndexError:
297             return None
298
299 ############################################################
300
301 class Priority_Completer:
302     def __init__ (self):
303         self.priorities = [];
304         q = projectB.query("SELECT priority FROM priority");
305         for i in q.getresult():
306             self.priorities.append(i[0]);
307
308     def complete(self, text, state):
309         if state == 0:
310             self.matches = [];
311             n = len(text);
312             for word in self.priorities:
313                 if word[:n] == text:
314                     self.matches.append(word);
315         try:
316             return self.matches[state]
317         except IndexError:
318             return None
319
320 ################################################################################
321
322 def check_valid (new):
323     for pkg in new.keys():
324         section = new[pkg]["section"];
325         priority = new[pkg]["priority"];
326         type = new[pkg]["type"];
327         new[pkg]["section id"] = db_access.get_section_id(section);
328         new[pkg]["priority id"] = db_access.get_priority_id(new[pkg]["priority"]);
329         # Sanity checks
330         if (section == "debian-installer" and type != "udeb") or \
331            (section != "debian-installer" and type == "udeb"):
332             new[pkg]["section id"] = -1;
333         if (priority == "source" and type != "dsc") or \
334            (priority != "source" and type == "dsc"):
335             new[pkg]["priority id"] = -1;
336
337 ################################################################################
338
339 def print_new (new, indexed, file=sys.stdout):
340     check_valid(new);
341     broken = 0;
342     index = 0;
343     for pkg in new.keys():
344         index += 1;
345         section = new[pkg]["section"];
346         priority = new[pkg]["priority"];
347         if new[pkg]["section id"] == -1:
348             section += "[!]";
349             broken = 1;
350         if new[pkg]["priority id"] == -1:
351             priority += "[!]";
352             broken = 1;
353         if indexed:
354             line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section);
355         else:
356             line = "%-20s %-20s %-20s" % (pkg, priority, section);
357         line = line.strip()+'\n';
358         file.write(line);
359     note = Katie.pkg.changes.get("lisa note");
360     if note:
361         print "*"*75;
362         print note;
363         print "*"*75;
364     return broken, note;
365
366 ################################################################################
367
368 def get_type (f):
369     # Determine the type
370     if f.has_key("dbtype"):
371         type = f["dbtype"];
372     elif f["type"] == "orig.tar.gz" or f["type"] == "tar.gz" or f["type"] == "diff.gz" or f["type"] == "dsc":
373         type = "dsc";
374     else:
375         utils.fubar("invalid type (%s) for new.  Dazed, confused and sure as heck not continuing." % (type));
376
377     # Validate the override type
378     type_id = db_access.get_override_type_id(type);
379     if type_id == -1:
380         utils.fubar("invalid type (%s) for new.  Say wha?" % (type));
381
382     return type;
383
384 ################################################################################
385
386 def index_range (index):
387     if index == 1:
388         return "1";
389     else:
390         return "1-%s" % (index);
391
392 ################################################################################
393 ################################################################################
394
395 def edit_new (new):
396     # Write the current data to a temporary file
397     temp_filename = utils.temp_filename();
398     temp_file = utils.open_file(temp_filename, 'w');
399     print_new (new, 0, temp_file);
400     temp_file.close();
401     # Spawn an editor on that file
402     editor = os.environ.get("EDITOR","vi")
403     result = os.system("%s %s" % (editor, temp_filename))
404     if result != 0:
405         utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
406     # Read the edited data back in
407     temp_file = utils.open_file(temp_filename);
408     lines = temp_file.readlines();
409     temp_file.close();
410     os.unlink(temp_filename);
411     # Parse the new data
412     for line in lines:
413         line = line.strip();
414         if line == "":
415             continue;
416         s = line.split();
417         # Pad the list if necessary
418         s[len(s):3] = [None] * (3-len(s));
419         (pkg, priority, section) = s[:3];
420         if not new.has_key(pkg):
421             utils.warn("Ignoring unknown package '%s'" % (pkg));
422         else:
423             # Strip off any invalid markers, print_new will readd them.
424             if section.endswith("[!]"):
425                 section = section[:-3];
426             if priority.endswith("[!]"):
427                 priority = priority[:-3];
428             for file in new[pkg]["files"]:
429                 Katie.pkg.files[file]["section"] = section;
430                 Katie.pkg.files[file]["priority"] = priority;
431             new[pkg]["section"] = section;
432             new[pkg]["priority"] = priority;
433
434 ################################################################################
435
436 def edit_index (new, index):
437     priority = new[index]["priority"]
438     section = new[index]["section"]
439     type = new[index]["type"];
440     done = 0
441     while not done:
442         print "\t".join([index, priority, section]);
443
444         answer = "XXX";
445         if type != "dsc":
446             prompt = "[B]oth, Priority, Section, Done ? ";
447         else:
448             prompt = "[S]ection, Done ? ";
449         edit_priority = edit_section = 0;
450
451         while prompt.find(answer) == -1:
452             answer = utils.our_raw_input(prompt);
453             m = katie.re_default_answer.match(prompt)
454             if answer == "":
455                 answer = m.group(1)
456             answer = answer[:1].upper()
457
458         if answer == 'P':
459             edit_priority = 1;
460         elif answer == 'S':
461             edit_section = 1;
462         elif answer == 'B':
463             edit_priority = edit_section = 1;
464         elif answer == 'D':
465             done = 1;
466
467         # Edit the priority
468         if edit_priority:
469             readline.set_completer(Priorities.complete);
470             got_priority = 0;
471             while not got_priority:
472                 new_priority = utils.our_raw_input("New priority: ").strip();
473                 if new_priority not in Priorities.priorities:
474                     print "E: '%s' is not a valid priority, try again." % (new_priority);
475                 else:
476                     got_priority = 1;
477                     priority = new_priority;
478
479         # Edit the section
480         if edit_section:
481             readline.set_completer(Sections.complete);
482             got_section = 0;
483             while not got_section:
484                 new_section = utils.our_raw_input("New section: ").strip();
485                 if new_section not in Sections.sections:
486                     print "E: '%s' is not a valid section, try again." % (new_section);
487                 else:
488                     got_section = 1;
489                     section = new_section;
490
491         # Reset the readline completer
492         readline.set_completer(None);
493
494     for file in new[index]["files"]:
495         Katie.pkg.files[file]["section"] = section;
496         Katie.pkg.files[file]["priority"] = priority;
497     new[index]["priority"] = priority;
498     new[index]["section"] = section;
499     return new;
500
501 ################################################################################
502
503 def edit_overrides (new):
504     print;
505     done = 0
506     while not done:
507         print_new (new, 1);
508         new_index = {};
509         index = 0;
510         for i in new.keys():
511             index += 1;
512             new_index[index] = i;
513
514         prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index));
515
516         got_answer = 0
517         while not got_answer:
518             answer = utils.our_raw_input(prompt);
519             if not utils.str_isnum(answer):
520                 answer = answer[:1].upper();
521             if answer == "E" or answer == "D":
522                 got_answer = 1;
523             elif katie.re_isanum.match (answer):
524                 answer = int(answer);
525                 if (answer < 1) or (answer > index):
526                     print "%s is not a valid index (%s).  Please retry." % (answer, index_range(index));
527                 else:
528                     got_answer = 1;
529
530         if answer == 'E':
531             edit_new(new);
532         elif answer == 'D':
533             done = 1;
534         else:
535             edit_index (new, new_index[answer]);
536
537     return new;
538
539 ################################################################################
540
541 def edit_note(note):
542     # Write the current data to a temporary file
543     temp_filename = utils.temp_filename();
544     temp_file = utils.open_file(temp_filename, 'w');
545     temp_file.write(note);
546     temp_file.close();
547     editor = os.environ.get("EDITOR","vi")
548     answer = 'E';
549     while answer == 'E':
550         os.system("%s %s" % (editor, temp_filename))
551         temp_file = utils.open_file(temp_filename);
552         note = temp_file.read().rstrip();
553         temp_file.close();
554         print "Note:";
555         print utils.prefix_multi_line_string(note,"  ");
556         prompt = "[D]one, Edit, Abandon, Quit ?"
557         answer = "XXX";
558         while prompt.find(answer) == -1:
559             answer = utils.our_raw_input(prompt);
560             m = katie.re_default_answer.search(prompt);
561             if answer == "":
562                 answer = m.group(1);
563             answer = answer[:1].upper();
564     os.unlink(temp_filename);
565     if answer == 'A':
566         return;
567     elif answer == 'Q':
568         sys.exit(0);
569     Katie.pkg.changes["lisa note"] = note;
570     Katie.dump_vars(Cnf["Dir::Queue::New"]);
571
572 ################################################################################
573
574 def check_pkg ():
575     try:
576         less_fd = os.popen("less -R -", 'w', 0);
577         stdout_fd = sys.stdout;
578         try:
579             sys.stdout = less_fd;
580             fernanda.display_changes(Katie.pkg.changes_file);
581             files = Katie.pkg.files;
582             for file in files.keys():
583                 if files[file].has_key("new"):
584                     type = files[file]["type"];
585                     if type == "deb":
586                         fernanda.check_deb(file);
587                     elif type == "dsc":
588                         fernanda.check_dsc(file);
589         finally:
590             sys.stdout = stdout_fd;
591     except IOError, e:
592         if errno.errorcode[e.errno] == 'EPIPE':
593             utils.warn("[fernanda] Caught EPIPE; skipping.");
594             pass;
595         else:
596             raise;
597     except KeyboardInterrupt:
598         utils.warn("[fernanda] Caught C-c; skipping.");
599         pass;
600
601 ################################################################################
602
603 ## FIXME: horribly Debian specific
604
605 def do_bxa_notification():
606     files = Katie.pkg.files;
607     summary = "";
608     for file in files.keys():
609         if files[file]["type"] == "deb":
610             control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(file)));
611             summary += "\n";
612             summary += "Package: %s\n" % (control.Find("Package"));
613             summary += "Description: %s\n" % (control.Find("Description"));
614     Katie.Subst["__BINARY_DESCRIPTIONS__"] = summary;
615     bxa_mail = utils.TemplateSubst(Katie.Subst,Cnf["Dir::Templates"]+"/lisa.bxa_notification");
616     utils.send_mail(bxa_mail);
617
618 ################################################################################
619
620 def add_overrides (new):
621     changes = Katie.pkg.changes;
622     files = Katie.pkg.files;
623
624     projectB.query("BEGIN WORK");
625     for suite in changes["suite"].keys():
626         suite_id = db_access.get_suite_id(suite);
627         for pkg in new.keys():
628             component_id = db_access.get_component_id(new[pkg]["component"]);
629             type_id = db_access.get_override_type_id(new[pkg]["type"]);
630             priority_id = new[pkg]["priority id"];
631             section_id = new[pkg]["section id"];
632             projectB.query("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (%s, %s, %s, '%s', %s, %s, '')" % (suite_id, component_id, type_id, pkg, priority_id, section_id));
633             for file in new[pkg]["files"]:
634                 if files[file].has_key("new"):
635                     del files[file]["new"];
636             del new[pkg];
637
638     projectB.query("COMMIT WORK");
639
640     if Cnf.FindB("Dinstall::BXANotify"):
641         do_bxa_notification();
642
643 ################################################################################
644
645 def prod_maintainer ():
646     # Here we prepare an editor and get them ready to prod...
647     temp_filename = utils.temp_filename();
648     editor = os.environ.get("EDITOR","vi")
649     answer = 'E';
650     while answer == 'E':
651         os.system("%s %s" % (editor, temp_filename))
652         file = utils.open_file(temp_filename);
653         prod_message = "".join(file.readlines());
654         file.close();
655         print "Prod message:";
656         print utils.prefix_multi_line_string(prod_message,"  ",include_blank_lines=1);
657         prompt = "[P]rod, Edit, Abandon, Quit ?"
658         answer = "XXX";
659         while prompt.find(answer) == -1:
660             answer = utils.our_raw_input(prompt);
661             m = katie.re_default_answer.search(prompt);
662             if answer == "":
663                 answer = m.group(1);
664             answer = answer[:1].upper();
665         os.unlink(temp_filename);
666         if answer == 'A':
667             return;
668         elif answer == 'Q':
669             sys.exit(0);
670     # Otherwise, do the proding...
671     user_email_address = utils.whoami() + " <%s>" % (
672         Cnf["Dinstall::MyAdminAddress"]);
673
674     Subst = Katie.Subst;
675
676     Subst["__FROM_ADDRESS__"] = user_email_address;
677     Subst["__PROD_MESSAGE__"] = prod_message;
678     Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"];
679
680     prod_mail_message = utils.TemplateSubst(
681         Subst,Cnf["Dir::Templates"]+"/lisa.prod");
682
683     # Send the prod mail if appropriate
684     if not Cnf["Dinstall::Options::No-Mail"]:
685         utils.send_mail(prod_mail_message);
686
687     print "Sent proding message";
688
689 ################################################################################
690
691 def do_new():
692     print "NEW\n";
693     files = Katie.pkg.files;
694     changes = Katie.pkg.changes;
695
696     # Make a copy of distribution we can happily trample on
697     changes["suite"] = copy.copy(changes["distribution"]);
698
699     # Fix up the list of target suites
700     for suite in changes["suite"].keys():
701         override = Cnf.Find("Suite::%s::OverrideSuite" % (suite));
702         if override:
703             del changes["suite"][suite];
704             changes["suite"][override] = 1;
705     # Validate suites
706     for suite in changes["suite"].keys():
707         suite_id = db_access.get_suite_id(suite);
708         if suite_id == -1:
709             utils.fubar("%s has invalid suite '%s' (possibly overriden).  say wha?" % (changes, suite));
710
711     # The main NEW processing loop
712     done = 0;
713     while not done:
714         # Find out what's new
715         new = determine_new(changes, files);
716
717         if not new:
718             break;
719
720         answer = "XXX";
721         if Options["No-Action"] or Options["Automatic"]:
722             answer = 'S';
723
724         (broken, note) = print_new(new, 0);
725         prompt = "";
726
727         if not broken and not note:
728             prompt = "Add overrides, ";
729         if broken:
730             print "W: [!] marked entries must be fixed before package can be processed.";
731         if note:
732             print "W: note must be removed before package can be processed.";
733             prompt += "Remove note, ";
734
735         prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?";
736
737         while prompt.find(answer) == -1:
738             answer = utils.our_raw_input(prompt);
739             m = katie.re_default_answer.search(prompt);
740             if answer == "":
741                 answer = m.group(1)
742             answer = answer[:1].upper()
743
744         if answer == 'A':
745             done = add_overrides (new);
746         elif answer == 'C':
747             check_pkg();
748         elif answer == 'E':
749             new = edit_overrides (new);
750         elif answer == 'M':
751             aborted = Katie.do_reject(1, Options["Manual-Reject"]);
752             if not aborted:
753                 os.unlink(Katie.pkg.changes_file[:-8]+".katie");
754                 done = 1;
755         elif answer == 'N':
756             edit_note(changes.get("lisa note", ""));
757         elif answer == 'P':
758             prod_maintainer();
759         elif answer == 'R':
760             confirm = utils.our_raw_input("Really clear note (y/N)? ").lower();
761             if confirm == "y":
762                 del changes["lisa note"];
763         elif answer == 'S':
764             done = 1;
765         elif answer == 'Q':
766             sys.exit(0)
767
768 ################################################################################
769 ################################################################################
770 ################################################################################
771
772 def usage (exit_code=0):
773     print """Usage: lisa [OPTION]... [CHANGES]...
774   -a, --automatic           automatic run
775   -h, --help                show this help and exit.
776   -m, --manual-reject=MSG   manual reject with `msg'
777   -n, --no-action           don't do anything
778   -V, --version             display the version number and exit"""
779     sys.exit(exit_code)
780
781 ################################################################################
782
783 def init():
784     global Cnf, Options, Logger, Katie, projectB, Sections, Priorities;
785
786     Cnf = utils.get_conf();
787
788     Arguments = [('a',"automatic","Lisa::Options::Automatic"),
789                  ('h',"help","Lisa::Options::Help"),
790                  ('m',"manual-reject","Lisa::Options::Manual-Reject", "HasArg"),
791                  ('n',"no-action","Lisa::Options::No-Action"),
792                  ('V',"version","Lisa::Options::Version")];
793
794     for i in ["automatic", "help", "manual-reject", "no-action", "version"]:
795         if not Cnf.has_key("Lisa::Options::%s" % (i)):
796             Cnf["Lisa::Options::%s" % (i)] = "";
797
798     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
799     Options = Cnf.SubTree("Lisa::Options")
800
801     if Options["Help"]:
802         usage();
803
804     if Options["Version"]:
805         print "lisa %s" % (lisa_version);
806         sys.exit(0);
807
808     Katie = katie.Katie(Cnf);
809
810     if not Options["No-Action"]:
811         Logger = Katie.Logger = logging.Logger(Cnf, "lisa");
812
813     projectB = Katie.projectB;
814
815     Sections = Section_Completer();
816     Priorities = Priority_Completer();
817     readline.parse_and_bind("tab: complete");
818
819     return changes_files;
820
821 ################################################################################
822
823 def do_byhand():
824     done = 0;
825     while not done:
826         files = Katie.pkg.files;
827         will_install = 1;
828         byhand = [];
829
830         for file in files.keys():
831             if files[file]["type"] == "byhand":
832                 if os.path.exists(file):
833                     print "W: %s still present; please process byhand components and try again." % (file);
834                     will_install = 0;
835                 else:
836                     byhand.append(file);
837
838         answer = "XXXX";
839         if Options["No-Action"]:
840             answer = "S";
841         if will_install:
842             if Options["Automatic"] and not Options["No-Action"]:
843                 answer = 'A';
844             prompt = "[A]ccept, Manual reject, Skip, Quit ?";
845         else:
846             prompt = "Manual reject, [S]kip, Quit ?";
847
848         while prompt.find(answer) == -1:
849             answer = utils.our_raw_input(prompt);
850             m = katie.re_default_answer.search(prompt);
851             if answer == "":
852                 answer = m.group(1);
853             answer = answer[:1].upper();
854
855         if answer == 'A':
856             done = 1;
857             for file in byhand:
858                 del files[file];
859         elif answer == 'M':
860             Katie.do_reject(1, Options["Manual-Reject"]);
861             os.unlink(Katie.pkg.changes_file[:-8]+".katie");
862             done = 1;
863         elif answer == 'S':
864             done = 1;
865         elif answer == 'Q':
866             sys.exit(0);
867
868 ################################################################################
869
870 def do_accept():
871     print "ACCEPT";
872     if not Options["No-Action"]:
873         retry = 0;
874         while retry < 10:
875             try:
876                 lock_fd = os.open(Cnf["Lisa::AcceptedLockFile"], os.O_RDONLY | os.O_CREAT | os.O_EXCL);
877                 retry = 10;
878             except OSError, e:
879                 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
880                     retry += 1;
881                     if (retry >= 10):
882                         utils.fubar("Couldn't obtain lock; assuming jennifer is already running.");
883                     else:
884                         print("Unable to get accepted lock (try %d of 10)" % retry);
885                     time.sleep(60);
886                 else:
887                     raise;
888         (summary, short_summary) = Katie.build_summaries();
889         Katie.accept(summary, short_summary);
890         os.unlink(Katie.pkg.changes_file[:-8]+".katie");
891         os.unlink(Cnf["Lisa::AcceptedLockFile"]);
892
893 def check_status(files):
894     new = byhand = 0;
895     for file in files.keys():
896         if files[file]["type"] == "byhand":
897             byhand = 1;
898         elif files[file].has_key("new"):
899             new = 1;
900     return (new, byhand);
901
902 def do_pkg(changes_file):
903     Katie.pkg.changes_file = changes_file;
904     Katie.init_vars();
905     Katie.update_vars();
906     Katie.update_subst();
907     files = Katie.pkg.files;
908
909     if not recheck():
910         return;
911
912     (new, byhand) = check_status(files);
913     if new or byhand:
914         if new:
915             do_new();
916         if byhand:
917             do_byhand();
918         (new, byhand) = check_status(files);
919
920     if not new and not byhand:
921         do_accept();
922
923 ################################################################################
924
925 def end():
926     accept_count = Katie.accept_count;
927     accept_bytes = Katie.accept_bytes;
928
929     if accept_count:
930         sets = "set"
931         if accept_count > 1:
932             sets = "sets"
933         sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))));
934         Logger.log(["total",accept_count,accept_bytes]);
935
936     if not Options["No-Action"]:
937         Logger.close();
938
939 ################################################################################
940
941 def main():
942     changes_files = init();
943     if len(changes_files) > 50:
944         sys.stderr.write("Sorting changes...\n");
945     changes_files = sort_changes(changes_files);
946
947     # Kill me now? **FIXME**
948     Cnf["Dinstall::Options::No-Mail"] = "";
949     bcc = "X-Katie: lisa %s" % (lisa_version);
950     if Cnf.has_key("Dinstall::Bcc"):
951         Katie.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
952     else:
953         Katie.Subst["__BCC__"] = bcc;
954
955     for changes_file in changes_files:
956         changes_file = utils.validate_changes_file_arg(changes_file, 0);
957         if not changes_file:
958             continue;
959         print "\n" + changes_file;
960         do_pkg (changes_file);
961
962     end();
963
964 ################################################################################
965
966 if __name__ == '__main__':
967     main()