Coverage for dak/process_new.py: 53%

511 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-01-04 16:18 +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 index_range(index: int) -> str: 

239 if index == 1: 

240 return "1" 

241 else: 

242 return "1-%s" % (index) 

243 

244 

245################################################################################ 

246 

247 

248def edit_new( 

249 overrides: list[MissingOverride], 

250 upload: daklib.dbconn.PolicyQueueUpload, 

251 session: "Session", 

252) -> list[MissingOverride]: 

253 with tempfile.NamedTemporaryFile(mode="w+t") as fh: 

254 # Write the current data to a temporary file 

255 print_new(upload, overrides, indexed=False, session=session, file=fh) 

256 fh.flush() 

257 utils.call_editor_for_file(fh.name) 

258 # Read the edited data back in 

259 fh.seek(0) 

260 lines = fh.readlines() 

261 

262 overrides_map = dict([((o["type"], o["package"]), o) for o in overrides]) 

263 new_overrides: list[MissingOverride] = [] 

264 # Parse the new data 

265 for line in lines: 

266 line = line.strip() 

267 if line == "" or line[0] == "#": 

268 continue 

269 s = line.split() 

270 # Pad the list if necessary 

271 s[len(s) : 3] = ["None"] * (3 - len(s)) 

272 (pkg, priority, section) = s[:3] 

273 if pkg.find(":") != -1: 

274 type, pkg = pkg.split(":", 1) 

275 else: 

276 type = "deb" 

277 o = overrides_map.get((type, pkg), None) 

278 if o is None: 

279 utils.warn("Ignoring unknown package '%s'" % (pkg)) 

280 else: 

281 if section.find("/") != -1: 

282 component = section.split("/", 1)[0] 

283 else: 

284 component = "main" 

285 new_overrides.append( 

286 dict( 

287 package=pkg, 

288 type=type, 

289 section=section, 

290 component=component, 

291 priority=priority, 

292 included=o["included"], 

293 ) 

294 ) 

295 return new_overrides 

296 

297 

298################################################################################ 

299 

300 

301def edit_index( 

302 new: list[MissingOverride], upload: daklib.dbconn.PolicyQueueUpload, index: int 

303) -> list[MissingOverride]: 

304 package = new[index]["package"] 

305 priority = new[index]["priority"] 

306 section = new[index]["section"] 

307 ftype = new[index]["type"] 

308 done = False 

309 while not done: 

310 print("\t".join([package, priority, section])) 

311 

312 answer = "XXX" 

313 if ftype != "dsc": 

314 prompt = "[B]oth, Priority, Section, Done ? " 

315 else: 

316 prompt = "[S]ection, Done ? " 

317 edit_priority = edit_section = 0 

318 

319 while prompt.find(answer) == -1: 

320 answer = utils.input_or_exit(prompt) 

321 if answer == "": 

322 m = re_default_answer.match(prompt) 

323 assert m is not None 

324 answer = m.group(1) 

325 answer = answer[:1].upper() 

326 

327 if answer == "P": 

328 edit_priority = 1 

329 elif answer == "S": 

330 edit_section = 1 

331 elif answer == "B": 

332 edit_priority = edit_section = 1 

333 elif answer == "D": 

334 done = True 

335 

336 # Edit the priority 

337 if edit_priority: 

338 readline.set_completer(Priorities.complete) 

339 got_priority = 0 

340 while not got_priority: 

341 new_priority = utils.input_or_exit("New priority: ").strip() 

342 if new_priority not in Priorities.priorities: 

343 print( 

344 "E: '%s' is not a valid priority, try again." % (new_priority) 

345 ) 

346 else: 

347 got_priority = 1 

348 priority = new_priority 

349 

350 # Edit the section 

351 if edit_section: 

352 readline.set_completer(Sections.complete) 

353 got_section = 0 

354 while not got_section: 

355 new_section = utils.input_or_exit("New section: ").strip() 

356 if new_section not in Sections.sections: 

357 print("E: '%s' is not a valid section, try again." % (new_section)) 

358 else: 

359 got_section = 1 

360 section = new_section 

361 

362 # Reset the readline completer 

363 readline.set_completer(None) 

364 

365 new[index]["priority"] = priority 

366 new[index]["section"] = section 

367 if section.find("/") != -1: 

368 component = section.split("/", 1)[0] 

369 else: 

370 component = "main" 

371 new[index]["component"] = component 

372 

