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