Coverage for dak/process_new.py: 53%
511 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-01-04 16:18 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-01-04 16:18 +0000
1#! /usr/bin/env python3
2# vim:set et ts=4 sw=4:
4"""Handles NEW and BYHAND packages
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.
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.
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
26################################################################################
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!!!
43################################################################################
45import contextlib
46import datetime
47import errno
48import os
49import pwd
50import readline
51import stat
52import subprocess
53import sys
54import tempfile
55from collections import defaultdict
56from collections.abc import Iterable, Iterator
57from typing import TYPE_CHECKING, NoReturn, Optional, TypedDict
59import apt_pkg
60from sqlalchemy import or_, sql
62import dak.examine_package
63import daklib.dbconn
64from daklib import daklog, utils
65from daklib.config import Config
66from daklib.dak_exceptions import AlreadyLockedError
67from daklib.dbconn import (
68 BinaryMetadata,
69 DBBinary,
70 DBChange,
71 DBConn,
72 DBSource,
73 MetadataKey,
74 NewComment,
75 PolicyQueue,
76 PolicyQueueUpload,
77 Priority,
78 Section,
79 Suite,
80 get_new_comments,
81)
82from daklib.policy import MissingOverride, PolicyQueueUploadHandler, UploadCopy
83from daklib.queue import check_valid, edit_note, prod_maintainer
84from daklib.regexes import re_default_answer, re_isanum
85from daklib.summarystats import SummaryStats
86from daklib.termcolor import colorize as Color
88if TYPE_CHECKING:
89 from sqlalchemy.engine import Row
90 from sqlalchemy.orm import Session
92# Globals
93Options: apt_pkg.Configuration
94Logger: daklog.Logger
96Priorities: "Priority_Completer"
97Sections: "Section_Completer"
99################################################################################
100################################################################################
101################################################################################
104class Section_Completer:
105 def __init__(self, session: "Session"):
106 self.sections = []
107 self.matches: list[str] = []
108 for (s,) in session.query(Section.section):
109 self.sections.append(s)
111 def complete(self, text: str, state: int) -> str | None:
112 if state == 0:
113 self.matches = []
114 n = len(text)
115 for word in self.sections:
116 if word[:n] == text:
117 self.matches.append(word)
118 try:
119 return self.matches[state]
120 except IndexError:
121 return None
124############################################################
127class Priority_Completer:
128 def __init__(self, session: "Session"):
129 self.priorities = []
130 self.matches: list[str] = []
131 for (p,) in session.query(Priority.priority):
132 self.priorities.append(p)
134 def complete(self, text: str, state: int) -> str | None:
135 if state == 0:
136 self.matches = []
137 n = len(text)
138 for word in self.priorities:
139 if word[:n] == text:
140 self.matches.append(word)
141 try:
142 return self.matches[state]
143 except IndexError:
144 return None
147################################################################################
150def takenover_binaries(
151 upload: daklib.dbconn.PolicyQueueUpload,
152 missing: list[MissingOverride],
153 session: "Session",
154) -> "list[Row[tuple[str, str]]]":
155 rows = []
156 binaries = set([x.package for x in upload.binaries])
157 for m in missing:
158 if m["type"] != "dsc":
159 binaries.discard(m["package"])
160 if binaries:
161 source = upload.binaries[0].source.source
162 suite = upload.target_suite.overridesuite or upload.target_suite.suite_name
163 suites = [
164 s[0]
165 for s in session.query(Suite.suite_name)
166 .filter(or_(Suite.suite_name == suite, Suite.overridesuite == suite))
167 .all()
168 ]
169 rows = (
170 session.query(DBSource.source, DBBinary.package)
171 .distinct()
172 .filter(DBBinary.package.in_(binaries))
173 .join(DBBinary.source)
174 .filter(DBSource.source != source)
175 .join(DBBinary.suites)
176 .filter(Suite.suite_name.in_(suites))
177 .order_by(DBSource.source, DBBinary.package)
178 .all()
179 )
180 return rows
183################################################################################
186def print_new(
187 upload: daklib.dbconn.PolicyQueueUpload,
188 missing: list[MissingOverride],
189 indexed: bool,
190 session: "Session",
191 file=sys.stdout,
192) -> bool:
193 check_valid(missing, session)
194 for index, m in enumerate(missing, 1):
195 if m["type"] != "deb":
196 package = "{0}:{1}".format(m["type"], m["package"])
197 else:
198 package = m["package"]
199 section = m["section"]
200 priority = m["priority"]
201 if m["type"] == "deb" and priority != "optional": 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true
202 priority = Color(priority, "red")
203 included = "" if m["included"] else "NOT UPLOADED"
204 if indexed: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 line = "(%s): %-20s %-20s %-20s %s" % (
206 index,
207 package,
208 priority,
209 section,
210 included,
211 )
212 else:
213 line = "%-20s %-20s %-20s %s" % (package, priority, section, included)
214 line = line.strip()
215 if not m["valid"]: 215 ↛ 216line 215 didn't jump to line 216 because the condition on line 215 was never true
216 line = line + " [!]"
217 print(line, file=file)
218 takenover = takenover_binaries(upload, missing, session)
219 if takenover: 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 print("\n\nBINARIES TAKEN OVER\n")
221 for t in takenover:
222 print("%s: %s" % (t[0], t[1]))
223 notes = get_new_comments(upload.policy_queue, upload.changes.source)
224 for note in notes: 224 ↛ 225line 224 didn't jump to line 225 because the loop on line 224 never started
225 print("\n")
226 print(Color("Author:", "yellow"), "%s" % note.author)
227 print(Color("Version:", "yellow"), "%s" % note.version)
228 print(Color("Timestamp:", "yellow"), "%s" % note.notedate)
229 print("\n\n")
230 print(note.comment)
231 print("-" * 72)
232 return len(notes) > 0
235################################################################################
238def index_range(index: int) -> str:
239 if index == 1:
240 return "1"
241 else:
242 return "1-%s" % (index)
245################################################################################
248def edit_new(
249 overrides: list[MissingOverride],
250 upload: daklib.dbconn.PolicyQueueUpload,
251 session: "Session",
252) -> list[MissingOverride]:
253 with tempfile.NamedTemporaryFile(mode="w+t") as fh:
254 # Write the current data to a temporary file
255 print_new(upload, overrides, indexed=False, session=session, file=fh)
256 fh.flush()
257 utils.call_editor_for_file(fh.name)
258 # Read the edited data back in
259 fh.seek(0)
260 lines = fh.readlines()
262 overrides_map = dict([((o["type"], o["package"]), o) for o in overrides])
263 new_overrides: list[MissingOverride] = []
264 # Parse the new data
265 for line in lines:
266 line = line.strip()
267 if line == "" or line[0] == "#":
268 continue
269 s = line.split()
270 # Pad the list if necessary
271 s[len(s) : 3] = ["None"] * (3 - len(s))
272 (pkg, priority, section) = s[:3]
273 if pkg.find(":") != -1:
274 type, pkg = pkg.split(":", 1)
275 else:
276 type = "deb"
277 o = overrides_map.get((type, pkg), None)
278 if o is None:
279 utils.warn("Ignoring unknown package '%s'" % (pkg))
280 else:
281 if section.find("/") != -1:
282 component = section.split("/", 1)[0]
283 else:
284 component = "main"
285 new_overrides.append(
286 dict(
287 package=pkg,
288 type=type,
289 section=section,
290 component=component,
291 priority=priority,
292 included=o["included"],
293 )
294 )
295 return new_overrides
298################################################################################
301def edit_index(
302 new: list[MissingOverride], upload: daklib.dbconn.PolicyQueueUpload, index: int
303) -> list[MissingOverride]:
304 package = new[index]["package"]
305 priority = new[index]["priority"]
306 section = new[index]["section"]
307 ftype = new[index]["type"]
308 done = False
309 while not done:
310 print("\t".join([package, priority, section]))
312 answer = "XXX"
313 if ftype != "dsc":
314 prompt = "[B]oth, Priority, Section, Done ? "
315 else:
316 prompt = "[S]ection, Done ? "
317 edit_priority = edit_section = 0
319 while prompt.find(answer) == -1:
320 answer = utils.input_or_exit(prompt)
321 if answer == "":
322 m = re_default_answer.match(prompt)
323 assert m is not None
324 answer = m.group(1)
325 answer = answer[:1].upper()
327 if answer == "P":
328 edit_priority = 1
329 elif answer == "S":
330 edit_section = 1
331 elif answer == "B":
332 edit_priority = edit_section = 1
333 elif answer == "D":
334 done = True
336 # Edit the priority
337 if edit_priority:
338 readline.set_completer(Priorities.complete)
339 got_priority = 0
340 while not got_priority:
341 new_priority = utils.input_or_exit("New priority: ").strip()
342 if new_priority not in Priorities.priorities:
343 print(
344 "E: '%s' is not a valid priority, try again." % (new_priority)
345 )
346 else:
347 got_priority = 1
348 priority = new_priority
350 # Edit the section
351 if edit_section:
352 readline.set_completer(Sections.complete)
353 got_section = 0
354 while not got_section:
355 new_section = utils.input_or_exit("New section: ").strip()
356 if new_section not in Sections.sections:
357 print("E: '%s' is not a valid section, try again." % (new_section))
358 else:
359 got_section = 1
360 section = new_section
362 # Reset the readline completer
363 readline.set_completer(None)
365 new[index]["priority"] = priority
366 new[index]["section"] = section
367 if section.find("/") != -1:
368 component = section.split("/", 1)[0]
369 else:
370 component = "main"
371 new[index]["component"] = component
373 return new
376################################################################################
379def edit_overrides(
380 new: list[MissingOverride],
381 upload: daklib.dbconn.PolicyQueueUpload,
382 session: "Session",
383) -> list[MissingOverride]:
384 print()
385 done = False
386 while not done:
387 print_new(upload, new, indexed=True, session=session)
388 prompt = "edit override <n>, Editor, Done ? "
390 got_answer = 0
391 while not got_answer:
392 answer = utils.input_or_exit(prompt)
393 if not answer.isdigit():
394 answer = answer[:1].upper()
395 if answer == "E" or answer == "D":
396 got_answer = 1
397 elif re_isanum.match(answer):
398 answer_int = int(answer)
399 if answer_int < 1 or answer_int > len(new):
400 print("{0} is not a valid index. Please retry.".format(answer_int))
401 else:
402 got_answer = 1
404 if answer == "E":
405 new = edit_new(new, upload, session)
406 elif answer == "D":
407 done = True
408 else:
409 edit_index(new, upload, answer_int - 1)
411 return new
414################################################################################
417def check_pkg(
418 upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy, session: "Session"
419):
420 changes = os.path.join(upload_copy.directory, upload.changes.changesname)
421 suite_name = upload.target_suite.suite_name
422 handler = PolicyQueueUploadHandler(upload, session)
423 missing = [(m["type"], m["package"]) for m in handler.missing_overrides()]
425 less_cmd = ("less", "-r", "-")
426 less_process = subprocess.Popen(
427 less_cmd, bufsize=0, stdin=subprocess.PIPE, text=True
428 )
429 try:
430 less_fd = less_process.stdin
431 assert less_fd is not None
432 less_fd.write(dak.examine_package.display_changes(suite_name, changes))
434 source = upload.source
435 if source is not None: 435 ↛ 441line 435 didn't jump to line 441 because the condition on line 435 was always true
436 source_file = os.path.join(
437 upload_copy.directory, os.path.basename(source.poolfile.filename)
438 )
439 less_fd.write(dak.examine_package.check_dsc(suite_name, source_file))
441 for binary in upload.binaries:
442 binary_file = os.path.join(
443 upload_copy.directory, os.path.basename(binary.poolfile.filename)
444 )
445 examined = dak.examine_package.check_deb(suite_name, binary_file)
446 # We always need to call check_deb to display package relations for every binary,
447 # but we print its output only if new overrides are being added.
448 if ("deb", binary.package) in missing: 448 ↛ 441line 448 didn't jump to line 441 because the condition on line 448 was always true
449 less_fd.write(examined)
451 less_fd.write(dak.examine_package.output_package_relations())
452 less_fd.close()
453 except OSError as e:
454 if e.errno == errno.EPIPE:
455 utils.warn("[examine_package] Caught EPIPE; skipping.")
456 else:
457 raise
458 except KeyboardInterrupt:
459 utils.warn("[examine_package] Caught C-c; skipping.")
460 finally:
461 less_process.communicate()
464################################################################################
466## FIXME: horribly Debian specific
469def do_bxa_notification(
470 new: list[MissingOverride],
471 upload: daklib.dbconn.PolicyQueueUpload,
472 session: "Session",
473) -> None:
474 cnf = Config()
476 new_packages = set(o["package"] for o in new if o["type"] == "deb")
477 if len(new_packages) == 0:
478 return
480 key = session.query(MetadataKey).filter_by(key="Description").one()
481 summary = ""
482 for binary in upload.binaries:
483 if binary.package not in new_packages:
484 continue
485 description = (
486 session.query(BinaryMetadata).filter_by(binary=binary, key=key).one().value
487 )
488 summary += "\n"
489 summary += "Package: {0}\n".format(binary.package)
490 summary += "Description: {0}\n".format(description)
492 subst = {
493 "__DISTRO__": cnf["Dinstall::MyDistribution"],
494 "__BCC__": "X-DAK: dak process-new",
495 "__BINARY_DESCRIPTIONS__": summary,
496 "__CHANGES_FILENAME__": upload.changes.changesname,
497 "__SOURCE__": upload.changes.source,
498 "__VERSION__": upload.changes.version,
499 "__ARCHITECTURE__": upload.changes.architecture,
500 }
502 bxa_mail = utils.TemplateSubst(
503 subst, os.path.join(cnf["Dir::Templates"], "process-new.bxa_notification")
504 )
505 utils.send_mail(bxa_mail)
508################################################################################
511def run_user_inspect_command(
512 upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy
513) -> None:
514 command = os.environ.get("DAK_INSPECT_UPLOAD")
515 if command is None: 515 ↛ 518line 515 didn't jump to line 518 because the condition on line 515 was always true
516 return
518 directory = upload_copy.directory
519 if upload.source:
520 dsc = os.path.basename(upload.source.poolfile.filename)
521 else:
522 dsc = ""
523 changes = upload.changes.changesname
525 shell_command = command.format(
526 directory=directory,
527 dsc=dsc,
528 changes=changes,
529 )
531 subprocess.check_call(shell_command, shell=True)
534################################################################################
537def get_reject_reason(reason: str = "") -> Optional[str]:
538 """get reason for rejection
540 :return: string giving the reason for the rejection or :const:`None` if the
541 rejection should be cancelled
542 """
543 answer = "E"
544 if Options["Automatic"]: 544 ↛ 545line 544 didn't jump to line 545 because the condition on line 544 was never true
545 answer = "R"
547 while answer == "E":
548 reason = utils.call_editor(reason)
549 print("Reject message:")
550 print(utils.prefix_multi_line_string(reason, " ", include_blank_lines=True))
551 prompt = "[R]eject, Edit, Abandon, Quit ?"
552 answer = "XXX"
553 while prompt.find(answer) == -1:
554 answer = utils.input_or_exit(prompt)
555 if answer == "": 555 ↛ 556line 555 didn't jump to line 556 because the condition on line 555 was never true
556 m = re_default_answer.search(prompt)
557 assert m is not None
558 answer = m.group(1)
559 answer = answer[:1].upper()
561 if answer == "Q": 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true
562 sys.exit(0)
564 if answer == "R": 564 ↛ 566line 564 didn't jump to line 566 because the condition on line 564 was always true
565 return reason
566 return None
569################################################################################
572def do_new(
573 upload: daklib.dbconn.PolicyQueueUpload,
574 upload_copy: UploadCopy,
575 handler: PolicyQueueUploadHandler,
576 session: "Session",
577) -> None:
578 run_user_inspect_command(upload, upload_copy)
580 # The main NEW processing loop
581 done = False
582 missing: list[MissingOverride] = []
583 while not done:
584 queuedir = upload.policy_queue.path
585 byhand = upload.byhand
587 missing = handler.missing_overrides(hints=missing)
588 broken = not check_valid(missing, session)
590 changesname = os.path.basename(upload.changes.changesname)
592 print()
593 print(changesname)
594 print("-" * len(changesname))
595 print()
596 print(" Target: {0}".format(upload.target_suite.suite_name))
597 print(" Changed-By: {0}".format(upload.changes.changedby))
598 print(" Date: {0}".format(upload.changes.date))
599 print()
601 if missing:
602 print("NEW\n")
604 for package in missing:
605 if package["type"] == "deb" and package["priority"] == "extra": 605 ↛ 606line 605 didn't jump to line 606 because the condition on line 605 was never true
606 package["priority"] = "optional"
608 answer = "XXX"
609 if Options["No-Action"] or Options["Automatic"]:
610 answer = "S"
612 note = print_new(upload, missing, indexed=False, session=session)
613 prompt = ""
615 has_unprocessed_byhand = False
616 for f in byhand: 616 ↛ 617line 616 didn't jump to line 617 because the loop on line 616 never started
617 path = os.path.join(queuedir, f.filename)
618 if not f.processed and os.path.exists(path):
619 print(
620 "W: {0} still present; please process byhand components and try again".format(
621 f.filename
622 )
623 )
624 has_unprocessed_byhand = True
626 if not has_unprocessed_byhand and not broken and not note: 626 ↛ 632line 626 didn't jump to line 632 because the condition on line 626 was always true
627 if len(missing) == 0:
628 prompt = "Accept, "
629 answer = "A"
630 else:
631 prompt = "Add overrides, "
632 if broken: 632 ↛ 633line 632 didn't jump to line 633 because the condition on line 632 was never true
633 print(
634 "W: [!] marked entries must be fixed before package can be processed."
635 )
636 if note: 636 ↛ 637line 636 didn't jump to line 637 because the condition on line 636 was never true
637 print("W: note must be removed before package can be processed.")
638 prompt += "RemOve all notes, Remove note, "
640 prompt += (
641 "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
642 )
644 while prompt.find(answer) == -1:
645 answer = utils.input_or_exit(prompt)
646 if answer == "": 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true
647 m = re_default_answer.search(prompt)
648 assert m is not None
649 answer = m.group(1)
650 answer = answer[:1].upper()
652 if answer in ("A", "E", "M", "O", "R") and Options["Trainee"]: 652 ↛ 653line 652 didn't jump to line 653 because the condition on line 652 was never true
653 utils.warn("Trainees can't do that")
654 continue
656 if answer == "A" and not Options["Trainee"]:
657 handler.add_overrides(missing, upload.target_suite)
658 if Config().find_b("Dinstall::BXANotify"): 658 ↛ 660line 658 didn't jump to line 660 because the condition on line 658 was always true
659 do_bxa_notification(missing, upload, session)
660 handler.accept()
661 done = True
662 Logger.log(["NEW ACCEPT", upload.changes.changesname])
663 elif answer == "C":
664 check_pkg(upload, upload_copy, session)
665 elif answer == "E" and not Options["Trainee"]: 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true
666 missing = edit_overrides(missing, upload, session)
667 elif answer == "M" and not Options["Trainee"]:
668 reason = Options.get("Manual-Reject", "") + "\n"
669 reason = reason + "\n\n=====\n\n".join(
670 [
671 n.comment
672 for n in get_new_comments(
673 upload.policy_queue, upload.changes.source, session=session
674 )
675 ]
676 )
677 edited_reason = get_reject_reason(reason)
678 if edited_reason is not None: 678 ↛ 734line 678 didn't jump to line 734 because the condition on line 678 was always true
679 Logger.log(["NEW REJECT", upload.changes.changesname])
680 handler.reject(edited_reason)
681 done = True
682 elif answer == "N": 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true
683 if (
684 edit_note(
685 upload,
686 session,
687 bool(Options["Trainee"]),
688 )
689 == 0
690 ):
691 end()
692 sys.exit(0)
693 elif answer == "P" and not Options["Trainee"]: 693 ↛ 694line 693 didn't jump to line 694 because the condition on line 693 was never true
694 if (
695 prod_maintainer(
696 get_new_comments(
697 upload.policy_queue, upload.changes.source, session=session
698 ),
699 upload,
700 session,
701 bool(Options["Trainee"]),
702 )
703 == 0
704 ):
705 end()
706 sys.exit(0)
707 Logger.log(["NEW PROD", upload.changes.changesname])
708 elif answer == "R" and not Options["Trainee"]: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true
709 confirm = utils.input_or_exit("Really clear note (y/N)? ").lower()
710 if confirm == "y":
711 for c in get_new_comments(
712 upload.policy_queue,
713 upload.changes.source,
714 upload.changes.version,
715 session=session,
716 ):
717 session.delete(c)
718 session.commit()
719 elif answer == "O" and not Options["Trainee"]: 719 ↛ 720line 719 didn't jump to line 720 because the condition on line 719 was never true
720 confirm = utils.input_or_exit("Really clear all notes (y/N)? ").lower()
721 if confirm == "y":
722 for c in get_new_comments(
723 upload.policy_queue, upload.changes.source, session=session
724 ):
725 session.delete(c)
726 session.commit()
728 elif answer == "S": 728 ↛ 730line 728 didn't jump to line 730 because the condition on line 728 was always true
729 done = True
730 elif answer == "Q":
731 end()
732 sys.exit(0)
734 if handler.get_action():
735 print("PENDING %s\n" % handler.get_action())
738################################################################################
739################################################################################
740################################################################################
743def usage(exit_code: int = 0) -> NoReturn:
744 print(
745 """Usage: dak process-new [OPTION]... [CHANGES]...
746 -a, --automatic automatic run
747 -b, --no-binaries do not sort binary-NEW packages first
748 -c, --comments show NEW comments
749 -h, --help show this help and exit.
750 -m, --manual-reject=MSG manual reject with `msg'
751 -n, --no-action don't do anything
752 -q, --queue=QUEUE operate on a different queue
753 -t, --trainee FTP Trainee mode
754 -V, --version display the version number and exit
756ENVIRONMENT VARIABLES
758 DAK_INSPECT_UPLOAD: shell command to run to inspect a package
759 The command is automatically run in a shell when an upload
760 is checked. The following substitutions are available:
762 {directory}: directory the upload is contained in
763 {dsc}: name of the included dsc or the empty string
764 {changes}: name of the changes file
766 Note that Python's 'format' method is used to format the command.
768 Example: run mc in a tmux session to inspect the upload
770 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"'
772 and run
774 tmux attach -t process-new
776 in a separate terminal session.
777"""
778 )
779 sys.exit(exit_code)
782################################################################################
785@contextlib.contextmanager
786def lock_package(package: str) -> Iterator[int]:
787 """
788 Lock `package` so that noone else jumps in processing it.
790 :param package: source package name to lock
791 """
793 cnf = Config()
795 path = os.path.join(cnf.get("Process-New::LockDir", cnf["Dir::Lock"]), package)
797 try:
798 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY, 0o644)
799 except OSError as e:
800 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
801 try:
802 user = (
803 pwd.getpwuid(os.stat(path)[stat.ST_UID])[4]
804 .split(",")[0]
805 .replace(".", "")
806 )
807 except KeyError:
808 user = "TotallyUnknown"
809 raise AlreadyLockedError(user)
810 raise
812 try:
813 yield fd
814 finally:
815 os.unlink(path)
818def do_pkg(upload: daklib.dbconn.PolicyQueueUpload, session: "Session") -> None:
819 cnf = Config()
820 group = cnf.get("Dinstall::UnprivGroup") or None
822 try:
823 with (
824 lock_package(upload.changes.source),
825 UploadCopy(upload, group=group) as upload_copy,
826 ):
827 handler = PolicyQueueUploadHandler(upload, session)
828 if handler.get_action() is not None:
829 print("PENDING %s\n" % handler.get_action())
830 return
832 do_new(upload, upload_copy, handler, session)
833 except AlreadyLockedError as e:
834 print("Seems to be locked by %s already, skipping..." % (e))
837def show_new_comments(
838 uploads: Iterable[daklib.dbconn.PolicyQueueUpload], session: "Session"
839) -> None:
840 sources = [upload.changes.source for upload in uploads]
841 if len(sources) == 0:
842 return
844 query = """SELECT package, version, comment, author
845 FROM new_comments
846 WHERE package IN :sources
847 ORDER BY package, version"""
849 r = session.execute(sql.text(query), params={"sources": tuple(sources)})
851 for i in r:
852 print("%s_%s\n%s\n(%s)\n\n\n" % (i[0], i[1], i[2], i[3]))
854 session.rollback()
857################################################################################
860class Change(TypedDict):
861 upload: daklib.dbconn.PolicyQueueUpload
862 date: datetime.datetime
863 stack: int
864 binary: bool
865 comments: bool
868def sort_uploads(
869 new_queue: PolicyQueue,
870 uploads: Iterable[daklib.dbconn.PolicyQueueUpload],
871 session: "Session",
872 nobinaries: bool = False,
873) -> list[daklib.dbconn.PolicyQueueUpload]:
874 sources: dict[str, list[Change]] = defaultdict(list)
875 sortedchanges = []
876 suitesrc = [
877 s.source
878 for s in session.query(DBSource.source).filter(
879 DBSource.suites.any(Suite.suite_name.in_(["unstable", "experimental"]))
880 )
881 ]
882 comments = [
883 p.package
884 for p in session.query(NewComment.package)
885 .filter_by(trainee=False, policy_queue=new_queue)
886 .distinct()
887 ]
888 for upload in uploads:
889 source = upload.changes.source
890 sources[source].append(
891 {
892 "upload": upload,
893 "date": upload.changes.created,
894 "stack": 1,
895 "binary": True if source in suitesrc else False,
896 "comments": True if source in comments else False,
897 }
898 )
899 for src in sources:
900 if len(sources[src]) > 1:
901 changes = sources[src]
902 firstseen = sorted(changes, key=lambda k: (k["date"]))[0]["date"]
903 changes.sort(key=lambda item: item["date"])
904 for i in range(0, len(changes)):
905 changes[i]["date"] = firstseen
906 changes[i]["stack"] = i + 1
907 sortedchanges += sources[src]
908 if nobinaries: 908 ↛ 909line 908 didn't jump to line 909 because the condition on line 908 was never true
909 sortedchanges.sort(
910 key=lambda k: (k["comments"], k["binary"], k["date"], -k["stack"])
911 )
912 else:
913 sortedchanges.sort(
914 key=lambda k: (k["comments"], -k["binary"], k["date"], -k["stack"])
915 )
916 return [u["upload"] for u in sortedchanges]
919################################################################################
922def end() -> None:
923 accept_count = SummaryStats().accept_count
924 accept_bytes = SummaryStats().accept_bytes
926 if accept_count: 926 ↛ 927line 926 didn't jump to line 927 because the condition on line 926 was never true
927 sets = "set"
928 if accept_count > 1:
929 sets = "sets"
930 print(
931 "Accepted %d package %s, %s."
932 % (accept_count, sets, utils.size_type(int(accept_bytes))),
933 file=sys.stderr,
934 )
935 Logger.log(["total", accept_count, accept_bytes])
937 if not Options["No-Action"] and not Options["Trainee"]: 937 ↛ exitline 937 didn't return from function 'end' because the condition on line 937 was always true
938 Logger.close()
941################################################################################
944def main() -> None:
945 global Options, Logger, Sections, Priorities
947 cnf = Config()
948 session = DBConn().session()
950 Arguments = [
951 ("a", "automatic", "Process-New::Options::Automatic"),
952 ("b", "no-binaries", "Process-New::Options::No-Binaries"),
953 ("c", "comments", "Process-New::Options::Comments"),
954 ("h", "help", "Process-New::Options::Help"),
955 ("m", "manual-reject", "Process-New::Options::Manual-Reject", "HasArg"),
956 ("t", "trainee", "Process-New::Options::Trainee"),
957 ("q", "queue", "Process-New::Options::Queue", "HasArg"),
958 ("n", "no-action", "Process-New::Options::No-Action"),
959 ]
961 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
963 for i in [
964 "automatic",
965 "no-binaries",
966 "comments",
967 "help",
968 "manual-reject",
969 "no-action",
970 "version",
971 "trainee",
972 ]:
973 key = "Process-New::Options::%s" % i
974 if key not in cnf:
975 cnf[key] = ""
977 queue_name = cnf.get("Process-New::Options::Queue", "new")
978 new_queue = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
979 if len(changes_files) == 0:
980 uploads = new_queue.uploads
981 else:
982 uploads = (
983 session.query(PolicyQueueUpload)
984 .filter_by(policy_queue=new_queue)
985 .join(DBChange)
986 .filter(DBChange.changesname.in_(changes_files))
987 .all()
988 )
990 Options = cnf.subtree("Process-New::Options")
992 if Options["Help"]:
993 usage()
995 if not Options["No-Action"]: 995 ↛ 1001line 995 didn't jump to line 1001 because the condition on line 995 was always true
996 try:
997 Logger = daklog.Logger("process-new")
998 except OSError:
999 Options["Trainee"] = "True" # type: ignore[index]
1001 Sections = Section_Completer(session)
1002 Priorities = Priority_Completer(session)
1003 readline.parse_and_bind("tab: complete")
1005 if len(uploads) > 1:
1006 print("Sorting changes...", file=sys.stderr)
1007 uploads = sort_uploads(
1008 new_queue, uploads, session, bool(Options["No-Binaries"])
1009 )
1011 if Options["Comments"]: 1011 ↛ 1012line 1011 didn't jump to line 1012 because the condition on line 1011 was never true
1012 show_new_comments(uploads, session)
1013 else:
1014 for upload in uploads:
1015 do_pkg(upload, session)
1017 end()
1020################################################################################
1023if __name__ == "__main__":
1024 main()