373 return new 

374 

375 

376################################################################################ 

377 

378 

379def edit_overrides( 

380 new: list[MissingOverride], 

381 upload: daklib.dbconn.PolicyQueueUpload, 

382 session: "Session", 

383) -> list[MissingOverride]: 

384 print() 

385 done = False 

386 while not done: 

387 print_new(upload, new, indexed=True, session=session) 

388 prompt = "edit override <n>, Editor, Done ? " 

389 

390 got_answer = 0 

391 while not got_answer: 

392 answer = utils.input_or_exit(prompt) 

393 if not answer.isdigit(): 

394 answer = answer[:1].upper() 

395 if answer == "E" or answer == "D": 

396 got_answer = 1 

397 elif re_isanum.match(answer): 

398 answer_int = int(answer) 

399 if answer_int < 1 or answer_int > len(new): 

400 print("{0} is not a valid index. Please retry.".format(answer_int)) 

401 else: 

402 got_answer = 1 

403 

404 if answer == "E": 

405 new = edit_new(new, upload, session) 

406 elif answer == "D": 

407 done = True 

408 else: 

409 edit_index(new, upload, answer_int - 1) 

410 

411 return new 

412 

413 

414################################################################################ 

415 

416 

417def check_pkg( 

418 upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy, session: "Session" 

419): 

420 changes = os.path.join(upload_copy.directory, upload.changes.changesname) 

421 suite_name = upload.target_suite.suite_name 

422 handler = PolicyQueueUploadHandler(upload, session) 

423 missing = [(m["type"], m["package"]) for m in handler.missing_overrides()] 

424 

425 less_cmd = ("less", "-r", "-") 

426 less_process = subprocess.Popen( 

427 less_cmd, bufsize=0, stdin=subprocess.PIPE, text=True 

428 ) 

429 try: 

430 less_fd = less_process.stdin 

431 assert less_fd is not None 

432 less_fd.write(dak.examine_package.display_changes(suite_name, changes)) 

433 

434 source = upload.source 

435 if source is not None: 435 ↛ 441line 435 didn't jump to line 441 because the condition on line 435 was always true

436 source_file = os.path.join( 

437 upload_copy.directory, os.path.basename(source.poolfile.filename) 

438 ) 

439 less_fd.write(dak.examine_package.check_dsc(suite_name, source_file)) 

440 

441 for binary in upload.binaries: 

442 binary_file = os.path.join( 

443 upload_copy.directory, os.path.basename(binary.poolfile.filename) 

444 ) 

445 examined = dak.examine_package.check_deb(suite_name, binary_file) 

446 # We always need to call check_deb to display package relations for every binary, 

447 # but we print its output only if new overrides are being added. 

448 if ("deb", binary.package) in missing: 448 ↛ 441line 448 didn't jump to line 441 because the condition on line 448 was always true

449 less_fd.write(examined) 

450 

451 less_fd.write(dak.examine_package.output_package_relations()) 

452 less_fd.close() 

453 except OSError as e: 

454 if e.errno == errno.EPIPE: 

455 utils.warn("[examine_package] Caught EPIPE; skipping.") 

456 else: 

457 raise 

458 except KeyboardInterrupt: 

459 utils.warn("[examine_package] Caught C-c; skipping.") 

460 finally: 

461 less_process.communicate() 

462 

463 

464################################################################################ 

465 

466## FIXME: horribly Debian specific 

467 

468 

469def do_bxa_notification( 

470 new: list[MissingOverride], 

471 upload: daklib.dbconn.PolicyQueueUpload, 

472 session: "Session", 

473) -> None: 

474 cnf = Config() 

475 

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

477 if len(new_packages) == 0: 

478 return 

479 

480 key = session.query(MetadataKey).filter_by(key="Description").one() 

481 summary = "" 

482 for binary in upload.binaries: 

483 if binary.package not in new_packages: 

484 continue 

485 description = ( 

486 session.query(BinaryMetadata).filter_by(binary=binary, key=key).one().value 

487 ) 

488 summary += "\n" 

489 summary += "Package: {0}\n".format(binary.package) 

490 summary += "Description: {0}\n".format(description) 

491 

492 subst = { 

493 "__DISTRO__": cnf["Dinstall::MyDistribution"], 

494 "__BCC__": "X-DAK: dak process-new", 

495 "__BINARY_DESCRIPTIONS__": summary, 

496 "__CHANGES_FILENAME__": upload.changes.changesname, 

497 "__SOURCE__": upload.changes.source, 

498 "__VERSION__": upload.changes.version, 

499 "__ARCHITECTURE__": upload.changes.architecture, 

500 } 

