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