Coverage for daklib/rm.py: 53%

348 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-08-03 16:46 +0000

1"""General purpose package removal code for ftpmaster 

2 

3@contact: Debian FTP Master <ftpmaster@debian.org> 

4@copyright: 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org> 

5@copyright: 2010 Alexander Reichle-Schmehl <tolimar@debian.org> 

6@copyright: 2015 Niels Thykier <niels@thykier.net> 

7@license: GNU General Public License version 2 or later 

8""" 

9 

10# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org> 

11# Copyright (C) 2010 Alexander Reichle-Schmehl <tolimar@debian.org> 

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. 

17 

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. 

22 

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 

26 

27################################################################################ 

28 

29# From: Andrew Morton <akpm@osdl.org> 

30# Subject: 2.6.6-mm5 

31# To: linux-kernel@vger.kernel.org 

32# Date: Sat, 22 May 2004 01:36:36 -0700 

33# X-Mailer: Sylpheed version 0.9.7 (GTK+ 1.2.10; i386-redhat-linux-gnu) 

34# 

35# [...] 

36# 

37# Although this feature has been around for a while it is new code, and the 

38# usual cautions apply. If it munches all your files please tell Jens and 

39# he'll type them in again for you. 

40 

41################################################################################ 

42 

43import email.utils 

44import fcntl 

45import functools 

46from collections import defaultdict 

47from collections.abc import Collection 

48from re import sub 

49from typing import TYPE_CHECKING, Any 

50 

51import apt_pkg 

52from sqlalchemy import sql 

53 

54from daklib import btsutils as bts 

55from daklib import utils 

56from daklib.dbconn import ( 

57 get_component, 

58 get_or_set_metadatakey, 

59 get_override_type, 

60 get_suite, 

61 get_suite_architectures, 

62) 

63from daklib.regexes import re_bin_only_nmu 

64 

65from .regexes import re_build_dep_arch 

66 

67if TYPE_CHECKING: 

68 from sqlalchemy.orm import Session 

69 

70################################################################################ 

71 

72 

73class ReverseDependencyChecker: 

74 """A bulk tester for reverse dependency checks 

75 

76 This class is similar to the check_reverse_depends method from "utils". However, 

77 it is primarily focused on facilitating bulk testing of reverse dependencies. 

78 It caches the state of the suite and then uses that as basis for answering queries. 

79 This saves a significant amount of time if multiple reverse dependency checks are 

80 required. 

81 """ 

82 

83 def __init__(self, session: "Session", suite: str): 

84 """Creates a new ReverseDependencyChecker instance 

85 

86 This will spend a significant amount of time caching data. 

87 

88 :param session: The database session in use 

89 :param suite: The name of the suite that is used as basis for removal tests. 

90 """ 

91 self._session = session 

92 dbsuite = get_suite(suite, session) 

93 assert dbsuite is not None 

94 suite_archs2id = { 

95 x.arch_string: x.arch_id for x in get_suite_architectures(suite) 

96 } 

97 package_dependencies, arch_providers_of, arch_provided_by = ( 

98 self._load_package_information(session, dbsuite.suite_id, suite_archs2id) 

99 ) 

100 self._package_dependencies = package_dependencies 

101 self._arch_providers_of = arch_providers_of 

102 self._arch_provided_by = arch_provided_by 

103 self._archs_in_suite = set(suite_archs2id) 

104 

105 @staticmethod 

106 def _load_package_information( 

107 session: "Session", suite_id: int, suite_archs2id: dict[str, int] 

108 ) -> tuple[ 

109 dict[str, dict[str, set[frozenset[str]]]], 

110 dict[str, dict[str, set[str]]], 

111 dict[str, dict[str, set[str]]], 

112 ]: 

113 package_dependencies: dict[str, dict[str, set[frozenset[str]]]] = defaultdict( 113 ↛ exitline 113 didn't jump to the function exit

114 lambda: defaultdict(set) 

115 ) 

116 arch_providers_of: dict[str, dict[str, set[str]]] = defaultdict( 116 ↛ exitline 116 didn't jump to the function exit

117 lambda: defaultdict(set) 

118 ) 

119 arch_provided_by: dict[str, dict[str, set[str]]] = defaultdict( 119 ↛ exitline 119 didn't jump to the function exit

120 lambda: defaultdict(set) 

121 ) 

