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 errno 

46import os 

47import readline 

48import stat 

49import subprocess 

50import sys 

51import tempfile 

52import contextlib 

53import pwd 

54import apt_pkg 

55import dak.examine_package 

56from collections import defaultdict 

57from collections.abc import Iterable 

58from sqlalchemy import or_ 

59from typing import NoReturn, Optional 

60 

61import daklib.dbconn 

62from daklib.queue import * 

63from daklib import daklog 

64from daklib import utils 

65from daklib.regexes import re_default_answer, re_isanum 

66from daklib.dak_exceptions import AlreadyLockedError 

67from daklib.summarystats import SummaryStats 

68from daklib.config import Config 

69from daklib.policy import UploadCopy, PolicyQueueUploadHandler 

70from daklib.termcolor import colorize as Color 

71 

72# Globals 

73Options = None 

74Logger = None 

75 

76Priorities = None 

77Sections = None 

78 

79################################################################################ 

80################################################################################ 

81################################################################################ 

82 

83 

84class Section_Completer: 

85 def __init__(self, session): 

86 self.sections = [] 

87 self.matches = [] 

88 for s, in session.query(Section.section): 

89 self.sections.append(s) 

90 

91 def complete(self, text, state): 

92 if state == 0: 

93 self.matches = [] 

94 n = len(text) 

95 for word in self.sections: 

96 if word[:n] == text: 

97 self.matches.append(word) 

98 try: 

99 return self.matches[state] 

100 except IndexError: 

101 return None 

102 

103############################################################ 

104 

105 

106class Priority_Completer: 

107 def __init__(self, session): 

108 self.priorities = [] 

109 self.matches = [] 

110 for p, in session.query(Priority.priority): 

111 self.priorities.append(p) 

112 

113 def complete(self, text, state): 

114 if state == 0: 

115 self.matches = [] 

116 n = len(text) 

117 for word in self.priorities: 

118 if word[:n] == text: 

119 self.matches.append(word) 

120 try: 

121 return self.matches[state] 

122 except IndexError: 

123 return None 

124 

125################################################################################ 

126 

127 

128def takenover_binaries(upload, missing, session): 

129 rows = [] 

130 binaries = set([x.package for x in upload.binaries]) 

131 for m in missing: 

132 if m['type'] != 'dsc': 

133 binaries.discard(m['package']) 

134 if binaries: 

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

136 suite = upload.target_suite.overridesuite or \ 

137 upload.target_suite.suite_name 

138 suites = [s[0] for s in session.query(Suite.suite_name).filter(or_(Suite.suite_name == suite, 

139 Suite.overridesuite == suite)).all()] 

140 rows = session.query(DBSource.source, DBBinary.package).distinct(). \ 

141 filter(DBBinary.package.in_(binaries)). \ 

142 join(DBBinary.source). \ 

143 filter(DBSource.source != source). \ 

144 join(DBBinary.suites). \ 

145 filter(Suite.suite_name.in_(suites)). \ 

146 order_by(DBSource.source, DBBinary.package).all() 

147 return rows 

148 

149################################################################################ 

150 

151 

152def print_new(upload: daklib.dbconn.PolicyQueueUpload, missing, indexed: bool, session, file=sys.stdout): 

153 check_valid(missing, session) 

154 for index, m in enumerate(missing, 1): 

155 if m['type'] != 'deb': 

156 package = '{0}:{1}'.format(m['type'], m['package']) 

157 else: 

158 package = m['package'] 

159 section = m['section'] 

160 priority = m['priority'] 

161 if m["type"] == 'deb' and priority != 'optional': 161 ↛ 162line 161 didn't jump to line 162, because the condition on line 161 was never true

162 priority = Color(priority, "red") 

163 included = "" if m['included'] else "NOT UPLOADED" 

164 if indexed: 164 ↛ 165line 164 didn't jump to line 165, because the condition on line 164 was never true

165 line = "(%s): %-20s %-20s %-20s %s" % (index, package, priority, section, included) 

166 else: 

167 line = "%-20s %-20s %-20s %s" % (package, priority, section, included) 

