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