]> git.decadent.org.uk Git - dak.git/blob - dak/process_new.py
let's use pythonesque imports, shall we?
[dak.git] / dak / process_new.py
1 #!/usr/bin/env python
2
3 # Handles NEW and BYHAND packages
4 # Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 ################################################################################
21
22 # 23:12|<aj> I will not hush!
23 # 23:12|<elmo> :>
24 # 23:12|<aj> Where there is injustice in the world, I shall be there!
25 # 23:13|<aj> I shall not be silenced!
26 # 23:13|<aj> The world shall know!
27 # 23:13|<aj> The world *must* know!
28 # 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
29 # 23:13|<aj> yay powerpuff girls!!
30 # 23:13|<aj> buttercup's my favourite, who's yours?
31 # 23:14|<aj> you're backing away from the keyboard right now aren't you?
32 # 23:14|<aj> *AREN'T YOU*?!
33 # 23:15|<aj> I will not be treated like this.
34 # 23:15|<aj> I shall have my revenge.
35 # 23:15|<aj> I SHALL!!!
36
37 ################################################################################
38
39 import copy, errno, os, readline, stat, sys, time
40 import apt_pkg, apt_inst
41 import examine_package
42 from daklib import database
43 from daklib import logging
44 from daklib import queue
45 from daklib import utils
46
47 # Globals
48 Cnf = None
49 Options = None
50 Upload = None
51 projectB = None
52 Logger = None
53
54 Priorities = None
55 Sections = None
56
57 reject_message = ""
58
59 ################################################################################
60 ################################################################################
61 ################################################################################
62
63 def reject (str, prefix="Rejected: "):
64     global reject_message
65     if str:
66         reject_message += prefix + str + "\n"
67
68 def recheck():
69     global reject_message
70     files = Upload.pkg.files
71     reject_message = ""
72
73     for f in files.keys():
74         # The .orig.tar.gz can disappear out from under us is it's a
75         # duplicate of one in the archive.
76         if not files.has_key(f):
77             continue
78         # Check that the source still exists
79         if files[f]["type"] == "deb":
80             source_version = files[f]["source version"]
81             source_package = files[f]["source package"]
82             if not Upload.pkg.changes["architecture"].has_key("source") \
83                and not Upload.source_exists(source_package, source_version, Upload.pkg.changes["distribution"].keys()):
84                 source_epochless_version = utils.re_no_epoch.sub('', source_version)
85                 dsc_filename = "%s_%s.dsc" % (source_package, source_epochless_version)
86                 found = 0
87                 for q in ["Accepted", "Embargoed", "Unembargoed"]:
88                     if Cnf.has_key("Dir::Queue::%s" % (q)):
89                         if os.path.exists(Cnf["Dir::Queue::%s" % (q)] + '/' + dsc_filename):
90                             found = 1
91                 if not found:
92                     reject("no source found for %s %s (%s)." % (source_package, source_version, f))
93
94         # Version and file overwrite checks
95         if files[f]["type"] == "deb":
96             reject(Upload.check_binary_against_db(f))
97         elif files[f]["type"] == "dsc":
98             reject(Upload.check_source_against_db(f))
99             (reject_msg, is_in_incoming) = Upload.check_dsc_against_db(f)
100             reject(reject_msg, "")
101
102     if reject_message.find("Rejected") != -1:
103         answer = "XXX"
104         if Options["No-Action"] or Options["Automatic"]:
105             answer = 'S'
106
107         print "REJECT\n" + reject_message,
108         prompt = "[R]eject, Skip, Quit ?"
109
110         while prompt.find(answer) == -1:
111             answer = utils.our_raw_input(prompt)
112             m = queue.re_default_answer.match(prompt)
113             if answer == "":
114                 answer = m.group(1)
115             answer = answer[:1].upper()
116
117         if answer == 'R':
118             Upload.do_reject(0, reject_message)
119             os.unlink(Upload.pkg.changes_file[:-8]+".dak")
120             return 0
121         elif answer == 'S':
122             return 0
123         elif answer == 'Q':
124             end()
125             sys.exit(0)
126
127     return 1
128
129 ################################################################################
130
131 def indiv_sg_compare (a, b):
132     """Sort by source name, source, version, 'have source', and
133        finally by filename."""
134     # Sort by source version
135     q = apt_pkg.VersionCompare(a["version"], b["version"])
136     if q:
137         return -q
138
139     # Sort by 'have source'
140     a_has_source = a["architecture"].get("source")
141     b_has_source = b["architecture"].get("source")
142     if a_has_source and not b_has_source:
143         return -1
144     elif b_has_source and not a_has_source:
145         return 1
146
147     return cmp(a["filename"], b["filename"])
148
149 ############################################################
150
151 def sg_compare (a, b):
152     a = a[1]
153     b = b[1]
154     """Sort by have note, time of oldest upload."""
155     # Sort by have note
156     a_note_state = a["note_state"]
157     b_note_state = b["note_state"]
158     if a_note_state < b_note_state:
159         return -1
160     elif a_note_state > b_note_state:
161         return 1
162
163     # Sort by time of oldest upload
164     return cmp(a["oldest"], b["oldest"])
165
166 def sort_changes(changes_files):
167     """Sort into source groups, then sort each source group by version,
168     have source, filename.  Finally, sort the source groups by have
169     note, time of oldest upload of each source upload."""
170     if len(changes_files) == 1:
171         return changes_files
172
173     sorted_list = []
174     cache = {}
175     # Read in all the .changes files
176     for filename in changes_files:
177         try:
178             Upload.pkg.changes_file = filename
179             Upload.init_vars()
180             Upload.update_vars()
181             cache[filename] = copy.copy(Upload.pkg.changes)
182             cache[filename]["filename"] = filename
183         except:
184             sorted_list.append(filename)
185             break
186     # Divide the .changes into per-source groups
187     per_source = {}
188     for filename in cache.keys():
189         source = cache[filename]["source"]
190         if not per_source.has_key(source):
191             per_source[source] = {}
192             per_source[source]["list"] = []
193         per_source[source]["list"].append(cache[filename])
194     # Determine oldest time and have note status for each source group
195     for source in per_source.keys():
196         source_list = per_source[source]["list"]
197         first = source_list[0]
198         oldest = os.stat(first["filename"])[stat.ST_MTIME]
199         have_note = 0
200         for d in per_source[source]["list"]:
201             mtime = os.stat(d["filename"])[stat.ST_MTIME]
202             if mtime < oldest:
203                 oldest = mtime
204             have_note += (d.has_key("process-new note"))
205         per_source[source]["oldest"] = oldest
206         if not have_note:
207             per_source[source]["note_state"] = 0; # none
208         elif have_note < len(source_list):
209             per_source[source]["note_state"] = 1; # some
210         else:
211             per_source[source]["note_state"] = 2; # all
212         per_source[source]["list"].sort(indiv_sg_compare)
213     per_source_items = per_source.items()
214     per_source_items.sort(sg_compare)
215     for i in per_source_items:
216         for j in i[1]["list"]:
217             sorted_list.append(j["filename"])
218     return sorted_list
219
220 ################################################################################
221
222 class Section_Completer:
223     def __init__ (self):
224         self.sections = []
225         q = projectB.query("SELECT section FROM section")
226         for i in q.getresult():
227             self.sections.append(i[0])
228
229     def complete(self, text, state):
230         if state == 0:
231             self.matches = []
232             n = len(text)
233             for word in self.sections:
234                 if word[:n] == text:
235                     self.matches.append(word)
236         try:
237             return self.matches[state]
238         except IndexError:
239             return None
240
241 ############################################################
242
243 class Priority_Completer:
244     def __init__ (self):
245         self.priorities = []
246         q = projectB.query("SELECT priority FROM priority")
247         for i in q.getresult():
248             self.priorities.append(i[0])
249
250     def complete(self, text, state):
251         if state == 0:
252             self.matches = []
253             n = len(text)
254             for word in self.priorities:
255                 if word[:n] == text:
256                     self.matches.append(word)
257         try:
258             return self.matches[state]
259         except IndexError:
260             return None
261
262 ################################################################################
263
264 def print_new (new, indexed, file=sys.stdout):
265     queue.check_valid(new)
266     broken = 0
267     index = 0
268     for pkg in new.keys():
269         index += 1
270         section = new[pkg]["section"]
271         priority = new[pkg]["priority"]
272         if new[pkg]["section id"] == -1:
273             section += "[!]"
274             broken = 1
275         if new[pkg]["priority id"] == -1:
276             priority += "[!]"
277             broken = 1
278         if indexed:
279             line = "(%s): %-20s %-20s %-20s" % (index, pkg, priority, section)
280         else:
281             line = "%-20s %-20s %-20s" % (pkg, priority, section)
282         line = line.strip()+'\n'
283         file.write(line)
284     note = Upload.pkg.changes.get("process-new note")
285     if note:
286         print "*"*75
287         print note
288         print "*"*75
289     return broken, note
290
291 ################################################################################
292
293 def index_range (index):
294     if index == 1:
295         return "1"
296     else:
297         return "1-%s" % (index)
298
299 ################################################################################
300 ################################################################################
301
302 def edit_new (new):
303     # Write the current data to a temporary file
304     temp_filename = utils.temp_filename()
305     temp_file = utils.open_file(temp_filename, 'w')
306     print_new (new, 0, temp_file)
307     temp_file.close()
308     # Spawn an editor on that file
309     editor = os.environ.get("EDITOR","vi")
310     result = os.system("%s %s" % (editor, temp_filename))
311     if result != 0:
312         utils.fubar ("%s invocation failed for %s." % (editor, temp_filename), result)
313     # Read the edited data back in
314     temp_file = utils.open_file(temp_filename)
315     lines = temp_file.readlines()
316     temp_file.close()
317     os.unlink(temp_filename)
318     # Parse the new data
319     for line in lines:
320         line = line.strip()
321         if line == "":
322             continue
323         s = line.split()
324         # Pad the list if necessary
325         s[len(s):3] = [None] * (3-len(s))
326         (pkg, priority, section) = s[:3]
327         if not new.has_key(pkg):
328             utils.warn("Ignoring unknown package '%s'" % (pkg))
329         else:
330             # Strip off any invalid markers, print_new will readd them.
331             if section.endswith("[!]"):
332                 section = section[:-3]
333             if priority.endswith("[!]"):
334                 priority = priority[:-3]
335             for f in new[pkg]["files"]:
336                 Upload.pkg.files[f]["section"] = section
337                 Upload.pkg.files[f]["priority"] = priority
338             new[pkg]["section"] = section
339             new[pkg]["priority"] = priority
340
341 ################################################################################
342
343 def edit_index (new, index):
344     priority = new[index]["priority"]
345     section = new[index]["section"]
346     ftype = new[index]["type"]
347     done = 0
348     while not done:
349         print "\t".join([index, priority, section])
350
351         answer = "XXX"
352         if ftype != "dsc":
353             prompt = "[B]oth, Priority, Section, Done ? "
354         else:
355             prompt = "[S]ection, Done ? "
356         edit_priority = edit_section = 0
357
358         while prompt.find(answer) == -1:
359             answer = utils.our_raw_input(prompt)
360             m = queue.re_default_answer.match(prompt)
361             if answer == "":
362                 answer = m.group(1)
363             answer = answer[:1].upper()
364
365         if answer == 'P':
366             edit_priority = 1
367         elif answer == 'S':
368             edit_section = 1
369         elif answer == 'B':
370             edit_priority = edit_section = 1
371         elif answer == 'D':
372             done = 1
373
374         # Edit the priority
375         if edit_priority:
376             readline.set_completer(Priorities.complete)
377             got_priority = 0
378             while not got_priority:
379                 new_priority = utils.our_raw_input("New priority: ").strip()
380                 if new_priority not in Priorities.priorities:
381                     print "E: '%s' is not a valid priority, try again." % (new_priority)
382                 else:
383                     got_priority = 1
384                     priority = new_priority
385
386         # Edit the section
387         if edit_section:
388             readline.set_completer(Sections.complete)
389             got_section = 0
390             while not got_section:
391                 new_section = utils.our_raw_input("New section: ").strip()
392                 if new_section not in Sections.sections:
393                     print "E: '%s' is not a valid section, try again." % (new_section)
394                 else:
395                     got_section = 1
396                     section = new_section
397
398         # Reset the readline completer
399         readline.set_completer(None)
400
401     for f in new[index]["files"]:
402         Upload.pkg.files[f]["section"] = section
403         Upload.pkg.files[f]["priority"] = priority
404     new[index]["priority"] = priority
405     new[index]["section"] = section
406     return new
407
408 ################################################################################
409
410 def edit_overrides (new):
411     print
412     done = 0
413     while not done:
414         print_new (new, 1)
415         new_index = {}
416         index = 0
417         for i in new.keys():
418             index += 1
419             new_index[index] = i
420
421         prompt = "(%s) edit override <n>, Editor, Done ? " % (index_range(index))
422
423         got_answer = 0
424         while not got_answer:
425             answer = utils.our_raw_input(prompt)
426             if not answer.isdigit():
427                 answer = answer[:1].upper()
428             if answer == "E" or answer == "D":
429                 got_answer = 1
430             elif queue.re_isanum.match (answer):
431                 answer = int(answer)
432                 if (answer < 1) or (answer > index):
433                     print "%s is not a valid index (%s).  Please retry." % (answer, index_range(index))
434                 else:
435                     got_answer = 1
436
437         if answer == 'E':
438             edit_new(new)
439         elif answer == 'D':
440             done = 1
441         else:
442             edit_index (new, new_index[answer])
443
444     return new
445
446 ################################################################################
447
448 def edit_note(note):
449     # Write the current data to a temporary file
450     temp_filename = utils.temp_filename()
451     temp_file = utils.open_file(temp_filename, 'w')
452     temp_file.write(note)
453     temp_file.close()
454     editor = os.environ.get("EDITOR","vi")
455     answer = 'E'
456     while answer == 'E':
457         os.system("%s %s" % (editor, temp_filename))
458         temp_file = utils.open_file(temp_filename)
459         note = temp_file.read().rstrip()
460         temp_file.close()
461         print "Note:"
462         print utils.prefix_multi_line_string(note,"  ")
463         prompt = "[D]one, Edit, Abandon, Quit ?"
464         answer = "XXX"
465         while prompt.find(answer) == -1:
466             answer = utils.our_raw_input(prompt)
467             m = queue.re_default_answer.search(prompt)
468             if answer == "":
469                 answer = m.group(1)
470             answer = answer[:1].upper()
471     os.unlink(temp_filename)
472     if answer == 'A':
473         return
474     elif answer == 'Q':
475         end()
476         sys.exit(0)
477     Upload.pkg.changes["process-new note"] = note
478     Upload.dump_vars(Cnf["Dir::Queue::New"])
479
480 ################################################################################
481
482 def check_pkg ():
483     try:
484         less_fd = os.popen("less -R -", 'w', 0)
485         stdout_fd = sys.stdout
486         try:
487             sys.stdout = less_fd
488             examine_package.display_changes(Upload.pkg.changes_file)
489             files = Upload.pkg.files
490             for f in files.keys():
491                 if files[f].has_key("new"):
492                     ftype = files[f]["type"]
493                     if ftype == "deb":
494                         examine_package.check_deb(f)
495                     elif ftype == "dsc":
496                         examine_package.check_dsc(f)
497         finally:
498             sys.stdout = stdout_fd
499     except IOError, e:
500         if e.errno == errno.EPIPE:
501             utils.warn("[examine_package] Caught EPIPE; skipping.")
502             pass
503         else:
504             raise
505     except KeyboardInterrupt:
506         utils.warn("[examine_package] Caught C-c; skipping.")
507         pass
508
509 ################################################################################
510
511 ## FIXME: horribly Debian specific
512
513 def do_bxa_notification():
514     files = Upload.pkg.files
515     summary = ""
516     for f in files.keys():
517         if files[f]["type"] == "deb":
518             control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(f)))
519             summary += "\n"
520             summary += "Package: %s\n" % (control.Find("Package"))
521             summary += "Description: %s\n" % (control.Find("Description"))
522     Upload.Subst["__BINARY_DESCRIPTIONS__"] = summary
523     bxa_mail = utils.TemplateSubst(Upload.Subst,Cnf["Dir::Templates"]+"/process-new.bxa_notification")
524     utils.send_mail(bxa_mail)
525
526 ################################################################################
527
528 def add_overrides (new):
529     changes = Upload.pkg.changes
530     files = Upload.pkg.files
531
532     projectB.query("BEGIN WORK")
533     for suite in changes["suite"].keys():
534         suite_id = database.get_suite_id(suite)
535         for pkg in new.keys():
536             component_id = database.get_component_id(new[pkg]["component"])
537             type_id = database.get_override_type_id(new[pkg]["type"])
538             priority_id = new[pkg]["priority id"]
539             section_id = new[pkg]["section id"]
540             projectB.query("INSERT INTO override (suite, component, type, package, priority, section, maintainer) VALUES (%s, %s, %s, '%s', %s, %s, '')" % (suite_id, component_id, type_id, pkg, priority_id, section_id))
541             for f in new[pkg]["files"]:
542                 if files[f].has_key("new"):
543                     del files[f]["new"]
544             del new[pkg]
545
546     projectB.query("COMMIT WORK")
547
548     if Cnf.FindB("Dinstall::BXANotify"):
549         do_bxa_notification()
550
551 ################################################################################
552
553 def prod_maintainer ():
554     # Here we prepare an editor and get them ready to prod...
555     temp_filename = utils.temp_filename()
556     editor = os.environ.get("EDITOR","vi")
557     answer = 'E'
558     while answer == 'E':
559         os.system("%s %s" % (editor, temp_filename))
560         f = utils.open_file(temp_filename)
561         prod_message = "".join(f.readlines())
562         f.close()
563         print "Prod message:"
564         print utils.prefix_multi_line_string(prod_message,"  ",include_blank_lines=1)
565         prompt = "[P]rod, Edit, Abandon, Quit ?"
566         answer = "XXX"
567         while prompt.find(answer) == -1:
568             answer = utils.our_raw_input(prompt)
569             m = queue.re_default_answer.search(prompt)
570             if answer == "":
571                 answer = m.group(1)
572             answer = answer[:1].upper()
573         os.unlink(temp_filename)
574         if answer == 'A':
575             return
576         elif answer == 'Q':
577             end()
578             sys.exit(0)
579     # Otherwise, do the proding...
580     user_email_address = utils.whoami() + " <%s>" % (
581         Cnf["Dinstall::MyAdminAddress"])
582
583     Subst = Upload.Subst
584
585     Subst["__FROM_ADDRESS__"] = user_email_address
586     Subst["__PROD_MESSAGE__"] = prod_message
587     Subst["__CC__"] = "Cc: " + Cnf["Dinstall::MyEmailAddress"]
588
589     prod_mail_message = utils.TemplateSubst(
590         Subst,Cnf["Dir::Templates"]+"/process-new.prod")
591
592     # Send the prod mail if appropriate
593     if not Cnf["Dinstall::Options::No-Mail"]:
594         utils.send_mail(prod_mail_message)
595
596     print "Sent proding message"
597
598 ################################################################################
599
600 def do_new():
601     print "NEW\n"
602     files = Upload.pkg.files
603     changes = Upload.pkg.changes
604
605     # Make a copy of distribution we can happily trample on
606     changes["suite"] = copy.copy(changes["distribution"])
607
608     # Fix up the list of target suites
609     for suite in changes["suite"].keys():
610         override = Cnf.Find("Suite::%s::OverrideSuite" % (suite))
611         if override:
612             (olderr, newerr) = (database.get_suite_id(suite) == -1,
613               database.get_suite_id(override) == -1)
614             if olderr or newerr:
615                 (oinv, newinv) = ("", "")
616                 if olderr: oinv = "invalid "
617                 if newerr: ninv = "invalid "
618                 print "warning: overriding %ssuite %s to %ssuite %s" % (
619                         oinv, suite, ninv, override)
620             del changes["suite"][suite]
621             changes["suite"][override] = 1
622     # Validate suites
623     for suite in changes["suite"].keys():
624         suite_id = database.get_suite_id(suite)
625         if suite_id == -1:
626             utils.fubar("%s has invalid suite '%s' (possibly overriden).  say wha?" % (changes, suite))
627
628     # The main NEW processing loop
629     done = 0
630     while not done:
631         # Find out what's new
632         new = queue.determine_new(changes, files, projectB)
633
634         if not new:
635             break
636
637         answer = "XXX"
638         if Options["No-Action"] or Options["Automatic"]:
639             answer = 'S'
640
641         (broken, note) = print_new(new, 0)
642         prompt = ""
643
644         if not broken and not note:
645             prompt = "Add overrides, "
646         if broken:
647             print "W: [!] marked entries must be fixed before package can be processed."
648         if note:
649             print "W: note must be removed before package can be processed."
650             prompt += "Remove note, "
651
652         prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
653
654         while prompt.find(answer) == -1:
655             answer = utils.our_raw_input(prompt)
656             m = queue.re_default_answer.search(prompt)
657             if answer == "":
658                 answer = m.group(1)
659             answer = answer[:1].upper()
660
661         if answer == 'A':
662             done = add_overrides (new)
663         elif answer == 'C':
664             check_pkg()
665         elif answer == 'E':
666             new = edit_overrides (new)
667         elif answer == 'M':
668             aborted = Upload.do_reject(1, Options["Manual-Reject"])
669             if not aborted:
670                 os.unlink(Upload.pkg.changes_file[:-8]+".dak")
671                 done = 1
672         elif answer == 'N':
673             edit_note(changes.get("process-new note", ""))
674         elif answer == 'P':
675             prod_maintainer()
676         elif answer == 'R':
677             confirm = utils.our_raw_input("Really clear note (y/N)? ").lower()
678             if confirm == "y":
679                 del changes["process-new note"]
680         elif answer == 'S':
681             done = 1
682         elif answer == 'Q':
683             end()
684             sys.exit(0)
685
686 ################################################################################
687 ################################################################################
688 ################################################################################
689
690 def usage (exit_code=0):
691     print """Usage: dak process-new [OPTION]... [CHANGES]...
692   -a, --automatic           automatic run
693   -h, --help                show this help and exit.
694   -C, --comments-dir=DIR    use DIR as comments-dir, for [o-]p-u-new
695   -m, --manual-reject=MSG   manual reject with `msg'
696   -n, --no-action           don't do anything
697   -V, --version             display the version number and exit"""
698     sys.exit(exit_code)
699
700 ################################################################################
701
702 def init():
703     global Cnf, Options, Logger, Upload, projectB, Sections, Priorities
704
705     Cnf = utils.get_conf()
706
707     Arguments = [('a',"automatic","Process-New::Options::Automatic"),
708                  ('h',"help","Process-New::Options::Help"),
709                  ('C',"comments-dir","Process-New::Options::Comments-Dir", "HasArg"),
710                  ('m',"manual-reject","Process-New::Options::Manual-Reject", "HasArg"),
711                  ('n',"no-action","Process-New::Options::No-Action")]
712
713     for i in ["automatic", "help", "manual-reject", "no-action", "version", "comments-dir"]:
714         if not Cnf.has_key("Process-New::Options::%s" % (i)):
715             Cnf["Process-New::Options::%s" % (i)] = ""
716
717     changes_files = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv)
718     Options = Cnf.SubTree("Process-New::Options")
719
720     if Options["Help"]:
721         usage()
722
723     Upload = queue.Upload(Cnf)
724
725     if not Options["No-Action"]:
726         Logger = Upload.Logger = logging.Logger(Cnf, "process-new")
727
728     projectB = Upload.projectB
729
730     Sections = Section_Completer()
731     Priorities = Priority_Completer()
732     readline.parse_and_bind("tab: complete")
733
734     return changes_files
735
736 ################################################################################
737
738 def do_byhand():
739     done = 0
740     while not done:
741         files = Upload.pkg.files
742         will_install = 1
743         byhand = []
744
745         for f in files.keys():
746             if files[f]["type"] == "byhand":
747                 if os.path.exists(f):
748                     print "W: %s still present; please process byhand components and try again." % (f)
749                     will_install = 0
750                 else:
751                     byhand.append(f)
752
753         answer = "XXXX"
754         if Options["No-Action"]:
755             answer = "S"
756         if will_install:
757             if Options["Automatic"] and not Options["No-Action"]:
758                 answer = 'A'
759             prompt = "[A]ccept, Manual reject, Skip, Quit ?"
760         else:
761             prompt = "Manual reject, [S]kip, Quit ?"
762
763         while prompt.find(answer) == -1:
764             answer = utils.our_raw_input(prompt)
765             m = queue.re_default_answer.search(prompt)
766             if answer == "":
767                 answer = m.group(1)
768             answer = answer[:1].upper()
769
770         if answer == 'A':
771             done = 1
772             for f in byhand:
773                 del files[f]
774         elif answer == 'M':
775             Upload.do_reject(1, Options["Manual-Reject"])
776             os.unlink(Upload.pkg.changes_file[:-8]+".dak")
777             done = 1
778         elif answer == 'S':
779             done = 1
780         elif answer == 'Q':
781             end()
782             sys.exit(0)
783
784 ################################################################################
785
786 def get_accept_lock():
787     retry = 0
788     while retry < 10:
789         try:
790             os.open(Cnf["Process-New::AcceptedLockFile"], os.O_RDONLY | os.O_CREAT | os.O_EXCL)
791             retry = 10
792         except OSError, e:
793             if e.errno == errno.EACCES or e.errno == errno.EEXIST:
794                 retry += 1
795                 if (retry >= 10):
796                     utils.fubar("Couldn't obtain lock; assuming 'dak process-unchecked' is already running.")
797                 else:
798                     print("Unable to get accepted lock (try %d of 10)" % retry)
799                 time.sleep(60)
800             else:
801                 raise
802
803 def move_to_dir (dest, perms=0660, changesperms=0664):
804     utils.move (Upload.pkg.changes_file, dest, perms=changesperms)
805     file_keys = Upload.pkg.files.keys()
806     for f in file_keys:
807         utils.move (f, dest, perms=perms)
808
809 def do_accept():
810     print "ACCEPT"
811     if not Options["No-Action"]:
812         get_accept_lock()
813         (summary, short_summary) = Upload.build_summaries()
814     if Cnf.FindB("Dinstall::SecurityQueueHandling"):
815         Upload.dump_vars(Cnf["Dir::Queue::Embargoed"])
816         move_to_dir(Cnf["Dir::Queue::Embargoed"])
817         Upload.queue_build("embargoed", Cnf["Dir::Queue::Embargoed"])
818         # Check for override disparities
819         Upload.Subst["__SUMMARY__"] = summary
820     else:
821         Upload.accept(summary, short_summary)
822         os.unlink(Upload.pkg.changes_file[:-8]+".dak")
823     os.unlink(Cnf["Process-New::AcceptedLockFile"])
824
825 def check_status(files):
826     new = byhand = 0
827     for f in files.keys():
828         if files[f]["type"] == "byhand":
829             byhand = 1
830         elif files[f].has_key("new"):
831             new = 1
832     return (new, byhand)
833
834 def do_pkg(changes_file):
835     Upload.pkg.changes_file = changes_file
836     Upload.init_vars()
837     Upload.update_vars()
838     Upload.update_subst()
839     files = Upload.pkg.files
840
841     if not recheck():
842         return
843
844     (new, byhand) = check_status(files)
845     if new or byhand:
846         if new:
847             do_new()
848         if byhand:
849             do_byhand()
850         (new, byhand) = check_status(files)
851
852     if not new and not byhand:
853         do_accept()
854
855 ################################################################################
856
857 def end():
858     accept_count = Upload.accept_count
859     accept_bytes = Upload.accept_bytes
860
861     if accept_count:
862         sets = "set"
863         if accept_count > 1:
864             sets = "sets"
865         sys.stderr.write("Accepted %d package %s, %s.\n" % (accept_count, sets, utils.size_type(int(accept_bytes))))
866         Logger.log(["total",accept_count,accept_bytes])
867
868     if not Options["No-Action"]:
869         Logger.close()
870
871 ################################################################################
872
873 def do_comments(dir, opref, npref, line, fn):
874     for comm in [ x for x in os.listdir(dir) if x.startswith(opref) ]:
875         lines = open("%s/%s" % (dir, comm)).readlines()
876         if len(lines) == 0 or lines[0] != line + "\n": continue
877         changes_files = [ x for x in os.listdir(".") if x.startswith(comm[7:]+"_")
878                                 and x.endswith(".changes") ]
879         changes_files = sort_changes(changes_files)
880         for f in changes_files:
881             f = utils.validate_changes_file_arg(f, 0)
882             if not f: continue
883             print "\n" + f
884             fn(f, "".join(lines[1:]))
885
886         if opref != npref and not Options["No-Action"]:
887             newcomm = npref + comm[len(opref):]
888             os.rename("%s/%s" % (dir, comm), "%s/%s" % (dir, newcomm))
889
890 ################################################################################
891
892 def comment_accept(changes_file, comments):
893     Upload.pkg.changes_file = changes_file
894     Upload.init_vars()
895     Upload.update_vars()
896     Upload.update_subst()
897     files = Upload.pkg.files
898
899     if not recheck():
900         return # dak wants to REJECT, crap
901
902     (new, byhand) = check_status(files)
903     if not new and not byhand:
904         do_accept()
905
906 ################################################################################
907
908 def comment_reject(changes_file, comments):
909     Upload.pkg.changes_file = changes_file
910     Upload.init_vars()
911     Upload.update_vars()
912     Upload.update_subst()
913
914     if not recheck():
915         pass # dak has its own reasons to reject as well, which is fine
916
917     reject(comments)
918     print "REJECT\n" + reject_message,
919     if not Options["No-Action"]:
920         Upload.do_reject(0, reject_message)
921         os.unlink(Upload.pkg.changes_file[:-8]+".dak")
922
923 ################################################################################
924
925 def main():
926     changes_files = init()
927     if len(changes_files) > 50:
928         sys.stderr.write("Sorting changes...\n")
929     changes_files = sort_changes(changes_files)
930
931     # Kill me now? **FIXME**
932     Cnf["Dinstall::Options::No-Mail"] = ""
933     bcc = "X-DAK: dak process-new\nX-Katie: lisa $Revision: 1.31 $"
934     if Cnf.has_key("Dinstall::Bcc"):
935         Upload.Subst["__BCC__"] = bcc + "\nBcc: %s" % (Cnf["Dinstall::Bcc"])
936     else:
937         Upload.Subst["__BCC__"] = bcc
938
939     commentsdir = Cnf.get("Process-New::Options::Comments-Dir","")
940     if commentsdir:
941         if changes_files != []:
942             sys.stderr.write("Can't specify any changes files if working with comments-dir")
943             sys.exit(1)
944         do_comments(commentsdir, "ACCEPT.", "ACCEPTED.", "OK", comment_accept)
945         do_comments(commentsdir, "REJECT.", "REJECTED.", "NOTOK", comment_reject)
946     else:
947         for changes_file in changes_files:
948             changes_file = utils.validate_changes_file_arg(changes_file, 0)
949             if not changes_file:
950                 continue
951             print "\n" + changes_file
952             do_pkg (changes_file)
953
954     end()
955
956 ################################################################################
957
958 if __name__ == '__main__':
959     main()