168 line = line.strip() 

169 if not m['valid']: 169 ↛ 170line 169 didn't jump to line 170, because the condition on line 169 was never true

170 line = line + ' [!]' 

171 print(line, file=file) 

172 takenover = takenover_binaries(upload, missing, session) 

173 if takenover: 173 ↛ 174line 173 didn't jump to line 174, because the condition on line 173 was never true

174 print('\n\nBINARIES TAKEN OVER\n') 

175 for t in takenover: 

176 print('%s: %s' % (t[0], t[1])) 

177 notes = get_new_comments(upload.policy_queue, upload.changes.source) 

178 for note in notes: 178 ↛ 179line 178 didn't jump to line 179, because the loop on line 178 never started

179 print("\n") 

180 print(Color("Author:", "yellow"), "%s" % note.author) 

181 print(Color("Version:", "yellow"), "%s" % note.version) 

182 print(Color("Timestamp:", "yellow"), "%s" % note.notedate) 

183 print("\n\n") 

184 print(note.comment) 

185 print("-" * 72) 

186 return len(notes) > 0 

187 

188################################################################################ 

189 

190 

191def index_range(index: int) -> str: 

192 if index == 1: 

193 return "1" 

194 else: 

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

196 

197################################################################################ 

198################################################################################ 

199 

200 

201def edit_new(overrides, upload: daklib.dbconn.PolicyQueueUpload, session): 

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

203 # Write the current data to a temporary file 

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

205 fh.flush() 

206 utils.call_editor_for_file(fh.name) 

207 # Read the edited data back in 

208 fh.seek(0) 

209 lines = fh.readlines() 

210 

211 overrides_map = dict([((o['type'], o['package']), o) for o in overrides]) 

212 new_overrides = [] 

213 # Parse the new data 

214 for line in lines: 

215 line = line.strip() 

216 if line == "" or line[0] == '#': 

217 continue 

218 s = line.split() 

219 # Pad the list if necessary 

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

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

222 if pkg.find(':') != -1: 

223 type, pkg = pkg.split(':', 1) 

224 else: 

225 type = 'deb' 

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

227 if o is None: 

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

229 else: 

230 if section.find('/') != -1: 

231 component = section.split('/', 1)[0] 

232 else: 

233 component = 'main' 

234 new_overrides.append(dict( 

235 package=pkg, 

236 type=type, 

237 section=section, 

238 component=component, 

239 priority=priority, 

240 included=o['included'], 

241 )) 

242 return new_overrides 

243 

244################################################################################ 

245 

246 

247def edit_index(new, upload: daklib.dbconn.PolicyQueueUpload, index): 

248 package = new[index]['package'] 

249 priority = new[index]["priority"] 

250 section = new[index]["section"] 

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

252 done = False 

253 while not done: 

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

255 

256 answer = "XXX" 

257 if ftype != "dsc": 

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

259 else: 

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

261 edit_priority = edit_section = 0 

262 

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

264 answer = utils.input_or_exit(prompt) 

265 m = re_default_answer.match(prompt) 

266 if answer == "": 

267 answer = m.group(1) 

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

269 

270 if answer == 'P': 

271 edit_priority = 1 

272 elif answer == 'S': 

273 edit_section = 1 

274 elif answer == 'B': 

275 edit_priority = edit_section = 1 

276 elif answer == 'D': 

277 done = True 

278 

279 # Edit the priority 

280 if edit_priority: 

281 readline.set_completer(Priorities.complete) 

282 got_priority = 0 

283 while not got_priority: 

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

285 if new_priority not in Priorities.priorities: 

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

287 else: 

288 got_priority = 1 

289 priority = new_priority 

290 

291 # Edit the section 

292 if edit_section: 

293 readline.set_completer(Sections.complete) 

294 got_section = 0 

295 while not got_section: 

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

297 if new_section not in Sections.sections: 

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

299 else: 

300 got_section = 1 

301 section = new_section 

302 

303 # Reset the readline completer 

304 readline.set_completer(None) 

305 

306 new[index]["priority"] = priority 

307 new[index]["section"] = section 

308 if section.find('/') != -1: 