122 source_deps: dict[str, set[frozenset[str]]] = defaultdict(set) 

123 metakey_d = get_or_set_metadatakey("Depends", session) 

124 metakey_p = get_or_set_metadatakey("Provides", session) 

125 params: dict[str, Any] = { 

126 "suite_id": suite_id, 

127 "arch_all_id": suite_archs2id["all"], 

128 "metakey_d_id": metakey_d.key_id, 

129 "metakey_p_id": metakey_p.key_id, 

130 } 

131 all_arches = set(suite_archs2id) 

132 all_arches.discard("source") 

133 

134 package_dependencies["source"] = source_deps 

135 

136 for architecture in all_arches: 

137 deps: dict[str, set[frozenset[str]]] = defaultdict(set) 

138 providers_of: dict[str, set[str]] = defaultdict(set) 

139 provided_by: dict[str, set[str]] = defaultdict(set) 

140 arch_providers_of[architecture] = providers_of 

141 arch_provided_by[architecture] = provided_by 

142 package_dependencies[architecture] = deps 

143 

144 params["arch_id"] = suite_archs2id[architecture] 

145 

146 statement = sql.text( 

147 """ 

148 SELECT b.package, 

149 (SELECT bmd.value FROM binaries_metadata bmd WHERE bmd.bin_id = b.id AND bmd.key_id = :metakey_d_id) AS depends, 

150 (SELECT bmp.value FROM binaries_metadata bmp WHERE bmp.bin_id = b.id AND bmp.key_id = :metakey_p_id) AS provides 

151 FROM binaries b 

152 JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :suite_id 

153 WHERE b.architecture = :arch_id OR b.architecture = :arch_all_id""" 

154 ) 

155 query = session.execute(statement, params) 

156 for package, depends, provides in query: 156 ↛ 158line 156 didn't jump to line 158 because the loop on line 156 never started

157 

158 if depends is not None: 

159 try: 

160 parsed_dep = [] 

161 for dep in apt_pkg.parse_depends(depends): 

162 parsed_dep.append(frozenset(d[0] for d in dep)) 

163 deps[package].update(parsed_dep) 

164 except ValueError as e: 

165 print("Error for package %s: %s" % (package, e)) 

166 # Maintain a counter for each virtual package. If a 

167 # Provides: exists, set the counter to 0 and count all 

168 # provides by a package not in the list for removal. 

169 # If the counter stays 0 at the end, we know that only 

170 # the to-be-removed packages provided this virtual 

171 # package. 

172 if provides is not None: 

173 for virtual_pkg in provides.split(","): 

174 virtual_pkg = virtual_pkg.strip() 

175 if virtual_pkg == package: 

176 continue 

177 provided_by[virtual_pkg].add(package) 

178 providers_of[package].add(virtual_pkg) 

179 

180 # Check source dependencies (Build-Depends, Build-Depends-Arch, Build-Depends-Indep) 

181 metakey_bd = get_or_set_metadatakey("Build-Depends", session) 

182 metakey_bda = get_or_set_metadatakey("Build-Depends-Arch", session) 

183 metakey_bdi = get_or_set_metadatakey("Build-Depends-Indep", session) 

184 params = { 

185 "suite_id": suite_id, 

186 "metakey_ids": (metakey_bd.key_id, metakey_bda.key_id, metakey_bdi.key_id), 

187 } 

188 statement = sql.text( 

189 """ 

190 SELECT s.source, string_agg(sm.value, ', ') as build_dep 

191 FROM source s 

192 JOIN source_metadata sm ON s.id = sm.src_id 

193 WHERE s.id in 

194 (SELECT src FROM newest_src_association 

195 WHERE suite = :suite_id) 

196 AND sm.key_id in :metakey_ids 

197 GROUP BY s.id, s.source""" 

198 ) 

199 query = session.execute(statement, params) 

200 for source, build_dep in query: 

201 if build_dep is not None: 201 ↛ 200line 201 didn't jump to line 200 because the condition on line 201 was always true

202 # Remove [arch] information since we want to see breakage on all arches 

203 build_dep = re_build_dep_arch.sub("", build_dep) 

204 try: 

205 parsed_dep = [] 

206 for dep in apt_pkg.parse_src_depends(build_dep): 

207 parsed_dep.append(frozenset(d[0] for d in dep)) 

208 source_deps[source].update(parsed_dep) 

