Coverage for dak/process_new.py: 54%
508 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-05-10 21:38 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-05-10 21:38 +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 edit_new(
239 overrides: list[MissingOverride],
240 upload: daklib.dbconn.PolicyQueueUpload,
241 session: "Session",
242) -> list[MissingOverride]:
243 with tempfile.NamedTemporaryFile(mode="w+t") as fh:
244 # Write the current data to a temporary file
245 print_new(upload, overrides, indexed=False, session=session, file=fh)
246 fh.flush()
247 utils.call_editor_for_file(fh.name)
248 # Read the edited data back in
249 fh.seek(0)
250 lines = fh.readlines()
252 overrides_map = dict([((o["type"], o["package"]), o) for o in overrides])
253 new_overrides: list[MissingOverride] = []
254 # Parse the new data
255 for line in lines:
256 line = line.strip()
257 if line == "" or line[0] == "#":
258 continue
259 s = line.split()
260 # Pad the list if necessary
261 s[len(s) : 3] = ["None"] * (3 - len(s))
262 (pkg, priority, section) = s[:3]
263 if pkg.find(":") != -1:
264 type, pkg = pkg.split(":", 1)
265 else:
266 type = "deb"
267 o = overrides_map.get((type, pkg), None)
268 if o is None:
269 utils.warn("Ignoring unknown package '%s'" % (pkg))
270 else:
271 if section.find("/") != -1:
272 component = section.split("/", 1)[0]
273 else:
274 component = "main"
275 new_overrides.append(
276 dict(
277 package=pkg,
278 type=type,
279 section=section,
280 component=component,
281 priority=priority,
282 included=o["included"],
283 )
284 )
285 return new_overrides
288################################################################################
291def edit_index(
292 new: list[MissingOverride], upload: daklib.dbconn.PolicyQueueUpload, index: int
293) -> list[MissingOverride]:
294 package = new[index]["package"]
295 priority = new[index]["priority"]
296 section = new[index]["section"]
297 ftype = new[index]["type"]
298 done = False
299 while not done:
300 print("\t".join([package, priority, section]))
302 answer = "XXX"
303 if ftype != "dsc":
304 prompt = "[B]oth, Priority, Section, Done ? "
305 else:
306 prompt = "[S]ection, Done ? "
307 edit_priority = edit_section = 0
309 while prompt.find(answer) == -1:
310 answer = utils.input_or_exit(prompt)
311 if answer == "":
312 m = re_default_answer.match(prompt)
313 assert m is not None
314 answer = m.group(1)
315 answer = answer[:1].upper()
317 if answer == "P":
318 edit_priority = 1
319 elif answer == "S":
320 edit_section = 1
321 elif answer == "B":
322 edit_priority = edit_section = 1
323 elif answer == "D":
324 done = True
326 # Edit the priority
327 if edit_priority:
328 readline.set_completer(Priorities.complete)
329 got_priority = 0
330 while not got_priority:
331 new_priority = utils.input_or_exit("New priority: ").strip()
332 if new_priority not in Priorities.priorities:
333 print(
334 "E: '%s' is not a valid priority, try again." % (new_priority)
335 )
336 else:
337 got_priority = 1
338 priority = new_priority
340 # Edit the section
341 if edit_section:
342 readline.set_completer(Sections.complete)
343 got_section = 0
344 while not got_section:
345 new_section = utils.input_or_exit("New section: ").strip()
346 if new_section not in Sections.sections:
347 print("E: '%s' is not a valid section, try again." % (new_section))
348 else:
349 got_section = 1
350 section = new_section
352 # Reset the readline completer
353 readline.set_completer(None)
355 new[index]["priority"] = priority
356 new[index]["section"] = section
357 if section.find("/") != -1:
358 component = section.split("/", 1)[0]
359 else:
360 component = "main"
361 new[index]["component"] = component
363 return new
366################################################################################
369def edit_overrides(
370 new: list[MissingOverride],
371 upload: daklib.dbconn.PolicyQueueUpload,
372 session: "Session",
373) -> list[MissingOverride]:
374 print()
375 done = False
376 while not done:
377 print_new(upload, new, indexed=True, session=session)
378 prompt = "edit override <n>, Editor, Done ? "
380 got_answer = 0
381 while not got_answer:
382 answer = utils.input_or_exit(prompt)
383 if not answer.isdigit():
384 answer = answer[:1].upper()
385 if answer == "E" or answer == "D":
386 got_answer = 1
387 elif re_isanum.match(answer):
388 answer_int = int(answer)
389 if answer_int < 1 or answer_int > len(new):
390 print("{0} is not a valid index. Please retry.".format(answer_int))
391 else:
392 got_answer = 1
394 if answer == "E":
395 new = edit_new(new, upload, session)
396 elif answer == "D":
397 done = True
398 else:
399 edit_index(new, upload, answer_int - 1)
401 return new
404################################################################################
407def check_pkg(
408 upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy, session: "Session"
409):
410 changes = os.path.join(upload_copy.directory, upload.changes.changesname)
411 suite_name = upload.target_suite.suite_name
412 handler = PolicyQueueUploadHandler(upload, session)
413 missing = [(m["type"], m["package"]) for m in handler.missing_overrides()]
415 less_cmd = ("less", "-r", "-")
416 less_process = subprocess.Popen(
417 less_cmd, bufsize=0, stdin=subprocess.PIPE, text=True
418 )
419 try:
420 less_fd = less_process.stdin
421 assert less_fd is not None
422 less_fd.write(dak.examine_package.display_changes(suite_name, changes))
424 source = upload.source
425 if source is not None: 425 ↛ 431line 425 didn't jump to line 431 because the condition on line 425 was always true
426 source_file = os.path.join(
427 upload_copy.directory, os.path.basename(source.poolfile.filename)
428 )
429 less_fd.write(dak.examine_package.check_dsc(suite_name, source_file))
431 for binary in upload.binaries:
432 binary_file = os.path.join(
433 upload_copy.directory, os.path.basename(binary.poolfile.filename)
434 )
435 examined = dak.examine_package.check_deb(suite_name, binary_file)
436 # We always need to call check_deb to display package relations for every binary,
437 # but we print its output only if new overrides are being added.
438 if ("deb", binary.package) in missing: 438 ↛ 431line 438 didn't jump to line 431 because the condition on line 438 was always true
439 less_fd.write(examined)
441 less_fd.write(dak.examine_package.output_package_relations())
442 less_fd.close()
443 except OSError as e:
444 if e.errno == errno.EPIPE:
445 utils.warn("[examine_package] Caught EPIPE; skipping.")
446 else:
447 raise
448 except KeyboardInterrupt:
449 utils.warn("[examine_package] Caught C-c; skipping.")
450 finally:
451 less_process.communicate()
454################################################################################
456## FIXME: horribly Debian specific
459def do_bxa_notification(
460 new: list[MissingOverride],
461 upload: daklib.dbconn.PolicyQueueUpload,
462 session: "Session",
463) -> None:
464 cnf = Config()
466 new_packages = set(o["package"] for o in new if o["type"] == "deb")
467 if len(new_packages) == 0:
468 return
470 key = session.query(MetadataKey).filter_by(key="Description").one()
471 summary = ""
472 for binary in upload.binaries:
473 if binary.package not in new_packages:
474 continue
475 description = (
476 session.query(BinaryMetadata).filter_by(binary=binary, key=key).one().value
477 )
478 summary += "\n"
479 summary += "Package: {0}\n".format(binary.package)
480 summary += "Description: {0}\n".format(description)
482 subst = {
483 "__DISTRO__": cnf["Dinstall::MyDistribution"],
484 "__BCC__": "X-DAK: dak process-new",
485 "__BINARY_DESCRIPTIONS__": summary,
486 "__CHANGES_FILENAME__": upload.changes.changesname,
487 "__SOURCE__": upload.changes.source,
488 "__VERSION__": upload.changes.version,
489 "__ARCHITECTURE__": upload.changes.architecture,
490 }
492 bxa_mail = utils.TemplateSubst(
493 subst, os.path.join(cnf["Dir::Templates"], "process-new.bxa_notification")
494 )
495 utils.send_mail(bxa_mail)
498################################################################################
501def run_user_inspect_command(
502 upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy
503) -> None:
504 command = os.environ.get("DAK_INSPECT_UPLOAD")
505 if command is None: 505 ↛ 508line 505 didn't jump to line 508 because the condition on line 505 was always true
506 return
508 directory = upload_copy.directory
509 if upload.source:
510 dsc = os.path.basename(upload.source.poolfile.filename)
511 else:
512 dsc = ""
513 changes = upload.changes.changesname
515 shell_command = command.format(
516 directory=directory,
517 dsc=dsc,
518 changes=changes,
519 )
521 subprocess.check_call(shell_command, shell=True)
524################################################################################
527def get_reject_reason(reason: str = "") -> Optional[str]:
528 """get reason for rejection
530 :return: string giving the reason for the rejection or :const:`None` if the
531 rejection should be cancelled
532 """
533 answer = "E"
534 if Options["Automatic"]: 534 ↛ 535line 534 didn't jump to line 535 because the condition on line 534 was never true
535 answer = "R"
537 while answer == "E":
538 reason = utils.call_editor(reason)
539 print("Reject message:")
540 print(utils.prefix_multi_line_string(reason, " ", include_blank_lines=True))
541 prompt = "[R]eject, Edit, Abandon, Quit ?"
542 answer = "XXX"
543 while prompt.find(answer) == -1:
544 answer = utils.input_or_exit(prompt)
545 if answer == "": 545 ↛ 546line 545 didn't jump to line 546 because the condition on line 545 was never true
546 m = re_default_answer.search(prompt)
547 assert m is not None
548 answer = m.group(1)
549 answer = answer[:1].upper()
551 if answer == "Q": 551 ↛ 552line 551 didn't jump to line 552 because the condition on line 551 was never true
552 sys.exit(0)
554 if answer == "R": 554 ↛ 556line 554 didn't jump to line 556 because the condition on line 554 was always true
555 return reason
556 return None
559################################################################################
562def do_new(
563 upload: daklib.dbconn.PolicyQueueUpload,
564 upload_copy: UploadCopy,
565 handler: PolicyQueueUploadHandler,
566 session: "Session",
567) -> None:
568 cnf = Config()
570 run_user_inspect_command(upload, upload_copy)
572 # The main NEW processing loop
573 done = False
574 missing: list[MissingOverride] = []
575 while not done:
576 queuedir = upload.policy_queue.path
577 byhand = upload.byhand
579 missing = handler.missing_overrides(hints=missing)
580 broken = not check_valid(missing, session)
582 changesname = os.path.basename(upload.changes.changesname)
584 print()
585 print(changesname)
586 print("-" * len(changesname))
587 print()
588 print(" Target: {0}".format(upload.target_suite.suite_name))
589 print(" Changed-By: {0}".format(upload.changes.changedby))
590 print(" Date: {0}".format(upload.changes.date))
591 print()
593 if missing:
594 print("NEW\n")
596 for package in missing:
597 if package["type"] == "deb" and package["priority"] == "extra": 597 ↛ 598line 597 didn't jump to line 598 because the condition on line 597 was never true
598 package["priority"] = "optional"
600 answer = "XXX"
601 if Options["No-Action"] or Options["Automatic"]:
602 answer = "S"
604 note = print_new(upload, missing, indexed=False, session=session)
605 prompt = ""
607 has_unprocessed_byhand = False
608 for f in byhand: 608 ↛ 609line 608 didn't jump to line 609 because the loop on line 608 never started
609 path = os.path.join(queuedir, f.filename)
610 if not f.processed and os.path.exists(path):
611 print(
612 "W: {0} still present; please process byhand components and try again".format(
613 f.filename
614 )
615 )
616 has_unprocessed_byhand = True
618 if not has_unprocessed_byhand and not broken and not note: 618 ↛ 624line 618 didn't jump to line 624 because the condition on line 618 was always true
619 if len(missing) == 0:
620 prompt = "Accept, "
621 answer = "A"
622 else:
623 prompt = "Add overrides, "
624 if broken: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true
625 print(
626 "W: [!] marked entries must be fixed before package can be processed."
627 )
628 if note: 628 ↛ 629line 628 didn't jump to line 629 because the condition on line 628 was never true
629 print("W: note must be removed before package can be processed.")
630 prompt += "RemOve all notes, Remove note, "
632 prompt += (
633 "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?"
634 )
636 while prompt.find(answer) == -1:
637 answer = utils.input_or_exit(prompt)
638 if answer == "": 638 ↛ 639line 638 didn't jump to line 639 because the condition on line 638 was never true
639 m = re_default_answer.search(prompt)
640 assert m is not None
641 answer = m.group(1)
642 answer = answer[:1].upper()
644 if answer in ("A", "E", "M", "O", "R") and Options["Trainee"]: 644 ↛ 645line 644 didn't jump to line 645 because the condition on line 644 was never true
645 utils.warn("Trainees can't do that")
646 continue
648 if answer == "A" and not Options["Trainee"]:
649 handler.add_overrides(missing, upload.target_suite)
650 if Config().find_b("Dinstall::BXANotify"): 650 ↛ 652line 650 didn't jump to line 652 because the condition on line 650 was always true
651 do_bxa_notification(missing, upload, session)
652 handler.accept()
653 done = True
654 Logger.log(["NEW ACCEPT", upload.changes.changesname])
655 elif answer == "C":
656 check_pkg(upload, upload_copy, session)
657 elif answer == "E" and not Options["Trainee"]: 657 ↛ 658line 657 didn't jump to line 658 because the condition on line 657 was never true
658 missing = edit_overrides(missing, upload, session)
659 elif answer == "M" and not Options["Trainee"]:
660 reason = Options.get("Manual-Reject", "") + "\n"
661 reason = reason + "\n\n=====\n\n".join(
662 [
663 n.comment
664 for n in get_new_comments(
665 upload.policy_queue, upload.changes.source, session=session
666 )
667 ]
668 )
669 edited_reason = get_reject_reason(reason)
670 if edited_reason is not None: 670 ↛ 729line 670 didn't jump to line 729 because the condition on line 670 was always true
671 Logger.log(["NEW REJECT", upload.changes.changesname])
672 handler.reject(
673 edited_reason,
674 rejected_by=f"{utils.whoami()} <{cnf['Dinstall::MyAdminAddress']}>",
675 )
676 done = True
677 elif answer == "N": 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true
678 if (
679 edit_note(
680 upload,
681 session,
682 bool(Options["Trainee"]),
683 )
684 == 0
685 ):
686 end()
687 sys.exit(0)
688 elif answer == "P" and not Options["Trainee"]: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true
689 if (
690 prod_maintainer(
691 get_new_comments(
692 upload.policy_queue, upload.changes.source, session=session
693 ),
694 upload,
695 session,
696 bool(Options["Trainee"]),
697 )
698 == 0
699 ):
700 end()
701 sys.exit(0)
702 Logger.log(["NEW PROD", upload.changes.changesname])
703 elif answer == "R" and not Options["Trainee"]: 703 ↛ 704line 703 didn't jump to line 704 because the condition on line 703 was never true
704 confirm = utils.input_or_exit("Really clear note (y/N)? ").lower()
705 if confirm == "y":
706 for c in get_new_comments(
707 upload.policy_queue,
708 upload.changes.source,
709 upload.changes.version,
710 session=session,
711 ):
712 session.delete(c)
713 session.commit()
714 elif answer == "O" and not Options["Trainee"]: 714 ↛ 715line 714 didn't jump to line 715 because the condition on line 714 was never true
715 confirm = utils.input_or_exit("Really clear all notes (y/N)? ").lower()
716 if confirm == "y":
717 for c in get_new_comments(
718 upload.policy_queue, upload.changes.source, session=session
719 ):
720 session.delete(c)
721 session.commit()
723 elif answer == "S": 723 ↛ 725line 723 didn't jump to line 725 because the condition on line 723 was always true
724 done = True
725 elif answer == "Q":
726 end()
727 sys.exit(0)
729 if handler.get_action():
730 print("PENDING %s\n" % handler.get_action())
733################################################################################
734################################################################################
735################################################################################
738def usage(exit_code: int = 0) -> NoReturn:
739 print(
740 """Usage: dak process-new [OPTION]... [CHANGES]...
741 -a, --automatic automatic run
742 -b, --no-binaries do not sort binary-NEW packages first
743 -c, --comments show NEW comments
744 -h, --help show this help and exit.
745 -m, --manual-reject=MSG manual reject with `msg'
746 -n, --no-action don't do anything
747 -q, --queue=QUEUE operate on a different queue
748 -t, --trainee FTP Trainee mode
749 -V, --version display the version number and exit
751ENVIRONMENT VARIABLES
753 DAK_INSPECT_UPLOAD: shell command to run to inspect a package
754 The command is automatically run in a shell when an upload
755 is checked. The following substitutions are available:
757 {directory}: directory the upload is contained in
758 {dsc}: name of the included dsc or the empty string
759 {changes}: name of the changes file
761 Note that Python's 'format' method is used to format the command.
763 Example: run mc in a tmux session to inspect the upload
765 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"'
767 and run
769 tmux attach -t process-new
771 in a separate terminal session.
772"""
773 )
774 sys.exit(exit_code)
777################################################################################
780@contextlib.contextmanager
781def lock_package(package: str) -> Iterator[int]:
782 """
783 Lock `package` so that noone else jumps in processing it.
785 :param package: source package name to lock
786 """
788 cnf = Config()
790 path = os.path.join(cnf.get("Process-New::LockDir", cnf["Dir::Lock"]), package)
792 try:
793 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY, 0o644)
794 except OSError as e:
795 if e.errno == errno.EEXIST or e.errno == errno.EACCES:
796 try:
797 user = (
798 pwd.getpwuid(os.stat(path)[stat.ST_UID])[4]
799 .split(",")[0]
800 .replace(".", "")
801 )
802 except KeyError:
803 user = "TotallyUnknown"
804 raise AlreadyLockedError(user)
805 raise
807 try:
808 yield fd
809 finally:
810 os.unlink(path)
813def do_pkg(upload: daklib.dbconn.PolicyQueueUpload, session: "Session") -> None:
814 cnf = Config()
815 group = cnf.get("Dinstall::UnprivGroup") or None
817 try:
818 with (
819 lock_package(upload.changes.source),
820 UploadCopy(upload, group=group) as upload_copy,
821 ):
822 handler = PolicyQueueUploadHandler(upload, session)
823 if handler.get_action() is not None:
824 print("PENDING %s\n" % handler.get_action())
825 return
827 do_new(upload, upload_copy, handler, session)
828 except AlreadyLockedError as e:
829 print("Seems to be locked by %s already, skipping..." % (e))
832def show_new_comments(
833 uploads: Iterable[daklib.dbconn.PolicyQueueUpload], session: "Session"
834) -> None:
835 sources = [upload.changes.source for upload in uploads]
836 if len(sources) == 0:
837 return
839 query = """SELECT package, version, comment, author
840 FROM new_comments
841 WHERE package IN :sources
842 ORDER BY package, version"""
844 r = session.execute(sql.text(query), params={"sources": tuple(sources)})
846 for i in r:
847 print("%s_%s\n%s\n(%s)\n\n\n" % (i[0], i[1], i[2], i[3]))
849 session.rollback()
852################################################################################
855class Change(TypedDict):
856 upload: daklib.dbconn.PolicyQueueUpload
857 date: datetime.datetime
858 stack: int
859 binary: bool
860 comments: bool
863def sort_uploads(
864 new_queue: PolicyQueue,
865 uploads: Iterable[daklib.dbconn.PolicyQueueUpload],
866 session: "Session",
867 nobinaries: bool = False,
868) -> list[daklib.dbconn.PolicyQueueUpload]:
869 sources: dict[str, list[Change]] = defaultdict(list)
870 sortedchanges = []
871 suitesrc = [
872 s.source
873 for s in session.query(DBSource.source).filter(
874 DBSource.suites.any(Suite.suite_name.in_(["unstable", "experimental"]))
875 )
876 ]
877 comments = [
878 p.package
879 for p in session.query(NewComment.package)
880 .filter_by(trainee=False, policy_queue=new_queue)
881 .distinct()
882 ]
883 for upload in uploads:
884 source = upload.changes.source
885 sources[source].append(
886 {
887 "upload": upload,
888 "date": upload.changes.created,
889 "stack": 1,
890 "binary": True if source in suitesrc else False,
891 "comments": True if source in comments else False,
892 }
893 )
894 for src in sources:
895 if len(sources[src]) > 1:
896 changes = sources[src]
897 firstseen = sorted(changes, key=lambda k: (k["date"]))[0]["date"]
898 changes.sort(key=lambda item: item["date"])
899 for i in range(0, len(changes)):
900 changes[i]["date"] = firstseen
901 changes[i]["stack"] = i + 1
902 sortedchanges += sources[src]
903 if nobinaries: 903 ↛ 904line 903 didn't jump to line 904 because the condition on line 903 was never true
904 sortedchanges.sort(
905 key=lambda k: (k["comments"], k["binary"], k["date"], -k["stack"])
906 )
907 else:
908 sortedchanges.sort(
909 key=lambda k: (k["comments"], -k["binary"], k["date"], -k["stack"])
910 )
911 return [u["upload"] for u in sortedchanges]
914################################################################################
917def end() -> None:
918 accept_count = SummaryStats().accept_count
919 accept_bytes = SummaryStats().accept_bytes
921 if accept_count: 921 ↛ 922line 921 didn't jump to line 922 because the condition on line 921 was never true
922 sets = "set"
923 if accept_count > 1:
924 sets = "sets"
925 print(
926 "Accepted %d package %s, %s."
927 % (accept_count, sets, utils.size_type(int(accept_bytes))),
928 file=sys.stderr,
929 )
930 Logger.log(["total", accept_count, accept_bytes])
932 if not Options["No-Action"] and not Options["Trainee"]: 932 ↛ exitline 932 didn't return from function 'end' because the condition on line 932 was always true
933 Logger.close()
936################################################################################
939def main() -> None:
940 global Options, Logger, Sections, Priorities
942 cnf = Config()
943 session = DBConn().session()
945 Arguments = [
946 ("a", "automatic", "Process-New::Options::Automatic"),
947 ("b", "no-binaries", "Process-New::Options::No-Binaries"),
948 ("c", "comments", "Process-New::Options::Comments"),
949 ("h", "help", "Process-New::Options::Help"),
950 ("m", "manual-reject", "Process-New::Options::Manual-Reject", "HasArg"),
951 ("t", "trainee", "Process-New::Options::Trainee"),
952 ("q", "queue", "Process-New::Options::Queue", "HasArg"),
953 ("n", "no-action", "Process-New::Options::No-Action"),
954 ]
956 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
958 for i in [
959 "automatic",
960 "no-binaries",
961 "comments",
962 "help",
963 "manual-reject",
964 "no-action",
965 "version",
966 "trainee",
967 ]:
968 key = "Process-New::Options::%s" % i
969 if key not in cnf:
970 cnf[key] = ""
972 queue_name = cnf.get("Process-New::Options::Queue", "new")
973 new_queue = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
974 if len(changes_files) == 0:
975 uploads = new_queue.uploads
976 else:
977 uploads = (
978 session.query(PolicyQueueUpload)
979 .filter_by(policy_queue=new_queue)
980 .join(DBChange)
981 .filter(DBChange.changesname.in_(changes_files))
982 .all()
983 )
985 Options = cnf.subtree("Process-New::Options")
987 if Options["Help"]:
988 usage()
990 if not Options["No-Action"]: 990 ↛ 996line 990 didn't jump to line 996 because the condition on line 990 was always true
991 try:
992 Logger = daklog.Logger("process-new")
993 except OSError:
994 Options["Trainee"] = "True" # type: ignore[index]
996 Sections = Section_Completer(session)
997 Priorities = Priority_Completer(session)
998 readline.parse_and_bind("tab: complete")
1000 if len(uploads) > 1:
1001 print("Sorting changes...", file=sys.stderr)
1002 uploads = sort_uploads(
1003 new_queue, uploads, session, bool(Options["No-Binaries"])
1004 )
1006 if Options["Comments"]: 1006 ↛ 1007line 1006 didn't jump to line 1007 because the condition on line 1006 was never true
1007 show_new_comments(uploads, session)
1008 else:
1009 for upload in uploads:
1010 do_pkg(upload, session)
1012 end()
1015################################################################################
1018if __name__ == "__main__":
1019 main()