2 # vim:set et ts=4 sw=4:
4 """ Handles NEW and BYHAND packages
6 @contact: Debian FTP Master <ftpmaster@debian.org>
7 @copyright: 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
8 @copyright: 2009 Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009 Frank Lichtenheld <djpig@debian.org>
10 @license: GNU General Public License version 2 or later
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 ################################################################################
28 # 23:12|<aj> I will not hush!
30 # 23:12|<aj> Where there is injustice in the world, I shall be there!
31 # 23:13|<aj> I shall not be silenced!
32 # 23:13|<aj> The world shall know!
33 # 23:13|<aj> The world *must* know!
34 # 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
35 # 23:13|<aj> yay powerpuff girls!!
36 # 23:13|<aj> buttercup's my favourite, who's yours?
37 # 23:14|<aj> you're backing away from the keyboard right now aren't you?
38 # 23:14|<aj> *AREN'T YOU*?!
39 # 23:15|<aj> I will not be treated like this.
40 # 23:15|<aj> I shall have my revenge.
41 # 23:15|<aj> I SHALL!!!
43 ################################################################################
45 from __future__ import with_statement
56 import apt_pkg, apt_inst
57 import examine_package
59 from daklib.dbconn import *
60 from daklib.queue import *
61 from daklib import daklog
62 from daklib import utils
63 from daklib.regexes import re_no_epoch, re_default_answer, re_isanum
64 from daklib.dak_exceptions import CantOpenError, AlreadyLockedError, CantGetLockError
65 from daklib.summarystats import SummaryStats
66 from daklib.config import Config
75 ################################################################################
76 ################################################################################
77 ################################################################################
79 def recheck(upload, session):
81 if len(upload.rejects) > 0:
83 if Options["No-Action"] or Options["Automatic"] or Options["Trainee"]:
86 print "REJECT\n%s" % '\n'.join(upload.rejects)
87 prompt = "[R]eject, Skip, Quit ?"
89 while prompt.find(answer) == -1:
90 answer = utils.our_raw_input(prompt)
91 m = re_default_answer.match(prompt)
94 answer = answer[:1].upper()
97 upload.do_reject(manual=0, reject_message='\n'.join(upload.rejects))
98 os.unlink(upload.pkg.changes_file[:-8]+".dak")
108 ################################################################################
110 def indiv_sg_compare (a, b):
111 """Sort by source name, source, version, 'have source', and
112 finally by filename."""
113 # Sort by source version
114 q = apt_pkg.VersionCompare(a["version"], b["version"])
118 # Sort by 'have source'
119 a_has_source = a["architecture"].get("source")
120 b_has_source = b["architecture"].get("source")
121 if a_has_source and not b_has_source:
123 elif b_has_source and not a_has_source:
126 return cmp(a["filename"], b["filename"])
128 ############################################################
130 def sg_compare (a, b):
133 """Sort by have note, source already in database and time of oldest upload."""
135 a_note_state = a["note_state"]
136 b_note_state = b["note_state"]
137 if a_note_state < b_note_state:
139 elif a_note_state > b_note_state:
141 # Sort by source already in database (descending)
142 source_in_database = cmp(a["source_in_database"], b["source_in_database"])
143 if source_in_database:
144 return -source_in_database
146 # Sort by time of oldest upload
147 return cmp(a["oldest"], b["oldest"])
149 def sort_changes(changes_files, session):
150 """Sort into source groups, then sort each source group by version,
151 have source, filename. Finally, sort the source groups by have
152 note, time of oldest upload of each source upload."""
153 if len(changes_files) == 1:
158 # Read in all the .changes files
159 for filename in changes_files:
162 u.pkg.load_dot_dak(filename)
164 cache[filename] = copy.copy(u.pkg.changes)
165 cache[filename]["filename"] = filename
167 sorted_list.append(filename)
169 # Divide the .changes into per-source groups
171 for filename in cache.keys():
172 source = cache[filename]["source"]
173 if not per_source.has_key(source):
174 per_source[source] = {}
175 per_source[source]["list"] = []
176 per_source[source]["list"].append(cache[filename])
177 # Determine oldest time and have note status for each source group
178 for source in per_source.keys():
179 q = session.query(DBSource).filter_by(source = source).all()
180 per_source[source]["source_in_database"] = len(q)>0
181 source_list = per_source[source]["list"]
182 first = source_list[0]
183 oldest = os.stat(first["filename"])[stat.ST_MTIME]
185 for d in per_source[source]["list"]:
186 mtime = os.stat(d["filename"])[stat.ST_MTIME]
189 have_note += has_new_comment(d["source"], d["version"], session)
190 per_source[source]["oldest"] = oldest
192 per_source[source]["note_state"] = 0; # none
193 elif have_note < len(source_list):
194 per_source[source]["note_state"] = 1; # some
196 per_source[source]["note_state"] = 2; # all
197 per_source[source]["list"].sort(indiv_sg_compare)
198 per_source_items = per_source.items()
199 per_source_items.sort(sg_compare)
200 for i in per_source_items:
201 for j in i[1]["list"]:
202 sorted_list.append(j["filename"])
205 ################################################################################
207 class Section_Completer:
208 def __init__ (self, session):
211 for s, in session.query(Section.section):
212 self.sections.append(s)
214 def complete(self, text, state):
218 for word in self.sections:
220 self.matches.append(word)
222 return self.matches[state]
226 ############################################################
228 class Priority_Completer:
229 def __init__ (self, session):
232 for p, in session.query(Priority.priority):
233 self.priorities.append(p)
235 def complete(self, text, state):
239 for word in self.priorities:
241 self.matches.append(word)
243 return self.matches[state]
247 ################################################################################
249 def print_new (new, upload, indexed, file=sys.stdout):
253 for pkg in new.keys():
255 section = new[pkg]["section"]
256 priority = new[pkg]["priority"]
257 if new[pkg]["section id"] == -1:
260 if new[pkg]["priority id"] == -1:
264 line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section)
266 line = "%-20s %-20s %-20s" % (pkg, priority, section)
267 line = line.strip()+'\n'
269 notes = get_new_comments(upload.pkg.changes.get("source"))
271 print "\nAuthor: %s\nVersion: %s\nTimestamp: %s\n\n%s" \
272 % (note.author, note.version, note.notedate, note.comment)
274 return broken, len(notes) > 0
276 ################################################################################
278 def index_range (index):
282 return "1-%s" % (index)
284 ################################################################################
285 ################################################################################
287 def edit_new (new, upload):
288 # Write the current data to a temporary file
289 (fd, temp_filename) = utils.temp_filename()
290 temp_file = os.fdopen(fd, 'w')
291 print_new (new, upload, indexed=0, file=temp_file)
293 # Spawn an editor on that file
294 editor = os.environ.get("EDITOR","vi")
295 result = os.system("%s %s" % (editor, temp_filename))
297 utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
298 # Read the edited data back in
299 temp_file = utils.open_file(temp_filename)
300 lines = temp_file.readlines()
302 os.unlink(temp_filename)
309 # Pad the list if necessary
310 s[len(s):3] = [None] * (3-len(s))
311 (pkg, priority, section) = s[:3]
312 if not new.has_key(pkg):
313 utils.warn("Ignoring unknown package '%s'" % (pkg))
315 # Strip off any invalid markers, print_new will readd them.
316 if section.endswith("[!]"):
317 section = section[:-3]
318 if priority.endswith("[!]"):
319 priority = priority[:-3]
320 for f in new[pkg]["files"]:
321 upload.pkg.files[f]["section"] = section
322 upload.pkg.files[f]["priority"] = priority
323 new[pkg]["section"] = section
324 new[pkg]["priority"] = priority
326 ################################################################################
328 def edit_index (new, upload, index):
329 priority = new[index]["priority"]
330 section = new[index]["section"]
331 ftype = new[index]["type"]
334 print "\t".join([index, priority, section])
338 prompt = "[B]oth, Priority, Section, Done ? "
340 prompt = "[S]ection, Done ? "
341 edit_priority = edit_section = 0
343 while prompt.find(answer) == -1:
344 answer = utils.our_raw_input(prompt)
345 m = re_default_answer.match(prompt)
348 answer = answer[:1].upper()
355 edit_priority = edit_section = 1
361 readline.set_completer(Priorities.complete)
363 while not got_priority:
364 new_priority = utils.our_raw_input("New priority: ").strip()
365 if new_priority not in Priorities.priorities:
366 print "E: '%s' is not a valid priority, try again." % (new_priority)
369 priority = new_priority
373 readline.set_completer(Sections.complete)
375 while not got_section:
376 new_section = utils.our_raw_input("New section: ").strip()
377 if new_section not in Sections.sections:
378 print "E: '%s' is not a valid section, try again." % (new_section)
381 section = new_section
383 # Reset the readline completer
384 readline.set_completer(None)
386 for f in new[index]["files"]:
387 upload.pkg.files[f]["section"] = section
388 upload.pkg.files[f]["priority"] = priority
389 new[index]["priority"] = priority
390 new[index]["section"] = section
393 ################################################################################
395 def edit_overrides (new, upload, session):
399 print_new (new, upload, indexed=1)
406 prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index))
409 while not got_answer:
410 answer = utils.our_raw_input(prompt)
411 if not answer.isdigit():
412 answer = answer[:1].upper()
413 if answer == "E" or answer == "D":
415 elif re_isanum.match (answer):
417 if (answer < 1) or (answer > index):
418 print "%s is not a valid index (%s). Please retry." % (answer, index_range(index))
423 edit_new(new, upload)
427 edit_index (new, upload, new_index[answer])
431 ################################################################################
433 def edit_note(note, upload, session):
434 # Write the current data to a temporary file
435 (fd, temp_filename) = utils.temp_filename()
436 editor = os.environ.get("EDITOR","vi")
439 os.system("%s %s" % (editor, temp_filename))
440 temp_file = utils.open_file(temp_filename)
441 newnote = temp_file.read().rstrip()
444 print utils.prefix_multi_line_string(newnote," ")
445 prompt = "[D]one, Edit, Abandon, Quit ?"
447 while prompt.find(answer) == -1:
448 answer = utils.our_raw_input(prompt)
449 m = re_default_answer.search(prompt)
452 answer = answer[:1].upper()
453 os.unlink(temp_filename)
460 comment = NewComment()
461 comment.package = upload.pkg.changes["source"]
462 comment.version = upload.pkg.changes["version"]
463 comment.comment = newnote
464 comment.author = utils.whoami()
465 comment.trainee = bool(Options["Trainee"])
469 ################################################################################
471 def check_pkg (upload):
473 less_fd = os.popen("less -R -", 'w', 0)
474 stdout_fd = sys.stdout
477 changes = utils.parse_changes (upload.pkg.changes_file)
478 examine_package.display_changes(changes['distribution'], upload.pkg.changes_file)
479 files = upload.pkg.files
480 for f in files.keys():
481 if files[f].has_key("new"):
482 ftype = files[f]["type"]
484 examine_package.check_deb(changes['distribution'], f)
486 examine_package.check_dsc(changes['distribution'], f)
488 examine_package.output_package_relations()
489 sys.stdout = stdout_fd
491 if e.errno == errno.EPIPE:
492 utils.warn("[examine_package] Caught EPIPE; skipping.")
496 except KeyboardInterrupt:
497 utils.warn("[examine_package] Caught C-c; skipping.")
500 ################################################################################
502 ## FIXME: horribly Debian specific
504 def do_bxa_notification(upload):
505 files = upload.pkg.files
507 for f in files.keys():
508 if files[f]["type"] == "deb":
509 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(f)))
511 summary += "Package: %s\n" % (control.Find("Package"))
512 summary += "Description: %s\n" % (control.Find("Description"))
513 upload.Subst["__BINARY_DESCRIPTIONS__"] = summary
514 bxa_mail = utils.TemplateSubst(upload.Subst,Config()["Dir::Templates"]+"/process-new.bxa_notification")
515 utils.send_mail(bxa_mail)
517 ################################################################################
519 def add_overrides (new, upload, session):
520 changes = upload.pkg.changes
521 files = upload.pkg.files
522 srcpkg = changes.get("source")
524 for suite in changes["suite"].keys():
525 suite_id = get_suite(suite).suite_id
526 for pkg in new.keys():
527 component_id = get_component(new[pkg]["component"]).component_id
528 type_id = get_override_type(new[pkg]["type"]).overridetype_id
529 priority_id = new[pkg]["priority id"]
530 section_id = new[pkg]["section id"]
531 Logger.log(["%s overrides" % (srcpkg), suite, new[pkg]["component"], new[pkg]["type"], new[pkg]["priority"], new[pkg]["section"]])
532 session.execute("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (:sid, :cid, :tid, :pkg, :pid, :sectid, '')",
533 { 'sid': suite_id, 'cid': component_id, 'tid':type_id, 'pkg': pkg, 'pid': priority_id, 'sectid': section_id})
534 for f in new[pkg]["files"]:
535 if files[f].has_key("new"):
541 if Config().FindB("Dinstall::BXANotify"):
542 do_bxa_notification(upload)
544 ################################################################################
546 def prod_maintainer (note, upload):
548 # Here we prepare an editor and get them ready to prod...
549 (fd, temp_filename) = utils.temp_filename()
550 temp_file = os.fdopen(fd, 'w')
553 temp_file.write(line)
555 editor = os.environ.get("EDITOR","vi")
558 os.system("%s %s" % (editor, temp_filename))
559 temp_fh = utils.open_file(temp_filename)
560 prod_message = "".join(temp_fh.readlines())
562 print "Prod message:"
563 print utils.prefix_multi_line_string(prod_message," ",include_blank_lines=1)
564 prompt = "[P]rod, Edit, Abandon, Quit ?"
566 while prompt.find(answer) == -1:
567 answer = utils.our_raw_input(prompt)
568 m = re_default_answer.search(prompt)
571 answer = answer[:1].upper()
572 os.unlink(temp_filename)
578 # Otherwise, do the proding...
579 user_email_address = utils.whoami() + " <%s>" % (
580 cnf["Dinstall::MyAdminAddress"])
584 Subst["__FROM_ADDRESS__"] = user_email_address
585 Subst["__PROD_MESSAGE__"] = prod_message
586 Subst["__CC__"] = "Cc: " + cnf["Dinstall::MyEmailAddress"]
588 prod_mail_message = utils.TemplateSubst(
589 Subst,cnf["Dir::Templates"]+"/process-new.prod")
591 # Send the prod mail if appropriate
592 if not cnf["Dinstall::Options::No-Mail"]:
593 utils.send_mail(prod_mail_message)
595 print "Sent proding message"
597 ################################################################################
599 def do_new(upload, session):
601 files = upload.pkg.files
602 changes = upload.pkg.changes
605 # Make a copy of distribution we can happily trample on
606 changes["suite"] = copy.copy(changes["distribution"])
608 # Fix up the list of target suites
609 for suite in changes["suite"].keys():
610 override = cnf.Find("Suite::%s::OverrideSuite" % (suite))
612 (olderr, newerr) = (get_suite(suite, session) == None,
613 get_suite(override, session) == None)
615 (oinv, newinv) = ("", "")
616 if olderr: oinv = "invalid "
617 if newerr: ninv = "invalid "
618 print "warning: overriding %ssuite %s to %ssuite %s" % (
619 oinv, suite, ninv, override)
620 del changes["suite"][suite]
621 changes["suite"][override] = 1
623 for suite in changes["suite"].keys():
624 if get_suite(suite, session) is None:
625 utils.fubar("%s has invalid suite '%s' (possibly overriden). say wha?" % (changes, suite))
627 # The main NEW processing loop
630 # Find out what's new
631 new = determine_new(changes, files)
637 if Options["No-Action"] or Options["Automatic"]:
640 (broken, note) = print_new(new, upload, indexed=0)
643 if not broken and not note:
644 prompt = "Add overrides, "
646 print "W: [!] marked entries must be fixed before package can be processed."
648 print "W: note must be removed before package can be processed."
649 prompt += "RemOve all notes, Remove note, "
651 prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
653 while prompt.find(answer) == -1:
654 answer = utils.our_raw_input(prompt)
655 m = re_default_answer.search(prompt)
658 answer = answer[:1].upper()
660 if answer in ( 'A', 'E', 'M', 'O', 'R' ) and Options["Trainee"]:
661 utils.warn("Trainees can't do that")
664 if answer == 'A' and not Options["Trainee"]:
667 done = add_overrides (new, upload, session)
668 Logger.log(["NEW ACCEPT: %s" % (upload.pkg.changes_file)])
669 except CantGetLockError:
670 print "Hello? Operator! Give me the number for 911!"
671 print "Dinstall in the locked area, cant process packages, come back later"
674 elif answer == 'E' and not Options["Trainee"]:
675 new = edit_overrides (new, upload, session)
676 elif answer == 'M' and not Options["Trainee"]:
677 upload.pkg.remove_known_changes()
678 aborted = upload.do_reject(manual=1,
679 reject_message=Options["Manual-Reject"],
680 note=get_new_comments(changes.get("source", ""), session=session))
682 Logger.log(["NEW REJECT: %s" % (upload.pkg.changes_file)])
683 os.unlink(upload.pkg.changes_file[:-8]+".dak")
686 edit_note(get_new_comments(changes.get("source", ""), session=session),
688 elif answer == 'P' and not Options["Trainee"]:
689 prod_maintainer(get_new_comments(changes.get("source", ""), session=session),
691 Logger.log(["NEW PROD: %s" % (upload.pkg.changes_file)])
692 elif answer == 'R' and not Options["Trainee"]:
693 confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
695 for c in get_new_comments(changes.get("source", ""), changes.get("version", ""), session=session):
698 elif answer == 'O' and not Options["Trainee"]:
699 confirm = utils.our_raw_input("Really clear all notes (y/N)? ").lower()
701 for c in get_new_comments(changes.get("source", ""), session=session):
711 ################################################################################
712 ################################################################################
713 ################################################################################
715 def usage (exit_code=0):
716 print """Usage: dak process-new [OPTION]... [CHANGES]...
717 -a, --automatic automatic run
718 -h, --help show this help and exit.
719 -m, --manual-reject=MSG manual reject with `msg'
720 -n, --no-action don't do anything
721 -t, --trainee FTP Trainee mode
722 -V, --version display the version number and exit"""
725 ################################################################################
727 def do_byhand(upload, session):
730 files = upload.pkg.files
734 for f in files.keys():
735 if files[f]["type"] == "byhand":
736 if os.path.exists(f):
737 print "W: %s still present; please process byhand components and try again." % (f)
743 if Options["No-Action"]:
746 if Options["Automatic"] and not Options["No-Action"]:
748 prompt = "[A]ccept, Manual reject, Skip, Quit ?"
750 prompt = "Manual reject, [S]kip, Quit ?"
752 while prompt.find(answer) == -1:
753 answer = utils.our_raw_input(prompt)
754 m = re_default_answer.search(prompt)
757 answer = answer[:1].upper()
765 Logger.log(["BYHAND ACCEPT: %s" % (upload.pkg.changes_file)])
766 except CantGetLockError:
767 print "Hello? Operator! Give me the number for 911!"
768 print "Dinstall in the locked area, cant process packages, come back later"
770 Logger.log(["BYHAND REJECT: %s" % (upload.pkg.changes_file)])
771 upload.do_reject(manual=1, reject_message=Options["Manual-Reject"])
772 os.unlink(upload.pkg.changes_file[:-8]+".dak")
780 ################################################################################
782 def check_daily_lock():
784 Raises CantGetLockError if the dinstall daily.lock exists.
789 os.open(cnf["Process-New::DinstallLockFile"],
790 os.O_RDONLY | os.O_CREAT | os.O_EXCL)
792 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
793 raise CantGetLockError
795 os.unlink(cnf["Process-New::DinstallLockFile"])
798 @contextlib.contextmanager
799 def lock_package(package):
801 Lock C{package} so that noone else jumps in processing it.
803 @type package: string
804 @param package: source package name to lock
807 path = os.path.join(Config()["Process-New::LockDir"], package)
809 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY)
811 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
812 user = pwd.getpwuid(os.stat(path)[stat.ST_UID])[4].split(',')[0].replace('.', '')
813 raise AlreadyLockedError, user
821 if Options["No-Action"]:
823 (summary, short_summary) = upload.build_summaries()
824 upload.accept(summary, short_summary, targetqueue)
825 os.unlink(upload.pkg.changes_file[:-8]+".dak")
827 def do_accept(upload):
830 if not Options["No-Action"]:
831 (summary, short_summary) = upload.build_summaries()
833 if cnf.FindB("Dinstall::SecurityQueueHandling"):
834 upload.dump_vars(cnf["Dir::Queue::Embargoed"])
835 upload.move_to_queue(get_policy_queue('embargoed'))
836 upload.queue_build("embargoed", cnf["Dir::Queue::Embargoed"])
837 # Check for override disparities
838 upload.Subst["__SUMMARY__"] = summary
840 # Just a normal upload, accept it...
843 def do_pkg(changes_file, session):
845 u.pkg.load_dot_dak(changes_file)
849 bcc = "X-DAK: dak process-new"
850 if cnf.has_key("Dinstall::Bcc"):
851 u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
853 u.Subst["__BCC__"] = bcc
858 with lock_package(u.pkg.changes["source"]):
859 if not recheck(u, session):
862 (new, byhand) = check_status(files)
867 do_byhand(u, session)
868 (new, byhand) = check_status(files)
870 if not new and not byhand:
874 except CantGetLockError:
875 print "Hello? Operator! Give me the number for 911!"
876 print "Dinstall in the locked area, cant process packages, come back later"
877 except AlreadyLockedError, e:
878 print "Seems to be locked by %s already, skipping..." % (e)
880 ################################################################################
883 accept_count = SummaryStats().accept_count
884 accept_bytes = SummaryStats().accept_bytes
890 sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
891 Logger.log(["total",accept_count,accept_bytes])
893 if not Options["No-Action"] and not Options["Trainee"]:
896 ################################################################################
899 global Options, Logger, Sections, Priorities
901 print "NO NEW PROCESSING CURRENTLY AVAILABLE"
902 print "(Go and do something more interesting)"
906 session = DBConn().session()
908 Arguments = [('a',"automatic","Process-New::Options::Automatic"),
909 ('h',"help","Process-New::Options::Help"),
910 ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
911 ('t',"trainee","Process-New::Options::Trainee"),
912 ('n',"no-action","Process-New::Options::No-Action")]
914 for i in ["automatic", "help", "manual-reject", "no-action", "version", "trainee"]:
915 if not cnf.has_key("Process-New::Options::%s" % (i)):
916 cnf["Process-New::Options::%s" % (i)] = ""
918 changes_files = apt_pkg.ParseCommandLine(cnf.Cnf,Arguments,sys.argv)
919 if len(changes_files) == 0:
920 changes_files = utils.get_changes_files(cnf["Dir::Queue::New"])
922 Options = cnf.SubTree("Process-New::Options")
927 if not Options["No-Action"]:
929 Logger = daklog.Logger(cnf, "process-new")
930 except CantOpenError, e:
931 Options["Trainee"] = "True"
933 Sections = Section_Completer(session)
934 Priorities = Priority_Completer(session)
935 readline.parse_and_bind("tab: complete")
937 if len(changes_files) > 1:
938 sys.stderr.write("Sorting changes...\n")
939 changes_files = sort_changes(changes_files, session)
941 # Kill me now? **FIXME**
942 cnf["Dinstall::Options::No-Mail"] = ""
944 for changes_file in changes_files:
945 changes_file = utils.validate_changes_file_arg(changes_file, 0)
948 print "\n" + changes_file
950 do_pkg (changes_file, session)
954 ################################################################################
956 if __name__ == '__main__':