209 except ValueError as e: 

210 print("Error for package %s: %s" % (source, e)) 

211 

212 return package_dependencies, arch_providers_of, arch_provided_by 

213 

214 def check_reverse_depends( 

215 self, removal_requests: Collection[tuple[str, Collection[str] | None]] 

216 ) -> dict[tuple[str, str], set[tuple[str, str]]]: 

217 """Bulk check reverse dependencies 

218 

219 Example: 

220 removal_request = { 

221 "eclipse-rcp": None, # means ALL architectures (incl. source) 

222 "eclipse": None, # means ALL architectures (incl. source) 

223 "lintian": ["source", "all"], # Only these two "architectures". 

224 } 

225 obj.check_reverse_depends(removal_request) 

226 

227 :param removal_requests: A dictionary mapping a package name to a list of architectures. The list of 

228 architectures decides from which the package will be removed - if the list is empty the package will 

229 be removed on ALL architectures in the suite (including "source"). 

230 

231 :return: A mapping of "removed package" (as a "(pkg, arch)"-tuple) to a set of broken 

232 broken packages (also as "(pkg, arch)"-tuple). Note that the architecture values 

233 in these tuples /can/ be "source" to reflect a breakage in build-dependencies. 

234 """ 

235 

236 archs_in_suite = self._archs_in_suite 

237 removals_by_arch: dict[str, set[str]] = defaultdict(set) 

238 affected_virtual_by_arch = defaultdict(set) 

239 package_dependencies = self._package_dependencies 

240 arch_providers_of = self._arch_providers_of 

241 arch_provided_by = self._arch_provided_by 

242 arch_provides2removal: dict[str, dict[str, str]] = defaultdict(dict) 

243 dep_problems: defaultdict[tuple[str, str], set[tuple[str, str]]] = defaultdict( 

244 set 

245 ) 

246 src_deps = package_dependencies["source"] 

247 src_removals = set() 

248 arch_all_removals = set() 

249 

250 for pkg, arch_list in removal_requests: 

251 if not arch_list: 

252 arch_list = archs_in_suite 

253 for arch in arch_list: 

254 if arch == "source": 

255 src_removals.add(pkg) 

256 continue 

257 if arch == "all": 

258 arch_all_removals.add(pkg) 

259 continue 

260 removals_by_arch[arch].add(pkg) 

261 if pkg in arch_providers_of[arch]: 

262 affected_virtual_by_arch[arch].add(pkg) 

263 

264 if arch_all_removals: 

265 for arch in archs_in_suite: 

266 if arch in ("all", "source"): 

267 continue 

268 removals_by_arch[arch].update(arch_all_removals) 

269 for pkg in arch_all_removals: 

270 if pkg in arch_providers_of[arch]: 

271 affected_virtual_by_arch[arch].add(pkg) 

272 

273 if not removals_by_arch: 

274 # Nothing to remove => no problems 

275 return dep_problems 

276 

277 for arch, removed_providers in affected_virtual_by_arch.items(): 

278 provides2removal = arch_provides2removal[arch] 

279 removals = removals_by_arch[arch] 

280 for virtual_pkg, virtual_providers in arch_provided_by[arch].items(): 

281 v = virtual_providers & removed_providers 

282 if len(v) == len(virtual_providers): 

283 # We removed all the providers of virtual_pkg 

284 removals.add(virtual_pkg) 

285 # Pick one to take the blame for the removal 

286 # - we sort for determinism, optimally we would prefer to blame the same package 

287 # to minimise the number of blamed packages. 

288 provides2removal[virtual_pkg] = sorted(v)[0] 

289 

290 for arch, removals in removals_by_arch.items(): 

291 deps = package_dependencies[arch] 

292 provides2removal = arch_provides2removal[arch] 

293 

294 # Check binary dependencies (Depends) 

295 for package, dependencies in deps.items(): 

296 if package in removals: 

297 continue 

298 for clause in dependencies: 

299 if not (clause <= removals): 

300 # Something probably still satisfies this relation 

301 continue 

302 # whoops, we seemed to have removed all packages that could possibly satisfy 

303 # this relation. Lets blame something for it 

304 for dep_package in clause: 

305 removal = dep_package 

306 if dep_package in provides2removal: 

307 removal = provides2removal[dep_package] 

308 dep_problems[(removal, arch)].add((package, arch)) 

309 

