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