501 

502 bxa_mail = utils.TemplateSubst( 

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

504 ) 

505 utils.send_mail(bxa_mail) 

506 

507 

508################################################################################ 

509 

510 

511def run_user_inspect_command( 

512 upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy 

513) -> None: 

514 command = os.environ.get("DAK_INSPECT_UPLOAD") 

515 if command is None: 515 ↛ 518line 515 didn't jump to line 518 because the condition on line 515 was always true

516 return 

517 

518 directory = upload_copy.directory 

519 if upload.source: 

520 dsc = os.path.basename(upload.source.poolfile.filename) 

521 else: 

522 dsc = "" 

523 changes = upload.changes.changesname 

524 

525 shell_command = command.format( 

526 directory=directory, 

527 dsc=dsc, 

528 changes=changes, 

529 ) 

530 

531 subprocess.check_call(shell_command, shell=True) 

532 

533 

534################################################################################ 

535 

536 

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

538 """get reason for rejection 

539 

540 :return: string giving the reason for the rejection or :const:`None` if the 

541 rejection should be cancelled 

542 """ 

543 answer = "E" 

544 if Options["Automatic"]: 544 ↛ 545line 544 didn't jump to line 545 because the condition on line 544 was never true

545 answer = "R" 

546 

547 while answer == "E": 

548 reason = utils.call_editor(reason) 

549 print("Reject message:") 

550 print(utils.prefix_multi_line_string(reason, " ", include_blank_lines=True)) 

551 prompt = "[R]eject, Edit, Abandon, Quit ?" 

552 answer = "XXX" 

553 while prompt.find(answer) == -1: 

554 answer = utils.input_or_exit(prompt) 

555 if answer == "": 555 ↛ 556line 555 didn't jump to line 556 because the condition on line 555 was never true

556 m = re_default_answer.search(prompt) 

557 assert m is not None 

558 answer = m.group(1) 

559 answer = answer[:1].upper() 

560 

561 if answer == "Q": 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true

562 sys.exit(0) 

563 

564 if answer == "R": 564 ↛ 566line 564 didn't jump to line 566 because the condition on line 564 was always true

565 return reason 

566 return None 

567 

568 

569################################################################################ 

570 

571 

572def do_new( 

573 upload: daklib.dbconn.PolicyQueueUpload, 

574 upload_copy: UploadCopy, 

575 handler: PolicyQueueUploadHandler, 

576 session: "Session", 

577) -> None: 

578 run_user_inspect_command(upload, upload_copy) 

579 

580 # The main NEW processing loop 

581 done = False 

582 missing: list[MissingOverride] = [] 

583 while not done: 

584 queuedir = upload.policy_queue.path 

585 byhand = upload.byhand 

586 

587 missing = handler.missing_overrides(hints=missing) 

588 broken = not check_valid(missing, session) 

589 

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

591 

592 print() 

593 print(changesname) 

594 print("-" * len(changesname)) 

595 print() 

596 print(" Target: {0}".format(upload.target_suite.suite_name)) 

597 print(" Changed-By: {0}".format(upload.changes.changedby)) 

598 print(" Date: {0}".format(upload.changes.date)) 

599 print() 

600 

601 if missing: 

602 print("NEW\n") 

603 

604 for package in missing: 

605 if package["type"] == "deb" and package["priority"] == "extra": 605 ↛ 606line 605 didn't jump to line 606 because the condition on line 605 was never true

606 package["priority"] = "optional" 

607 

608 answer = "XXX" 

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

610 answer = "S" 

611 

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

613 prompt = "" 

614 

615 has_unprocessed_byhand = False 

616 for f in byhand: 616 ↛ 617line 616 didn't jump to line 617 because the loop on line 616 never started

617 path = os.path.join(queuedir, f.filename) 

618 if not f.processed and os.path.exists(path): 

619 print( 

620 "W: {0} still present; please process byhand components and try again".format( 

621 f.filename 

622 ) 

623 ) 

624 has_unprocessed_byhand = True 

625 

626 if not has_unprocessed_byhand and not broken and not note: 626 ↛ 632line 626 didn't jump to line 632 because the condition on line 626 was always true

627 if len(missing) == 0: 