310 for source, build_dependencies in src_deps.items(): 

311 if source in src_removals: 

312 continue 

313 for clause in build_dependencies: 

314 if not (clause <= removals): 

315 # Something probably still satisfies this relation 

316 continue 

317 # whoops, we seemed to have removed all packages that could possibly satisfy 

318 # this relation. Lets blame something for it 

319 for dep_package in clause: 

320 removal = dep_package 

321 if dep_package in provides2removal: 

322 removal = provides2removal[dep_package] 

323 dep_problems[(removal, arch)].add((source, "source")) 

324 

325 return dep_problems 

326 

327 

328def _find_related_bugs(source_pkg: str) -> list[int]: 

329 """Find open bugs for the given source package. 

330 

331 Queries the bug tracking system for open or forwarded bugs against 

332 the source package, keeping only one representative per set of 

333 merged bugs. 

334 """ 

335 other_bugs = bts.get_bugs(src=source_pkg, status=("open", "forwarded")) # type: ignore[arg-type] 

336 merged_bugs: set[int] = set() 

337 for bugno in other_bugs: 

338 if bugno in merged_bugs: 

339 continue 

340 for bugreport in bts.get_status(bugno): 

341 merged_bugs.update(bugreport.mergedwith) 

342 return [bugno for bugno in other_bugs if bugno not in merged_bugs] 

343 

344 

345def remove( 

346 session: "Session", 

347 reason: str, 

348 suites: list[str], 

349 removals: list[tuple[str, str, str, int]], 

350 whoami: str | None = None, 

351 partial: bool = False, 

352 components: list[str] | None = None, 

353 done_bugs: list[str] | None = None, 

354 date: str | None = None, 

355 carbon_copy: list[str] | None = None, 

356 close_related_bugs: bool = False, 

357) -> None: 

358 """Batch remove a number of packages 

359 Verify that the files listed in the Files field of the .dsc are 

360 those expected given the announced Format. 

361 

362 :param session: The database session in use 

363 :param reason: The reason for the removal (e.g. "[auto-cruft] NBS (no longer built by <source>)") 

364 :param suites: A list of the suite names in which the removal should occur 

365 :param removals: A list of the removals. Each element should be a tuple (or list) of at least the following 

366 for 4 items from the database (in order): package, version, architecture, (database) id. 

367 For source packages, the "architecture" should be set to "source". 

368 :param whoami: The person (or entity) doing the removal. Defaults to utils.whoami() 

369 :param partial: Whether the removal is "partial" (e.g. architecture specific). 

370 :param components: List of components involved in a partial removal. Can be an empty list to not restrict the 

371 removal to any components. 

372 :param done_bugs: A list of bugs to be closed when doing this removal. 

373 :param date: The date of the removal. Defaults to `date -R` 

374 :param carbon_copy: A list of mail addresses to CC when doing removals. NB: all items are taken "as-is" unlike 

375 "dak rm". 

376 :param close_related_bugs: Whether bugs related to the package being removed should be closed as well. NB: Not implemented 

377 for more than one suite. 

378 """ 

379 # Generate the summary of what's to be removed 

380 d: dict[str, dict[str, list[str]]] = {} 

381 summary = "" 

382 affected_sources: set[str] = set() 

383 sources = [] 

384 binaries = [] 

385 whitelists = [] 

386 versions = [] 

387 newest_source = "" 

388 suite_ids_list = [] 

389 suites_list = utils.join_with_commas_and(suites) 

390 cnf = utils.get_conf() 

391 con_components = "" 

392 

393 ####################################################################################################### 

394 

395 if not reason: 395 ↛ 396line 395 didn't jump to line 396 because the condition on line 395 was never true

396 raise ValueError("Empty removal reason not permitted") 

397 reason = reason.strip() 

398 

399 if not removals: 399 ↛ 400line 399 didn't jump to line 400 because the condition on line 399 was never true

400 raise ValueError("Nothing to remove!?") 

401 

402 if not suites: 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true

403 raise ValueError("Removals without a suite!?") 

404 

405 if whoami is None: 

406 whoami = utils.whoami() 

407 

408 if date is None: 408 ↛ 411line 408 didn't jump to line 411 because the condition on line 408 was always true

409 date = email.utils.formatdate() 

410 

411 if partial and components: 411 ↛ 413line 411 didn't jump to line 413 because the condition on line 411 was never true

