Coverage for dak/process_new.py: 53%

507 statements  

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

1"""Handles NEW and BYHAND packages 

2 

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

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

5@copyright: 2009 Joerg Jaspert <joerg@debian.org> 

6@copyright: 2009 Frank Lichtenheld <djpig@debian.org> 

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

8""" 

9 

10# This program is free software; you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation; either version 2 of the License, or 

13# (at your option) any later version. 

14 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19 

20# You should have received a copy of the GNU General Public License 

21# along with this program; if not, write to the Free Software 

22# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

23 

24################################################################################ 

25 

26# 23:12|<aj> I will not hush! 

27# 23:12|<elmo> :> 

28# 23:12|<aj> Where there is injustice in the world, I shall be there! 

29# 23:13|<aj> I shall not be silenced! 

30# 23:13|<aj> The world shall know! 

31# 23:13|<aj> The world *must* know! 

32# 23:13|<elmo> oh dear, he's gone back to powerpuff girls... ;-) 

33# 23:13|<aj> yay powerpuff girls!! 

34# 23:13|<aj> buttercup's my favourite, who's yours? 

35# 23:14|<aj> you're backing away from the keyboard right now aren't you? 

36# 23:14|<aj> *AREN'T YOU*?! 

37# 23:15|<aj> I will not be treated like this. 

38# 23:15|<aj> I shall have my revenge. 

39# 23:15|<aj> I SHALL!!! 

40 

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

42 

43import contextlib 

44import datetime 

45import errno 

46import os 

47import pwd 

48import readline 

49import stat 

50import subprocess 

51import sys 

52import tempfile 

53from collections import defaultdict 

54from collections.abc import Iterable, Iterator 

55from typing import TYPE_CHECKING, NoReturn, TypedDict 

56 

57import apt_pkg 

58from sqlalchemy import or_, select, sql 

59 

60import dak.examine_package 

61import daklib.dbconn 

62from daklib import daklog, utils 

63from daklib.config import Config 

64from daklib.dak_exceptions import AlreadyLockedError 

65from daklib.dbconn import ( 

66 BinaryMetadata, 

67 DBBinary, 

68 DBChange, 

69 DBConn, 

70 DBSource, 

71 MetadataKey, 

72 NewComment, 

73 PolicyQueue, 

74 PolicyQueueUpload, 

75 Priority, 

76 Section, 

77 Suite, 

78 get_new_comments, 

79) 

80from daklib.policy import MissingOverride, PolicyQueueUploadHandler, UploadCopy 

81from daklib.queue import check_valid, edit_note, prod_maintainer 

82from daklib.regexes import re_default_answer, re_isanum 

83from daklib.summarystats import SummaryStats 

84from daklib.termcolor import colorize as Color 

85 

86if TYPE_CHECKING: 

87 from sqlalchemy.engine import Row 

88 from sqlalchemy.orm import Session 

89 

90# Globals 

91Options: apt_pkg.Configuration 

92Logger: daklog.Logger 

93 

94Priorities: "Priority_Completer" 

95Sections: "Section_Completer" 

96 

97################################################################################ 

98################################################################################ 

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

100 

101 

102class Section_Completer: 

103 def __init__(self, session: "Session"): 

104 self.sections = [] 

105 self.matches: list[str] = [] 

106 for s in session.scalars(select(Section.section)): 

107 self.sections.append(s) 

108 

109 def complete(self, text: str, state: int) -> str | None: 

110 if state == 0: 

111 self.matches = [] 

112 n = len(text) 

113 for word in self.sections: 

114 if word[:n] == text: 

115 self.matches.append(word) 

116 try: 

117 return self.matches[state] 

118 except IndexError: 

119 return None 

120 

121 

122############################################################ 

123 

124 

125class Priority_Completer: 

126 def __init__(self, session: "Session"): 

127 self.priorities = [] 

128 self.matches: list[str] = [] 

129 for p in session.scalars(select(Priority.priority)): 

130 self.priorities.append(p) 

131 

132 def complete(self, text: str, state: int) -> str | None: 

133 if state == 0: 

134 self.matches = [] 

135 n = len(text) 

136 for word in self.priorities: 

137 if word[:n] == text: 

138 self.matches.append(word) 

139 try: 

140 return self.matches[state] 

141 except IndexError: 

142 return None 

143 

144 

145################################################################################ 

146 

147 

148def takenover_binaries( 

149 upload: daklib.dbconn.PolicyQueueUpload, 

150 missing: list[MissingOverride], 

151 session: "Session", 

152) -> "list[Row[tuple[str, str]]]": 