628 prompt = "Accept, " 

629 answer = "A" 

630 else: 

631 prompt = "Add overrides, " 

632 if broken: 632 ↛ 633line 632 didn't jump to line 633 because the condition on line 632 was never true

633 print( 

634 "W: [!] marked entries must be fixed before package can be processed." 

635 ) 

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

637 print("W: note must be removed before package can be processed.") 

638 prompt += "RemOve all notes, Remove note, " 

639 

640 prompt += ( 

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

642 ) 

643 

644 while prompt.find(answer) == -1: 

645 answer = utils.input_or_exit(prompt) 

646 if answer == "": 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true

647 m = re_default_answer.search(prompt) 

648 assert m is not None 

649 answer = m.group(1) 

650 answer = answer[:1].upper() 

651 

652 if answer in ("A", "E", "M", "O", "R") and Options["Trainee"]: 652 ↛ 653line 652 didn't jump to line 653 because the condition on line 652 was never true

653 utils.warn("Trainees can't do that") 

654 continue 

655 

656 if answer == "A" and not Options["Trainee"]: 

657 handler.add_overrides(missing, upload.target_suite) 

658 if Config().find_b("Dinstall::BXANotify"): 658 ↛ 660line 658 didn't jump to line 660 because the condition on line 658 was always true

659 do_bxa_notification(missing, upload, session) 

660 handler.accept() 

661 done = True 

662 Logger.log(["NEW ACCEPT", upload.changes.changesname]) 

663 elif answer == "C": 

664 check_pkg(upload, upload_copy, session) 

665 elif answer == "E" and not Options["Trainee"]: 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true

666 missing = edit_overrides(missing, upload, session) 

667 elif answer == "M" and not Options["Trainee"]: 

668 reason = Options.get("Manual-Reject", "") + "\n" 

669 reason = reason + "\n\n=====\n\n".join( 

670 [ 

671 n.comment 

672 for n in get_new_comments( 

673 upload.policy_queue, upload.changes.source, session=session 

674 ) 

675 ] 

676 ) 

677 edited_reason = get_reject_reason(reason) 

678 if edited_reason is not None: 678 ↛ 734line 678 didn't jump to line 734 because the condition on line 678 was always true

679 Logger.log(["NEW REJECT", upload.changes.changesname]) 

680 handler.reject(edited_reason) 

681 done = True 

682 elif answer == "N": 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true

683 if ( 

684 edit_note( 

685 upload, 

686 session, 

687 bool(Options["Trainee"]), 

688 ) 

689 == 0 

690 ): 

691 end() 

692 sys.exit(0) 

693 elif answer == "P" and not Options["Trainee"]: 693 ↛ 694line 693 didn't jump to line 694 because the condition on line 693 was never true

694 if ( 

695 prod_maintainer( 

696 get_new_comments( 

697 upload.policy_queue, upload.changes.source, session=session 

698 ), 

699 upload, 

700 session, 

701 bool(Options["Trainee"]), 

702 ) 

703 == 0 

704 ): 

705 end() 

706 sys.exit(0) 

707 Logger.log(["NEW PROD", upload.changes.changesname]) 

708 elif answer == "R" and not Options["Trainee"]: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true

709 confirm = utils.input_or_exit("Really clear note (y/N)? ").lower() 

710 if confirm == "y": 

711 for c in get_new_comments( 

712 upload.policy_queue, 

713 upload.changes.source, 

714 upload.changes.version, 

715 session=session, 

716 ): 

717 session.delete(c) 

718 session.commit() 

719 elif answer == "O" and not Options["Trainee"]: 719 ↛ 720line 719 didn't jump to line 720 because the condition on line 719 was never true

720 confirm = utils.input_or_exit("Really clear all notes (y/N)? ").lower() 

721 if confirm == "y": 

722 for c in get_new_comments( 

723 upload.policy_queue, upload.changes.source, session=session 

724 ): 

725 session.delete(c) 

726 session.commit() 

727 

728 elif answer == "S": 728 ↛ 730line 728 didn't jump to line 730 because the condition on line 728 was always true

729 done = True 

730 elif answer == "Q": 

731 end() 

732 sys.exit(0) 

733 

734 if handler.get_action(): 

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

736 

737 

738################################################################################ 

739################################################################################ 

740################################################################################ 

741 

742 

743def usage(exit_code: int = 0) -> NoReturn: 

