3 # Handles NEW and BYHAND packages
4 # Copyright (C) 2001, 2002 James Troup <james@nocrew.org>
5 # $Id: lisa,v 1.17 2002-05-23 12:19:05 troup Exp $
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.
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.
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
21 ################################################################################
23 # 23:12|<aj> I will not hush!
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!!!
38 ################################################################################
40 import copy, errno, os, readline, string, stat, sys, tempfile;
41 import apt_pkg, apt_inst;
42 import db_access, fernanda, katie, logging, utils;
45 lisa_version = "$Revision: 1.17 $";
58 ################################################################################
59 ################################################################################
60 ################################################################################
62 def reject (str, prefix="Rejected: "):
63 global reject_message;
65 reject_message = reject_message + prefix + str + "\n";
68 global reject_message;
69 files = Katie.pkg.files;
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));
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);
94 if Options["No-Action"] or Options["Automatic"]:
97 print "REJECT\n" + reject_message,;
98 prompt = "[R]eject, Skip, Quit ?";
100 while string.find(prompt, answer) == -1:
101 answer = utils.our_raw_input(prompt);
102 m = katie.re_default_answer.match(prompt);
105 answer = string.upper(answer[:1]);
108 Katie.do_reject(0, reject_message);
109 os.unlink(Katie.pkg.changes_file[:-8]+".katie");
118 ################################################################################
120 def determine_new (changes, files):
123 # Build up a list of potentially new things
124 for file in files.keys():
126 # Skip byhand elements
127 if f["type"] == "byhand":
130 priority = f["priority"];
131 section = f["section"];
133 if section == "non-US/main":
136 component = f["component"];
140 if not new.has_key(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"] = [];
148 old_type = new[pkg]["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"];
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));
168 for file in new[pkg]["files"]:
169 if files[file].has_key("new"):
170 del files[file]["new"];
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"]);
181 ################################################################################
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"]);
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:
196 elif b_has_source and not a_has_source:
199 return cmp(a["filename"], b["filename"]);
201 ############################################################
203 def sg_compare (a, b):
206 """Sort by have note, time of oldest upload."""
208 a_note_state = a["note_state"];
209 b_note_state = b["note_state"];
210 if a_note_state < b_note_state:
212 elif a_note_state > b_note_state:
215 # Sort by time of oldest upload
216 return cmp(a["oldest"], b["oldest"]);
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;
227 # Read in all the .changes files
228 for filename in changes_files:
230 Katie.pkg.changes_file = filename;
233 cache[filename] = copy.copy(Katie.pkg.changes);
234 cache[filename]["filename"] = filename;
236 sorted_list.append(filename);
238 # Divide the .changes into per-source groups
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];
252 for d in per_source[source]["list"]:
253 ctime = os.stat(d["filename"])[stat.ST_CTIME];
256 have_note = have_note + (d.has_key("lisa note"));
257 per_source[source]["oldest"] = oldest;
259 per_source[source]["note_state"] = 0; # none
260 elif have_note < len(source_list):
261 per_source[source]["note_state"] = 1; # some
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"]);
272 ################################################################################
274 class Section_Completer:
277 q = projectB.query("SELECT section FROM section");
278 for i in q.getresult():
279 self.sections.append(i[0]);
281 def complete(self, text, state):
285 for word in self.sections:
287 self.matches.append(word);
289 return self.matches[state]
293 ############################################################
295 class Priority_Completer:
297 self.priorities = [];
298 q = projectB.query("SELECT priority FROM priority");
299 for i in q.getresult():
300 self.priorities.append(i[0]);
302 def complete(self, text, state):
306 for word in self.priorities:
308 self.matches.append(word);
310 return self.matches[state]
314 ################################################################################
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"]);
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;
331 ################################################################################
333 def print_new (new, indexed, file=sys.stdout):
337 for pkg in new.keys():
339 section = new[pkg]["section"];
340 priority = new[pkg]["priority"];
341 if new[pkg]["section id"] == -1:
342 section = section + "[!]";
344 if new[pkg]["priority id"] == -1:
345 priority = priority + "[!]";
348 line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section);
350 line = "%-20s %-20s %-20s" % (pkg, priority, section);
351 line = string.strip(line)+'\n';
353 note = Katie.pkg.changes.get("lisa note");
360 ################################################################################
364 if f.has_key("dbtype"):
366 elif f["type"] == "orig.tar.gz" or f["type"] == "tar.gz" or f["type"] == "diff.gz" or f["type"] == "dsc":
369 utils.fubar("invalid type (%s) for new. Dazed, confused and sure as heck not continuing." % (type));
371 # Validate the override type
372 type_id = db_access.get_override_type_id(type);
374 utils.fubar("invalid type (%s) for new. Say wha?" % (type));
378 ################################################################################
380 def index_range (index):
384 return "1-%s" % (index);
386 ################################################################################
387 ################################################################################
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);
394 temp_file = utils.open_file(temp_filename, 'w');
395 print_new (new, 0, temp_file);
397 # Spawn an editor on that file
398 editor = os.environ.get("EDITOR","vi")
399 result = os.system("%s %s" % (editor, temp_filename))
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();
406 os.unlink(temp_filename);
409 line = string.strip(line[:-1]);
412 s = string.split(line);
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));
419 # Strip off any invalid markers, print_new will readd them.
420 if section[-3:] == "[!]":
421 section = section[:-3];
422 if priority[-3:] == "[!]":
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;
430 ################################################################################
432 def edit_index (new, index):
433 priority = new[index]["priority"]
434 section = new[index]["section"]
435 type = new[index]["type"];
438 print string.join([index, priority, section], '\t');
442 prompt = "[B]oth, Priority, Section, Done ? ";
444 prompt = "[S]ection, Done ? ";
445 edit_priority = edit_section = 0;
447 while string.find(prompt, answer) == -1:
448 answer = utils.our_raw_input(prompt);
449 m = katie.re_default_answer.match(prompt)
452 answer = string.upper(answer[:1])
459 edit_priority = edit_section = 1;
465 readline.set_completer(Priorities.complete);
467 while not got_priority:
468 new_priority = string.strip(utils.our_raw_input("New priority: "));
469 if Priorities.priorities.count(new_priority) == 0:
470 print "E: '%s' is not a valid priority, try again." % (new_priority);
473 priority = new_priority;
477 readline.set_completer(Sections.complete);
479 while not got_section:
480 new_section = string.strip(utils.our_raw_input("New section: "));
481 if Sections.sections.count(new_section) == 0:
482 print "E: '%s' is not a valid section, try again." % (new_section);
485 section = new_section;
487 # Reset the readline completer
488 readline.set_completer(None);
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;
497 ################################################################################
499 def edit_overrides (new):
508 new_index[index] = i;
510 prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index));
513 while not got_answer:
514 answer = utils.our_raw_input(prompt);
515 answer = string.upper(answer[:1]);
516 if answer == "E" or answer == "D":
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);
530 edit_index (new, new_index[answer]);
534 ################################################################################
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);
541 temp_file = utils.open_file(temp_filename, 'w');
542 temp_file.write(note);
544 editor = os.environ.get("EDITOR","vi")
547 os.system("%s %s" % (editor, temp_filename))
548 temp_file = utils.open_file(temp_filename);
549 note = string.rstrip(temp_file.read());
552 print utils.prefix_multi_line_string(note," ");
553 prompt = "[D]one, Edit, Abandon, Quit ?"
555 while string.find(prompt, answer) == -1:
556 answer = utils.our_raw_input(prompt);
557 m = katie.re_default_answer.search(prompt);
560 answer = string.upper(answer[:1]);
561 os.unlink(temp_filename);
566 Katie.pkg.changes["lisa note"] = note;
567 Katie.dump_vars(Cnf["Dir::Queue::New"]);
569 ################################################################################
573 less_fd = os.popen("less -", 'w', 0);
574 stdout_fd = sys.stdout;
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"];
583 fernanda.check_deb(file);
585 fernanda.check_dsc(file);
587 sys.stdout = stdout_fd;
589 if errno.errorcode[e.errno] == 'EPIPE':
590 utils.warn("[fernanda] Caught EPIPE; skipping.");
594 except KeyboardInterrupt:
595 utils.warn("[fernanda] Caught C-c; skipping.");
598 ################################################################################
600 ## FIXME: horribly Debian specific
602 def do_bxa_notification():
603 files = Katie.pkg.files;
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 = summary + "\n";
609 summary = summary + "Package: %s\n" % (control.Find("Package"));
610 summary = 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,"");
615 ################################################################################
617 def add_overrides (new):
618 changes = Katie.pkg.changes;
619 files = Katie.pkg.files;
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"];
635 projectB.query("COMMIT WORK");
637 if Cnf.FindB("Dinstall::BXANotify"):
638 do_bxa_notification();
640 ################################################################################
644 files = Katie.pkg.files;
645 changes = Katie.pkg.changes;
647 # Make a copy of distribution we can happily trample on
648 changes["suite"] = copy.copy(changes["distribution"]);
650 # Fix up the list of target suites
651 for suite in changes["suite"].keys():
652 override = Cnf.Find("Suite::%s::OverrideSuite" % (suite));
654 del changes["suite"][suite];
655 changes["suite"][override] = 1;
657 for suite in changes["suite"].keys():
658 suite_id = db_access.get_suite_id(suite);
660 utils.fubar("%s has invalid suite '%s' (possibly overriden). say wha?" % (changes, suite));
662 # The main NEW processing loop
665 # Find out what's new
666 new = determine_new(changes, files);
672 if Options["No-Action"] or Options["Automatic"]:
675 (broken, note) = print_new(new, 0);
678 if not broken and not note:
679 prompt = "Add overrides, ";
681 print "W: [!] marked entries must be fixed before package can be processed.";
683 print "W: note must be removed before package can be processed.";
684 prompt = prompt + "Remove note, ";
686 prompt = prompt + "Edit overrides, Check, Manual reject, Note edit, [S]kip, Quit ?";
688 while string.find(prompt, answer) == -1:
689 answer = utils.our_raw_input(prompt);
690 m = katie.re_default_answer.search(prompt);
693 answer = string.upper(answer[:1])
696 done = add_overrides (new);
700 new = edit_overrides (new);
702 aborted = Katie.do_reject(1, Options["Manual-Reject"]);
704 os.unlink(Katie.pkg.changes_file[:-8]+".katie");
707 edit_note(changes.get("lisa note", ""));
709 confirm = string.lower(utils.our_raw_input("Really clear note (y/N)? "));
711 del changes["lisa note"];
717 ################################################################################
718 ################################################################################
719 ################################################################################
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"""
730 ################################################################################
733 global Cnf, Options, Logger, Katie, projectB, Sections, Priorities;
735 Cnf = utils.get_conf();
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")];
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)] = "";
747 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
748 Options = Cnf.SubTree("Lisa::Options")
753 if Options["Version"]:
754 print "lisa %s" % (lisa_version);
757 Katie = katie.Katie(Cnf);
759 if not Options["No-Action"]:
760 Logger = Katie.Logger = logging.Logger(Cnf, "lisa");
762 projectB = Katie.projectB;
764 Sections = Section_Completer();
765 Priorities = Priority_Completer();
766 readline.parse_and_bind("tab: complete");
768 return changes_files;
770 ################################################################################
775 files = Katie.pkg.files;
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);
788 if Options["No-Action"]:
791 if Options["Automatic"] and not Options["No-Action"]:
793 prompt = "[A]ccept, Manual reject, Skip, Quit ?";
795 prompt = "Manual reject, [S]kip, Quit ?";
797 while string.find(prompt, answer) == -1:
798 answer = utils.our_raw_input(prompt);
799 m = katie.re_default_answer.search(prompt);
802 answer = string.upper(answer[:1]);
809 Katie.do_reject(1, Options["Manual-Reject"]);
810 os.unlink(Katie.pkg.changes_file[:-8]+".katie");
817 ################################################################################
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");
826 def check_status(files):
828 for file in files.keys():
829 if files[file]["type"] == "byhand":
831 elif files[file].has_key("new"):
833 return (new, byhand);
835 def do_pkg(changes_file):
836 Katie.pkg.changes_file = changes_file;
839 Katie.update_subst();
840 files = Katie.pkg.files;
845 (new, byhand) = check_status(files);
851 (new, byhand) = check_status(files);
853 if not new and not byhand:
856 ################################################################################
859 accept_count = Katie.accept_count;
860 accept_bytes = Katie.accept_bytes;
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]);
869 if not Options["No-Action"]:
872 ################################################################################
875 changes_files = init();
876 if len(changes_files) > 50:
877 sys.stderr.write("Sorting changes...\n");
878 changes_files = sort_changes(changes_files);
880 # Kill me now? **FIXME**
881 Cnf["Dinstall::Options::No-Mail"] = "";
882 bcc = "X-Katie: %s" % (lisa_version);
883 if Cnf.has_key("Dinstall::Bcc"):
884 Katie.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"]);
886 Katie.Subst["__BCC__"] = bcc;
888 for changes_file in changes_files:
889 changes_file = utils.validate_changes_file_arg(changes_file, 0);
892 print "\n" + changes_file;
893 do_pkg (changes_file);
897 ################################################################################
899 if __name__ == '__main__':