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.
- {patch_manager-0.0.11 → patch_manager-0.1.0}/PKG-INFO +1 -1
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/PKG-INFO +1 -1
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/SOURCES.txt +1 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/__main__.py +2 -1
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/cmdline.py +39 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/control.py +5 -1
- patch_manager-0.1.0/patman/coverity.py +127 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/cseries.py +43 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/database.py +35 -9
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/gmail.py +18 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/patchwork.py +13 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/patman.rst +154 -8
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/review.py +780 -66
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/settings.py +25 -9
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test_cseries.py +658 -0
- patch_manager-0.1.0/patman/test_settings.py +149 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/pyproject.toml +1 -1
- patch_manager-0.0.11/patman/test_settings.py +0 -67
- {patch_manager-0.0.11 → patch_manager-0.1.0}/LICENSE +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/README.rst +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/dependency_links.txt +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/entry_points.txt +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/requires.txt +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patch_manager.egg-info/top_level.txt +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/__init__.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/checkpatch.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/commit.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/cser_helper.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/func_test.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/get_maintainer.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/patchstream.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/project.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/send.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/series.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/status.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/0000-cover-letter.patch +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/0001-pci-Correct-cast-for-sandbox.patch +0 -0
- {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
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/get_maintainer +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/maintainers +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test/test01.txt +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test_checkpatch.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/test_common.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/patman/workflow.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/setup.cfg +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/__init__.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/__main__.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/claude.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/command.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/cros_subprocess.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/gitutil.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/terminal.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_claude.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_command.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_gitutil.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/test_util.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/tools.py +0 -0
- {patch_manager-0.0.11 → patch_manager-0.1.0}/u_boot_pylib/tout.py +0 -0
|
@@ -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
|
|
1457
|
+
"""Get reviews from the most recent earlier version of a series
|
|
1435
1458
|
|
|
1436
|
-
|
|
1437
|
-
they can be provided as context when
|
|
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
|
|
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
|
-
'
|
|
1452
|
-
|
|
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
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
|
1439
|
-
whether earlier issues
|
|
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
|
-------
|