Coverage for dak/process_new.py: 53%
507 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1"""Handles NEW and BYHAND packages
3@contact: Debian FTP Master <ftpmaster@debian.org>
4@copyright: 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
5@copyright: 2009 Joerg Jaspert <joerg@debian.org>
6@copyright: 2009 Frank Lichtenheld <djpig@debian.org>
7@license: GNU General Public License version 2 or later
8"""
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software
22# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24################################################################################
26# 23:12|<aj> I will not hush!
27# 23:12|<elmo> :>
28# 23:12|<aj> Where there is injustice in the world, I shall be there!
29# 23:13|<aj> I shall not be silenced!
30# 23:13|<aj> The world shall know!
31# 23:13|<aj> The world *must* know!
32# 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-)
33# 23:13|<aj> yay powerpuff girls!!
34# 23:13|<aj> buttercup's my favourite, who's yours?
35# 23:14|<aj> you're backing away from the keyboard right now aren't you?
36# 23:14|<aj> *AREN'T YOU*?!
37# 23:15|<aj> I will not be treated like this.
38# 23:15|<aj> I shall have my revenge.
39# 23:15|<aj> I SHALL!!!
41################################################################################
43import contextlib
44import datetime
45import errno
46import os
47import pwd
48import readline
49import stat
50import subprocess
51import sys
52import tempfile
53from collections import defaultdict
54from collections.abc import Iterable, Iterator
55from typing import TYPE_CHECKING, NoReturn, TypedDict
57import apt_pkg
58from sqlalchemy import or_, select, sql
60import dak.examine_package
61import daklib.dbconn
62from daklib import daklog, utils
63from daklib.config import Config
64from daklib.dak_exceptions import AlreadyLockedError
65from daklib.dbconn import (
66 BinaryMetadata,
67 DBBinary,
68 DBChange,
69 DBConn,
70 DBSource,
71 MetadataKey,
72 NewComment,
73 PolicyQueue,
74 PolicyQueueUpload,
75 Priority,
76 Section,
77 Suite,
78 get_new_comments,
79)
80from daklib.policy import MissingOverride, PolicyQueueUploadHandler, UploadCopy
81from daklib.queue import check_valid, edit_note, prod_maintainer
82from daklib.regexes import re_default_answer, re_isanum
83from daklib.summarystats import SummaryStats
84from daklib.termcolor import colorize as Color
86if TYPE_CHECKING:
87 from sqlalchemy.engine import Row
88 from sqlalchemy.orm import Session
90# Globals
91Options: apt_pkg.Configuration
92Logger: daklog.Logger
94Priorities: "Priority_Completer"
95Sections: "Section_Completer"
97################################################################################
98################################################################################
99################################################################################
102class Section_Completer:
103 def __init__(self, session: "Session"):
104 self.sections = []
105 self.matches: list[str] = []
106 for s in session.scalars(select(Section.section)):
107 self.sections.append(s)
109 def complete(self, text: str, state: int) -> str | None:
110 if state == 0:
111 self.matches = []
112 n = len(text)
113 for word in self.sections:
114 if word[:n] == text:
115 self.matches.append(word)
116 try:
117 return self.matches[state]
118 except IndexError:
119 return None
122############################################################
125class Priority_Completer:
126 def __init__(self, session: "Session"):
127 self.priorities = []
128 self.matches: list[str] = []
129 for p in session.scalars(select(Priority.priority)):
130 self.priorities.append(p)
132 def complete(self, text: str, state: int) -> str | None:
133 if state == 0:
134 self.matches = []
135 n = len(text)
136 for word in self.priorities:
137 if word[:n] == text:
138 self.matches.append(word)
139 try:
140 return self.matches[state]
141 except IndexError:
142 return None
145################################################################################
148def takenover_binaries(
149 upload: daklib.dbconn.PolicyQueueUpload,
150 missing: list[MissingOverride],
151 session: "Session",
152) -> "list[Row[tuple[str, str]]]":
153 rows = []
154 binaries = {x.package for x in upload.binaries}
155 for m in missing:
156 if m["type"] != "dsc":
157 binaries.discard(m["package"])
158 if binaries:
159 source = upload.binaries[0].source.source
160 suite = upload.target_suite.overridesuite or upload.target_suite.suite_name
161 suites = list(
162 session.scalars(
163 select(Suite.suite_name).where(
164 or_(Suite.suite_name == suite, Suite.overridesuite == suite)
165 )
166 )
167 )
168 rows = list(
169 session.execute(
170 select(DBSource.source, DBBinary.package)
171 .distinct()
172 .where(DBBinary.package.in_(binaries))
173 .join(DBBinary.source)
174 .where(DBSource.source != source)
175 .join(DBBinary.suites)
176 .where(Suite.suite_name.in_(suites))
177 .order_by(DBSource.source, DBBinary.package)
178 )
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 {
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 = {o["package"] for o in new if o["type"] == "deb"}
467 if len(new_packages) == 0:
468 return
470 key = session.execute(select(MetadataKey).filter_by(key="Description")).scalar_one()
471 summary = ""
472 for binary in upload.binaries:
473 if binary.package not in new_packages:
474 continue
475 description = session.execute(
476 select(BinaryMetadata.value).filter_by(binary=binary, key=key)
477 ).scalar_one()
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 = "") -> str | None:
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 = list(
872 session.scalars(
873 select(DBSource.source).where(
874 DBSource.suites.any(Suite.suite_name.in_(["unstable", "experimental"]))
875 )
876 )
877 )
878 comments = list(
879 session.scalars(
880 select(NewComment.package)
881 .filter_by(trainee=False, policy_queue=new_queue)
882 .distinct()
883 )
884 )
885 for upload in uploads:
886 source = upload.changes.source
887 sources[source].append(
888 {
889 "upload": upload,
890 "date": upload.changes.created,
891 "stack": 1,
892 "binary": True if source in suitesrc else False,
893 "comments": True if source in comments else False,
894 }
895 )
896 for changes in sources.values():
897 if len(changes) > 1:
898 changes.sort(key=lambda item: item["date"])
899 firstseen = changes[0]["date"]
900 for i in range(len(changes)):
901 changes[i]["date"] = firstseen
902 changes[i]["stack"] = i + 1
903 sortedchanges += changes
904 if nobinaries: 904 ↛ 905line 904 didn't jump to line 905 because the condition on line 904 was never true
905 sortedchanges.sort(
906 key=lambda k: (k["comments"], k["binary"], k["date"], -k["stack"])
907 )
908 else:
909 sortedchanges.sort(
910 key=lambda k: (k["comments"], -k["binary"], k["date"], -k["stack"])
911 )
912 return [u["upload"] for u in sortedchanges]
915################################################################################
918def end() -> None:
919 accept_count = SummaryStats().accept_count
920 accept_bytes = SummaryStats().accept_bytes
922 if accept_count: 922 ↛ 923line 922 didn't jump to line 923 because the condition on line 922 was never true
923 sets = "set"
924 if accept_count > 1:
925 sets = "sets"
926 print(
927 "Accepted %d package %s, %s."
928 % (accept_count, sets, utils.size_type(int(accept_bytes))),
929 file=sys.stderr,
930 )
931 Logger.log(["total", accept_count, accept_bytes])
933 if not Options["No-Action"] and not Options["Trainee"]: 933 ↛ exitline 933 didn't return from function 'end' because the condition on line 933 was always true
934 Logger.close()
937################################################################################
940def main() -> None:
941 global Options, Logger, Sections, Priorities
943 cnf = Config()
944 session = DBConn().session()
946 Arguments = [
947 ("a", "automatic", "Process-New::Options::Automatic"),
948 ("b", "no-binaries", "Process-New::Options::No-Binaries"),
949 ("c", "comments", "Process-New::Options::Comments"),
950 ("h", "help", "Process-New::Options::Help"),
951 ("m", "manual-reject", "Process-New::Options::Manual-Reject", "HasArg"),
952 ("t", "trainee", "Process-New::Options::Trainee"),
953 ("q", "queue", "Process-New::Options::Queue", "HasArg"),
954 ("n", "no-action", "Process-New::Options::No-Action"),
955 ]
957 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
959 for i in [
960 "automatic",
961 "no-binaries",
962 "comments",
963 "help",
964 "manual-reject",
965 "no-action",
966 "version",
967 "trainee",
968 ]:
969 key = "Process-New::Options::%s" % i
970 if key not in cnf:
971 cnf[key] = ""
973 queue_name = cnf.get("Process-New::Options::Queue", "new")
974 new_queue = session.execute(
975 select(PolicyQueue).filter_by(queue_name=queue_name)
976 ).scalar_one()
977 if len(changes_files) == 0:
978 uploads = new_queue.uploads
979 else:
980 uploads = list(
981 session.scalars(
982 select(PolicyQueueUpload)
983 .filter_by(policy_queue=new_queue)
984 .join(DBChange)
985 .where(DBChange.changesname.in_(changes_files))
986 )
987 )
989 Options = cnf.subtree("Process-New::Options")
991 if Options["Help"]:
992 usage()
994 if not Options["No-Action"]: 994 ↛ 1000line 994 didn't jump to line 1000 because the condition on line 994 was always true
995 try:
996 Logger = daklog.Logger("process-new")
997 except OSError:
998 Options["Trainee"] = "True" # type: ignore[index]
1000 Sections = Section_Completer(session)
1001 Priorities = Priority_Completer(session)
1002 readline.parse_and_bind("tab: complete")
1004 if len(uploads) > 1:
1005 print("Sorting changes...", file=sys.stderr)
1006 uploads = sort_uploads(
1007 new_queue, uploads, session, bool(Options["No-Binaries"])
1008 )
1010 if Options["Comments"]: 1010 ↛ 1011line 1010 didn't jump to line 1011 because the condition on line 1010 was never true
1011 show_new_comments(uploads, session)
1012 else:
1013 for upload in uploads:
1014 do_pkg(upload, session)
1016 end()