1#! /usr/bin/env python3
2# vim:set et ts=4 sw=4:
4"""Handles packages from policy queues
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@copyright: 2009 Mark Hymers <mhy@debian.org>
11@license: GNU General Public License version 2 or later
12"""
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License as published by
15# the Free Software Foundation; either version 2 of the License, or
16# (at your option) any later version.
18# This program is distributed in the hope that it will be useful,
19# but WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21# GNU General Public License for more details.
23# You should have received a copy of the GNU General Public License
24# along with this program; if not, write to the Free Software
25# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27################################################################################
29# <mhy> So how do we handle that at the moment?
30# <stew> Probably incorrectly.
32################################################################################
34import datetime
35import functools
36import os
37import re
38import sys
39import traceback
40from collections.abc import Callable
41from typing import NoReturn
43import apt_pkg
44import sqlalchemy.sql as sql
45from sqlalchemy.orm.exc import NoResultFound
47import daklib.announce
48import daklib.upload
49import daklib.utils
50from daklib import daklog, utils
51from daklib.archive import ArchiveTransaction, source_component_from_package_list
52from daklib.config import Config
53from daklib.dbconn import (
54 ArchiveFile,
55 Component,
56 DBBinary,
57 DBChange,
58 DBConn,
59 DBSource,
60 Override,
61 OverrideType,
62 PolicyQueue,
63 PolicyQueueUpload,
64 PoolFile,
65 Suite,
66 get_mapped_component,
67)
68from daklib.externalsignature import check_upload_for_external_signature_request
69from daklib.packagelist import PackageList
70from daklib.urgencylog import UrgencyLog
72# Globals
73Options = None
74Logger = None
76################################################################################
78ProcessingCallable = Callable[
79 [PolicyQueueUpload, PolicyQueue, str, ArchiveTransaction], None
80]
83def do_comments(
84 dir: str,
85 srcqueue: PolicyQueue,
86 opref: str,
87 npref: str,
88 line: str,
89 fn: ProcessingCallable,
90 transaction: ArchiveTransaction,
91) -> None:
92 session = transaction.session
93 actions: list[tuple[PolicyQueueUpload, str]] = []
94 for comm in [x for x in os.listdir(dir) if x.startswith(opref)]:
95 with open(os.path.join(dir, comm)) as fd:
96 lines = fd.readlines()
97 if len(lines) == 0 or lines[0] != line + "\n": 97 ↛ 98line 97 didn't jump to line 98, because the condition on line 97 was never true
98 continue
100 # If the ACCEPT includes a _<arch> we only accept that .changes.
101 # Otherwise we accept all .changes that start with the given prefix
102 changes_prefix = comm[len(opref) :]
103 if changes_prefix.count("_") < 2:
104 changes_prefix = changes_prefix + "_"
105 else:
106 changes_prefix = changes_prefix + ".changes"
108 # We need to escape "_" as we use it with the LIKE operator (via the
109 # SQLA startwith) later.
110 changes_prefix = changes_prefix.replace("_", r"\_")
112 uploads = (
113 session.query(PolicyQueueUpload)
114 .filter_by(policy_queue=srcqueue)
115 .join(PolicyQueueUpload.changes)
116 .filter(DBChange.changesname.startswith(changes_prefix))
117 .order_by(PolicyQueueUpload.source_id)
118 )
119 reason = "".join(lines[1:])
120 actions.extend((u, reason) for u in uploads)
122 if opref != npref:
123 newcomm = npref + comm[len(opref) :]
124 newcomm = utils.find_next_free(os.path.join(dir, newcomm))
125 transaction.fs.move(os.path.join(dir, comm), newcomm)
127 actions.sort()
129 for u, reason in actions:
130 print(("Processing changes file: {0}".format(u.changes.changesname)))
131 fn(u, srcqueue, reason, transaction)
134################################################################################
137def try_or_reject(function: ProcessingCallable) -> ProcessingCallable:
138 @functools.wraps(function)
139 def wrapper(
140 upload: PolicyQueueUpload,
141 srcqueue: PolicyQueue,
142 comments: str,
143 transaction: ArchiveTransaction,
144 ) -> None:
145 try:
146 function(upload, srcqueue, comments, transaction)
147 except Exception:
148 comments = "An exception was raised while processing the package:\n{0}\nOriginal comments:\n{1}".format(
149 traceback.format_exc(), comments
150 )
151 try:
152 transaction.rollback()
153 real_comment_reject(upload, srcqueue, comments, transaction)
154 except Exception:
155 comments = "In addition an exception was raised while trying to reject the upload:\n{0}\nOriginal rejection:\n{1}".format(
156 traceback.format_exc(), comments
157 )
158 transaction.rollback()
159 real_comment_reject(
160 upload, srcqueue, comments, transaction, notify=False
161 )
162 if not Options["No-Action"]: 162 ↛ 165line 162 didn't jump to line 165, because the condition on line 162 was never false
163 transaction.commit()
164 else:
165 transaction.rollback()
167 return wrapper
170################################################################################
173@try_or_reject
174def comment_accept(
175 upload: PolicyQueueUpload,
176 srcqueue: PolicyQueue,
177 comments: str,
178 transaction: ArchiveTransaction,
179) -> None:
180 for byhand in upload.byhand: 180 ↛ 181line 180 didn't jump to line 181, because the loop on line 180 never started
181 path = os.path.join(srcqueue.path, byhand.filename)
182 if os.path.exists(path):
183 raise Exception(
184 "E: cannot ACCEPT upload with unprocessed byhand file {0}".format(
185 byhand.filename
186 )
187 )
189 cnf = Config()
191 fs = transaction.fs
192 session = transaction.session
193 changesname = upload.changes.changesname
194 allow_tainted = srcqueue.suite.archive.tainted
196 # We need overrides to get the target component
197 overridesuite = upload.target_suite
198 if overridesuite.overridesuite is not None:
199 overridesuite = (
200 session.query(Suite).filter_by(suite_name=overridesuite.overridesuite).one()
201 )
203 def binary_component_func(db_binary: DBBinary) -> Component:
204 section = db_binary.proxy["Section"]
205 component_name = "main"
206 if section.find("/") != -1:
207 component_name = section.split("/", 1)[0]
208 return get_mapped_component(component_name, session=session)
210 def is_debug_binary(db_binary: DBBinary) -> bool:
211 return daklib.utils.is_in_debug_section(db_binary.proxy)
213 def has_debug_binaries(upload: PolicyQueueUpload) -> bool:
214 return any((is_debug_binary(x) for x in upload.binaries))
216 def source_component_func(db_source: DBSource) -> Component:
217 package_list = PackageList(db_source.proxy)
218 component = source_component_from_package_list(
219 package_list, upload.target_suite
220 )
221 if component is not None: 221 ↛ 225line 221 didn't jump to line 225
222 return get_mapped_component(component.component_name, session=session)
224 # Fallback for packages without Package-List field
225 query = (
226 session.query(Override)
227 .filter_by(suite=overridesuite, package=db_source.source)
228 .join(OverrideType)
229 .filter(OverrideType.overridetype == "dsc")
230 .join(Component)
231 )
232 return query.one().component
234 policy_queue = upload.target_suite.policy_queue
235 if policy_queue == srcqueue:
236 policy_queue = None
238 all_target_suites = [
239 upload.target_suite if policy_queue is None else policy_queue.suite
240 ]
241 if policy_queue is None or policy_queue.send_to_build_queues: 241 ↛ 244line 241 didn't jump to line 244, because the condition on line 241 was never false
242 all_target_suites.extend([q.suite for q in upload.target_suite.copy_queues])
244 throw_away_binaries = False
245 if upload.source is not None:
246 source_component = source_component_func(upload.source)
247 if upload.target_suite.suite_name in cnf.value_list(
248 "Dinstall::ThrowAwayNewBinarySuites"
249 ) and source_component.component_name in cnf.value_list(
250 "Dinstall::ThrowAwayNewBinaryComponents"
251 ):
252 throw_away_binaries = True
254 for suite in all_target_suites:
255 debug_suite = suite.debug_suite
257 if upload.source is not None:
258 # If we have Source in this upload, let's include it into
259 # upload suite.
260 transaction.copy_source(
261 upload.source,
262 suite,
263 source_component,
264 allow_tainted=allow_tainted,
265 )
267 if not throw_away_binaries:
268 if debug_suite is not None and has_debug_binaries(upload):
269 # If we're handing a debug package, we also need to include the
270 # source in the debug suite as well.
271 transaction.copy_source(
272 upload.source,
273 debug_suite,
274 source_component_func(upload.source),
275 allow_tainted=allow_tainted,
276 )
278 if not throw_away_binaries:
279 for db_binary in upload.binaries:
280 # Now, let's work out where to copy this guy to -- if it's
281 # a debug binary, and the suite has a debug suite, let's go
282 # ahead and target the debug suite rather then the stock
283 # suite.
284 copy_to_suite = suite
285 if debug_suite is not None and is_debug_binary(db_binary):
286 copy_to_suite = debug_suite
288 # build queues and debug suites may miss the source package
289 # if this is a binary-only upload.
290 if copy_to_suite != upload.target_suite:
291 transaction.copy_source(
292 db_binary.source,
293 copy_to_suite,
294 source_component_func(db_binary.source),
295 allow_tainted=allow_tainted,
296 )
298 transaction.copy_binary(
299 db_binary,
300 copy_to_suite,
301 binary_component_func(db_binary),
302 allow_tainted=allow_tainted,
303 extra_archives=[upload.target_suite.archive],
304 )
306 check_upload_for_external_signature_request(
307 session, suite, copy_to_suite, db_binary
308 )
310 suite.update_last_changed()
312 # Copy .changes if needed
313 if policy_queue is None and upload.target_suite.copychanges: 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true
314 src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
315 dst = os.path.join(upload.target_suite.path, upload.changes.changesname)
316 fs.copy(src, dst, mode=upload.target_suite.archive.mode)
318 # List of files in the queue directory
319 queue_files = [changesname]
320 chg = daklib.upload.Changes(
321 upload.policy_queue.path, changesname, keyrings=[], require_signature=False
322 )
323 queue_files.extend(f.filename for f in chg.buildinfo_files)
325 # TODO: similar code exists in archive.py's `ArchiveUpload._install_policy`
326 if policy_queue is not None:
327 # register upload in policy queue
328 new_upload = PolicyQueueUpload()
329 new_upload.policy_queue = policy_queue
330 new_upload.target_suite = upload.target_suite
331 new_upload.changes = upload.changes
332 new_upload.source = upload.source
333 new_upload.binaries = upload.binaries
334 session.add(new_upload)
335 session.flush()
337 # copy .changes & similar to policy queue
338 for fn in queue_files:
339 src = os.path.join(upload.policy_queue.path, fn)
340 dst = os.path.join(policy_queue.path, fn)
341 transaction.fs.copy(src, dst, mode=policy_queue.change_perms)
343 # Copy upload to Process-Policy::CopyDir
344 # Used on security.d.o to sync accepted packages to ftp-master, but this
345 # should eventually be replaced by something else.
346 copydir = cnf.get("Process-Policy::CopyDir") or None
347 if policy_queue is None and copydir is not None: 347 ↛ 348line 347 didn't jump to line 348, because the condition on line 347 was never true
348 mode = upload.target_suite.archive.mode
349 if upload.source is not None:
350 for f in [df.poolfile for df in upload.source.srcfiles]:
351 dst = os.path.join(copydir, f.basename)
352 if not os.path.exists(dst):
353 fs.copy(f.fullpath, dst, mode=mode)
355 for db_binary in upload.binaries:
356 f = db_binary.poolfile
357 dst = os.path.join(copydir, f.basename)
358 if not os.path.exists(dst):
359 fs.copy(f.fullpath, dst, mode=mode)
361 for fn in queue_files:
362 src = os.path.join(upload.policy_queue.path, fn)
363 dst = os.path.join(copydir, fn)
364 # We check for `src` to exist as old uploads in policy queues
365 # might still miss the `.buildinfo` files.
366 if os.path.exists(src) and not os.path.exists(dst):
367 fs.copy(src, dst, mode=mode)
369 if policy_queue is None:
370 utils.process_buildinfos(
371 upload.policy_queue.path, chg.buildinfo_files, fs, Logger
372 )
374 if policy_queue is None and upload.source is not None and not Options["No-Action"]:
375 urgency = upload.changes.urgency
376 # As per policy 5.6.17, the urgency can be followed by a space and a
377 # comment. Extract only the urgency from the string.
378 if " " in urgency: 378 ↛ 379line 378 didn't jump to line 379, because the condition on line 378 was never true
379 urgency, comment = urgency.split(" ", 1)
380 if urgency not in cnf.value_list("Urgency::Valid"): 380 ↛ 381line 380 didn't jump to line 381, because the condition on line 380 was never true
381 urgency = cnf["Urgency::Default"]
382 UrgencyLog().log(upload.source.source, upload.source.version, urgency)
384 if policy_queue is None:
385 print(" ACCEPT")
386 else:
387 print(" ACCEPT-TO-QUEUE")
388 if not Options["No-Action"]: 388 ↛ 391line 388 didn't jump to line 391, because the condition on line 388 was never false
389 Logger.log(["Policy Queue ACCEPT", srcqueue.queue_name, changesname])
391 if policy_queue is None:
392 pu = get_processed_upload(upload)
393 daklib.announce.announce_accept(pu)
395 # TODO: code duplication. Similar code is in process-upload.
396 # Move .changes to done
397 now = datetime.datetime.now()
398 donedir = os.path.join(cnf["Dir::Done"], now.strftime("%Y/%m/%d"))
399 if policy_queue is None:
400 for fn in queue_files:
401 src = os.path.join(upload.policy_queue.path, fn)
402 if os.path.exists(src): 402 ↛ 400line 402 didn't jump to line 400, because the condition on line 402 was never false
403 dst = os.path.join(donedir, fn)
404 dst = utils.find_next_free(dst)
405 fs.copy(src, dst, mode=0o644)
407 if throw_away_binaries and upload.target_suite.archive.use_morgue:
408 morguesubdir = cnf.get("New::MorgueSubDir", "new")
410 utils.move_to_morgue(
411 morguesubdir,
412 [db_binary.poolfile.fullpath for db_binary in upload.binaries],
413 fs,
414 Logger,
415 )
417 remove_upload(upload, transaction)
420################################################################################
423@try_or_reject
424def comment_reject(*args) -> None:
425 real_comment_reject(*args, manual=True)
428def real_comment_reject(
429 upload: PolicyQueueUpload,
430 srcqueue: PolicyQueue,
431 comments: str,
432 transaction: ArchiveTransaction,
433 notify=True,
434 manual=False,
435) -> None:
436 cnf = Config()
438 fs = transaction.fs
439 session = transaction.session
440 changesname = upload.changes.changesname
441 queuedir = upload.policy_queue.path
442 rejectdir = cnf["Dir::Reject"]
444 ### Copy files to reject/
446 poolfiles = [b.poolfile for b in upload.binaries]
447 if upload.source is not None: 447 ↛ 450line 447 didn't jump to line 450, because the condition on line 447 was never false
448 poolfiles.extend([df.poolfile for df in upload.source.srcfiles])
449 # Not beautiful...
450 files = [
451 af.path
452 for af in session.query(ArchiveFile)
453 .filter_by(archive=upload.policy_queue.suite.archive)
454 .join(ArchiveFile.file)
455 .filter(PoolFile.file_id.in_([f.file_id for f in poolfiles]))
456 ]
457 for byhand in upload.byhand: 457 ↛ 458line 457 didn't jump to line 458, because the loop on line 457 never started
458 path = os.path.join(queuedir, byhand.filename)
459 if os.path.exists(path):
460 files.append(path)
461 chg = daklib.upload.Changes(
462 queuedir, changesname, keyrings=[], require_signature=False
463 )
464 for f in chg.buildinfo_files:
465 path = os.path.join(queuedir, f.filename)
466 if os.path.exists(path): 466 ↛ 464line 466 didn't jump to line 464, because the condition on line 466 was never false
467 files.append(path)
468 files.append(os.path.join(queuedir, changesname))
470 for fn in files:
471 dst = utils.find_next_free(os.path.join(rejectdir, os.path.basename(fn)))
472 fs.copy(fn, dst, link=True)
474 ### Write reason
476 dst = utils.find_next_free(
477 os.path.join(rejectdir, "{0}.reason".format(changesname))
478 )
479 fh = fs.create(dst)
480 fh.write(comments)
481 fh.close()
483 ### Send mail notification
485 if notify: 485 ↛ 499line 485 didn't jump to line 499, because the condition on line 485 was never false
486 rejected_by = None
487 reason = comments
489 # Try to use From: from comment file if there is one.
490 # This is not very elegant...
491 match = re.match(r"\AFrom: ([^\n]+)\n\n", comments)
492 if match: 492 ↛ 496line 492 didn't jump to line 496, because the condition on line 492 was never false
493 rejected_by = match.group(1)
494 reason = "\n".join(comments.splitlines()[2:])
496 pu = get_processed_upload(upload)
497 daklib.announce.announce_reject(pu, reason, rejected_by)
499 print(" REJECT")
500 if not Options["No-Action"]: 500 ↛ 505line 500 didn't jump to line 505, because the condition on line 500 was never false
501 Logger.log(
502 ["Policy Queue REJECT", srcqueue.queue_name, upload.changes.changesname]
503 )
505 changes = upload.changes
506 remove_upload(upload, transaction)
507 session.delete(changes)
510################################################################################
513def remove_upload(upload: PolicyQueueUpload, transaction: ArchiveTransaction) -> None:
514 fs = transaction.fs
515 session = transaction.session
517 # Remove byhand and changes files. Binary and source packages will be
518 # removed from {bin,src}_associations and eventually removed by clean-suites automatically.
519 queuedir = upload.policy_queue.path
520 for byhand in upload.byhand: 520 ↛ 521line 520 didn't jump to line 521, because the loop on line 520 never started
521 path = os.path.join(queuedir, byhand.filename)
522 if os.path.exists(path):
523 fs.unlink(path)
524 session.delete(byhand)
526 chg = daklib.upload.Changes(
527 queuedir, upload.changes.changesname, keyrings=[], require_signature=False
528 )
529 queue_files = [upload.changes.changesname]
530 queue_files.extend(f.filename for f in chg.buildinfo_files)
531 for fn in queue_files:
532 # We check for `path` to exist as old uploads in policy queues
533 # might still miss the `.buildinfo` files.
534 path = os.path.join(queuedir, fn)
535 if os.path.exists(path): 535 ↛ 531line 535 didn't jump to line 531, because the condition on line 535 was never false
536 fs.unlink(path)
538 session.delete(upload)
539 session.flush()
542################################################################################
545def get_processed_upload(upload: PolicyQueueUpload) -> daklib.announce.ProcessedUpload:
546 pu = daklib.announce.ProcessedUpload()
548 pu.maintainer = upload.changes.maintainer
549 pu.changed_by = upload.changes.changedby
550 pu.fingerprint = upload.changes.fingerprint
552 pu.suites = [upload.target_suite]
553 pu.from_policy_suites = [upload.target_suite]
555 changes_path = os.path.join(upload.policy_queue.path, upload.changes.changesname)
556 with open(changes_path, "r") as fd:
557 pu.changes = fd.read()
558 pu.changes_filename = upload.changes.changesname
559 pu.sourceful = upload.source is not None
560 pu.source = upload.changes.source
561 pu.version = upload.changes.version
562 pu.architecture = upload.changes.architecture
563 pu.bugs = upload.changes.closes
565 pu.program = "process-policy"
567 return pu
570################################################################################
573def remove_unreferenced_binaries(
574 policy_queue: PolicyQueue, transaction: ArchiveTransaction
575) -> None:
576 """Remove binaries that are no longer referenced by an upload"""
577 session = transaction.session
578 suite = policy_queue.suite
580 query = sql.text(
581 """
582 SELECT b.*
583 FROM binaries b
584 JOIN bin_associations ba ON b.id = ba.bin
585 WHERE ba.suite = :suite_id
586 AND NOT EXISTS (SELECT 1 FROM policy_queue_upload_binaries_map pqubm
587 JOIN policy_queue_upload pqu ON pqubm.policy_queue_upload_id = pqu.id
588 WHERE pqu.policy_queue_id = :policy_queue_id
589 AND pqubm.binary_id = b.id)"""
590 )
591 binaries = (
592 session.query(DBBinary)
593 .from_statement(query)
594 .params(
595 {
596 "suite_id": policy_queue.suite_id,
597 "policy_queue_id": policy_queue.policy_queue_id,
598 }
599 )
600 )
602 for binary in binaries:
603 Logger.log(
604 [
605 "removed binary from policy queue",
606 policy_queue.queue_name,
607 binary.package,
608 binary.version,
609 ]
610 )
611 transaction.remove_binary(binary, suite)
614def remove_unreferenced_sources(
615 policy_queue: PolicyQueue, transaction: ArchiveTransaction
616) -> None:
617 """Remove sources that are no longer referenced by an upload or a binary"""
618 session = transaction.session
619 suite = policy_queue.suite
621 query = sql.text(
622 """
623 SELECT s.*
624 FROM source s
625 JOIN src_associations sa ON s.id = sa.source
626 WHERE sa.suite = :suite_id
627 AND NOT EXISTS (SELECT 1 FROM policy_queue_upload pqu
628 WHERE pqu.policy_queue_id = :policy_queue_id
629 AND pqu.source_id = s.id)
630 AND NOT EXISTS (SELECT 1 FROM binaries b
631 JOIN bin_associations ba ON b.id = ba.bin
632 WHERE b.source = s.id
633 AND ba.suite = :suite_id)"""
634 )
635 sources = (
636 session.query(DBSource)
637 .from_statement(query)
638 .params(
639 {
640 "suite_id": policy_queue.suite_id,
641 "policy_queue_id": policy_queue.policy_queue_id,
642 }
643 )
644 )
646 for source in sources:
647 Logger.log(
648 [
649 "removed source from policy queue",
650 policy_queue.queue_name,
651 source.source,
652 source.version,
653 ]
654 )
655 transaction.remove_source(source, suite)
658################################################################################
661def usage(status=0) -> NoReturn:
662 print("""Usage: dak process-policy QUEUE""")
663 sys.exit(status)
666################################################################################
669def main():
670 global Options, Logger
672 cnf = Config()
673 session = DBConn().session()
675 Arguments = [
676 ("h", "help", "Process-Policy::Options::Help"),
677 ("n", "no-action", "Process-Policy::Options::No-Action"),
678 ]
680 for i in ["help", "no-action"]:
681 key = "Process-Policy::Options::%s" % i
682 if key not in cnf: 682 ↛ 680line 682 didn't jump to line 680, because the condition on line 682 was never false
683 cnf[key] = ""
685 queue_name = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv)
687 Options = cnf.subtree("Process-Policy::Options")
688 if Options["Help"]:
689 usage()
691 if len(queue_name) != 1: 691 ↛ 692line 691 didn't jump to line 692, because the condition on line 691 was never true
692 print("E: Specify exactly one policy queue")
693 sys.exit(1)
695 queue_name = queue_name[0]
697 Logger = daklog.Logger("process-policy")
698 if not Options["No-Action"]: 698 ↛ 701line 698 didn't jump to line 701, because the condition on line 698 was never false
699 urgencylog = UrgencyLog()
701 with ArchiveTransaction() as transaction:
702 session = transaction.session
703 try:
704 pq = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
705 except NoResultFound:
706 print("E: Cannot find policy queue %s" % queue_name)
707 sys.exit(1)
709 commentsdir = os.path.join(pq.path, "COMMENTS")
710 # The comments stuff relies on being in the right directory
711 os.chdir(pq.path)
713 do_comments(
714 commentsdir,
715 pq,
716 "REJECT.",
717 "REJECTED.",
718 "NOTOK",
719 comment_reject,
720 transaction,
721 )
722 do_comments(
723 commentsdir, pq, "ACCEPT.", "ACCEPTED.", "OK", comment_accept, transaction
724 )
725 do_comments(
726 commentsdir, pq, "ACCEPTED.", "ACCEPTED.", "OK", comment_accept, transaction
727 )
729 remove_unreferenced_binaries(pq, transaction)
730 remove_unreferenced_sources(pq, transaction)
732 if not Options["No-Action"]: 732 ↛ exitline 732 didn't return from function 'main', because the condition on line 732 was never false
733 urgencylog.close()
736################################################################################
739if __name__ == "__main__":
740 main()