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 $
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 ################################################################################
43 # We don't check error codes very thoroughly; the old 'trust jennifer'
44 # chess nut... db_access calls in particular
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 (?)
54 ################################################################################
56 import copy, errno, os, readline, string, stat, sys, tempfile;
57 import apt_pkg, apt_inst;
58 import db_access, fernanda, katie, logging, utils;
61 lisa_version = "$Revision: 1.15 $";
74 ################################################################################
75 ################################################################################
76 ################################################################################
78 def reject (str, prefix="Rejected: "):
79 global reject_message;
81 reject_message = reject_message + prefix + str + "\n";
84 global reject_message;
85 files = Katie.pkg.files;
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));
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);
110 if Options["No-Action"] or Options["Automatic"]:
113 print "REJECT\n" + reject_message,;
114 prompt = "[R]eject, Skip, Quit ?";
116 while string.find(prompt, answer) == -1:
117 answer = utils.our_raw_input(prompt);
118 m = katie.re_default_answer.match(prompt);
121 answer = string.upper(answer[:1]);
124 Katie.do_reject(0, reject_message);
132 ################################################################################
134 def determine_new (changes, files):
137 # Build up a list of potentially new things
138 for file in files.keys():
140 # Skip byhand elements
141 if f["type"] == "byhand":
144 priority = f["priority"];
145 section = f["section"];
147 if section == "non-US/main":
150 component = f["component"];
154 if not new.has_key(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"] = [];
162 old_type = new[pkg]["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"];
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));
182 for file in new[pkg]["files"]:
183 if files[file].has_key("new"):
184 del files[file]["new"];
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"]);
195 ################################################################################
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"]);
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:
210 elif b_has_source and not a_has_source:
213 return cmp(a["filename"], b["filename"]);
215 ############################################################
217 def sg_compare (a, b):
220 """Sort by have note, time of oldest upload."""
222 a_note_state = a["note_state"];
223 b_note_state = b["note_state"];
224 if a_note_state != b_note_state:
227 # Sort by time of oldest upload
228 return cmp(a["oldest"], b["oldest"]);
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;
239 # Read in all the .changes files
240 for filename in changes_files:
242 Katie.pkg.changes_file = filename;
245 cache[filename] = copy.copy(Katie.pkg.changes);
246 cache[filename]["filename"] = filename;
248 sorted_list.append(filename);
250 # Divide the .changes into per-source groups
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];
264 for d in per_source[source]["list"]:
265 ctime = os.stat(d["filename"])[stat.ST_CTIME];
268 have_note = have_note + (d.has_key("lisa note"));
269 per_source[source]["oldest"] = oldest;
271 per_source[source]["note_state"] = 0; # none
272 elif have_note < len(source_list):
273 per_source[source]["note_state"] = 1; # some
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"]);
284 ################################################################################
286 class Section_Completer:
289 q = projectB.query("SELECT section FROM section");
290 for i in q.getresult():
291 self.sections.append(i[0]);
293 def complete(self, text, state):
297 for word in self.sections:
299 self.matches.append(word);
301 return self.matches[state]
305 ############################################################
307 class Priority_Completer:
309 self.priorities = [];
310 q = projectB.query("SELECT priority FROM priority");
311 for i in q.getresult():
312 self.priorities.append(i[0]);
314 def complete(self, text, state):
318 for word in self.priorities:
320 self.matches.append(word);
322 return self.matches[state]
326 ################################################################################
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"]);
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;
343 ################################################################################
345 def print_new (new, indexed, file=sys.stdout):
349 for pkg in new.keys():
351 section = new[pkg]["section"];
352 priority = new[pkg]["priority"];
353 if new[pkg]["section id"] == -1:
354 section = section + "[!]";
356 if new[pkg]["priority id"] == -1:
357 priority = priority + "[!]";
360 line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section);
362 line = "%-20s %-20s %-20s" % (pkg, priority, section);
363 line = string.strip(line)+'\n';
365 note = Katie.pkg.changes.get("lisa note");
372 ################################################################################
376 if f.has_key("dbtype"):
378 elif f["type"] == "orig.tar.gz" or f["type"] == "tar.gz" or f["type"] == "diff.gz" or f["type"] == "dsc":
381 utils.fubar("invalid type (%s) for new. Dazed, confused and sure as heck not continuing." % (type));
383 # Validate the override type
384 type_id = db_access.get_override_type_id(type);
386 utils.fubar("invalid type (%s) for new. Say wha?" % (type));
390 ################################################################################
392 def index_range (index):
396 return "1-%s" % (index);
398 ################################################################################
399 ################################################################################
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);
406 temp_file = utils.open_file(temp_filename, 'w');
407 print_new (new, 0, temp_file);
409 # Spawn an editor on that file
410 editor = os.environ.get("EDITOR","vi")
411 result = os.system("%s %s" % (editor, temp_filename))
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();
418 os.unlink(temp_filename);
421 line = string.strip(line[:-1]);
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));
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;
442 ################################################################################
444 def edit_index (new, index):
445 priority = new[index]["priority"]
446 section = new[index]["section"]
447 type = new[index]["type"];
450 print string.join([index, priority, section], '\t');
454 prompt = "[B]oth, Priority, Section, Done ? ";
456 prompt = "[S]ection, Done ? ";
457 edit_priority = edit_section = 0;
459 while string.find(prompt, answer) == -1:
460 answer = utils.our_raw_input(prompt);
461 m = katie.re_default_answer.match(prompt)
464 answer = string.upper(answer[:1])
471 edit_priority = edit_section = 1;
477 readline.set_completer(Priorities.complete);
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);
485 priority = new_priority;
489 readline.set_completer(Sections.complete);
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);
497 section = new_section;
499 # Reset the readline completer
500 readline.set_completer(None);
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;
509 ################################################################################
511 def edit_overrides (new):
520 new_index[index] = i;
522 prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index));
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":
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);
542 edit_index (new, new_index[answer]);
546 ################################################################################
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);
553 temp_file = utils.open_file(temp_filename, 'w');
554 temp_file.write(note);
556 editor = os.environ.get("EDITOR","vi")
559 os.system("%s %s" % (editor, temp_filename))
560 temp_file = utils.open_file(temp_filename);
561 note = string.rstrip(temp_file.read());
564 print utils.prefix_multi_line_string(note," ");
565 prompt = "[D]one, Edit, Abandon, Quit ?"
567 while string.find(prompt, answer) == -1:
568 answer = utils.our_raw_input(prompt);
569 m = katie.re_default_answer.search(prompt);
572 answer = string.upper(answer[:1]);
573 os.unlink(temp_filename);
578 Katie.pkg.changes["lisa note"] = note;
579 Katie.dump_vars(Cnf["Dir::Queue::New"]);
581 ################################################################################
585 less_fd = os.popen("less -", 'w', 0);
586 stdout_fd = sys.stdout;
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"];
595 fernanda.check_deb(file);
597 fernanda.check_dsc(file);
599 sys.stdout = stdout_fd;
601 if errno.errorcode[e.errno] == 'EPIPE':
602 utils.warn("[fernanda] Caught EPIPE; skipping.");
606 except KeyboardInterrupt:
607 utils.warn("[fernanda] Caught C-c; skipping.");
610 ################################################################################
612 ## FIXME: horribly Debian specific
614 def do_bxa_notification():
615 files = Katie.pkg.files;
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,"");
627 ################################################################################
629 def add_overrides (new):
630 changes = Katie.pkg.changes;
631 files = Katie.pkg.files;
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"];
647 projectB.query("COMMIT WORK");
649 if Cnf.FindB("Dinstall::BXANotify"):
650 do_bxa_notification();
652 ################################################################################
656 files = Katie.pkg.files;
657 changes = Katie.pkg.changes;
659 # Make a copy of distribution we can happily trample on
660 changes["suite"] = copy.copy(changes["distribution"]);
662 # Fix up the list of target suites
663 for suite in changes["suite"].keys():
664 override = Cnf.Find("Suite::%s::OverrideSuite" % (suite));
666 del changes["suite"][suite];
667 changes["suite"][override] = 1;
669 for suite in changes["suite"].keys():
670 suite_id = db_access.get_suite_id(suite);
672 utils.fubar("%s has invalid suite '%s' (possibly overriden). say wha?" % (changes, suite));
674 # The main NEW processing loop
677 # Find out what's new
678 new = determine_new(changes, files);
684 if Options["No-Action"] or Options["Automatic"]:
687 (broken, note) = print_new(new, 0);
690 if not broken and not note:
691 prompt = "Add overrides, ";
693 print "W: [!] marked entries must be fixed before package can be processed.";
695 print "W: note must be removed before package can be processed.";
696 prompt = prompt + "Remove note, ";
698 prompt = prompt + "Edit overrides, Check, Manual reject, Note edit, [S]kip, Quit ?";
700 while string.find(prompt, answer) == -1:
701 answer = utils.our_raw_input(prompt);
702 m = katie.re_default_answer.search(prompt);
705 answer = string.upper(answer[:1])
708 done = add_overrides (new);
712 new = edit_overrides (new);
714 aborted = Katie.do_reject(1, Options["Manual-Reject"]);
716 os.unlink(Katie.pkg.changes_file[:-8]+".katie");
719 edit_note(changes.get("lisa note", ""));
721 confirm = string.lower(utils.our_raw_input("Really clear note (y/N)? "));
723 del changes["lisa note"];
729 ################################################################################
730 ################################################################################
731 ################################################################################
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"""
742 ################################################################################
745 global Cnf, Options, Logger, Katie, projectB, Sections, Priorities;
747 Cnf = utils.get_conf();
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")];
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)] = "";
759 changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
760 Options = Cnf.SubTree("Lisa::Options")
765 if Options["Version"]:
766 print "lisa %s" % (lisa_version);
769 Katie = katie.Katie(Cnf);
771 if not Options["No-Action"]:
772 Logger = Katie.Logger = logging.Logger(Cnf, "lisa");
774 projectB = Katie.projectB;
776 Sections = Section_Completer();
777 Priorities = Priority_Completer();
778 readline.parse_and_bind("tab: complete");
780 return changes_files;
782 ################################################################################
787 files = Katie.pkg.files;
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);
800 if Options["No-Action"]:
803 if Options["Automatic"] and not Options["No-Action"]:
805 prompt = "[A]ccept, Manual reject, Skip, Quit ?";
807 prompt = "Manual reject, [S]kip, Quit ?";
809 while string.find(prompt, answer) == -1:
810 answer = utils.our_raw_input(prompt);
811 m = katie.re_default_answer.search(prompt);
814 answer = string.upper(answer[:1]);
821 Katie.do_reject(1, Options["Manual-Reject"]);
822 os.unlink(Katie.pkg.changes_file[:-8]+".katie");
829 ################################################################################
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");
838 def check_status(files):
840 for file in files.keys():
841 if files[file]["type"] == "byhand":
843 elif files[file].has_key("new"):
845 return (new, byhand);
847 def do_pkg(changes_file):
848 Katie.pkg.changes_file = changes_file;
851 Katie.update_subst();
852 files = Katie.pkg.files;
857 (new, byhand) = check_status(files);
863 (new, byhand) = check_status(files);
865 if not new and not byhand:
868 ################################################################################
871 accept_count = Katie.accept_count;
872 accept_bytes = Katie.accept_bytes;
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]);
881 if not Options["No-Action"]:
884 ################################################################################
887 changes_files = init();
888 if len(changes_files) > 50:
889 sys.stderr.write("Sorting changes...\n");
890 changes_files = sort_changes(changes_files);
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"]);
898 Katie.Subst["__BCC__"] = bcc;
900 for changes_file in changes_files:
901 changes_file = utils.validate_changes_file_arg(changes_file, 0);
904 print "\n" + changes_file;
905 do_pkg (changes_file);
909 ################################################################################
911 if __name__ == '__main__':