309 component = section.split('/', 1)[0] 

310 else: 

311 component = 'main' 

312 new[index]['component'] = component 

313 

314 return new 

315 

316################################################################################ 

317 

318 

319def edit_overrides(new, upload: daklib.dbconn.PolicyQueueUpload, session): 

320 print() 

321 done = False 

322 while not done: 

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

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

325 

326 got_answer = 0 

327 while not got_answer: 

328 answer = utils.input_or_exit(prompt) 

329 if not answer.isdigit(): 

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

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

332 got_answer = 1 

333 elif re_isanum.match(answer): 

334 answer = int(answer) 

335 if answer < 1 or answer > len(new): 

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

337 else: 

338 got_answer = 1 

339 

340 if answer == 'E': 

341 new = edit_new(new, upload, session) 

342 elif answer == 'D': 

343 done = True 

344 else: 

345 edit_index(new, upload, answer - 1) 

346 

347 return new 

348 

349 

350################################################################################ 

351 

352def check_pkg(upload, upload_copy: UploadCopy, session): 

353 missing = [] 

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

355 suite_name = upload.target_suite.suite_name 

356 handler = PolicyQueueUploadHandler(upload, session) 

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

358 

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

360 less_process = subprocess.Popen(less_cmd, bufsize=0, stdin=subprocess.PIPE, text=True) 

361 try: 

362 less_fd = less_process.stdin 

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

364 

365 source = upload.source 

366 if source is not None: 366 ↛ 370line 366 didn't jump to line 370, because the condition on line 366 was never false

367 source_file = os.path.join(upload_copy.directory, os.path.basename(source.poolfile.filename)) 

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

369 

370 for binary in upload.binaries: 

371 binary_file = os.path.join(upload_copy.directory, os.path.basename(binary.poolfile.filename)) 

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

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

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

375 if ("deb", binary.package) in missing: 375 ↛ 370line 375 didn't jump to line 370, because the condition on line 375 was never false

376 less_fd.write(examined) 

377 

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

379 less_process.stdin.close() 

380 except OSError as e: 

381 if e.errno == errno.EPIPE: 

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

383 else: 

384 raise 

385 except KeyboardInterrupt: 

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

387 finally: 

388 less_process.communicate() 

389 

390################################################################################ 

391 

392## FIXME: horribly Debian specific 

393 

394 

395def do_bxa_notification(new, upload: daklib.dbconn.PolicyQueueUpload, session): 

396 cnf = Config() 

397 

398 new = set([o['package'] for o in new if o['type'] == 'deb']) 

399 if len(new) == 0: 

400 return 

401 

402 key = session.query(MetadataKey).filter_by(key='Description').one() 

403 summary = "" 

404 for binary in upload.binaries: 

405 if binary.package not in new: 

406 continue 

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

408 summary += "\n" 

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

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

411 

412 subst = { 

413 '__DISTRO__': cnf['Dinstall::MyDistribution'], 

414 '__BCC__': 'X-DAK: dak process-new', 

415 '__BINARY_DESCRIPTIONS__': summary, 

416 '__CHANGES_FILENAME__': upload.changes.changesname, 

417 '__SOURCE__': upload.changes.source, 

418 '__VERSION__': upload.changes.version, 

419 '__ARCHITECTURE__': upload.changes.architecture, 

420 } 

421 

422 bxa_mail = utils.TemplateSubst(subst, os.path.join(cnf["Dir::Templates"], "process-new.bxa_notification")) 

423 utils.send_mail(bxa_mail) 

424 

425################################################################################ 

426 

427 

428def run_user_inspect_command(upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy): 

429 command = os.environ.get('DAK_INSPECT_UPLOAD') 

430 if command is None: 430 ↛ 433line 430 didn't jump to line 433, because the condition on line 430 was never false

431 return 

432 

433 directory = upload_copy.directory 

434 if upload.source: 

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

436 else: 

437 dsc = '' 

438 changes = upload.changes.changesname 

439 

440 shell_command = command.format( 

441 directory=directory, 

442 dsc=dsc, 

443 changes=changes, 

444 ) 