412 

413 component_ids_list = [] 

414 for componentname in components: 

415 component = get_component(componentname, session=session) 

416 if component is None: 

417 raise ValueError("component '%s' not recognised." % componentname) 

418 else: 

419 component_ids_list.append(component.component_id) 

420 if component_ids_list: 

421 con_components = "AND component IN (%s)" % ", ".join( 

422 [str(i) for i in component_ids_list] 

423 ) 

424 

425 for i in removals: 

426 package = i[0] 

427 version = i[1] 

428 architecture = i[2] 

429 if package not in d: 

430 d[package] = {} 

431 if version not in d[package]: 

432 d[package][version] = [] 

433 if architecture not in d[package][version]: 433 ↛ 425line 433 didn't jump to line 425 because the condition on line 433 was always true

434 d[package][version].append(architecture) 

435 

436 for package in sorted(d): 

437 versions = sorted(d[package], key=functools.cmp_to_key(apt_pkg.version_compare)) 

438 for version in versions: 

439 d[package][version].sort(key=utils.ArchKey) 

440 summary += "%10s | %10s | %s\n" % ( 

441 package, 

442 version, 

443 ", ".join(d[package][version]), 

444 ) 

445 if apt_pkg.version_compare(version, newest_source) > 0: 

446 newest_source = version 

447 

448 for package in summary.split("\n"): 

449 for row in package.split("\n"): 

450 element = row.split("|") 

451 if len(element) == 3: 

452 if element[2].find("source") > 0: 

453 sources.append( 

454 "%s_%s" % tuple(elem.strip(" ") for elem in element[:2]) 

455 ) 

456 element[2] = sub(r"source\s?,?", "", element[2]).strip(" ") 

457 if element[2]: 

458 binaries.append( 

459 "%s_%s [%s]" % tuple(elem.strip(" ") for elem in element) 

460 ) 

461 

462 dsc_type = get_override_type("dsc", session) 

463 assert dsc_type is not None 

464 dsc_type_id = dsc_type.overridetype_id 

465 deb_type = get_override_type("deb", session) 

466 assert deb_type is not None 

467 deb_type_id = deb_type.overridetype_id 

468 

469 for suite in suites: 

470 s = get_suite(suite, session=session) 

471 if s is not None: 471 ↛ 469line 471 didn't jump to line 469 because the condition on line 471 was always true

472 suite_ids_list.append(s.suite_id) 

473 whitelists.append(s.mail_whitelist) 

474 

475 # Retrieve the list of related bugs to close before starting the 

476 # removal: a failure here (unsupported removal, unreachable BTS) 

477 # must abort before anything was removed. 

478 related_bugs: list[int] | None = None 

479 related_wnpp_bugs: list[str] | None = None 

480 source_pkg: str | None = None 

481 related_version: str | None = None 

482 if close_related_bugs and "Dinstall::BugServer" in cnf: 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 source_names = {s.split("_", 1)[0] for s in sources} 

484 if len(source_names) != 1: 

485 raise ValueError( 

486 "Closing related bugs requires exactly one source package in the removal. Please do it yourself." 

487 ) 

488 source_pkg = source_names.pop() 

489 related_version = re_bin_only_nmu.sub("", newest_source) 

490 if related_version == "": 

491 raise ValueError("No versions can be found. Close bugs yourself.") 

492 done = set(done_bugs or ()) 

493 wnpp = utils.parse_wnpp_bug_file() 

494 # done_bugs get their own closing mail (and the wnpp-rm file also 

495 # contains our removal bugs); don't close them a second time 

496 related_wnpp_bugs = [bug for bug in wnpp.get(source_pkg, []) if bug not in done] 

497 related_bugs = [ 

498 bugno for bugno in _find_related_bugs(source_pkg) if str(bugno) not in done 

499 ] 

500 

501 ####################################################################################################### 

502 log_filename = cnf["Rm::LogFile"] 

503 log822_filename = cnf["Rm::LogFile822"] 

504 with open(log_filename, "a") as logfile, open(log822_filename, "a") as logfile822: 

505 fcntl.lockf(logfile, fcntl.LOCK_EX) 

506 fcntl.lockf(logfile822, fcntl.LOCK_EX) 

507 

508 logfile.write( 

509 "=========================================================================\n" 

510 ) 

511 logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami)) 

