patch-manager 0.0.10__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.10 → patch_manager-0.1.0}/PKG-INFO +5 -1
- {patch_manager-0.0.10 → patch_manager-0.1.0}/README.rst +4 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patch_manager.egg-info/PKG-INFO +5 -1
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patch_manager.egg-info/SOURCES.txt +1 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/__main__.py +2 -1
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/cmdline.py +39 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/control.py +5 -1
- patch_manager-0.1.0/patman/coverity.py +127 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/cseries.py +52 -3
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/database.py +35 -9
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/gmail.py +18 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/patchwork.py +13 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/patman.rst +154 -8
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/review.py +780 -66
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/settings.py +25 -9
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test_cseries.py +688 -0
- patch_manager-0.1.0/patman/test_settings.py +149 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/pyproject.toml +1 -1
- patch_manager-0.0.10/patman/test_settings.py +0 -67
- {patch_manager-0.0.10 → patch_manager-0.1.0}/LICENSE +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patch_manager.egg-info/dependency_links.txt +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patch_manager.egg-info/entry_points.txt +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patch_manager.egg-info/requires.txt +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patch_manager.egg-info/top_level.txt +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/__init__.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/checkpatch.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/commit.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/cser_helper.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/func_test.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/get_maintainer.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/patchstream.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/project.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/send.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/series.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/status.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test/0000-cover-letter.patch +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test/0001-pci-Correct-cast-for-sandbox.patch +0 -0
- {patch_manager-0.0.10 → 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.10 → patch_manager-0.1.0}/patman/test/get_maintainer +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test/maintainers +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test/test01.txt +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test_checkpatch.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/test_common.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/patman/workflow.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/setup.cfg +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/__init__.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/__main__.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/claude.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/command.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/cros_subprocess.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/gitutil.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/terminal.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/test_claude.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/test_command.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/test_gitutil.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/test_util.py +0 -0
- {patch_manager-0.0.10 → patch_manager-0.1.0}/u_boot_pylib/tools.py +0 -0
- {patch_manager-0.0.10 → 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
|
|
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
|
|
@@ -35,6 +35,10 @@ Requires-Dist: claude-agent-sdk; extra == "all"
|
|
|
35
35
|
Requires-Dist: concurrencytest; extra == "all"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
+
.. image:: https://raw.githubusercontent.com/nxtboot/patman/master/logo/patman-logo-primary.png
|
|
39
|
+
:alt: Patman
|
|
40
|
+
:width: 400px
|
|
41
|
+
|
|
38
42
|
Patman patch manager
|
|
39
43
|
====================
|
|
40
44
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patch-manager
|
|
3
|
-
Version: 0.0
|
|
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
|
|
@@ -35,6 +35,10 @@ Requires-Dist: claude-agent-sdk; extra == "all"
|
|
|
35
35
|
Requires-Dist: concurrencytest; extra == "all"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
+
.. image:: https://raw.githubusercontent.com/nxtboot/patman/master/logo/patman-logo-primary.png
|
|
39
|
+
:alt: Patman
|
|
40
|
+
:width: 400px
|
|
41
|
+
|
|
38
42
|
Patman patch manager
|
|
39
43
|
====================
|
|
40
44
|
|
|
@@ -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)
|
|
@@ -341,9 +341,15 @@ class Cseries(cser_helper.CseriesHelper):
|
|
|
341
341
|
_, ser, version, _, _, _, _, _ = self._get_patches(series, version)
|
|
342
342
|
svinfo = self.get_ser_ver(ser.idnum, version)
|
|
343
343
|
|
|
344
|
-
#
|
|
345
|
-
# series
|
|
346
|
-
|
|
344
|
+
# Prefer the live cover-letter title from the branch: it is what
|
|
345
|
+
# patchwork indexes the series by, and it reflects a rename made
|
|
346
|
+
# after the version was created (the stored description would be
|
|
347
|
+
# stale in that case). Fall back to the stored per-version
|
|
348
|
+
# description for series with no cover letter, where it is more
|
|
349
|
+
# stable than the first commit subject (e.g. if patches are
|
|
350
|
+
# reordered).
|
|
351
|
+
desc = ser.cover[0] if ser.cover else None
|
|
352
|
+
desc = desc or svinfo.desc or ser.desc
|
|
347
353
|
if not desc:
|
|
348
354
|
raise ValueError(f"Series '{ser.name}' has an empty description")
|
|
349
355
|
ser.desc = desc
|
|
@@ -1501,6 +1507,49 @@ class Cseries(cser_helper.CseriesHelper):
|
|
|
1501
1507
|
self.rollback()
|
|
1502
1508
|
tout.info('Dry run completed')
|
|
1503
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
|
+
|
|
1504
1553
|
def gather_all(self, pwork, show_comments, show_cover_comments,
|
|
1505
1554
|
sync_all_versions, gather_tags, dry_run=False):
|
|
1506
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
|
|