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