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

1#! /usr/bin/env python3 

2# vim:set et ts=4 sw=4: 

3 

4"""Handles NEW and BYHAND packages 

5 

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. 

16 

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. 

21 

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 

25 

26################################################################################ 

27 

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!!! 

42 

43################################################################################ 

44 

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 

58 

59import apt_pkg 

60from sqlalchemy import or_, sql 

61 

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 

87 

88if TYPE_CHECKING: 

89 from sqlalchemy.engine import Row 

90 from sqlalchemy.orm import Session 

91 

92# Globals 

93Options: apt_pkg.Configuration 

94Logger: daklog.Logger 

95 

96Priorities: "Priority_Completer" 

97Sections: "Section_Completer" 

98 

99################################################################################ 

100################################################################################ 

101################################################################################ 

102 

103 

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) 

110 

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 

122 

123 

124############################################################ 

125 

126 

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) 

133 

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 

145 

146 

147################################################################################ 

148 

149 

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 

181 

182 

183################################################################################ 

184 

185 

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 

233 

234 

235################################################################################ 

236 

237 

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() 

251 

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 

286 

287 

288################################################################################ 

289 

290 

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])) 

301 

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 

308 

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() 

316 

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 

325 

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 

339 

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 

351 

352 # Reset the readline completer 

353 readline.set_completer(None) 

354 

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 

362 

363 return new 

364 

365 

366################################################################################ 

367 

368 

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 ? " 

379 

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 

393 

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) 

400 

401 return new 

402 

403 

404################################################################################ 

405 

406 

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()] 

414 

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)) 

423 

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)) 

430 

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) 

440 

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() 

452 

453 

454################################################################################ 

455 

456## FIXME: horribly Debian specific 

457 

458 

459def do_bxa_notification( 

460 new: list[MissingOverride], 

461 upload: daklib.dbconn.PolicyQueueUpload, 

462 session: "Session", 

463) -> None: 

464 cnf = Config() 

465 

466 new_packages = set(o["package"] for o in new if o["type"] == "deb") 

467 if len(new_packages) == 0: 

468 return 

469 

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) 

481 

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 } 

491 

492 bxa_mail = utils.TemplateSubst( 

493 subst, os.path.join(cnf["Dir::Templates"], "process-new.bxa_notification") 

494 ) 

495 utils.send_mail(bxa_mail) 

496 

497 

498################################################################################ 

499 

500 

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 

507 

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 

514 

515 shell_command = command.format( 

516 directory=directory, 

517 dsc=dsc, 

518 changes=changes, 

519 ) 

520 

521 subprocess.check_call(shell_command, shell=True) 

522 

523 

524################################################################################ 

525 

526 

527def get_reject_reason(reason: str = "") -> Optional[str]: 

528 """get reason for rejection 

529 

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" 

536 

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() 

550 

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) 

553 

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 

557 

558 

559################################################################################ 

560 

561 

562def do_new( 

563 upload: daklib.dbconn.PolicyQueueUpload, 

564 upload_copy: UploadCopy, 

565 handler: PolicyQueueUploadHandler, 

566 session: "Session", 

567) -> None: 

568 cnf = Config() 

569 

570 run_user_inspect_command(upload, upload_copy) 

571 

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 

578 

579 missing = handler.missing_overrides(hints=missing) 

580 broken = not check_valid(missing, session) 

581 

582 changesname = os.path.basename(upload.changes.changesname) 

583 

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() 

592 

593 if missing: 

594 print("NEW\n") 

595 

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" 

599 

600 answer = "XXX" 

601 if Options["No-Action"] or Options["Automatic"]: 

602 answer = "S" 

603 

604 note = print_new(upload, missing, indexed=False, session=session) 

605 prompt = "" 

606 

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 

617 

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, " 

631 

632 prompt += ( 

633 "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?" 

634 ) 

635 

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() 

643 

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 

647 

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() 

722 

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) 

728 

729 if handler.get_action(): 

730 print("PENDING %s\n" % handler.get_action()) 

731 

732 

733################################################################################ 

734################################################################################ 

735################################################################################ 

736 

737 

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 

750 

751ENVIRONMENT VARIABLES 

752 

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: 

756 

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 

760 

761 Note that Python's 'format' method is used to format the command. 

762 

763 Example: run mc in a tmux session to inspect the upload 

764 

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"' 

766 

767 and run 

768 

769 tmux attach -t process-new 

770 

771 in a separate terminal session. 

772""" 

773 ) 

774 sys.exit(exit_code) 

775 

776 

777################################################################################ 

778 

779 

780@contextlib.contextmanager 

781def lock_package(package: str) -> Iterator[int]: 

782 """ 

783 Lock `package` so that noone else jumps in processing it. 

784 

785 :param package: source package name to lock 

786 """ 

787 

788 cnf = Config() 

789 

790 path = os.path.join(cnf.get("Process-New::LockDir", cnf["Dir::Lock"]), package) 

791 

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 

806 

807 try: 

808 yield fd 

809 finally: 

810 os.unlink(path) 

811 

812 

813def do_pkg(upload: daklib.dbconn.PolicyQueueUpload, session: "Session") -> None: 

814 cnf = Config() 

815 group = cnf.get("Dinstall::UnprivGroup") or None 

816 

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 

826 

827 do_new(upload, upload_copy, handler, session) 

828 except AlreadyLockedError as e: 

829 print("Seems to be locked by %s already, skipping..." % (e)) 

830 

831 

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 

838 

839 query = """SELECT package, version, comment, author 

840 FROM new_comments 

841 WHERE package IN :sources 

842 ORDER BY package, version""" 

843 

844 r = session.execute(sql.text(query), params={"sources": tuple(sources)}) 

845 

846 for i in r: 

847 print("%s_%s\n%s\n(%s)\n\n\n" % (i[0], i[1], i[2], i[3])) 

848 

849 session.rollback() 

850 

851 

852################################################################################ 

853 

854 

855class Change(TypedDict): 

856 upload: daklib.dbconn.PolicyQueueUpload 

857 date: datetime.datetime 

858 stack: int 

859 binary: bool 

860 comments: bool 

861 

862 

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] 

912 

913 

914################################################################################ 

915 

916 

917def end() -> None: 

918 accept_count = SummaryStats().accept_count 

919 accept_bytes = SummaryStats().accept_bytes 

920 

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]) 

931 

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() 

934 

935 

936################################################################################ 

937 

938 

939def main() -> None: 

940 global Options, Logger, Sections, Priorities 

941 

942 cnf = Config() 

943 session = DBConn().session() 

944 

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 ] 

955 

956 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined] 

957 

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] = "" 

971 

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 ) 

984 

985 Options = cnf.subtree("Process-New::Options") 

986 

987 if Options["Help"]: 

988 usage() 

989 

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] 

995 

996 Sections = Section_Completer(session) 

997 Priorities = Priority_Completer(session) 

998 readline.parse_and_bind("tab: complete") 

999 

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 ) 

1005 

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) 

1011 

1012 end() 

1013 

1014 

1015################################################################################ 

1016 

1017 

1018if __name__ == "__main__": 

1019 main()