153 rows = [] 

154 binaries = {x.package for x in upload.binaries} 

155 for m in missing: 

156 if m["type"] != "dsc": 

157 binaries.discard(m["package"]) 

158 if binaries: 

159 source = upload.binaries[0].source.source 

160 suite = upload.target_suite.overridesuite or upload.target_suite.suite_name 

161 suites = list( 

162 session.scalars( 

163 select(Suite.suite_name).where( 

164 or_(Suite.suite_name == suite, Suite.overridesuite == suite) 

165 ) 

166 ) 

167 ) 

168 rows = list( 

169 session.execute( 

170 select(DBSource.source, DBBinary.package) 

171 .distinct() 

172 .where(DBBinary.package.in_(binaries)) 

173 .join(DBBinary.source) 

174 .where(DBSource.source != source) 

175 .join(DBBinary.suites) 

176 .where(Suite.suite_name.in_(suites)) 

177 .order_by(DBSource.source, DBBinary.package) 

178 ) 

179 ) 

180 return rows 

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 { 

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 = {o["package"] for o in new if o["type"] == "deb"} 

467 if len(new_packages) == 0: 

468 return 

469 

470 key = session.execute(select(MetadataKey).filter_by(key="Description")).scalar_one() 

471 summary = "" 

472 for binary in upload.binaries: 

473 if binary.package not in new_packages: 

474 continue 

475 description = session.execute( 

476 select(BinaryMetadata.value).filter_by(binary=binary, key=key) 

477 ).scalar_one() 

478 summary += "\n" 

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

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

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 = "") -> str | None: 

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 = list( 

872 session.scalars( 

873 select(DBSource.source).where( 

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

875 ) 

876 ) 

877 ) 

878 comments = list( 

879 session.scalars( 

880 select(NewComment.package) 

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

882 .distinct() 

883 ) 

884 ) 

885 for upload in uploads: 

886 source = upload.changes.source 

887 sources[source].append( 

888 { 

889 "upload": upload, 

890 "date": upload.changes.created, 

891 "stack": 1, 

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

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

894 } 

895 ) 

896 for changes in sources.values(): 

897 if len(changes) > 1: 

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

899 firstseen = changes[0]["date"] 

900 for i in range(len(changes)): 

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

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

903 sortedchanges += changes 

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

905 sortedchanges.sort( 

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

907 ) 

908 else: 

909 sortedchanges.sort( 

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

911 ) 

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

913 

914 

915################################################################################ 

916 

917 

918def end() -> None: 

919 accept_count = SummaryStats().accept_count 

920 accept_bytes = SummaryStats().accept_bytes 

921 

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

923 sets = "set" 

924 if accept_count > 1: 

925 sets = "sets" 

926 print( 

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

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

929 file=sys.stderr, 

930 ) 

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

932 

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

934 Logger.close() 

935 

936 

937################################################################################ 

938 

939 

940def main() -> None: 

941 global Options, Logger, Sections, Priorities 

942 

943 cnf = Config() 

944 session = DBConn().session() 

945 

946 Arguments = [ 

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

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

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

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

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

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

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

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

955 ] 

956 

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

958 

959 for i in [ 

960 "automatic", 

961 "no-binaries", 

962 "comments", 

963 "help", 

964 "manual-reject", 

965 "no-action", 

966 "version", 

967 "trainee", 

968 ]: 

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

970 if key not in cnf: 

971 cnf[key] = "" 

972 

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

974 new_queue = session.execute( 

975 select(PolicyQueue).filter_by(queue_name=queue_name) 

976 ).scalar_one() 

977 if len(changes_files) == 0: 

978 uploads = new_queue.uploads 

979 else: 

980 uploads = list( 

981 session.scalars( 

982 select(PolicyQueueUpload) 

983 .filter_by(policy_queue=new_queue) 

984 .join(DBChange) 

985 .where(DBChange.changesname.in_(changes_files)) 

986 ) 

987 ) 

988 

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

990 

991 if Options["Help"]: 

992 usage() 

993 

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

995 try: 

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

997 except OSError: 

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

999 

1000 Sections = Section_Completer(session) 

1001 Priorities = Priority_Completer(session) 

1002 readline.parse_and_bind("tab: complete") 

1003 

1004 if len(uploads) > 1: 

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

1006 uploads = sort_uploads( 

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

1008 ) 

1009 

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

1011 show_new_comments(uploads, session) 

1012 else: 

1013 for upload in uploads: 

1014 do_pkg(upload, session) 

1015 

1016 end()