]> git.decadent.org.uk Git - dak.git/blob - dak/process_new.py
Merge remote-tracking branch 'ansgar/pu/multiarchive-2'
[dak.git] / dak / process_new.py
1 #!/usr/bin/env python
2 # vim:set et ts=4 sw=4:
3
4 """ Handles NEW and BYHAND packages
5
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
11 """
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.
16
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.
21
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
25
26 ################################################################################
27
28 # 23:12|<aj> I will not hush!
29 # 23:12|<elmo> :>
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!!!
42
43 ################################################################################
44
45 import copy
46 import errno
47 import os
48 import readline
49 import stat
50 import sys
51 import time
52 import contextlib
53 import pwd
54 import apt_pkg, apt_inst
55 import examine_package
56 import subprocess
57
58 from daklib.dbconn import *
59 from daklib.queue import *
60 from daklib import daklog
61 from daklib import utils
62 from daklib.regexes import re_no_epoch, re_default_answer, re_isanum, re_package
63 from daklib.dak_exceptions import CantOpenError, AlreadyLockedError, CantGetLockError
64 from daklib.summarystats import SummaryStats
65 from daklib.config import Config
66 from daklib.policy import UploadCopy, PolicyQueueUploadHandler
67
68 # Globals
69 Options = None
70 Logger = None
71
72 Priorities = None
73 Sections = None
74
75 ################################################################################
76 ################################################################################
77 ################################################################################
78
79 class Section_Completer:
80     def __init__ (self, session):
81         self.sections = []
82         self.matches = []
83         for s, in session.query(Section.section):
84             self.sections.append(s)
85
86     def complete(self, text, state):
87         if state == 0:
88             self.matches = []
89             n = len(text)
90             for word in self.sections:
91                 if word[:n] == text:
92                     self.matches.append(word)
93         try:
94             return self.matches[state]
95         except IndexError:
96             return None
97
98 ############################################################
99
100 class Priority_Completer:
101     def __init__ (self, session):
102         self.priorities = []
103         self.matches = []
104         for p, in session.query(Priority.priority):
105             self.priorities.append(p)
106
107     def complete(self, text, state):
108         if state == 0:
109             self.matches = []
110             n = len(text)
111             for word in self.priorities:
112                 if word[:n] == text:
113                     self.matches.append(word)
114         try:
115             return self.matches[state]
116         except IndexError:
117             return None
118
119 ################################################################################
120
121 def print_new (upload, missing, indexed, session, file=sys.stdout):
122     check_valid(missing, session)
123     index = 0
124     for m in missing:
125         index += 1
126         if m['type'] != 'deb':
127             package = '{0}:{1}'.format(m['type'], m['package'])
128         else:
129             package = m['package']
130         section = m['section']
131         priority = m['priority']
132         if indexed:
133             line = "(%s): %-20s %-20s %-20s" % (index, package, priority, section)
134         else:
135             line = "%-20s %-20s %-20s" % (package, priority, section)
136         line = line.strip()
137         if not m['valid']:
138             line = line + ' [!]'
139         print >>file, line
140     notes = get_new_comments(upload.changes.source)
141     for note in notes:
142         print "\nAuthor: %s\nVersion: %s\nTimestamp: %s\n\n%s" \
143               % (note.author, note.version, note.notedate, note.comment)
144         print "-" * 72
145     return len(notes) > 0
146
147 ################################################################################
148
149 def index_range (index):
150     if index == 1:
151         return "1"
152     else:
153         return "1-%s" % (index)
154
155 ################################################################################
156 ################################################################################
157
158 def edit_new (overrides, upload, session):
159     # Write the current data to a temporary file
160     (fd, temp_filename) = utils.temp_filename()
161     temp_file = os.fdopen(fd, 'w')
162     print_new (upload, overrides, indexed=0, session=session, file=temp_file)
163     temp_file.close()
164     # Spawn an editor on that file
165     editor = os.environ.get("EDITOR","vi")
166     result = os.system("%s %s" % (editor, temp_filename))
167     if result != 0:
168         utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
169     # Read the edited data back in
170     temp_file = utils.open_file(temp_filename)
171     lines = temp_file.readlines()
172     temp_file.close()
173     os.unlink(temp_filename)
174
175     overrides_map = dict([ ((o['type'], o['package']), o) for o in overrides ])
176     new_overrides = []
177     # Parse the new data
178     for line in lines:
179         line = line.strip()
180         if line == "" or line[0] == '#':
181             continue
182         s = line.split()
183         # Pad the list if necessary
184         s[len(s):3] = [None] * (3-len(s))
185         (pkg, priority, section) = s[:3]
186         if pkg.find(':') != -1:
187             type, pkg = pkg.split(':', 1)
188         else:
189             type = 'deb'
190         if (type, pkg) not in overrides_map:
191             utils.warn("Ignoring unknown package '%s'" % (pkg))
192         else:
193             if section.find('/') != -1:
194                 component = section.split('/', 1)[0]
195             else:
196                 component = 'main'
197             new_overrides.append(dict(
198                     package=pkg,
199                     type=type,
200                     section=section,
201                     component=component,
202                     priority=priority,
203                     ))
204     return new_overrides
205
206 ################################################################################
207
208 def edit_index (new, upload, index):
209     package = new[index]['package']
210     priority = new[index]["priority"]
211     section = new[index]["section"]
212     ftype = new[index]["type"]
213     done = 0
214     while not done:
215         print "\t".join([package, priority, section])
216
217         answer = "XXX"
218         if ftype != "dsc":
219             prompt = "[B]oth, Priority, Section, Done ? "
220         else:
221             prompt = "[S]ection, Done ? "
222         edit_priority = edit_section = 0
223
224         while prompt.find(answer) == -1:
225             answer = utils.our_raw_input(prompt)
226             m = re_default_answer.match(prompt)
227             if answer == "":
228                 answer = m.group(1)
229             answer = answer[:1].upper()
230
231         if answer == 'P':
232             edit_priority = 1
233         elif answer == 'S':
234             edit_section = 1
235         elif answer == 'B':
236             edit_priority = edit_section = 1
237         elif answer == 'D':
238             done = 1
239
240         # Edit the priority
241         if edit_priority:
242             readline.set_completer(Priorities.complete)
243             got_priority = 0
244             while not got_priority:
245                 new_priority = utils.our_raw_input("New priority: ").strip()
246                 if new_priority not in Priorities.priorities:
247                     print "E: '%s' is not a valid priority, try again." % (new_priority)
248                 else:
249                     got_priority = 1
250                     priority = new_priority
251
252         # Edit the section
253         if edit_section:
254             readline.set_completer(Sections.complete)
255             got_section = 0
256             while not got_section:
257                 new_section = utils.our_raw_input("New section: ").strip()
258                 if new_section not in Sections.sections:
259                     print "E: '%s' is not a valid section, try again." % (new_section)
260                 else:
261                     got_section = 1
262                     section = new_section
263
264         # Reset the readline completer
265         readline.set_completer(None)
266
267     new[index]["priority"] = priority
268     new[index]["section"] = section
269     if section.find('/') != -1:
270         component = section.split('/', 1)[0]
271     else:
272         component = 'main'
273     new[index]['component'] = component
274
275     return new
276
277 ################################################################################
278
279 def edit_overrides (new, upload, session):
280     print
281     done = 0
282     while not done:
283         print_new (upload, new, indexed=1, session=session)
284         prompt = "edit override <n>, Editor, Done ? "
285
286         got_answer = 0
287         while not got_answer:
288             answer = utils.our_raw_input(prompt)
289             if not answer.isdigit():
290                 answer = answer[:1].upper()
291             if answer == "E" or answer == "D":
292                 got_answer = 1
293             elif re_isanum.match (answer):
294                 answer = int(answer)
295                 if answer < 1 or answer > len(new):
296                     print "{0} is not a valid index.  Please retry.".format(answer)
297                 else:
298                     got_answer = 1
299
300         if answer == 'E':
301             new = edit_new(new, upload, session)
302         elif answer == 'D':
303             done = 1
304         else:
305             edit_index (new, upload, answer - 1)
306
307     return new
308
309
310 ################################################################################
311
312 def check_pkg (upload, upload_copy):
313     save_stdout = sys.stdout
314     changes = os.path.join(upload_copy.directory, upload.changes.changesname)
315     suite_name = upload.target_suite.suite_name
316     try:
317         sys.stdout = os.popen("less -R -", 'w', 0)
318         print examine_package.display_changes(suite_name, changes)
319
320         source = upload.source
321         if source is not None:
322             source_file = os.path.join(upload_copy.directory, os.path.basename(source.poolfile.filename))
323             print examine_package.check_dsc(suite_name, source_file)
324
325         for binary in upload.binaries:
326             binary_file = os.path.join(upload_copy.directory, os.path.basename(binary.poolfile.filename))
327             print examine_package.check_deb(suite_name, binary_file)
328
329         print examine_package.output_package_relations()
330     except IOError as e:
331         if e.errno == errno.EPIPE:
332             utils.warn("[examine_package] Caught EPIPE; skipping.")
333         else:
334             raise
335     except KeyboardInterrupt:
336         utils.warn("[examine_package] Caught C-c; skipping.")
337     finally:
338         sys.stdout = save_stdout
339
340 ################################################################################
341
342 ## FIXME: horribly Debian specific
343
344 def do_bxa_notification(new, upload, session):
345     cnf = Config()
346
347     new = set([ o['package'] for o in new if o['type'] == 'deb' ])
348     if len(new) == 0:
349         return
350
351     key = session.query(MetadataKey).filter_by(key='Description').one()
352     summary = ""
353     for binary in upload.binaries:
354         if binary.package not in new:
355             continue
356         description = session.query(BinaryMetadata).filter_by(binary=binary, key=key).one().value
357         summary += "\n"
358         summary += "Package: {0}\n".format(binary.package)
359         summary += "Description: {0}\n".format(description)
360
361     subst = {
362         '__DISTRO__': cnf['Dinstall::MyDistribution'],
363         '__BCC__': 'X-DAK: dak process-new',
364         '__BINARY_DESCRIPTIONS__': summary,
365         }
366
367     bxa_mail = utils.TemplateSubst(subst,os.path.join(cnf["Dir::Templates"], "process-new.bxa_notification"))
368     utils.send_mail(bxa_mail)
369
370 ################################################################################
371
372 def add_overrides (new_overrides, suite, session):
373     if suite.overridesuite is not None:
374         suite = session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
375
376     for override in new_overrides:
377         package = override['package']
378         priority = session.query(Priority).filter_by(priority=override['priority']).first()
379         section = session.query(Section).filter_by(section=override['section']).first()
380         component = get_mapped_component(override['component'], session)
381         overridetype = session.query(OverrideType).filter_by(overridetype=override['type']).one()
382
383         if priority is None:
384             raise Exception('Invalid priority {0} for package {1}'.format(priority, package))
385         if section is None:
386             raise Exception('Invalid section {0} for package {1}'.format(section, package))
387         if component is None:
388             raise Exception('Invalid component {0} for package {1}'.format(component, package))
389
390         o = Override(package=package, suite=suite, component=component, priority=priority, section=section, overridetype=overridetype)
391         session.add(o)
392
393     session.commit()
394
395 ################################################################################
396
397 def run_user_inspect_command(upload, upload_copy):
398     command = os.environ.get('DAK_INSPECT_UPLOAD')
399     if command is None:
400         return
401
402     directory = upload_copy.directory
403     if upload.source:
404         dsc = os.path.basename(upload.source.poolfile.filename)
405     else:
406         dsc = ''
407     changes = upload.changes.changesname
408
409     shell_command = command.format(
410             directory=directory,
411             dsc=dsc,
412             changes=changes,
413             )
414
415     subprocess.check_call(shell_command, shell=True)
416
417 ################################################################################
418
419 def get_reject_reason(reason=''):
420     """get reason for rejection
421
422     @rtype:  str
423     @return: string giving the reason for the rejection or C{None} if the
424              rejection should be cancelled
425     """
426     answer = 'E'
427     if Options['Automatic']:
428         answer = 'R'
429
430     while answer == 'E':
431         reason = utils.call_editor(reason)
432         print "Reject message:"
433         print utils.prefix_multi_line_string(reason, "  ", include_blank_lines=1)
434         prompt = "[R]eject, Edit, Abandon, Quit ?"
435         answer = "XXX"
436         while prompt.find(answer) == -1:
437             answer = utils.our_raw_input(prompt)
438             m = re_default_answer.search(prompt)
439             if answer == "":
440                 answer = m.group(1)
441             answer = answer[:1].upper()
442
443     if answer == 'Q':
444         sys.exit(0)
445
446     if answer == 'R':
447         return reason
448     return None
449
450 ################################################################################
451
452 def do_new(upload, upload_copy, handler, session):
453     print "NEW\n"
454     cnf = Config()
455
456     run_user_inspect_command(upload, upload_copy)
457
458     # The main NEW processing loop
459     done = False
460     missing = []
461     while not done:
462         queuedir = upload.policy_queue.path
463         byhand = upload.byhand
464
465         missing = handler.missing_overrides(hints=missing)
466         broken = not check_valid(missing, session)
467
468         #if len(byhand) == 0 and len(missing) == 0:
469         #    break
470
471         answer = "XXX"
472         if Options["No-Action"] or Options["Automatic"]:
473             answer = 'S'
474
475         note = print_new(upload, missing, indexed=0, session=session)
476         prompt = ""
477
478         has_unprocessed_byhand = False
479         for f in byhand:
480             path = os.path.join(queuedir, f.filename)
481             if not f.processed and os.path.exists(path):
482                 print "W: {0} still present; please process byhand components and try again".format(f.filename)
483                 has_unprocessed_byhand = True
484
485         if not has_unprocessed_byhand and not broken and not note:
486             if len(missing) == 0:
487                 prompt = "Accept, "
488                 answer = 'A'
489             else:
490                 prompt = "Add overrides, "
491         if broken:
492             print "W: [!] marked entries must be fixed before package can be processed."
493         if note:
494             print "W: note must be removed before package can be processed."
495             prompt += "RemOve all notes, Remove note, "
496
497         prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
498
499         while prompt.find(answer) == -1:
500             answer = utils.our_raw_input(prompt)
501             m = re_default_answer.search(prompt)
502             if answer == "":
503                 answer = m.group(1)
504             answer = answer[:1].upper()
505
506         if answer in ( 'A', 'E', 'M', 'O', 'R' ) and Options["Trainee"]:
507             utils.warn("Trainees can't do that")
508             continue
509
510         if answer == 'A' and not Options["Trainee"]:
511             try:
512                 check_daily_lock()
513                 add_overrides(missing, upload.target_suite, session)
514                 if Config().find_b("Dinstall::BXANotify"):
515                     do_bxa_notification(missing, upload, session)
516                 handler.accept()
517                 done = True
518                 Logger.log(["NEW ACCEPT", upload.changes.changesname])
519             except CantGetLockError:
520                 print "Hello? Operator! Give me the number for 911!"
521                 print "Dinstall in the locked area, cant process packages, come back later"
522         elif answer == 'C':
523             check_pkg(upload, upload_copy)
524         elif answer == 'E' and not Options["Trainee"]:
525             missing = edit_overrides (missing, upload, session)
526         elif answer == 'M' and not Options["Trainee"]:
527             reason = Options.get('Manual-Reject', '') + "\n"
528             reason = reason + "\n".join(get_new_comments(upload.changes.source, session=session))
529             reason = get_reject_reason(reason)
530             if reason is not None:
531                 Logger.log(["NEW REJECT", upload.changes.changesname])
532                 handler.reject(reason)
533                 done = True
534         elif answer == 'N':
535             edit_note(get_new_comments(upload.changes.source, session=session),
536                       upload, session, bool(Options["Trainee"]))
537         elif answer == 'P' and not Options["Trainee"]:
538             prod_maintainer(get_new_comments(upload.changes.source, session=session),
539                             upload)
540             Logger.log(["NEW PROD", upload.changes.changesname])
541         elif answer == 'R' and not Options["Trainee"]:
542             confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
543             if confirm == "y":
544                 for c in get_new_comments(upload.changes.source, upload.changes.version, session=session):
545                     session.delete(c)
546                 session.commit()
547         elif answer == 'O' and not Options["Trainee"]:
548             confirm = utils.our_raw_input("Really clear all notes (y/N)? ").lower()
549             if confirm == "y":
550                 for c in get_new_comments(upload.changes.source, session=session):
551                     session.delete(c)
552                 session.commit()
553
554         elif answer == 'S':
555             done = True
556         elif answer == 'Q':
557             end()
558             sys.exit(0)
559
560 ################################################################################
561 ################################################################################
562 ################################################################################
563
564 def usage (exit_code=0):
565     print """Usage: dak process-new [OPTION]... [CHANGES]...
566   -a, --automatic           automatic run
567   -b, --no-binaries         do not sort binary-NEW packages first
568   -c, --comments            show NEW comments
569   -h, --help                show this help and exit.
570   -m, --manual-reject=MSG   manual reject with `msg'
571   -n, --no-action           don't do anything
572   -t, --trainee             FTP Trainee mode
573   -V, --version             display the version number and exit
574
575 ENVIRONMENT VARIABLES
576
577   DAK_INSPECT_UPLOAD: shell command to run to inspect a package
578       The command is automatically run in a shell when an upload
579       is checked.  The following substitutions are available:
580
581         {directory}: directory the upload is contained in
582         {dsc}:       name of the included dsc or the empty string
583         {changes}:   name of the changes file
584
585       Note that Python's 'format' method is used to format the command.
586
587       Example: run mc in a tmux session to inspect the upload
588
589       export DAK_INSPECT_UPLOAD='tmux new-session -d -s process-new 2>/dev/null; tmux new-window -t process-new:0 -k "cd {directory}; mc"'
590
591       and run
592
593       tmux attach -t process-new
594
595       in a separate terminal session.
596 """
597     sys.exit(exit_code)
598
599 ################################################################################
600
601 def check_daily_lock():
602     """
603     Raises CantGetLockError if the dinstall daily.lock exists.
604     """
605
606     cnf = Config()
607     try:
608         lockfile = cnf.get("Process-New::DinstallLockFile",
609                            os.path.join(cnf['Dir::Lock'], 'processnew.lock'))
610
611         os.open(lockfile,
612                 os.O_RDONLY | os.O_CREAT | os.O_EXCL)
613     except OSError as e:
614         if e.errno == errno.EEXIST or e.errno == errno.EACCES:
615             raise CantGetLockError
616
617     os.unlink(lockfile)
618
619 @contextlib.contextmanager
620 def lock_package(package):
621     """
622     Lock C{package} so that noone else jumps in processing it.
623
624     @type package: string
625     @param package: source package name to lock
626     """
627
628     cnf = Config()
629
630     path = os.path.join(cnf.get("Process-New::LockDir", cnf['Dir::Lock']), package)
631
632     try:
633         fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY)
634     except OSError as e:
635         if e.errno == errno.EEXIST or e.errno == errno.EACCES:
636             user = pwd.getpwuid(os.stat(path)[stat.ST_UID])[4].split(',')[0].replace('.', '')
637             raise AlreadyLockedError(user)
638
639     try:
640         yield fd
641     finally:
642         os.unlink(path)
643
644 def do_pkg(upload, session):
645     # Try to get an included dsc
646     dsc = upload.source
647
648     cnf = Config()
649     #bcc = "X-DAK: dak process-new"
650     #if cnf.has_key("Dinstall::Bcc"):
651     #    u.Subst["__BCC__"] = bcc + "\nBcc: %s" % (cnf["Dinstall::Bcc"])
652     #else:
653     #    u.Subst["__BCC__"] = bcc
654
655     try:
656       with lock_package(upload.changes.source):
657        with UploadCopy(upload) as upload_copy:
658         handler = PolicyQueueUploadHandler(upload, session)
659         if handler.get_action() is not None:
660             return
661
662         do_new(upload, upload_copy, handler, session)
663     except AlreadyLockedError as e:
664         print "Seems to be locked by %s already, skipping..." % (e)
665
666 def show_new_comments(uploads, session):
667     sources = [ upload.changes.source for upload in uploads ]
668     if len(sources) == 0:
669         return
670
671     query = """SELECT package, version, comment, author
672                FROM new_comments
673                WHERE package IN :sources
674                ORDER BY package, version"""
675
676     r = session.execute(query, params=dict(sources=sources))
677
678     for i in r:
679         print "%s_%s\n%s\n(%s)\n\n\n" % (i[0], i[1], i[2], i[3])
680
681     session.rollback()
682
683 ################################################################################
684
685 def end():
686     accept_count = SummaryStats().accept_count
687     accept_bytes = SummaryStats().accept_bytes
688
689     if accept_count:
690         sets = "set"
691         if accept_count > 1:
692             sets = "sets"
693         sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
694         Logger.log(["total",accept_count,accept_bytes])
695
696     if not Options["No-Action"] and not Options["Trainee"]:
697         Logger.close()
698
699 ################################################################################
700
701 def main():
702     global Options, Logger, Sections, Priorities
703
704     cnf = Config()
705     session = DBConn().session()
706
707     Arguments = [('a',"automatic","Process-New::Options::Automatic"),
708                  ('b',"no-binaries","Process-New::Options::No-Binaries"),
709                  ('c',"comments","Process-New::Options::Comments"),
710                  ('h',"help","Process-New::Options::Help"),
711                  ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
712                  ('t',"trainee","Process-New::Options::Trainee"),
713                  ('q','queue','Process-New::Options::Queue', 'HasArg'),
714                  ('n',"no-action","Process-New::Options::No-Action")]
715
716     changes_files = apt_pkg.parse_commandline(cnf.Cnf,Arguments,sys.argv)
717
718     for i in ["automatic", "no-binaries", "comments", "help", "manual-reject", "no-action", "version", "trainee"]:
719         if not cnf.has_key("Process-New::Options::%s" % (i)):
720             cnf["Process-New::Options::%s" % (i)] = ""
721
722     queue_name = cnf.get('Process-New::Options::Queue', 'new')
723     new_queue = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
724     if len(changes_files) == 0:
725         uploads = new_queue.uploads
726     else:
727         uploads = session.query(PolicyQueueUpload).filter_by(policy_queue=new_queue) \
728             .join(DBChange).filter(DBChange.changesname.in_(changes_files)).all()
729
730     Options = cnf.subtree("Process-New::Options")
731
732     if Options["Help"]:
733         usage()
734
735     if not Options["No-Action"]:
736         try:
737             Logger = daklog.Logger("process-new")
738         except CantOpenError as e:
739             Options["Trainee"] = "True"
740
741     Sections = Section_Completer(session)
742     Priorities = Priority_Completer(session)
743     readline.parse_and_bind("tab: complete")
744
745     if len(uploads) > 1:
746         sys.stderr.write("Sorting changes...\n")
747         uploads.sort()
748
749     if Options["Comments"]:
750         show_new_comments(uploads, session)
751     else:
752         for upload in uploads:
753             print "\n" + os.path.basename(upload.changes.changesname)
754
755             do_pkg (upload, session)
756
757     end()
758
759 ################################################################################
760
761 if __name__ == '__main__':
762     main()