744 print( 

745 """Usage: dak process-new [OPTION]... [CHANGES]... 

746 -a, --automatic automatic run 

747 -b, --no-binaries do not sort binary-NEW packages first 

748 -c, --comments show NEW comments 

749 -h, --help show this help and exit. 

750 -m, --manual-reject=MSG manual reject with `msg' 

751 -n, --no-action don't do anything 

752 -q, --queue=QUEUE operate on a different queue 

753 -t, --trainee FTP Trainee mode 

754 -V, --version display the version number and exit 

755 

756ENVIRONMENT VARIABLES 

757 

758 DAK_INSPECT_UPLOAD: shell command to run to inspect a package 

759 The command is automatically run in a shell when an upload 

760 is checked. The following substitutions are available: 

761 

762 {directory}: directory the upload is contained in 

763 {dsc}: name of the included dsc or the empty string 

764 {changes}: name of the changes file 

765 

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

767 

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

769 

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

771 

772 and run 

773 

774 tmux attach -t process-new 

775 

776 in a separate terminal session. 

777""" 

778 ) 

779 sys.exit(exit_code) 

780 

781 

782################################################################################ 

783 

784 

785@contextlib.contextmanager 

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

787 """ 

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

789 

790 :param package: source package name to lock 

791 """ 

792 

793 cnf = Config() 

794 

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

796 

797 try: 

798 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDONLY, 0o644) 

799 except OSError as e: 

800 if e.errno == errno.EEXIST or e.errno == errno.EACCES: 

801 try: 

802 user = ( 

803 pwd.getpwuid(os.stat(path)[stat.ST_UID])[4] 

804 .split(",")[0] 

805 .replace(".", "") 

806 ) 

807 except KeyError: 

808 user = "TotallyUnknown" 

809 raise AlreadyLockedError(user) 

810 raise 

811 

812 try: 

813 yield fd 

814 finally: 

815 os.unlink(path) 

816 

817 

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

819 cnf = Config() 

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

821 

822 try: 

823 with ( 

824 lock_package(upload.changes.source), 

825 UploadCopy(upload, group=group) as upload_copy, 

826 ): 

827 handler = PolicyQueueUploadHandler(upload, session) 

828 if handler.get_action() is not None: 

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

830 return 

831 

832 do_new(upload, upload_copy, handler, session) 

833 except AlreadyLockedError as e: 

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

835 

836 

837def show_new_comments( 

838 uploads: Iterable[daklib.dbconn.PolicyQueueUpload], session: "Session" 

839) -> None: 

840 sources = [upload.changes.source for upload in uploads] 

841 if len(sources) == 0: 

842 return 

843 

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

845 FROM new_comments 

846 WHERE package IN :sources 