445 

446 subprocess.check_call(shell_command, shell=True) 

447 

448################################################################################ 

449 

450 

451def get_reject_reason(reason: str = '') -> Optional[str]: 

452 """get reason for rejection 

453 

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

455 rejection should be cancelled 

456 """ 

457 answer = 'E' 

458 if Options['Automatic']: 458 ↛ 459line 458 didn't jump to line 459, because the condition on line 458 was never true

459 answer = 'R' 

460 

461 while answer == 'E': 

462 reason = utils.call_editor(reason) 

463 print("Reject message:") 

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

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

466 answer = "XXX" 

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

468 answer = utils.input_or_exit(prompt) 

469 m = re_default_answer.search(prompt) 

470 if answer == "": 470 ↛ 471line 470 didn't jump to line 471, because the condition on line 470 was never true

471 answer = m.group(1) 

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

473 

474 if answer == 'Q': 474 ↛ 475line 474 didn't jump to line 475, because the condition on line 474 was never true

475 sys.exit(0) 

476 

477 if answer == 'R': 477 ↛ 479line 477 didn't jump to line 479, because the condition on line 477 was never false

478 return reason 

479 return None 

480 

481################################################################################ 

482 

483 

484def do_new(upload: daklib.dbconn.PolicyQueueUpload, upload_copy: UploadCopy, handler, session): 

485 run_user_inspect_command(upload, upload_copy) 

486 

487 # The main NEW processing loop 

488 done = False 

489 missing = [] 

490 while not done: 

491 queuedir = upload.policy_queue.path 

492 byhand = upload.byhand 

493 

494 missing = handler.missing_overrides(hints=missing) 

495 broken = not check_valid(missing, session) 

496 

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

498 

499 print() 

500 print(changesname) 

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

502 print() 

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

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

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

506 print() 

507 

508 if missing: 

509 print("NEW\n") 

510 

511 for package in missing: 

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

513 package["priority"] = "optional" 

514 

515 answer = "XXX" 

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

517 answer = 'S' 

518 

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

520 prompt = "" 

521 

522 has_unprocessed_byhand = False 

523 for f in byhand: 523 ↛ 524line 523 didn't jump to line 524, because the loop on line 523 never started

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

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

526 print("W: {0} still present; please process byhand components and try again".format(f.filename)) 

527 has_unprocessed_byhand = True 

528 

529 if not has_unprocessed_byhand and not broken and not note: 529 ↛ 535line 529 didn't jump to line 535, because the condition on line 529 was never false

530 if len(missing) == 0: 

531 prompt = "Accept, " 

532 answer = 'A' 

533 else: 

534 prompt = "Add overrides, " 

535 if broken: 535 ↛ 536line 535 didn't jump to line 536, because the condition on line 535 was never true

536 print("W: [!] marked entries must be fixed before package can be processed.") 

537 if note: 537 ↛ 538line 537 didn't jump to line 538, because the condition on line 537 was never true

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

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

540 

541 prompt += "Edit overrides, Check, Manual reject, Note edit, Prod, [S]kip, Quit ?" 

542 

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

544 answer = utils.input_or_exit(prompt) 

545 m = re_default_answer.search(prompt) 

546 if answer == "": 546 ↛ 547line 546 didn't jump to line 547, because the condition on line 546 was never true

547 answer = m.group(1) 

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

549 

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

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

552 continue 

553 

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

555 handler.add_overrides(missing, upload.target_suite) 

556 if Config().find_b("Dinstall::BXANotify"): 556 ↛ 558line 556 didn't jump to line 558, because the condition on line 556 was never false

557 do_bxa_notification(missing, upload, session) 

558 handler.accept() 

559 done = True 

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

561 elif answer == 'C': 

562 check_pkg(upload, upload_copy, session) 

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

564 missing = edit_overrides(missing, upload, session) 

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

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

567 reason = reason + "\n\n=====\n\n".join([n.comment for n in get_new_comments(upload.policy_queue, upload.changes.source, session=session)]) 

568 reason = get_reject_reason(reason) 

