patch-manager 0.0.11__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {patch_manager-0.0.11 → patch_manager-0.1.0}/PKG-INFO +1 -1
  2. {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/PKG-INFO +1 -1
  3. {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/SOURCES.txt +1 -0
  4. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/__main__.py +2 -1
  5. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/cmdline.py +39 -0
  6. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/control.py +5 -1
  7. patch_manager-0.1.0/patman/coverity.py +127 -0
  8. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/cseries.py +43 -0
  9. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/database.py +35 -9
  10. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/gmail.py +18 -0
  11. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/patchwork.py +13 -0
  12. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/patman.rst +154 -8
  13. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/review.py +780 -66
  14. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/settings.py +25 -9
  15. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test_cseries.py +658 -0
  16. patch_manager-0.1.0/patman/test_settings.py +149 -0
  17. {patch_manager-0.0.11 → patch_manager-0.1.0}/pyproject.toml +1 -1
  18. patch_manager-0.0.11/patman/test_settings.py +0 -67
  19. {patch_manager-0.0.11 → patch_manager-0.1.0}/LICENSE +0 -0
  20. {patch_manager-0.0.11 → patch_manager-0.1.0}/README.rst +0 -0
  21. {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/dependency_links.txt +0 -0
  22. {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/entry_points.txt +0 -0
  23. {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/requires.txt +0 -0
  24. {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/top_level.txt +0 -0
  25. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/__init__.py +0 -0
  26. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/checkpatch.py +0 -0
  27. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/commit.py +0 -0
  28. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/cser_helper.py +0 -0
  29. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/func_test.py +0 -0
  30. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/get_maintainer.py +0 -0
  31. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/patchstream.py +0 -0
  32. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/project.py +0 -0
  33. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/send.py +0 -0
  34. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/series.py +0 -0
  35. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/status.py +0 -0
  36. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/0000-cover-letter.patch +0 -0
  37. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/0001-pci-Correct-cast-for-sandbox.patch +0 -0
  38. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch +0 -0
  39. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/get_maintainer +0 -0
  40. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/maintainers +0 -0
  41. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/test01.txt +0 -0
  42. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test_checkpatch.py +0 -0
  43. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test_common.py +0 -0
  44. {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/workflow.py +0 -0
  45. {patch_manager-0.0.11 → patch_manager-0.1.0}/setup.cfg +0 -0
  46. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/__init__.py +0 -0
  47. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/__main__.py +0 -0
  48. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/claude.py +0 -0
  49. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/command.py +0 -0
  50. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/cros_subprocess.py +0 -0
  51. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/gitutil.py +0 -0
  52. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/terminal.py +0 -0
  53. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_claude.py +0 -0
  54. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_command.py +0 -0
  55. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_gitutil.py +0 -0
  56. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_util.py +0 -0
  57. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/tools.py +0 -0
  58. {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/tout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patch-manager
3
- Version: 0.0.11
3
+ Version: 0.1.0
4
4
  Summary: Create, check and email patch series, with Patchwork integration
5
5
  Author-email: Simon Glass <sjg@chromium.org>
6
6
  License-Expression: GPL-2.0-or-later
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patch-manager
3
- Version: 0.0.11
3
+ Version: 0.1.0
4
4
  Summary: Create, check and email patch series, with Patchwork integration
5
5
  Author-email: Simon Glass <sjg@chromium.org>
6
6
  License-Expression: GPL-2.0-or-later
@@ -13,6 +13,7 @@ patman/checkpatch.py
13
13
  patman/cmdline.py
14
14
  patman/commit.py
15
15
  patman/control.py
16
+ patman/coverity.py
16
17
  patman/cser_helper.py
17
18
  patman/cseries.py
18
19
  patman/database.py
@@ -40,6 +40,7 @@ def run_patman():
40
40
  from patman import func_test
41
41
  from patman import test_checkpatch
42
42
  from patman import test_cseries
43
+ from patman import test_settings
43
44
  from u_boot_pylib import test_command
44
45
 
45
46
  to_run = args.testname if args.testname not in [None, 'test'] else None
@@ -48,7 +49,7 @@ def run_patman():
48
49
  args.test_preserve_dirs, None, to_run, None,
49
50
  [test_checkpatch.TestPatch, func_test.TestFunctional,
50
51
  'patman.settings', test_cseries.TestCseries,
51
- test_command.TestRunInteractive])
52
+ test_settings.TestSettings, test_command.TestRunInteractive])
52
53
  sys.exit(0 if result.wasSuccessful() else 1)
53
54
 
54
55
  # Process commits, produce patches files, check them, email them
@@ -342,6 +342,16 @@ def add_series_subparser(subparsers):
342
342
  ren = series_subparsers.add_parser('rename')
343
343
  ren.add_argument('-N', '--new-name', help='New name for the series')
344
344
 
345
+ rev = series_subparsers.add_parser(
346
+ 'review', help='AI-review the series and store the result')
347
+ rev.add_argument('-f', '--force', action='store_true',
348
+ help='Re-review even if reviews are already stored')
349
+ rev.add_argument('--spelling', type=str, default='British',
350
+ help='Spelling convention for review comments')
351
+ rev.add_argument('-c', '--context', type=str, default=None,
352
+ help="Extra context for the review agent, or '@path' "
353
+ 'to read it from a file')
354
+
345
355
  series_subparsers.add_parser('rm')
346
356
 
347
357
  snotes = series_subparsers.add_parser('save-notes')
@@ -587,6 +597,10 @@ def add_review_subparser(subparsers):
587
597
  review.add_argument(
588
598
  '-d', '--create-drafts', action='store_true',
589
599
  help='Create Gmail draft emails for each review')
600
+ review.add_argument(
601
+ '--redraft', action='store_true',
602
+ help='Recreate Gmail drafts from the stored reviews even when a '
603
+ 'draft already exists, to recover after an error')
590
604
  review.add_argument(
591
605
  '--gmail-account', type=str, default=None,
592
606
  help='Gmail account to create drafts in (e.g. user@gmail.com)')
@@ -607,6 +621,14 @@ def add_review_subparser(subparsers):
607
621
  review.add_argument(
608
622
  '--apply-only', action='store_true',
609
623
  help='Only download and apply patches, skip AI review')
624
+ review.add_argument(
625
+ '--coverity', action='store_true',
626
+ help='Run Coverity on the base and the series, and feed the new '
627
+ 'defects into the review (needs the cov-* tools on PATH)')
628
+ review.add_argument(
629
+ '--coverity-defconfig', type=str, default=None,
630
+ help='Board defconfig to build for --coverity '
631
+ '(default: sandbox_defconfig)')
610
632
  review.add_argument(
611
633
  '--signoff', type=str, default='',
612
634
  help="Sign-off for reviews with comments (from .patman settings)")
@@ -627,9 +649,26 @@ def add_review_subparser(subparsers):
627
649
  '--sync', action='store_true',
628
650
  help='Check if review drafts have been sent and record the '
629
651
  'final email content')
652
+ review.add_argument(
653
+ '--scan', action='store_true',
654
+ help='Scan patchwork for new versions of already-reviewed series '
655
+ 'and review the latest version once it has fully appeared')
656
+ review.add_argument(
657
+ '--relink', action='store_true',
658
+ help='Repair the database by merging review series that were split '
659
+ 'across versions, so follow-up reviews see earlier feedback')
660
+ review.add_argument(
661
+ '-j', '--jobs', type=int, default=4,
662
+ help='Number of series to review in parallel with --scan '
663
+ '(default: 4)')
630
664
  review.add_argument(
631
665
  '-f', '--force', action='store_true',
632
666
  help='Force re-review even if the series was already reviewed')
667
+ review.add_argument(
668
+ '--any-state', action='store_true',
669
+ help='Review the series even if no patch is in an active state '
670
+ '(new, RFC, under-review, changes-requested or '
671
+ 'needs-review-ack)')
633
672
  review.add_argument(
634
673
  '-c', '--context', type=str, default=None,
635
674
  help="Extra context for the review agent — e.g. 'this is RFC, "
@@ -245,6 +245,9 @@ def do_series(args, test_db=None, pwork=None, cser=None):
245
245
  cser.show_notes(args.series)
246
246
  elif args.subcmd == 'rename':
247
247
  cser.rename(args.series, args.new_name, dry_run=args.dry_run)
248
+ elif args.subcmd == 'review':
249
+ cser.review(args.series, args.version, force=args.force,
250
+ spelling=args.spelling, context=args.context)
248
251
  elif args.subcmd == 'set-upstream':
249
252
  cser.set_upstream(args.series, args.upstream_name,
250
253
  dry_run=args.dry_run)
@@ -433,7 +436,8 @@ def do_review(args, test_db=None, pwork=None, cser=None):
433
436
  cser.open_database()
434
437
 
435
438
  # Resolve patchwork URL
436
- if not pwork and not args.learn_voice and not args.sync:
439
+ if not pwork and not args.learn_voice and not args.sync \
440
+ and not args.relink:
437
441
  ups = args.upstream
438
442
  if not ups:
439
443
  ups = cser.db.upstream_get_default()
@@ -0,0 +1,127 @@
1
+ # SPDX-License-Identifier: GPL-2.0+
2
+ #
3
+ # Copyright 2026 Simon Glass <sjg@chromium.org>
4
+ #
5
+ """Run Coverity static analysis on a series and find new defects
6
+
7
+ Coverity works on a whole build rather than a diff, so to find the
8
+ defects a series introduces we analyse the base tree and the patched
9
+ tree separately and compare. Each Coverity defect carries a stable
10
+ 'mergeKey' that identifies the same defect across builds, so the new
11
+ defects are those whose mergeKey is not present in the base.
12
+
13
+ The cov-build, cov-analyze and cov-format-errors tools must be on the
14
+ PATH; they are part of a (commercial) Coverity installation.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+
22
+ from u_boot_pylib import tout
23
+
24
+ # Default board config to build for analysis (host build, no toolchain)
25
+ DEFAULT_DEFCONFIG = 'sandbox_defconfig'
26
+
27
+
28
+ def check_available():
29
+ """Check whether the Coverity command-line tools are installed
30
+
31
+ Returns:
32
+ bool: True if cov-build, cov-analyze and cov-format-errors are all
33
+ on the PATH
34
+ """
35
+ return all(shutil.which(tool) for tool in
36
+ ('cov-build', 'cov-analyze', 'cov-format-errors'))
37
+
38
+
39
+ def _run(cmd, cwd):
40
+ """Run a command, raising on failure
41
+
42
+ Args:
43
+ cmd (list of str): Command and arguments
44
+ cwd (str): Directory to run in
45
+
46
+ Raises:
47
+ subprocess.CalledProcessError: if the command fails
48
+ """
49
+ subprocess.run(cmd, cwd=cwd, check=True, stdout=subprocess.PIPE,
50
+ stderr=subprocess.STDOUT, text=True)
51
+
52
+
53
+ def analyze(repo_path, defconfig, emit_dir):
54
+ """Build and analyse a tree, returning its Coverity defects
55
+
56
+ Configures the given defconfig, builds it under cov-build, runs
57
+ cov-analyze and reads the defects back as JSON.
58
+
59
+ Args:
60
+ repo_path (str): Worktree to configure and build in
61
+ defconfig (str): Board defconfig to build, e.g. 'sandbox_defconfig'
62
+ emit_dir (str): Coverity intermediate directory to write
63
+
64
+ Returns:
65
+ list of dict: Defect records, each with at least 'mergeKey'
66
+
67
+ Raises:
68
+ subprocess.CalledProcessError: if a build or analysis step fails
69
+ """
70
+ jobs = str(os.cpu_count() or 1)
71
+ _run(['make', defconfig], repo_path)
72
+ _run(['cov-build', '--dir', emit_dir, 'make', '-j', jobs], repo_path)
73
+ _run(['cov-analyze', '--dir', emit_dir], repo_path)
74
+ out_json = os.path.join(emit_dir, 'defects.json')
75
+ _run(['cov-format-errors', '--dir', emit_dir, '--json-output-v7',
76
+ out_json], repo_path)
77
+ with open(out_json, encoding='utf-8') as fd:
78
+ data = json.load(fd)
79
+ return data.get('issues', [])
80
+
81
+
82
+ def find_new_defects(base, patched):
83
+ """Find defects present in the patched tree but not the base
84
+
85
+ Args:
86
+ base (list of dict): Defects from the base tree
87
+ patched (list of dict): Defects from the patched tree
88
+
89
+ Returns:
90
+ list of dict: Defects whose mergeKey is not in the base
91
+ """
92
+ base_keys = {defect.get('mergeKey') for defect in base}
93
+ return [defect for defect in patched
94
+ if defect.get('mergeKey') not in base_keys]
95
+
96
+
97
+ def format_defect(defect):
98
+ """Format a single defect as a one-line summary
99
+
100
+ Args:
101
+ defect (dict): Coverity defect record
102
+
103
+ Returns:
104
+ str: Summary like 'RESOURCE_LEAK: drivers/foo.c:42 (probe): ...'
105
+ """
106
+ checker = defect.get('checkerName', '?')
107
+ path = defect.get('mainEventFilePathname', '?')
108
+ line = defect.get('mainEventLineNumber', '?')
109
+ func = defect.get('functionDisplayName', '')
110
+ desc = (defect.get('subcategoryLongDescription') or
111
+ defect.get('subcategoryShortDescription') or '')
112
+ loc = f'{path}:{line}'
113
+ if func:
114
+ loc += f' ({func})'
115
+ return f'{checker}: {loc}: {desc}'.rstrip(': ')
116
+
117
+
118
+ def format_defects(defects):
119
+ """Format a list of defects as a bullet list, most useful first
120
+
121
+ Args:
122
+ defects (list of dict): Defects to format
123
+
124
+ Returns:
125
+ str: One '- <summary>' line per defect, or '' if there are none
126
+ """
127
+ return '\n'.join(f'- {format_defect(defect)}' for defect in defects)
@@ -1507,6 +1507,49 @@ class Cseries(cser_helper.CseriesHelper):
1507
1507
  self.rollback()
1508
1508
  tout.info('Dry run completed')
1509
1509
 
1510
+ def review(self, series, version, force=False, spelling='British',
1511
+ context=None):
1512
+ """AI-review the series and store the result in the database
1513
+
1514
+ Reviews the commits on the series branch with the review agent and
1515
+ stores the findings, keyed by version, so they can be viewed later
1516
+ with 'patman series info -r'. The series must already have been
1517
+ added with 'patman series add'.
1518
+
1519
+ Args:
1520
+ series (str): Name of series, or None to use the current branch
1521
+ version (int): Version number, or None to detect from the branch
1522
+ force (bool): Re-review even if reviews are already stored
1523
+ spelling (str): Spelling convention for review comments
1524
+ context (str or None): Extra context for the review agent, or
1525
+ '@path' to read it from a file
1526
+ """
1527
+ from patman import review
1528
+
1529
+ ser, version = self._parse_series_and_version(series, version)
1530
+ self._ensure_in_db(ser)
1531
+ self._ensure_version(ser, version)
1532
+ svid = self.get_ser_ver(ser.idnum, version).idnum
1533
+
1534
+ existing = self.db.review_get_for_version(svid)
1535
+ if existing and not force:
1536
+ raise ValueError(
1537
+ f"Series '{ser.name}' v{version} already has "
1538
+ f"{len(existing)} stored review(s); view with 'patman "
1539
+ "series info -r' or use -f to re-review")
1540
+ if existing:
1541
+ self.db.review_delete_for_version(svid)
1542
+
1543
+ name = self._get_branch_name(ser.name, version)
1544
+ if not gitutil.check_branch(name, git_dir=self.gitdir):
1545
+ raise ValueError(f"No branch named '{name}'")
1546
+ count = gitutil.count_commits_to_branch(name, self.gitdir)
1547
+ series_obj = patchstream.get_metadata(name, 0, count,
1548
+ git_dir=self.gitdir)
1549
+
1550
+ review.review_series(self, ser.idnum, svid, version, name,
1551
+ series_obj, spelling=spelling, context=context)
1552
+
1510
1553
  def gather_all(self, pwork, show_comments, show_cover_comments,
1511
1554
  sync_all_versions, gather_tags, dry_run=False):
1512
1555
  to_fetch, missing = self._get_fetch_dict(sync_all_versions)
@@ -830,6 +830,29 @@ class Database: # pylint:disable=R0904
830
830
  return SerVer(*recs[0])
831
831
  return [SerVer(*x) for x in recs]
832
832
 
833
+ def ser_ver_get_svids(self, series_idnum):
834
+ """Get all ser_ver IDs for a series
835
+
836
+ Args:
837
+ series_idnum (int): Series ID
838
+
839
+ Return:
840
+ list of int: svids of every version of the series
841
+ """
842
+ res = self.execute('SELECT id FROM ser_ver WHERE series_id = ?',
843
+ (series_idnum,))
844
+ return [row[0] for row in res.fetchall()]
845
+
846
+ def ser_ver_set_series(self, svid, series_idnum):
847
+ """Move a ser_ver record to a different series
848
+
849
+ Args:
850
+ svid (int): ser_ver ID to move
851
+ series_idnum (int): Series ID to attach it to
852
+ """
853
+ self.execute('UPDATE ser_ver SET series_id = ? WHERE id = ?',
854
+ (series_idnum, svid))
855
+
833
856
  def ser_ver_get_ids_for_series(self, series_idnum, version=None):
834
857
  """Get a list of ser_ver records for a given series ID
835
858
 
@@ -1431,25 +1454,28 @@ class Database: # pylint:disable=R0904
1431
1454
  return [Review(*row) for row in res.fetchall()]
1432
1455
 
1433
1456
  def review_get_previous(self, series_id, version):
1434
- """Get reviews from the previous version of a series
1457
+ """Get reviews from the most recent earlier version of a series
1435
1458
 
1436
- Looks up the ser_ver for version-1 and returns its reviews, so
1437
- they can be provided as context when reviewing a new version.
1459
+ Returns the reviews of the highest version below the current one
1460
+ that actually has reviews, so they can be provided as context when
1461
+ reviewing a new version. Skipping over versions with no reviews
1462
+ means a gap (e.g. an intermediate version that was never reviewed)
1463
+ still yields the earlier feedback rather than nothing.
1438
1464
 
1439
1465
  Args:
1440
1466
  series_id (int): Series ID
1441
1467
  version (int): Current version being reviewed
1442
1468
 
1443
1469
  Return:
1444
- list of Review: Reviews from version-1, or empty list
1470
+ list of Review: Reviews from the latest earlier reviewed
1471
+ version, or empty list
1445
1472
  """
1446
- prev_version = version - 1
1447
- if prev_version < 1:
1448
- return []
1449
1473
  res = self.execute(
1450
1474
  'SELECT sv.id FROM ser_ver sv '
1451
- 'WHERE sv.series_id = ? AND sv.version = ?',
1452
- (series_id, prev_version))
1475
+ 'JOIN review r ON r.svid = sv.id '
1476
+ 'WHERE sv.series_id = ? AND sv.version < ? '
1477
+ 'ORDER BY sv.version DESC LIMIT 1',
1478
+ (series_id, version))
1453
1479
  row = res.fetchone()
1454
1480
  if not row:
1455
1481
  return []
@@ -324,6 +324,24 @@ def create_draft(service, to, subject, body,
324
324
  return draft
325
325
 
326
326
 
327
+ def delete_draft(service, draft_id):
328
+ """Delete a Gmail draft
329
+
330
+ A draft that no longer exists (already sent or deleted by hand) is
331
+ ignored rather than raising.
332
+
333
+ Args:
334
+ service (googleapiclient.discovery.Resource): Gmail API service
335
+ draft_id (str): ID of the draft to delete
336
+ """
337
+ from googleapiclient.errors import HttpError
338
+ try:
339
+ service.users().drafts().delete(userId='me', id=draft_id).execute()
340
+ except HttpError as exc:
341
+ if exc.resp.status != 404:
342
+ raise
343
+
344
+
327
345
  def _build_cc(headers, list_email):
328
346
  """Build a CC list from the original patch headers
329
347
 
@@ -452,6 +452,19 @@ class Patchwork:
452
452
  """
453
453
  return await self._request(client, f'series/{link}/')
454
454
 
455
+ async def get_series_patch_states(self, client, link):
456
+ """Get the patchwork state of each patch in a series
457
+
458
+ Args:
459
+ client (aiohttp.ClientSession): Session to use
460
+ link (str): Patchwork series ID
461
+
462
+ Returns:
463
+ list of str: State slug for each patch, e.g. ['new', 'rfc']
464
+ """
465
+ res = await self._request(client, f'patches/?series={link}')
466
+ return [patch.get('state') for patch in res]
467
+
455
468
  async def get_series_mbox(self, client, link):
456
469
  """Download the raw mbox file for a series.
457
470
 
@@ -121,6 +121,31 @@ possible to override the `project` settings variable or anything else
121
121
  in a project-specific way. The values of this "local" configuration
122
122
  file take precedence over those of the "global" one.
123
123
 
124
+ Config is read from three files, each overriding the previous one on a
125
+ per-key basis (lowest to highest priority):
126
+
127
+ 1. ``.patman-defaults`` at the root of the git repository. This is
128
+ tracked in git and shipped by the project to provide sensible
129
+ defaults (such as the patchwork URL or the get_maintainer.pl
130
+ invocation). Because it is the lowest layer, anyone can override it,
131
+ so it never takes control away from the user. In a tree whose
132
+ ``.gitignore`` ignores dot-files (as U-Boot's does), force-add it
133
+ with ``git add -f .patman-defaults``.
134
+ 2. ``~/.patman``, the user's global config.
135
+ 3. ``.patman`` at the root of the git repository, the user's per-repo
136
+ override (not tracked).
137
+
138
+ Command-line arguments override all three. A project shipping
139
+ ``.patman-defaults`` might contain::
140
+
141
+ [settings]
142
+ patchwork_url: https://patchwork.ozlabs.org
143
+ get_maintainer_script: scripts/get_maintainer.pl --norolestats
144
+
145
+ A user who sets ``patchwork_url`` in their own ``~/.patman`` (or passes
146
+ ``-P``) overrides it; anything they leave unset falls back to these
147
+ defaults.
148
+
124
149
  Aliases are recursive.
125
150
 
126
151
  If the project provides a checkpatch.pl (in its 'scripts/' or 'tools/'
@@ -1289,6 +1314,15 @@ To create Gmail drafts threaded under the original emails::
1289
1314
  Use ``-n`` with ``-d`` for a dry run that shows what would be created
1290
1315
  without calling the Gmail API.
1291
1316
 
1317
+ If a draft failed to create or was lost, ``--redraft`` recreates the
1318
+ Gmail drafts from the stored reviews, even when the series has already
1319
+ been reviewed and the drafts already exist::
1320
+
1321
+ patman review -s 497923 --redraft --gmail-account your@email
1322
+
1323
+ Any existing draft is deleted first, so it is not left as a duplicate
1324
+ in the thread.
1325
+
1292
1326
  Use ``--apply-only`` to download and apply patches without running the
1293
1327
  AI review -- useful for checking that patches apply cleanly.
1294
1328
 
@@ -1310,10 +1344,48 @@ again::
1310
1344
 
1311
1345
  patman review -s 497923 -U us -f --reviewer 'Your Name <your@email>'
1312
1346
 
1347
+ With ``-d``, the Gmail drafts from the previous review are deleted too,
1348
+ so the re-review does not leave duplicates in the thread.
1349
+
1313
1350
  If the reviewer email (from ``--reviewer`` or git config) differs from
1314
1351
  the ``--gmail-account``, patman sets the From header on the draft so
1315
1352
  the email is sent with the correct identity.
1316
1353
 
1354
+ Reviewing your own series before sending
1355
+ ----------------------------------------
1356
+
1357
+ To get an AI review of your own series before sending it, add the
1358
+ series with ``patman series add`` and then run::
1359
+
1360
+ patman series review
1361
+
1362
+ This reviews the commits on the current branch (or ``-s <name>``) with
1363
+ the same agent, along with the cover letter (stored as patch 0), and
1364
+ saves the findings in the database, keyed by version. Because the
1365
+ review is expensive, it is saved so you can read it later without
1366
+ re-running::
1367
+
1368
+ patman series info -r # show the stored review
1369
+ patman series info -r 2 3 # just patches 2 and 3
1370
+
1371
+ Re-running is refused when a review is already stored; pass ``-f`` to
1372
+ review again, replacing the stored one. Unlike ``patman review``, this
1373
+ needs no patchwork link and creates no drafts -- it is purely a local
1374
+ pre-send check. Reviews from the previous version are used as context,
1375
+ so a follow-up review focuses on what changed.
1376
+
1377
+ Active series only
1378
+ ------------------
1379
+
1380
+ By default patman only reviews a series when at least one of its
1381
+ patches is in an active patchwork state: new, RFC, under-review,
1382
+ changes-requested or needs-review-ack. Reviewing a series
1383
+ that patchwork has finished with (accepted, superseded, rejected and so
1384
+ on) fails with a message naming the override. Use ``--any-state`` to
1385
+ review regardless::
1386
+
1387
+ patman review -s 497923 --any-state
1388
+
1317
1389
  How the review works
1318
1390
  --------------------
1319
1391
 
@@ -1331,9 +1403,21 @@ over the drafts to tighten the language, remove cross-patch duplicates
1331
1403
  and check voice consistency. Approved reviews without comments are
1332
1404
  excluded from refinement to preserve their quoted commit messages.
1333
1405
 
1406
+ If the agent has nothing to say about a patch -- no comments and not an
1407
+ approval -- no review is produced for it, rather than an empty
1408
+ greeting-only reply.
1409
+
1410
+ Comments on the commit message are placed before comments on the code,
1411
+ matching the usual reply order.
1412
+
1334
1413
  A mechanical cleanup step also runs to remove backticks and fix function
1335
1414
  quoting style (e.g. ``malloc()`` not ```malloc```).
1336
1415
 
1416
+ Only one review of a given series runs at a time. If a review for the
1417
+ same series is already in progress -- from another patman invocation or
1418
+ a scan -- the second is refused rather than corrupting the shared
1419
+ review worktree and database records.
1420
+
1337
1421
  Apply step
1338
1422
  ~~~~~~~~~~
1339
1423
 
@@ -1342,12 +1426,37 @@ changes -- including untracked files -- so build artefacts on the
1342
1426
  current branch don't leak into the review. The original branch and
1343
1427
  stash are restored at the end of the run, including on failure.
1344
1428
 
1345
- If the apply agent finishes but the resulting branch holds fewer
1346
- commits than the series cover letter advertises, patman aborts with a
1347
- message of the form ``Only N of M patches applied to <branch>;
1348
- aborting. Fix the conflicts manually and retry.`` The database row
1349
- for the new version is rolled back so a retry starts from a clean
1350
- state.
1429
+ If the apply agent applies some but not all of the patches -- for
1430
+ example one depends on a prerequisite series that is not yet upstream
1431
+ -- patman warns (``Only N of M patches applied to <branch>; reviewing
1432
+ what is there``) and reviews the patches that did apply. Only when
1433
+ nothing applies at all does it fail and roll back the new version's
1434
+ database row.
1435
+
1436
+ Each review is matched to its patchwork patch by subject, not by
1437
+ position, so a patch that failed to apply does not shift the remaining
1438
+ reviews onto the wrong patches.
1439
+
1440
+ Coverity static analysis
1441
+ ------------------------
1442
+
1443
+ With ``--coverity``, patman runs Coverity on the series and feeds the
1444
+ new defects into the review. Coverity analyses a whole build rather
1445
+ than a diff, so patman builds and analyses the base branch and the
1446
+ patched branch separately and reports only the defects the series
1447
+ introduces (matched by Coverity's mergeKey)::
1448
+
1449
+ patman review -s 497923 -U us --coverity \
1450
+ --reviewer 'Your Name <your@email>'
1451
+
1452
+ This needs the ``cov-build``, ``cov-analyze`` and ``cov-format-errors``
1453
+ tools on your PATH; if they are missing the check is skipped with a
1454
+ warning. The build uses ``sandbox_defconfig`` by default; use
1455
+ ``--coverity-defconfig`` to pick another board. Building twice makes
1456
+ this much slower than a normal review.
1457
+
1458
+ The new defects are passed to the review agent as context, so it can
1459
+ raise the ones that fall in the code each patch changes.
1351
1460
 
1352
1461
  Patchwork subcommands
1353
1462
  ---------------------
@@ -1423,6 +1532,34 @@ This:
1423
1532
  - Generates response drafts when appropriate (e.g. answering
1424
1533
  questions, pushing back on objections, or conceding gracefully)
1425
1534
 
1535
+ Scanning for new versions
1536
+ -------------------------
1537
+
1538
+ Rather than reviewing one series at a time, ``--scan`` looks on
1539
+ patchwork for new versions of series you have already reviewed and
1540
+ reviews them::
1541
+
1542
+ patman review --scan -U us --reviewer 'Your Name <your@email>'
1543
+
1544
+ For each reviewed series it takes the highest version on patchwork
1545
+ above the latest one reviewed. A version is reviewed only once it has
1546
+ fully appeared on patchwork; if the newest version is still arriving,
1547
+ the series is left to wait rather than reviewing an older, now
1548
+ superseded one. Inactive series (see `Active series only`_) are
1549
+ skipped.
1550
+
1551
+ Reviews run in parallel, each in its own child process and review
1552
+ worktree, up to ``--jobs`` (``-j``) at a time (default 4).
1553
+
1554
+ Each review's output is buffered and printed as one block when it
1555
+ finishes, prefixed with a ``[done/total]`` counter and ending with a
1556
+ summary line::
1557
+
1558
+ Scanned: 3 new, 1 reviewed, 1 waiting, 1 skipped, 0 failed
1559
+
1560
+ Use ``-n`` / ``--dry-run`` to see which series would be reviewed,
1561
+ waiting or skipped, without launching any reviews.
1562
+
1426
1563
  Review lifecycle
1427
1564
  ----------------
1428
1565
 
@@ -1435,8 +1572,17 @@ Each review goes through these states:
1435
1572
  - **replied**: Author or another reviewer has replied to our review
1436
1573
 
1437
1574
  When reviewing a new version of a previously reviewed series, patman
1438
- loads the previous review as context for the AI, so it can check
1439
- whether earlier issues have been addressed.
1575
+ loads the most recent earlier version that has reviews as context for
1576
+ the AI, so it can check whether earlier issues were addressed and avoid
1577
+ raising fresh points the earlier versions did not. Versions are linked
1578
+ by their cleaned cover-letter title.
1579
+
1580
+ An older database may have stored each version as a separate, unlinked
1581
+ record, leaving follow-up reviews without this context. Repair it by
1582
+ merging the split records, which backs up the database (to
1583
+ ``<db>.bak``) first::
1584
+
1585
+ patman review --relink
1440
1586
 
1441
1587
  Aliases
1442
1588
  -------