512 logfile.write( 

513 "Removed the following packages from %s:\n\n%s" % (suites_list, summary) 

514 ) 

515 if done_bugs: 

516 logfile.write("Closed bugs: %s\n" % (", ".join(done_bugs))) 

517 logfile.write("\n------------------- Reason -------------------\n%s\n" % reason) 

518 logfile.write("----------------------------------------------\n") 

519 

520 logfile822.write("Date: %s\n" % date) 

521 logfile822.write("Ftpmaster: %s\n" % whoami) 

522 logfile822.write("Suite: %s\n" % suites_list) 

523 

524 if sources: 524 ↛ 528line 524 didn't jump to line 528 because the condition on line 524 was always true

525 logfile822.write("Sources:\n") 

526 logfile822.writelines(" %s\n" % source for source in sources) 

527 

528 if binaries: 528 ↛ 532line 528 didn't jump to line 532 because the condition on line 528 was always true

529 logfile822.write("Binaries:\n") 

530 logfile822.writelines(" %s\n" % binary for binary in binaries) 

531 

532 logfile822.write("Reason: %s\n" % reason.replace("\n", "\n ")) 

533 if done_bugs: 

534 logfile822.write("Bug: %s\n" % (", ".join(done_bugs))) 

535 

536 for i in removals: 

537 package = i[0] 

538 architecture = i[2] 

539 package_id = i[3] 

540 for suite_id in suite_ids_list: 

541 if architecture == "source": 

542 q = session.execute( 

543 sql.text( 

544 "DELETE FROM src_associations sa USING source s WHERE sa.source = s.id AND sa.source = :packageid AND sa.suite = :suiteid RETURNING s.source" 

545 ), 

546 {"packageid": package_id, "suiteid": suite_id}, 

547 ) 

548 affected_sources.add(q.scalar_one()) 

549 else: 

550 q = session.execute( 

551 sql.text( 

552 "DELETE FROM bin_associations ba USING binaries b, source s WHERE ba.bin = b.id AND b.source = s.id AND ba.bin = :packageid AND ba.suite = :suiteid RETURNING s.source" 

553 ), 

554 {"packageid": package_id, "suiteid": suite_id}, 

555 ) 

556 affected_sources.add(q.scalar_one()) 

557 # Delete from the override file 

558 if not partial: 558 ↛ 540line 558 didn't jump to line 540 because the condition on line 558 was always true

559 if architecture == "source": 

560 type_id = dsc_type_id 

561 else: 

562 type_id = deb_type_id 

563 # TODO: Fix this properly to remove the remaining non-bind argument 

564 session.execute( 

565 sql.text( 

566 "DELETE FROM override WHERE package = :package AND type = :typeid AND suite = :suiteid %s" 

567 % (con_components) 

568 ), 

569 {"package": package, "typeid": type_id, "suiteid": suite_id}, 

570 ) 

571 

572 session.commit() 

573 # ### REMOVAL COMPLETE - send mail time ### # 

574 

575 # If we don't have a Bug server configured, we're done 

576 if "Dinstall::BugServer" not in cnf: 

577 if done_bugs or close_related_bugs: 577 ↛ 578line 577 didn't jump to line 578 because the condition on line 577 was never true

578 utils.warn( 

579 "Cannot send mail to BugServer as Dinstall::BugServer is not configured" 

580 ) 

581 

582 logfile.write( 

583 "=========================================================================\n" 

584 ) 

585 logfile822.write("\n") 

586 return 

587 

588 # read common subst variables for all bug closure mails 

589 Subst_common = {} 

590 Subst_common["__RM_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"] 

591 Subst_common["__BUG_SERVER__"] = cnf["Dinstall::BugServer"] 

592 Subst_common["__CC__"] = "X-DAK: dak rm" 

593 if carbon_copy: 593 ↛ 595line 593 didn't jump to line 595 because the condition on line 593 was always true

594 Subst_common["__CC__"] += "\nCc: " + ", ".join(carbon_copy) 

595 Subst_common["__SOURCES__"] = ", ".join(sorted(affected_sources)) 

596 Subst_common["__SUITE_LIST__"] = suites_list 

597 Subst_common["__SUITES__"] = ", ".join(sorted(suites)) 

598 Subst_common["__SUBJECT__"] = "Removed package(s) from %s" % (suites_list) 