569 if reason is not None: 569 ↛ 603line 569 didn't jump to line 603, because the condition on line 569 was never false

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

571 handler.reject(reason) 

572 done = True 

573 elif answer == 'N': 573 ↛ 574line 573 didn't jump to line 574, because the condition on line 573 was never true

574 if edit_note(get_new_comments(upload.policy_queue, upload.changes.source, session=session), 

575 upload, session, bool(Options["Trainee"])) == 0: 

576 end() 

577 sys.exit(0) 

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

579 if prod_maintainer(get_new_comments(upload.policy_queue, upload.changes.source, session=session), 

580 upload, session, bool(Options["Trainee"])) == 0: 

581 end() 

582 sys.exit(0) 

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

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

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

586 if confirm == "y": 

587 for c in get_new_comments(upload.policy_queue, upload.changes.source, upload.changes.version, session=session): 

588 session.delete(c) 

589 session.commit() 

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

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

592 if confirm == "y": 

593 for c in get_new_comments(upload.policy_queue, upload.changes.source, session=session): 

594 session.delete(c) 

595 session.commit() 

596 

597 elif answer == 'S': 597 ↛ 599line 597 didn't jump to line 599, because the condition on line 597 was never false

598 done = True 

599 elif answer == 'Q': 

600 end() 

601 sys.exit(0) 

602 

603 if handler.get_action(): 

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

605 

606################################################################################ 

607################################################################################ 

608################################################################################ 

609 

610 

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

612 print("""Usage: dak process-new [OPTION]... [CHANGES]... 

613 -a, --automatic automatic run 

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

615 -c, --comments show NEW comments 

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

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

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

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

620 -t, --trainee FTP Trainee mode 

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

622 

623ENVIRONMENT VARIABLES 

624 

625 DAK_INSPECT_UPLOAD: shell command to run to inspect a package 

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

627 is checked. The following substitutions are available: 

628 

629 {directory}: directory the upload is contained in 

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

631 {changes}: name of the changes file 

632 

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

634 

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

636 

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

638 

639 and run 

640 

641 tmux attach -t process-new 

642 

643 in a separate terminal session. 

644""") 

645 sys.exit(exit_code) 

646 

647################################################################################ 

648 

649 

650@contextlib.contextmanager 

651def lock_package(package: str): 

652 """ 

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

654 

655 :param package: source package name to lock 

656 """ 

657 

658 cnf = Config() 

659 

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

661 

662 try: 

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

664 except OSError as e: 

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

666 try: 

667 user = pwd.getpwuid(os.stat(path)[stat.ST_UID])[4].split(',')[0].replace('.', '') 

668 except KeyError: 

669 user = "TotallyUnknown" 

670 raise AlreadyLockedError(user) 

671 raise 

672 

673 try: 

674 yield fd 

675 finally: 

676 os.unlink(path) 

677 

678 

679def do_pkg(upload: daklib.dbconn.PolicyQueueUpload, session): 

680 # Try to get an included dsc 

681 dsc = upload.source 

682 

683 cnf = Config() 

684 group = cnf.get('Dinstall::UnprivGroup') or None 

685 

686 try: 

687 with lock_package(upload.changes.source), \ 

688 UploadCopy(upload, group=group) as upload_copy: 

689 handler = PolicyQueueUploadHandler(upload, session) 

690 if handler.get_action() is not None: 

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

692 return 

693 

694 do_new(upload, upload_copy, handler, session) 

695 except AlreadyLockedError as e: 

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

697 

698 

699def show_new_comments(uploads: Iterable[daklib.dbconn.PolicyQueueUpload], session): 

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

701 if len(sources) == 0: 

702 return 

703 

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

705 FROM new_comments 

706 WHERE package IN :sources 