847 ORDER BY package, version""" 

848 

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

850 

851 for i in r: 

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

853 

854 session.rollback() 

855 

856 

857################################################################################ 

858 

859 

860class Change(TypedDict): 

861 upload: daklib.dbconn.PolicyQueueUpload 

862 date: datetime.datetime 

863 stack: int 

864 binary: bool 

865 comments: bool 

866 

867 

868def sort_uploads( 

869 new_queue: PolicyQueue, 

870 uploads: Iterable[daklib.dbconn.PolicyQueueUpload], 

871 session: "Session", 

872 nobinaries: bool = False, 

873) -> list[daklib.dbconn.PolicyQueueUpload]: 

874 sources: dict[str, list[Change]] = defaultdict(list) 

875 sortedchanges = [] 

876 suitesrc = [ 

877 s.source 

878 for s in session.query(DBSource.source).filter( 

879 DBSource.suites.any(Suite.suite_name.in_(["unstable", "experimental"])) 

880 ) 

881 ] 

882 comments = [ 

883 p.package 

884 for p in session.query(NewComment.package) 

885 .filter_by(trainee=False, policy_queue=new_queue) 

886 .distinct() 

887 ] 

888 for upload in uploads: 

889 source = upload.changes.source 

890 sources[source].append( 

891 { 

892 "upload": upload, 

893 "date": upload.changes.created, 

894 "stack": 1, 

895 "binary": True if source in suitesrc else False, 

896 "comments": True if source in comments else False, 

897 } 

898 ) 

899 for src in sources: 

900 if len(sources[src]) > 1: 

901 changes = sources[src] 

902 firstseen = sorted(changes, key=lambda k: (k["date"]))[0]["date"] 

903 changes.sort(key=lambda item: item["date"]) 

904 for i in range(0, len(changes)): 

905 changes[i]["date"] = firstseen 

906 changes[i]["stack"] = i + 1 

907 sortedchanges += sources[src] 

908 if nobinaries: 908 ↛ 909line 908 didn't jump to line 909 because the condition on line 908 was never true

909 sortedchanges.sort( 

910 key=lambda k: (k["comments"], k["binary"], k["date"], -k["stack"]) 

911 ) 

912 else: 

913 sortedchanges.sort( 

914 key=lambda k: (k["comments"], -k["binary"], k["date"], -k["stack"]) 

915 ) 

916 return [u["upload"] for u in sortedchanges] 

917 

918 

919################################################################################ 

920 

921 

922def end() -> None: 

923 accept_count = SummaryStats().accept_count 

924 accept_bytes = SummaryStats().accept_bytes 

925 

926 if accept_count: 926 ↛ 927line 926 didn't jump to line 927 because the condition on line 926 was never true

927 sets = "set" 

928 if accept_count > 1: 

929 sets = "sets" 

930 print( 

931 "Accepted %d package %s, %s." 

932 % (accept_count, sets, utils.size_type(int(accept_bytes))), 

933 file=sys.stderr, 

934 ) 

935 Logger.log(["total", accept_count, accept_bytes]) 

936 

937 if not Options["No-Action"] and not Options["Trainee"]: 937 ↛ exitline 937 didn't return from function 'end' because the condition on line 937 was always true

938 Logger.close() 

939 

940 

941################################################################################ 

942 

943 

944def main() -> None: 

945 global Options, Logger, Sections, Priorities 

946 

947 cnf = Config() 

948 session = DBConn().session() 

949 

950 Arguments = [ 

951 ("a", "automatic", "Process-New::Options::Automatic"), 

952 ("b", "no-binaries", "Process-New::Options::No-Binaries"), 

953 ("c", "comments", "Process-New::Options::Comments"), 

954 ("h", "help", "Process-New::Options::Help"), 

955 ("m", "manual-reject", "Process-New::Options::Manual-Reject", "HasArg"), 

956 ("t", "trainee", "Process-New::Options::Trainee"), 

957 ("q", "queue", "Process-New::Options::Queue", "HasArg"), 

958 ("n", "no-action", "Process-New::Options::No-Action"), 

959 ] 

960 

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

962 

963 for i in [ 

964 "automatic", 

965 "no-binaries", 

966 "comments", 

967 "help", 

968 "manual-reject", 

969 "no-action", 

970 "version", 

971 "trainee", 

972 ]: 

973 key = "Process-New::Options::%s" % i 

974 if key not in cnf: 

975 cnf[key] = "" 

976 

977 queue_name = cnf.get("Process-New::Options::Queue", "new") 

978 new_queue = session.query(PolicyQueue).filter_by(queue_name=queue_name).one() 

979 if len(changes_files) == 0: 

980 uploads = new_queue.uploads 

981 else: 

982 uploads = ( 

983 session.query(PolicyQueueUpload) 

984 .filter_by(policy_queue=new_queue) 

985 .join(DBChange) 

986 .filter(DBChange.changesname.in_(changes_files)) 

987 .all() 

988 ) 

989 

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

991 

992 if Options["Help"]: 

993 usage() 

994 

995 if not Options["No-Action"]: 995 ↛ 1001line 995 didn't jump to line 1001 because the condition on line 995 was always true

996 try: 

997 Logger = daklog.Logger("process-new") 

998 except OSError: 

999 Options["Trainee"] = "True" # type: ignore[index] 

1000 

1001 Sections = Section_Completer(session) 

1002 Priorities = Priority_Completer(session) 

1003 readline.parse_and_bind("tab: complete") 

1004 

1005 if len(uploads) > 1: 

1006 print("Sorting changes...", file=sys.stderr) 

1007 uploads = sort_uploads( 

1008 new_queue, uploads, session, bool(Options["No-Binaries"]) 

1009 ) 

1010 

1011 if Options["Comments"]: 1011 ↛ 1012line 1011 didn't jump to line 1012 because the condition on line 1011 was never true

1012 show_new_comments(uploads, session) 

1013 else: 

1014 for upload in uploads: 

1015 do_pkg(upload, session) 

1016 

1017 end() 

1018 

1019 

1020################################################################################ 

1021 

1022 

1023if __name__ == "__main__": 

1024 main()