599 Subst_common["__ADMIN_ADDRESS__"] = cnf["Dinstall::MyAdminAddress"] 

600 Subst_common["__DISTRO__"] = cnf["Dinstall::MyDistribution"] 

601 Subst_common["__WHOAMI__"] = whoami 

602 

603 # Send the bug closing messages 

604 if done_bugs: 604 ↛ 636line 604 didn't jump to line 636 because the condition on line 604 was always true

605 Subst_close_rm = Subst_common 

606 bcc = [] 

607 if cnf.find("Dinstall::Bcc") != "": 607 ↛ 608line 607 didn't jump to line 608 because the condition on line 607 was never true

608 bcc.append(cnf["Dinstall::Bcc"]) 

609 if cnf.find("Rm::Bcc") != "": 609 ↛ 610line 609 didn't jump to line 610 because the condition on line 609 was never true

610 bcc.append(cnf["Rm::Bcc"]) 

611 if bcc: 611 ↛ 612line 611 didn't jump to line 612 because the condition on line 611 was never true

612 Subst_close_rm["__BCC__"] = "Bcc: " + ", ".join(bcc) 

613 else: 

614 Subst_close_rm["__BCC__"] = "X-Filler: 42" 

615 summarymail = "%s\n------------------- Reason -------------------\n%s\n" % ( 

616 summary, 

617 reason, 

618 ) 

619 summarymail += "----------------------------------------------\n" 

620 Subst_close_rm["__SUMMARY__"] = summarymail 

621 

622 for bug in done_bugs: 

623 Subst_close_rm["__BUG_NUMBER__"] = bug 

624 if close_related_bugs: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true

625 mail_message = utils.TemplateSubst( 

626 Subst_close_rm, 

627 cnf["Dir::Templates"] + "/rm.bug-close-with-related", 

628 ) 

629 else: 

630 mail_message = utils.TemplateSubst( 

631 Subst_close_rm, cnf["Dir::Templates"] + "/rm.bug-close" 

632 ) 

633 utils.send_mail(mail_message, whitelists=whitelists) 

634 

635 # close associated bug reports 

636 if close_related_bugs: 636 ↛ 638line 636 didn't jump to line 638 because the condition on line 636 was never true

637 # the related bugs were retrieved before the removal started 

638 assert source_pkg is not None and related_version is not None 

639 Subst_close_other = Subst_common 

640 bcc = [] 

641 Subst_close_other["__VERSION__"] = related_version 

642 if bcc: 

643 Subst_close_other["__BCC__"] = "Bcc: " + ", ".join(bcc) 

644 else: 

645 Subst_close_other["__BCC__"] = "X-Filler: 42" 

646 # at this point, I just assume, that the first closed bug gives 

647 # some useful information on why the package got removed 

648 Subst_close_other["__BUG_NUMBER__"] = done_bugs[0] if done_bugs else "" 

649 Subst_close_other["__BUG_NUMBER_ALSO__"] = "" 

650 Subst_close_other["__SOURCE__"] = source_pkg 

651 if related_bugs: 

652 Subst_close_other["__BUG_NUMBER_ALSO__"] += "".join( 

653 f"{bugno}-done@{cnf['Dinstall::BugServer']}," 

654 for bugno in related_bugs 

655 ) 

656 also_bugs = " ".join(str(bugno) for bugno in related_bugs) 

657 logfile.write(f"Also closing bug(s): {also_bugs}\n") 

658 logfile822.write(f"Also-Bugs: {also_bugs}\n") 

659 if related_wnpp_bugs: 

660 Subst_close_other["__BUG_NUMBER_ALSO__"] += "".join( 

661 f"{bug}-done@{cnf['Dinstall::BugServer']}," 

662 for bug in related_wnpp_bugs 

663 ) 

664 also_wnpp = " ".join(related_wnpp_bugs) 

665 logfile.write(f"Also closing WNPP bug(s): {also_wnpp}\n") 

666 logfile822.write(f"Also-WNPP: {also_wnpp}\n") 

667 

668 mail_message = utils.TemplateSubst( 

669 Subst_close_other, cnf["Dir::Templates"] + "/rm.bug-close-related" 

670 ) 

671 if Subst_close_other["__BUG_NUMBER_ALSO__"]: 

672 utils.send_mail(mail_message) 

673 

674 logfile.write( 

675 "=========================================================================\n" 

676 ) 

677 logfile822.write("\n")