707 ORDER BY package, version""" 

708 

709 r = session.execute(query, params=dict(sources=tuple(sources))) 

710 

711 for i in r: 

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

713 

714 session.rollback() 

715 

716################################################################################ 

717 

718 

719def sort_uploads( 

720 new_queue: PolicyQueue, 

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

722 session, 

723 nobinaries: bool = False 

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

725 sources = defaultdict(list) 

726 sorteduploads = [] 

727 suitesrc = [s.source for s in session.query(DBSource.source). 

728 filter(DBSource.suites.any(Suite.suite_name.in_(['unstable', 'experimental'])))] 

729 comments = [p.package for p in session.query(NewComment.package). 

730 filter_by(trainee=False, policy_queue=new_queue).distinct()] 

731 for upload in uploads: 

732 source = upload.changes.source 

733 sources[source].append({'upload': upload, 

734 'date': upload.changes.created, 

735 'stack': 1, 

736 'binary': True if source in suitesrc else False, 

737 'comments': True if source in comments else False}) 

738 for src in sources: 

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

740 changes = sources[src] 

741 firstseen = sorted(changes, key=lambda k: (k['date']))[0]['date'] 

742 changes.sort(key=lambda item: item['date']) 

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

744 changes[i]['date'] = firstseen 

745 changes[i]['stack'] = i + 1 

746 sorteduploads += sources[src] 

747 if nobinaries: 747 ↛ 748line 747 didn't jump to line 748, because the condition on line 747 was never true

748 sorteduploads = [u["upload"] for u in sorted(sorteduploads, 

749 key=lambda k: (k["comments"], k["binary"], 

750 k["date"], -k["stack"]))] 

751 else: 

752 sorteduploads = [u["upload"] for u in sorted(sorteduploads, 

753 key=lambda k: (k["comments"], -k["binary"], 

754 k["date"], -k["stack"]))] 

755 return sorteduploads 

756 

757################################################################################ 

758 

759 

760def end(): 

761 accept_count = SummaryStats().accept_count 

762 accept_bytes = SummaryStats().accept_bytes 

763 

764 if accept_count: 764 ↛ 765line 764 didn't jump to line 765, because the condition on line 764 was never true

765 sets = "set" 

766 if accept_count > 1: 

767 sets = "sets" 

768 print("Accepted %d package %s, %s." % (accept_count, sets, utils.size_type(int(accept_bytes))), file=sys.stderr) 

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

770 

771 if not Options["No-Action"] and not Options["Trainee"]: 771 ↛ exitline 771 didn't return from function 'end', because the condition on line 771 was never false

772 Logger.close() 

773 

774################################################################################ 

775 

776 

777def main(): 

778 global Options, Logger, Sections, Priorities 

779 

780 cnf = Config() 

781 session = DBConn().session() 

782 

783 Arguments = [('a', "automatic", "Process-New::Options::Automatic"), 

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

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

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

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

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

789 ('q', 'queue', 'Process-New::Options::Queue', 'HasArg'), 

790 ('n', "no-action", "Process-New::Options::No-Action")] 

791 

792 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) 

793 

794 for i in ["automatic", "no-binaries", "comments", "help", "manual-reject", "no-action", "version", "trainee"]: 

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

796 if key not in cnf: 

797 cnf[key] = "" 

798 

799 queue_name = cnf.get('Process-New::Options::Queue', 'new') 

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

801 if len(changes_files) == 0: 

802 uploads = new_queue.uploads 

803 else: 

804 uploads = session.query(PolicyQueueUpload).filter_by(policy_queue=new_queue) \ 

805 .join(DBChange).filter(DBChange.changesname.in_(changes_files)).all() 

806 

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

808 

809 if Options["Help"]: 

810 usage() 

811 

812 if not Options["No-Action"]: 812 ↛ 818line 812 didn't jump to line 818, because the condition on line 812 was never false

813 try: 

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

815 except OSError: 

816 Options["Trainee"] = "True" 

817 

818 Sections = Section_Completer(session) 

819 Priorities = Priority_Completer(session) 

820 readline.parse_and_bind("tab: complete") 

821 

822 if len(uploads) > 1: 

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

824 uploads = sort_uploads(new_queue, uploads, session, Options["No-Binaries"]) 

825 

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

827 show_new_comments(uploads, session) 

828 else: 

829 for upload in uploads: 

830 do_pkg(upload, session) 

831 

832 end() 

833 

834################################################################################ 

835 

836 

837if __name__ == '__main__': 

838 main()