Compare commits
2 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| 328479d4c6 | |||
| 336a0fdec3 |
327
0001-dig-initial-b4-dig-implementation.patch
Normal file
327
0001-dig-initial-b4-dig-implementation.patch
Normal file
@@ -0,0 +1,327 @@
|
||||
From: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Date: Fri, 10 Oct 2025 16:30:19 -0400
|
||||
Subject: dig: initial b4 dig implementation
|
||||
References: dig-support
|
||||
Git-commit: 16329336c1c8faba853b11238a16249306742505
|
||||
Patch-mainline: yes
|
||||
|
||||
For now, this only does `b4 dig -c [commitish]` and will do its best to
|
||||
figure out which lore message the commit may have come from. We never
|
||||
know for sure, so we always list multiple series when the patch exists
|
||||
in multiple ones and let the end-user figure out which one they want.
|
||||
|
||||
Still requires testing and docs.
|
||||
|
||||
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Signed-off-by: Jiri Slaby <jslaby@suse.cz>
|
||||
---
|
||||
src/b4/__init__.py | 39 ++++++++--
|
||||
src/b4/command.py | 11 +++
|
||||
src/b4/dig.py | 188 +++++++++++++++++++++++++++++++++++++++++++++
|
||||
3 files changed, 230 insertions(+), 8 deletions(-)
|
||||
create mode 100644 src/b4/dig.py
|
||||
|
||||
diff --git a/src/b4/__init__.py b/src/b4/__init__.py
|
||||
index c9ed54c3ca1c..b6eab255103f 100644
|
||||
--- a/src/b4/__init__.py
|
||||
+++ b/src/b4/__init__.py
|
||||
@@ -284,13 +284,25 @@ class LoreMailbox:
|
||||
self.trailer_map[patchid] = list()
|
||||
self.trailer_map[patchid] += fmsgs
|
||||
|
||||
+
|
||||
+ def get_latest_revision(self) -> Optional[int]:
|
||||
+ if not len(self.series):
|
||||
+ return None
|
||||
+ # Use the latest revision
|
||||
+ revs = list(self.series.keys())
|
||||
+ # sort by date of each series.submission_date
|
||||
+ revs.sort(key=lambda r: self.series[r].submission_date or 0)
|
||||
+ return revs[-1]
|
||||
+
|
||||
+
|
||||
def get_series(self, revision: Optional[int] = None, sloppytrailers: bool = False,
|
||||
reroll: bool = True, codereview_trailers: bool = True) -> Optional['LoreSeries']:
|
||||
if revision is None:
|
||||
if not len(self.series):
|
||||
return None
|
||||
- # Use the highest revision
|
||||
- revision = max(self.series.keys())
|
||||
+ revision = self.get_latest_revision()
|
||||
+ if revision is None:
|
||||
+ return None
|
||||
elif revision not in self.series.keys():
|
||||
return None
|
||||
|
||||
@@ -499,6 +511,7 @@ class LoreSeries:
|
||||
change_id: Optional[str] = None
|
||||
prereq_patch_ids: Optional[List[str]] = None
|
||||
prereq_base_commit: Optional[str] = None
|
||||
+ _submission_date: Optional[datetime.datetime] = None
|
||||
|
||||
def __init__(self, revision: int, expected: int) -> None:
|
||||
self.revision = revision
|
||||
@@ -783,8 +796,21 @@ class LoreSeries:
|
||||
|
||||
return msgs
|
||||
|
||||
+ @property
|
||||
+ def submission_date(self) -> Optional[datetime.datetime]:
|
||||
+ # Find the date of the first patch we have
|
||||
+ if self._submission_date is not None:
|
||||
+ return self._submission_date
|
||||
+ for lmsg in self.patches:
|
||||
+ if lmsg is None:
|
||||
+ continue
|
||||
+ self._submission_date = lmsg.date
|
||||
+ break
|
||||
+ return self._submission_date
|
||||
+
|
||||
def populate_indexes(self):
|
||||
self.indexes = list()
|
||||
+
|
||||
seenfiles = set()
|
||||
for lmsg in self.patches[1:]:
|
||||
if lmsg is None or not lmsg.blob_indexes:
|
||||
@@ -827,12 +853,9 @@ class LoreSeries:
|
||||
|
||||
def find_base(self, gitdir: str, branches: Optional[list] = None, maxdays: int = 30) -> Tuple[str, len, len]:
|
||||
# Find the date of the first patch we have
|
||||
- pdate = datetime.datetime.now()
|
||||
- for lmsg in self.patches:
|
||||
- if lmsg is None:
|
||||
- continue
|
||||
- pdate = lmsg.date
|
||||
- break
|
||||
+ pdate = self.submission_date
|
||||
+ if not pdate:
|
||||
+ pdate = datetime.datetime.now()
|
||||
|
||||
# Find the latest commit on that date
|
||||
guntil = pdate.strftime('%Y-%m-%d')
|
||||
diff --git a/src/b4/command.py b/src/b4/command.py
|
||||
index 5d90b4f52b4f..c90705ce9c5e 100644
|
||||
--- a/src/b4/command.py
|
||||
+++ b/src/b4/command.py
|
||||
@@ -118,6 +118,11 @@ def cmd_diff(cmdargs):
|
||||
b4.diff.main(cmdargs)
|
||||
|
||||
|
||||
+def cmd_dig(cmdargs: argparse.Namespace) -> None:
|
||||
+ import b4.dig
|
||||
+ b4.dig.main(cmdargs)
|
||||
+
|
||||
+
|
||||
class ConfigOption(argparse.Action):
|
||||
"""Action class for storing key=value arguments in a dict."""
|
||||
def __call__(self, parser, namespace, keyval, option_string=None):
|
||||
@@ -383,6 +388,12 @@ def setup_parser() -> argparse.ArgumentParser:
|
||||
help='Submit the token received via verification email')
|
||||
sp_send.set_defaults(func=cmd_send)
|
||||
|
||||
+ # b4 dig
|
||||
+ sp_dig = subparsers.add_parser('dig', help='Dig into the details of a specific commit')
|
||||
+ sp_dig.add_argument('-c', '--commit', dest='commit_id', metavar='COMMITISH',
|
||||
+ help='Commit-ish object to dig into')
|
||||
+ sp_dig.set_defaults(func=cmd_dig)
|
||||
+
|
||||
return parser
|
||||
|
||||
|
||||
diff --git a/src/b4/dig.py b/src/b4/dig.py
|
||||
new file mode 100644
|
||||
index 000000000000..8b49b90c8e05
|
||||
--- /dev/null
|
||||
+++ b/src/b4/dig.py
|
||||
@@ -0,0 +1,188 @@
|
||||
+#!/usr/bin/env python3
|
||||
+# -*- coding: utf-8 -*-
|
||||
+# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
+# Copyright (C) 2025 by the Linux Foundation
|
||||
+#
|
||||
+__author__ = 'Konstantin Ryabitsev <konstantin@linuxfoundation.org>'
|
||||
+
|
||||
+import os
|
||||
+import sys
|
||||
+import b4
|
||||
+import argparse
|
||||
+import email.parser
|
||||
+
|
||||
+from email.message import EmailMessage
|
||||
+from typing import List, Set, Optional
|
||||
+
|
||||
+logger = b4.logger
|
||||
+
|
||||
+# Supported diff algorithms we will try to match
|
||||
+try_diff_algos: List[str] = [
|
||||
+ 'myers',
|
||||
+ 'histogram',
|
||||
+ 'patience',
|
||||
+ 'minimal',
|
||||
+]
|
||||
+
|
||||
+
|
||||
+def dig_commit(cmdargs: argparse.Namespace) -> None:
|
||||
+ config = b4.get_main_config()
|
||||
+ cfg_llval = config.get('linkmask', '')
|
||||
+ if isinstance(cfg_llval, str) and '%s' in cfg_llval:
|
||||
+ linkmask = cfg_llval
|
||||
+ else:
|
||||
+ linkmask = f'{b4.LOREADDR}/%s/'
|
||||
+ # Are we inside a git repo?
|
||||
+ topdir = b4.git_get_toplevel()
|
||||
+ if not topdir:
|
||||
+ logger.error("Not inside a git repository.")
|
||||
+ sys.exit(1)
|
||||
+
|
||||
+ # Can we resolve this commit to an object?
|
||||
+ commit = b4.git_revparse_obj(cmdargs.commit_id, topdir)
|
||||
+ if not commit:
|
||||
+ logger.error('Cannot find a commit matching %s', cmdargs.commit_id)
|
||||
+ sys.exit(1)
|
||||
+
|
||||
+ logger.info('Digging into commit %s', commit)
|
||||
+ # Make sure it has exactly one parent (not a merge)
|
||||
+ ecode, out = b4.git_run_command(
|
||||
+ topdir, ['show', '--no-patch', '--format=%p', commit],
|
||||
+ )
|
||||
+ if ecode > 0:
|
||||
+ logger.error('Could not get commit info for %s', commit)
|
||||
+ sys.exit(1)
|
||||
+ if out.strip().count(' ') != 0:
|
||||
+ logger.error('Merge commit detected, please specify a single-parent commit.')
|
||||
+ sys.exit(1)
|
||||
+
|
||||
+ # Find commit's author and subject from git
|
||||
+ ecode, out = b4.git_run_command(
|
||||
+ topdir, ['show', '--no-patch', '--format=%ae %s', commit],
|
||||
+ )
|
||||
+ if ecode > 0:
|
||||
+ logger.error('Could not get commit info for %s', commit)
|
||||
+ sys.exit(1)
|
||||
+ fromeml, csubj = out.strip().split(maxsplit=1)
|
||||
+ logger.debug('fromeml=%s, csubj=%s', fromeml, csubj)
|
||||
+ logger.info('Attempting to match by exact patch-id...')
|
||||
+ showargs = [
|
||||
+ '--format=email',
|
||||
+ '--binary',
|
||||
+ '--encoding=utf-8',
|
||||
+ '--find-renames',
|
||||
+ ]
|
||||
+ # Keep a record so we don't try git-patch-id on identical patches
|
||||
+ bpatches: Set[bytes] = set()
|
||||
+ lmbx: Optional[b4.LoreMailbox] = None
|
||||
+ for algo in try_diff_algos:
|
||||
+ logger.debug('Trying with diff-algorithm=%s', algo)
|
||||
+ algoarg = f'--diff-algorithm={algo}'
|
||||
+ logger.debug('showargs=%s', showargs + [algoarg])
|
||||
+ ecode, bpatch = b4.git_run_command(
|
||||
+ topdir, ['show'] + showargs + [algoarg] + [commit],
|
||||
+ decode=False,
|
||||
+ )
|
||||
+ if ecode > 0:
|
||||
+ logger.error('Could not get a patch out of %s', commit)
|
||||
+ sys.exit(1)
|
||||
+ if bpatch in bpatches:
|
||||
+ logger.debug('Already saw this patch, skipping diff-algorithm=%s', algo)
|
||||
+ continue
|
||||
+ bpatches.add(bpatch)
|
||||
+ gitargs = ['patch-id', '--stable']
|
||||
+ ecode, out = b4.git_run_command(topdir, gitargs, stdin=bpatch)
|
||||
+ if ecode > 0 or not len(out.strip()):
|
||||
+ logger.error('Could not compute patch-id for commit %s', commit)
|
||||
+ sys.exit(1)
|
||||
+ patch_id = out.split(maxsplit=1)[0]
|
||||
+ logger.debug('Patch-id for commit %s is %s', commit, patch_id)
|
||||
+ logger.info('Trying to find matching series by patch-id %s', patch_id)
|
||||
+ lmbx = b4.get_series_by_patch_id(patch_id)
|
||||
+ if lmbx:
|
||||
+ logger.info('Found matching series by patch-id')
|
||||
+ break
|
||||
+
|
||||
+ if not lmbx:
|
||||
+ logger.info('Attempting to match by author and subject...')
|
||||
+ q = '(s:"%s" AND f:"%s")' % (csubj.replace('"', ''), fromeml)
|
||||
+ msgs = b4.get_pi_search_results(q)
|
||||
+ if msgs:
|
||||
+ logger.info('Found %s matching messages', len(msgs))
|
||||
+ lmbx = b4.LoreMailbox()
|
||||
+ for msg in msgs:
|
||||
+ lmbx.add_message(msg)
|
||||
+ else:
|
||||
+ logger.error('Could not find anything matching commit %s', commit)
|
||||
+ # Look at the commit message and find any Link: trailers
|
||||
+ ecode, out = b4.git_run_command(
|
||||
+ topdir, ['show', '--no-patch', '--format=%B', commit],
|
||||
+ )
|
||||
+ if ecode > 0:
|
||||
+ logger.error('Could not get commit message for %s', commit)
|
||||
+ sys.exit(1)
|
||||
+ trailers, _ = b4.LoreMessage.find_trailers(out)
|
||||
+ ltrs = [t for t in trailers if t.name.lower() == 'link']
|
||||
+ if ltrs:
|
||||
+ logger.info('---')
|
||||
+ logger.info('Try following these Link trailers:')
|
||||
+ for ltr in ltrs:
|
||||
+ logger.info(' %s', ltr.as_string())
|
||||
+ sys.exit(1)
|
||||
+
|
||||
+ # Grab the latest series and see if we have a change_id
|
||||
+ revs = list(lmbx.series.keys())
|
||||
+ revs.sort(key=lambda r: lmbx.series[r].submission_date or 0)
|
||||
+
|
||||
+ change_id: Optional[str] = None
|
||||
+ lser = lmbx.get_series(codereview_trailers=False)
|
||||
+ for rev in revs:
|
||||
+ change_id = lmbx.series[rev].change_id
|
||||
+ if not change_id:
|
||||
+ continue
|
||||
+ logger.info('Backfilling any missing series by change-id')
|
||||
+ logger.debug('change_id=%s', change_id)
|
||||
+ # Fill in the rest of the series by change_id
|
||||
+ q = f'nq:"change-id:{change_id}"'
|
||||
+ q_msgs = b4.get_pi_search_results(q, full_threads=True)
|
||||
+ if q_msgs:
|
||||
+ for q_msg in q_msgs:
|
||||
+ lmbx.add_message(q_msg)
|
||||
+ break
|
||||
+
|
||||
+ logger.debug('Number of series in the mbox: %d', len(lmbx.series))
|
||||
+ logger.info('---')
|
||||
+ logger.info('This patch is present in the following series:')
|
||||
+ logger.info('---')
|
||||
+ firstmsg: Optional[b4.LoreMessage] = None
|
||||
+ for rev in revs:
|
||||
+ pref = f' v{rev}: '
|
||||
+ lser = lmbx.series[rev]
|
||||
+ lmsg: Optional[b4.LoreMessage] = None
|
||||
+ if lser.has_cover:
|
||||
+ firstmsg = lser.patches[0]
|
||||
+ for lmsg in lser.patches[1:]:
|
||||
+ if lmsg is None:
|
||||
+ continue
|
||||
+ if firstmsg is None:
|
||||
+ firstmsg = lmsg
|
||||
+ if lmsg.git_patch_id == patch_id:
|
||||
+ logger.debug('Matched by exact patch-id')
|
||||
+ break
|
||||
+ if lmsg.subject == csubj:
|
||||
+ logger.debug('Matched by subject')
|
||||
+ break
|
||||
+
|
||||
+ if firstmsg is None:
|
||||
+ logger.error('Internal error: no patches in the series?')
|
||||
+ sys.exit(1)
|
||||
+ if lmsg is None:
|
||||
+ # Use the first patch in the series as a fallback
|
||||
+ lmsg = firstmsg
|
||||
+ logger.info('%s%s', pref, lmsg.full_subject)
|
||||
+ logger.info('%s%s', ' ' * len(pref), linkmask % firstmsg.msgid)
|
||||
+
|
||||
+
|
||||
+def main(cmdargs: argparse.Namespace) -> None:
|
||||
+ if cmdargs.commit_id:
|
||||
+ dig_commit(cmdargs)
|
||||
--
|
||||
2.51.0
|
||||
|
||||
316
0001-ez-do-a-patch-id-match-when-pulling-in-trailer-updat.patch
Normal file
316
0001-ez-do-a-patch-id-match-when-pulling-in-trailer-updat.patch
Normal file
@@ -0,0 +1,316 @@
|
||||
From: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Date: Thu, 3 Oct 2024 11:07:20 -0400
|
||||
Subject: ez: do a patch-id match when pulling in trailer updates
|
||||
References: dig-support
|
||||
Git-repo: https://git.kernel.org/pub/scm/utils/b4/b4.git
|
||||
Git-commit: a9f99a4bda313e21116e7d06c012d2aa35840745
|
||||
Patch-mainline: yes
|
||||
|
||||
Address two important limitations of trailers -u:
|
||||
|
||||
- trailer updates were applied even if the local patch has changed
|
||||
- trailers sent to the cover letter were applied to all patches in the
|
||||
current series, even if they were sent to a previous revision and the
|
||||
commits no longer matched
|
||||
|
||||
Reported-by: Mark Brown <broonie@kernel.org>
|
||||
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=219342
|
||||
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Signed-off-by: Jiri Slaby <jslaby@suse.cz>
|
||||
---
|
||||
.gitignore | 1 +
|
||||
pyproject.toml | 4 ++
|
||||
src/b4/__init__.py | 36 ++++++++++--
|
||||
src/b4/ez.py | 57 ++++++++-----------
|
||||
...lers-thread-with-followups-no-match.verify | 42 ++++++++++++++
|
||||
src/tests/test_ez.py | 2 +-
|
||||
6 files changed, 101 insertions(+), 41 deletions(-)
|
||||
create mode 100644 src/tests/samples/trailers-thread-with-followups-no-match.verify
|
||||
|
||||
diff --git a/.gitignore b/.gitignore
|
||||
index 85421da2716c..d5e578b42347 100644
|
||||
--- a/.gitignore
|
||||
+++ b/.gitignore
|
||||
@@ -18,3 +18,4 @@ __pycache__
|
||||
.venv
|
||||
qodana.yaml
|
||||
*.ipynb
|
||||
+pytest.log
|
||||
diff --git a/pyproject.toml b/pyproject.toml
|
||||
index b15873172937..00f8cbdae296 100644
|
||||
--- a/pyproject.toml
|
||||
+++ b/pyproject.toml
|
||||
@@ -45,6 +45,10 @@ b4 = "b4.command:cmd"
|
||||
[tool.pytest.ini_options]
|
||||
filterwarnings = "ignore:.*(pyopenssl|invalid escape sequence).*:DeprecationWarning"
|
||||
norecursedirs = ["tests/helpers", "patatt"]
|
||||
+log_file = "pytest.log"
|
||||
+log_file_level = "DEBUG"
|
||||
+log_file_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
||||
+log_file_date_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "0.14.3"
|
||||
diff --git a/src/b4/__init__.py b/src/b4/__init__.py
|
||||
index 2b7ba1d59797..80a54e2df4ea 100644
|
||||
--- a/src/b4/__init__.py
|
||||
+++ b/src/b4/__init__.py
|
||||
@@ -1286,9 +1286,11 @@ class LoreMessage:
|
||||
self.references = list()
|
||||
|
||||
if self.msg.get('References'):
|
||||
- for pair in email.utils.getaddresses([str(x) for x in self.msg.get_all('references', [])]):
|
||||
- if pair and pair[1].strip() and pair[1] not in self.references:
|
||||
- self.references.append(pair[1])
|
||||
+ for rbunch in self.msg.get_all('references', list()):
|
||||
+ for rchunk in LoreMessage.clean_header(rbunch).split():
|
||||
+ rmsgid = rchunk.strip('<>')
|
||||
+ if rmsgid not in self.references:
|
||||
+ self.references.append(rmsgid)
|
||||
|
||||
try:
|
||||
fromdata = email.utils.getaddresses([LoreMessage.clean_header(str(x))
|
||||
@@ -1298,7 +1300,8 @@ class LoreMessage:
|
||||
if not len(self.fromname.strip()):
|
||||
self.fromname = self.fromemail
|
||||
except IndexError:
|
||||
- pass
|
||||
+ self.fromname = ''
|
||||
+ self.fromemail = ''
|
||||
|
||||
msgdate = self.msg.get('Date')
|
||||
if msgdate:
|
||||
@@ -1331,7 +1334,7 @@ class LoreMessage:
|
||||
|
||||
trailers, others = LoreMessage.find_trailers(self.body, followup=True)
|
||||
# We only pay attention to trailers that are sent in reply
|
||||
- if trailers and self.in_reply_to and not self.has_diff and not self.reply:
|
||||
+ if trailers and self.references and not self.has_diff and not self.reply:
|
||||
logger.debug('A follow-up missing a Re: but containing a trailer with no patch diff')
|
||||
self.reply = True
|
||||
if self.reply:
|
||||
@@ -4424,7 +4427,9 @@ def map_codereview_trailers(qmsgs: List[email.message.Message],
|
||||
qmid_map = dict()
|
||||
ref_map = dict()
|
||||
patchid_map = dict()
|
||||
- seen_msgids = set(ignore_msgids)
|
||||
+ seen_msgids = set()
|
||||
+ if ignore_msgids is not None:
|
||||
+ seen_msgids.update(ignore_msgids)
|
||||
for qmsg in qmsgs:
|
||||
qmsgid = LoreMessage.get_clean_msgid(qmsg)
|
||||
if qmsgid in seen_msgids:
|
||||
@@ -4438,6 +4443,7 @@ def map_codereview_trailers(qmsgs: List[email.message.Message],
|
||||
ref_map[qref].append(qlmsg.msgid)
|
||||
|
||||
logger.info('Analyzing %s code-review messages', len(qmid_map))
|
||||
+ covers = dict()
|
||||
for qmid, qlmsg in qmid_map.items():
|
||||
logger.debug(' new message: %s', qmid)
|
||||
if not qlmsg.reply:
|
||||
@@ -4459,6 +4465,9 @@ def map_codereview_trailers(qmsgs: List[email.message.Message],
|
||||
if (_qmsg.counter == 0 and (not _qmsg.counters_inferred or _qmsg.has_diffstat)
|
||||
and _qmsg.msgid in ref_map):
|
||||
logger.debug(' stopping: found the cover letter for %s', qlmsg.full_subject)
|
||||
+ if _qmsg.msgid not in covers:
|
||||
+ covers[_qmsg.msgid] = set()
|
||||
+ covers[_qmsg.msgid].add(qlmsg.msgid)
|
||||
break
|
||||
elif _qmsg.has_diff:
|
||||
pqpid = _qmsg.git_patch_id
|
||||
@@ -4479,4 +4488,19 @@ def map_codereview_trailers(qmsgs: List[email.message.Message],
|
||||
# Does it have 'patch-id: ' in the body?
|
||||
# TODO: once we have that functionality in b4 cr
|
||||
|
||||
+ if not covers:
|
||||
+ return patchid_map
|
||||
+
|
||||
+ # find all patches directly below these covers
|
||||
+ for cmsgid, fwmsgids in covers.items():
|
||||
+ logger.debug('Looking at cover: %s', cmsgid)
|
||||
+ for qmid, qlmsg in qmid_map.items():
|
||||
+ if qlmsg.in_reply_to == cmsgid and qlmsg.git_patch_id:
|
||||
+ pqpid = qlmsg.git_patch_id
|
||||
+ for fwmsgid in fwmsgids:
|
||||
+ logger.debug('Adding cover follow-up %s to patch-id %s', fwmsgid, pqpid)
|
||||
+ if pqpid not in patchid_map:
|
||||
+ patchid_map[pqpid] = list()
|
||||
+ patchid_map[pqpid].append(qmid_map[fwmsgid])
|
||||
+
|
||||
return patchid_map
|
||||
diff --git a/src/b4/ez.py b/src/b4/ez.py
|
||||
index 7c708e52cecd..958f41aa1c7c 100644
|
||||
--- a/src/b4/ez.py
|
||||
+++ b/src/b4/ez.py
|
||||
@@ -1038,7 +1038,7 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
ignore_commits = None
|
||||
- changeid = None
|
||||
+ tracking = None
|
||||
cover = None
|
||||
msgid = None
|
||||
end = b4.git_revparse_obj('HEAD')
|
||||
@@ -1050,7 +1050,6 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
|
||||
limit_committer = None
|
||||
start = get_series_start()
|
||||
cover, tracking = load_cover(strip_comments=True)
|
||||
- changeid = tracking['series'].get('change-id')
|
||||
if cmdargs.trailers_from:
|
||||
msgid = cmdargs.trailers_from
|
||||
else:
|
||||
@@ -1145,15 +1144,16 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
|
||||
bbox.add_message(msg)
|
||||
|
||||
commit_map = dict()
|
||||
- by_subject = dict()
|
||||
+ by_patchid = dict()
|
||||
for lmsg in bbox.series[1].patches:
|
||||
if not lmsg:
|
||||
continue
|
||||
- by_subject[lmsg.subject] = lmsg.msgid
|
||||
+ by_patchid[lmsg.git_patch_id] = lmsg.msgid
|
||||
commit_map[lmsg.msgid] = lmsg
|
||||
|
||||
list_msgs = list()
|
||||
- if changeid and b4.can_network:
|
||||
+ if tracking and b4.can_network:
|
||||
+ changeid = tracking['series'].get('change-id')
|
||||
logger.info('Checking change-id "%s"', changeid)
|
||||
query = f'"change-id: {changeid}"'
|
||||
smsgs = b4.get_pi_search_results(query, nocache=True)
|
||||
@@ -1171,44 +1171,33 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
|
||||
if tmsgs is not None:
|
||||
list_msgs += tmsgs
|
||||
|
||||
- for list_msg in list_msgs:
|
||||
- llmsg = b4.LoreMessage(list_msg)
|
||||
- if not llmsg.trailers:
|
||||
+ mismatches = set()
|
||||
+ patchid_map = b4.map_codereview_trailers(list_msgs)
|
||||
+ for patchid, llmsgs in patchid_map.items():
|
||||
+ if patchid not in by_patchid:
|
||||
+ logger.debug('Skipping patch-id %s: not found in the current series', patchid)
|
||||
+ logger.debug('Ignoring follow-ups: %s', [x.subject for x in llmsgs])
|
||||
continue
|
||||
- if llmsg.subject in by_subject:
|
||||
- # Reparent to the commit and add to followups
|
||||
- commit = by_subject[llmsg.subject]
|
||||
- logger.debug('Mapped "%s" to commit %s', llmsg.subject, commit)
|
||||
- plmsg = commit_map[commit]
|
||||
- llmsg.in_reply_to = plmsg.msgid
|
||||
- bbox.followups.append(llmsg)
|
||||
- elif llmsg.counter == 0 and changeid:
|
||||
- logger.debug('Mapped "%s" to the cover letter', llmsg.subject)
|
||||
- # Reparent to the cover and add to followups
|
||||
- llmsg.in_reply_to = 'cover'
|
||||
- bbox.followups.append(llmsg)
|
||||
- else:
|
||||
- # Match by patch-id?
|
||||
- logger.debug('No match for %s', llmsg.subject)
|
||||
-
|
||||
- if msgid or changeid:
|
||||
+ for llmsg in llmsgs:
|
||||
+ ltrailers, lmismatches = llmsg.get_trailers(sloppy=cmdargs.sloppytrailers)
|
||||
+ for ltr in lmismatches:
|
||||
+ mismatches.add((ltr.name, ltr.value, llmsg.fromname, llmsg.fromemail))
|
||||
+ commit = by_patchid[patchid]
|
||||
+ lmsg = commit_map[commit]
|
||||
+ logger.debug('Adding %s to %s', [x.as_string() for x in ltrailers], lmsg.msgid)
|
||||
+ lmsg.followup_trailers += ltrailers
|
||||
+
|
||||
+ if msgid or tracking:
|
||||
logger.debug('Will query by change-id')
|
||||
codereview_trailers = False
|
||||
else:
|
||||
codereview_trailers = True
|
||||
|
||||
lser = bbox.get_series(sloppytrailers=cmdargs.sloppytrailers, codereview_trailers=codereview_trailers)
|
||||
- mismatches = list(lser.trailer_mismatches)
|
||||
+ mismatches.update(lser.trailer_mismatches)
|
||||
config = b4.get_main_config()
|
||||
seen_froms = set()
|
||||
logger.info('---')
|
||||
- # Do we have follow-up tralers sent to the cover?
|
||||
- if lser.patches[0] and lser.patches[0].followup_trailers:
|
||||
- logger.debug('Applying follow-up trailers from cover to all patches')
|
||||
- for pmsg in lser.patches[1:]:
|
||||
- logger.debug(' %s (%s)', pmsg.subject, pmsg.msgid)
|
||||
- logger.debug(' + %s', [x.as_string() for x in lser.patches[0].followup_trailers])
|
||||
- pmsg.followup_trailers += lser.patches[0].followup_trailers
|
||||
|
||||
updates = dict()
|
||||
for lmsg in lser.patches[1:]:
|
||||
@@ -1246,7 +1235,7 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
|
||||
if len(mismatches):
|
||||
logger.critical('---')
|
||||
logger.critical('NOTE: some trailers ignored due to from/email mismatches:')
|
||||
- for tname, tvalue, fname, femail in lser.trailer_mismatches:
|
||||
+ for tname, tvalue, fname, femail in mismatches:
|
||||
logger.critical(' ! Trailer: %s: %s', tname, tvalue)
|
||||
logger.critical(' Msg From: %s <%s>', fname, femail)
|
||||
logger.critical('NOTE: Rerun with -S to apply them anyway')
|
||||
diff --git a/src/tests/samples/trailers-thread-with-followups-no-match.verify b/src/tests/samples/trailers-thread-with-followups-no-match.verify
|
||||
new file mode 100644
|
||||
index 000000000000..e4f2f3cce278
|
||||
--- /dev/null
|
||||
+++ b/src/tests/samples/trailers-thread-with-followups-no-match.verify
|
||||
@@ -0,0 +1,42 @@
|
||||
+konstantin@linuxfoundation.org
|
||||
+Minor typo changes imitation
|
||||
+Life imitatus artem.
|
||||
+
|
||||
+Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
+Signed-off-by: Test Override <test-override@example.com>
|
||||
+---
|
||||
+konstantin@linuxfoundation.org
|
||||
+Add some paragraphs to lipsum
|
||||
+Mostly junk. As expected.
|
||||
+
|
||||
+Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
+Signed-off-by: Test Override <test-override@example.com>
|
||||
+---
|
||||
+konstantin@linuxfoundation.org
|
||||
+Add more lines to file 1
|
||||
+This is a second patch in the series. It needed a paragraph with the
|
||||
+words of wisdom.
|
||||
+
|
||||
+Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
+Signed-off-by: Test Override <test-override@example.com>
|
||||
+---
|
||||
+konstantin@linuxfoundation.org
|
||||
+Remove line 2 from file2
|
||||
+Etiam in rhoncus lacus. Ut velit nisl, mollis ac commodo vitae, ultrices
|
||||
+quis felis. Proin varius hendrerit volutpat. Pellentesque nec laoreet
|
||||
+quam, eu ullamcorper mi. Donec ut purus ac sapien dignissim elementum eu
|
||||
+ac ante. Mauris sed faucibus orci.
|
||||
+
|
||||
+Vivamus eleifend accumsan ultricies. Cras at erat nec mauris iaculis
|
||||
+eleifend sit amet eu libero. Suspendisse auctor a erat at vestibulum.
|
||||
+Nullam efficitur quis turpis quis sodales.
|
||||
+
|
||||
+Nunc elementum hendrerit arcu eget feugiat. Nulla placerat pellentesque
|
||||
+metus, nec rutrum nulla porttitor vel. Ut tristique commodo sem, ac
|
||||
+sollicitudin enim pharetra et. Mauris sed tellus vitae nunc sollicitudin
|
||||
+fermentum. Phasellus dui elit, malesuada quis metus vel, blandit
|
||||
+tristique felis. Aenean quis tempus enim.
|
||||
+
|
||||
+Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
+Signed-off-by: Test Override <test-override@example.com>
|
||||
+---
|
||||
diff --git a/src/tests/test_ez.py b/src/tests/test_ez.py
|
||||
index 1b02e7bede55..7351f7992079 100644
|
||||
--- a/src/tests/test_ez.py
|
||||
+++ b/src/tests/test_ez.py
|
||||
@@ -25,7 +25,7 @@ def prepdir(gitdir):
|
||||
{'shazam-am-flags': '--signoff'}),
|
||||
# Test matching trailer updates by subject when patch-id changes
|
||||
('trailers-thread-with-followups', None, (b'vivendum', b'addendum'), [],
|
||||
- ['log', '--format=%ae%n%s%n%b---', 'HEAD~4..'], 'trailers-thread-with-followups',
|
||||
+ ['log', '--format=%ae%n%s%n%b---', 'HEAD~4..'], 'trailers-thread-with-followups-no-match',
|
||||
{'shazam-am-flags': '--signoff'}),
|
||||
# Test that we properly perserve commits with --- in them
|
||||
('trailers-thread-with-followups', 'trailers-with-tripledash', None, [],
|
||||
--
|
||||
2.51.0
|
||||
|
||||
42
0002-dig-fix-wrong-msgid-output-for-matches.patch
Normal file
42
0002-dig-fix-wrong-msgid-output-for-matches.patch
Normal file
@@ -0,0 +1,42 @@
|
||||
From: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Date: Fri, 10 Oct 2025 16:42:54 -0400
|
||||
Subject: dig: fix wrong msgid output for matches
|
||||
References: dig-support
|
||||
Git-commit: 965071ee174d55969110de4cae739e76a6467ea8
|
||||
Patch-mainline: yes
|
||||
|
||||
Due to a logical bug, we were always outputting the msgid of the first
|
||||
message in the first matching series.
|
||||
|
||||
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Signed-off-by: Jiri Slaby <jslaby@suse.cz>
|
||||
---
|
||||
src/b4/dig.py | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/b4/dig.py b/src/b4/dig.py
|
||||
index 8b49b90c8e05..e5577a2f0a3a 100644
|
||||
--- a/src/b4/dig.py
|
||||
+++ b/src/b4/dig.py
|
||||
@@ -154,8 +154,8 @@ def dig_commit(cmdargs: argparse.Namespace) -> None:
|
||||
logger.info('---')
|
||||
logger.info('This patch is present in the following series:')
|
||||
logger.info('---')
|
||||
- firstmsg: Optional[b4.LoreMessage] = None
|
||||
for rev in revs:
|
||||
+ firstmsg: Optional[b4.LoreMessage] = None
|
||||
pref = f' v{rev}: '
|
||||
lser = lmbx.series[rev]
|
||||
lmsg: Optional[b4.LoreMessage] = None
|
||||
@@ -180,7 +180,7 @@ def dig_commit(cmdargs: argparse.Namespace) -> None:
|
||||
# Use the first patch in the series as a fallback
|
||||
lmsg = firstmsg
|
||||
logger.info('%s%s', pref, lmsg.full_subject)
|
||||
- logger.info('%s%s', ' ' * len(pref), linkmask % firstmsg.msgid)
|
||||
+ logger.info('%s%s', ' ' * len(pref), linkmask % lmsg.msgid)
|
||||
|
||||
|
||||
def main(cmdargs: argparse.Namespace) -> None:
|
||||
--
|
||||
2.51.0
|
||||
|
||||
70
0003-dig-actually-handle-commitish-strings.patch
Normal file
70
0003-dig-actually-handle-commitish-strings.patch
Normal file
@@ -0,0 +1,70 @@
|
||||
From: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Date: Tue, 14 Oct 2025 10:59:17 -0400
|
||||
Subject: dig: actually handle commitish strings
|
||||
References: dig-support
|
||||
Git-repo: https://git.kernel.org/pub/scm/utils/b4/b4.git
|
||||
Git-commit: e8937ac7791dfdba5b788ae128588409bb16778c
|
||||
Patch-mainline: yes
|
||||
|
||||
We claim to handle commitish strings, but we actually don't convert them
|
||||
into commits. Parse the objects into actual commitid's before we treat
|
||||
them as such.
|
||||
|
||||
Suggested-by: Linus Torvalds <torvalds@linuxfoundation.org>
|
||||
Link: https://lore.kernel.org/CAHk-=wgNveeyKbMth9d9_vUer4P7=VYArLqqrfHH12WAtth-zQ@mail.gmail.com
|
||||
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Signed-off-by: Jiri Slaby <jslaby@suse.cz>
|
||||
---
|
||||
src/b4/command.py | 2 +-
|
||||
src/b4/dig.py | 10 +++++-----
|
||||
2 files changed, 6 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/src/b4/command.py b/src/b4/command.py
|
||||
index c90705ce9c5e..80a96c365b3e 100644
|
||||
--- a/src/b4/command.py
|
||||
+++ b/src/b4/command.py
|
||||
@@ -390,7 +390,7 @@ def setup_parser() -> argparse.ArgumentParser:
|
||||
|
||||
# b4 dig
|
||||
sp_dig = subparsers.add_parser('dig', help='Dig into the details of a specific commit')
|
||||
- sp_dig.add_argument('-c', '--commit', dest='commit_id', metavar='COMMITISH',
|
||||
+ sp_dig.add_argument('-c', '--commitish', dest='commitish', metavar='COMMITISH',
|
||||
help='Commit-ish object to dig into')
|
||||
sp_dig.set_defaults(func=cmd_dig)
|
||||
|
||||
diff --git a/src/b4/dig.py b/src/b4/dig.py
|
||||
index e5577a2f0a3a..03ca3211c37b 100644
|
||||
--- a/src/b4/dig.py
|
||||
+++ b/src/b4/dig.py
|
||||
@@ -25,7 +25,7 @@ try_diff_algos: List[str] = [
|
||||
]
|
||||
|
||||
|
||||
-def dig_commit(cmdargs: argparse.Namespace) -> None:
|
||||
+def dig_commitish(cmdargs: argparse.Namespace) -> None:
|
||||
config = b4.get_main_config()
|
||||
cfg_llval = config.get('linkmask', '')
|
||||
if isinstance(cfg_llval, str) and '%s' in cfg_llval:
|
||||
@@ -39,9 +39,9 @@ def dig_commit(cmdargs: argparse.Namespace) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
# Can we resolve this commit to an object?
|
||||
- commit = b4.git_revparse_obj(cmdargs.commit_id, topdir)
|
||||
+ commit = b4.git_revparse_obj(f'{cmdargs.commitish}^0', topdir)
|
||||
if not commit:
|
||||
- logger.error('Cannot find a commit matching %s', cmdargs.commit_id)
|
||||
+ logger.error('Cannot find a commit matching %s', cmdargs.commitish)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info('Digging into commit %s', commit)
|
||||
@@ -184,5 +184,5 @@ def dig_commit(cmdargs: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def main(cmdargs: argparse.Namespace) -> None:
|
||||
- if cmdargs.commit_id:
|
||||
- dig_commit(cmdargs)
|
||||
+ if cmdargs.commitish:
|
||||
+ dig_commitish(cmdargs)
|
||||
--
|
||||
2.51.0
|
||||
|
||||
413
0004-dig-first-round-of-refinement-to-dig.patch
Normal file
413
0004-dig-first-round-of-refinement-to-dig.patch
Normal file
@@ -0,0 +1,413 @@
|
||||
From: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Date: Tue, 14 Oct 2025 16:55:05 -0400
|
||||
Subject: dig: first round of refinement to dig
|
||||
References: dig-support
|
||||
Git-repo: https://git.kernel.org/pub/scm/utils/b4/b4.git
|
||||
Git-commit: 3ae277e9c7dd3e1df61a14884aabdd5834ad1201
|
||||
Patch-mainline: yes
|
||||
|
||||
Lots of changes:
|
||||
|
||||
- filter search result to only match what is before the commit date,
|
||||
because after that date we're likely to get false-positives from
|
||||
cherry-picks and backports
|
||||
- output a single "most likely came from here" link to stdout, so people
|
||||
can pass this to pipes
|
||||
- add a -a switch that will dig deeper and try to find all previous
|
||||
revisions of that series, whether or not the patch showed up there or
|
||||
not
|
||||
|
||||
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
|
||||
Signed-off-by: Jiri Slaby <jslaby@suse.cz>
|
||||
---
|
||||
src/b4/__init__.py | 49 ++++++++++-
|
||||
src/b4/command.py | 4 +
|
||||
src/b4/dig.py | 203 ++++++++++++++++++++++++++++++++-------------
|
||||
3 files changed, 197 insertions(+), 59 deletions(-)
|
||||
|
||||
diff --git a/src/b4/__init__.py b/src/b4/__init__.py
|
||||
index b6eab255103f..2b7ba1d59797 100644
|
||||
--- a/src/b4/__init__.py
|
||||
+++ b/src/b4/__init__.py
|
||||
@@ -37,6 +37,8 @@ from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional, Tuple, Set, List, BinaryIO, Union, Sequence, Literal, Iterator, Dict
|
||||
|
||||
+from email.message import EmailMessage
|
||||
+
|
||||
from email import charset
|
||||
|
||||
charset.add_charset('utf-8', None)
|
||||
@@ -505,7 +507,9 @@ class LoreSeries:
|
||||
complete: bool = False
|
||||
has_cover: bool = False
|
||||
partial_reroll: bool = False
|
||||
- subject: str
|
||||
+ subject: Optional[str] = None
|
||||
+ fromname: Optional[str] = None
|
||||
+ fromemail: Optional[str] = None
|
||||
indexes: Optional[List[Tuple[str, str]]] = None
|
||||
base_commit: Optional[str] = None
|
||||
change_id: Optional[str] = None
|
||||
@@ -524,6 +528,7 @@ class LoreSeries:
|
||||
def __repr__(self):
|
||||
out = list()
|
||||
out.append('- Series: [v%s] %s' % (self.revision, self.subject))
|
||||
+ out.append(' author: %s <%s>' % (self.fromname, self.fromemail))
|
||||
out.append(' revision: %s' % self.revision)
|
||||
out.append(' expected: %s' % self.expected)
|
||||
out.append(' complete: %s' % self.complete)
|
||||
@@ -542,6 +547,27 @@ class LoreSeries:
|
||||
|
||||
return '\n'.join(out)
|
||||
|
||||
+ def __eq__(self, other: object) -> bool:
|
||||
+ if not isinstance(other, LoreSeries):
|
||||
+ return NotImplemented
|
||||
+ # We are the same series if all patch-id's are exactly the same
|
||||
+ my_patchids: List[Optional[str]] = list()
|
||||
+ for patch in self.patches[1:]:
|
||||
+ if patch is not None:
|
||||
+ my_patchids.append(patch.git_patch_id)
|
||||
+ other_patchids: List[Optional[str]] = list()
|
||||
+ for patch in other.patches[1:]:
|
||||
+ if patch is not None:
|
||||
+ other_patchids.append(patch.git_patch_id)
|
||||
+ return my_patchids == other_patchids
|
||||
+
|
||||
+ def get_patch_by_msgid(self, msgid: str) -> Optional['LoreMessage']:
|
||||
+ for lmsg in self.patches:
|
||||
+ if lmsg is not None and lmsg.msgid == msgid:
|
||||
+ return lmsg
|
||||
+ raise IndexError('No such patch in series')
|
||||
+
|
||||
+
|
||||
def add_patch(self, lmsg: 'LoreMessage') -> None:
|
||||
while len(self.patches) < lmsg.expected + 1:
|
||||
self.patches.append(None)
|
||||
@@ -587,8 +613,12 @@ class LoreSeries:
|
||||
|
||||
if self.patches[0] is not None:
|
||||
self.subject = self.patches[0].subject
|
||||
+ self.fromname = self.patches[0].fromname
|
||||
+ self.fromemail = self.patches[0].fromemail
|
||||
elif self.patches[1] is not None:
|
||||
self.subject = self.patches[1].subject
|
||||
+ self.fromname = self.patches[1].fromname
|
||||
+ self.fromemail = self.patches[1].fromemail
|
||||
|
||||
def get_slug(self, extended: bool = False) -> str:
|
||||
# Find the first non-None entry
|
||||
@@ -3386,9 +3416,22 @@ def get_series_by_change_id(change_id: str, nocache: bool = False) -> Optional['
|
||||
return lmbx
|
||||
|
||||
|
||||
-def get_series_by_patch_id(patch_id: str, nocache: bool = False) -> Optional['LoreMailbox']:
|
||||
+def get_msgs_by_patch_id(patch_id: str, extra_query: Optional[str] = None,
|
||||
+ nocache: bool = False, full_threads: bool = False
|
||||
+ ) -> Optional[List[EmailMessage]]:
|
||||
q = f'patchid:{patch_id}'
|
||||
- q_msgs = get_pi_search_results(q, nocache=nocache)
|
||||
+ if extra_query:
|
||||
+ q = f'{q} {extra_query}'
|
||||
+ logger.debug('Full query: %s (nocache=%s)', q, nocache)
|
||||
+ q_msgs = get_pi_search_results(q, nocache=nocache, full_threads=full_threads)
|
||||
+ if not q_msgs:
|
||||
+ return None
|
||||
+
|
||||
+ return q_msgs
|
||||
+
|
||||
+
|
||||
+def get_series_by_patch_id(patch_id: str, nocache: bool = False) -> Optional['LoreMailbox']:
|
||||
+ q_msgs = get_msgs_by_patch_id(patch_id, full_threads=True, nocache=nocache)
|
||||
if not q_msgs:
|
||||
return None
|
||||
lmbx = LoreMailbox()
|
||||
diff --git a/src/b4/command.py b/src/b4/command.py
|
||||
index 80a96c365b3e..4568b2089868 100644
|
||||
--- a/src/b4/command.py
|
||||
+++ b/src/b4/command.py
|
||||
@@ -392,6 +392,10 @@ def setup_parser() -> argparse.ArgumentParser:
|
||||
sp_dig = subparsers.add_parser('dig', help='Dig into the details of a specific commit')
|
||||
sp_dig.add_argument('-c', '--commitish', dest='commitish', metavar='COMMITISH',
|
||||
help='Commit-ish object to dig into')
|
||||
+ sp_dig.add_argument('-C', '--no-cache', dest='nocache', action='store_true', default=False,
|
||||
+ help='Do not use local cache')
|
||||
+ sp_dig.add_argument('-a', '--all-series', action='store_true', default=False,
|
||||
+ help='Show all series, not just the latest matching')
|
||||
sp_dig.set_defaults(func=cmd_dig)
|
||||
|
||||
return parser
|
||||
diff --git a/src/b4/dig.py b/src/b4/dig.py
|
||||
index 03ca3211c37b..5d19d59261e7 100644
|
||||
--- a/src/b4/dig.py
|
||||
+++ b/src/b4/dig.py
|
||||
@@ -9,10 +9,11 @@ import os
|
||||
import sys
|
||||
import b4
|
||||
import argparse
|
||||
-import email.parser
|
||||
+import re
|
||||
+import urllib.parse
|
||||
|
||||
from email.message import EmailMessage
|
||||
-from typing import List, Set, Optional
|
||||
+from typing import List, Set, Optional, Union
|
||||
|
||||
logger = b4.logger
|
||||
|
||||
@@ -25,6 +26,28 @@ try_diff_algos: List[str] = [
|
||||
]
|
||||
|
||||
|
||||
+def try_links(links: Set[str]) -> None:
|
||||
+ logger.info('Try following these Link trailers:')
|
||||
+ for link in links:
|
||||
+ logger.info(' Link: %s', link)
|
||||
+
|
||||
+
|
||||
+def print_one_match(subject: str, link: str) -> None:
|
||||
+ logger.info('---')
|
||||
+ logger.info(subject)
|
||||
+ sys.stdout.write(f'{link}\n')
|
||||
+
|
||||
+
|
||||
+def get_all_msgids_from_urls(urls: Set[str]) -> Set[str]:
|
||||
+ msgids: Set[str] = set()
|
||||
+ for url in urls:
|
||||
+ matches = re.search(r'^https?://[^@]+/([^/]+@[^/]+)', url, re.IGNORECASE)
|
||||
+ if matches:
|
||||
+ chunks = matches.groups()
|
||||
+ msgids.add(urllib.parse.unquote(chunks[0]))
|
||||
+ return msgids
|
||||
+
|
||||
+
|
||||
def dig_commitish(cmdargs: argparse.Namespace) -> None:
|
||||
config = b4.get_main_config()
|
||||
cfg_llval = config.get('linkmask', '')
|
||||
@@ -56,15 +79,30 @@ def dig_commitish(cmdargs: argparse.Namespace) -> None:
|
||||
logger.error('Merge commit detected, please specify a single-parent commit.')
|
||||
sys.exit(1)
|
||||
|
||||
+ # Look at the commit message and find any Link: trailers
|
||||
+ links: Set[str] = set()
|
||||
+ ecode, out = b4.git_run_command(
|
||||
+ topdir, ['show', '--no-patch', '--format=%B', commit],
|
||||
+ )
|
||||
+ if ecode > 0:
|
||||
+ logger.error('Could not get commit message for %s', commit)
|
||||
+ sys.exit(1)
|
||||
+ trailers, _ = b4.LoreMessage.find_trailers(out)
|
||||
+ ltrs = [t for t in trailers if t.name.lower() == 'link']
|
||||
+ if ltrs:
|
||||
+ links = set(ltr.value for ltr in ltrs)
|
||||
+
|
||||
+ msgids = get_all_msgids_from_urls(links)
|
||||
+
|
||||
# Find commit's author and subject from git
|
||||
ecode, out = b4.git_run_command(
|
||||
- topdir, ['show', '--no-patch', '--format=%ae %s', commit],
|
||||
+ topdir, ['show', '--no-patch', '--format=%as %ae %s', commit],
|
||||
)
|
||||
if ecode > 0:
|
||||
logger.error('Could not get commit info for %s', commit)
|
||||
sys.exit(1)
|
||||
- fromeml, csubj = out.strip().split(maxsplit=1)
|
||||
- logger.debug('fromeml=%s, csubj=%s', fromeml, csubj)
|
||||
+ cdate, fromeml, csubj = out.strip().split(maxsplit=2)
|
||||
+ logger.debug('cdate=%s, fromeml=%s, csubj=%s', cdate, fromeml, csubj)
|
||||
logger.info('Attempting to match by exact patch-id...')
|
||||
showargs = [
|
||||
'--format=email',
|
||||
@@ -74,7 +112,7 @@ def dig_commitish(cmdargs: argparse.Namespace) -> None:
|
||||
]
|
||||
# Keep a record so we don't try git-patch-id on identical patches
|
||||
bpatches: Set[bytes] = set()
|
||||
- lmbx: Optional[b4.LoreMailbox] = None
|
||||
+ msgs: Optional[List[EmailMessage]] = None
|
||||
for algo in try_diff_algos:
|
||||
logger.debug('Trying with diff-algorithm=%s', algo)
|
||||
algoarg = f'--diff-algorithm={algo}'
|
||||
@@ -97,68 +135,121 @@ def dig_commitish(cmdargs: argparse.Namespace) -> None:
|
||||
sys.exit(1)
|
||||
patch_id = out.split(maxsplit=1)[0]
|
||||
logger.debug('Patch-id for commit %s is %s', commit, patch_id)
|
||||
- logger.info('Trying to find matching series by patch-id %s', patch_id)
|
||||
- lmbx = b4.get_series_by_patch_id(patch_id)
|
||||
- if lmbx:
|
||||
+ logger.info('Trying to find matching series by patch-id %s (%s)', patch_id, algo)
|
||||
+ # Limit lookup by date prior to the commit date, to weed out any false-positives from
|
||||
+ # backports or from erroneously resent series
|
||||
+ extra_query = f'AND rt:..{cdate}'
|
||||
+ logger.debug('extra_query=%s', extra_query)
|
||||
+ msgs = b4.get_msgs_by_patch_id(patch_id, nocache=cmdargs.nocache, extra_query=extra_query)
|
||||
+ if msgs:
|
||||
logger.info('Found matching series by patch-id')
|
||||
+ for msg in msgs:
|
||||
+ msgid = b4.LoreMessage.get_clean_msgid(msg)
|
||||
+ if msgid:
|
||||
+ logger.debug('Adding from patch-id matches: %s', msgid)
|
||||
+ msgids.add(msgid)
|
||||
break
|
||||
|
||||
- if not lmbx:
|
||||
+ if not msgs:
|
||||
logger.info('Attempting to match by author and subject...')
|
||||
- q = '(s:"%s" AND f:"%s")' % (csubj.replace('"', ''), fromeml)
|
||||
- msgs = b4.get_pi_search_results(q)
|
||||
+ q = '(s:"%s" AND f:"%s" AND rt:..%s)' % (csubj.replace('"', ''), fromeml, cdate)
|
||||
+ msgs = b4.get_pi_search_results(q, nocache=cmdargs.nocache, full_threads=False)
|
||||
if msgs:
|
||||
- logger.info('Found %s matching messages', len(msgs))
|
||||
- lmbx = b4.LoreMailbox()
|
||||
for msg in msgs:
|
||||
- lmbx.add_message(msg)
|
||||
- else:
|
||||
+ msgid = b4.LoreMessage.get_clean_msgid(msg)
|
||||
+ if msgid:
|
||||
+ logger.debug('Adding from author+subject matches: %s', msgid)
|
||||
+ msgids.add(msgid)
|
||||
+ if not msgs and not msgids:
|
||||
logger.error('Could not find anything matching commit %s', commit)
|
||||
- # Look at the commit message and find any Link: trailers
|
||||
- ecode, out = b4.git_run_command(
|
||||
- topdir, ['show', '--no-patch', '--format=%B', commit],
|
||||
- )
|
||||
- if ecode > 0:
|
||||
- logger.error('Could not get commit message for %s', commit)
|
||||
- sys.exit(1)
|
||||
- trailers, _ = b4.LoreMessage.find_trailers(out)
|
||||
- ltrs = [t for t in trailers if t.name.lower() == 'link']
|
||||
- if ltrs:
|
||||
- logger.info('---')
|
||||
- logger.info('Try following these Link trailers:')
|
||||
- for ltr in ltrs:
|
||||
- logger.info(' %s', ltr.as_string())
|
||||
+ if links:
|
||||
+ try_links(links)
|
||||
sys.exit(1)
|
||||
|
||||
- # Grab the latest series and see if we have a change_id
|
||||
- revs = list(lmbx.series.keys())
|
||||
- revs.sort(key=lambda r: lmbx.series[r].submission_date or 0)
|
||||
-
|
||||
- change_id: Optional[str] = None
|
||||
- lser = lmbx.get_series(codereview_trailers=False)
|
||||
- for rev in revs:
|
||||
- change_id = lmbx.series[rev].change_id
|
||||
- if not change_id:
|
||||
+ logger.info('Will consider promising messages: %s', len(msgids))
|
||||
+ logger.debug('msgids: %s', msgids)
|
||||
+ # Go one by one and grab threads by message-id
|
||||
+ seen_msgids: Set[str] = set()
|
||||
+ lmbxs: List[b4.LoreMailbox] = list()
|
||||
+ for msgid in msgids:
|
||||
+ if not msgid or msgid in seen_msgids:
|
||||
+ logger.debug('Skipping duplicate or invalid msgid %s', msgid)
|
||||
+ continue
|
||||
+ seen_msgids.add(msgid)
|
||||
+ logger.debug('Fetching thread by msgid %s', msgid)
|
||||
+ lmbx = b4.get_series_by_msgid(msgid)
|
||||
+ if not lmbx:
|
||||
+ logger.error('Could not fetch thread for msgid %s, skipping', msgid)
|
||||
continue
|
||||
- logger.info('Backfilling any missing series by change-id')
|
||||
- logger.debug('change_id=%s', change_id)
|
||||
- # Fill in the rest of the series by change_id
|
||||
- q = f'nq:"change-id:{change_id}"'
|
||||
- q_msgs = b4.get_pi_search_results(q, full_threads=True)
|
||||
- if q_msgs:
|
||||
- for q_msg in q_msgs:
|
||||
- lmbx.add_message(q_msg)
|
||||
- break
|
||||
-
|
||||
- logger.debug('Number of series in the mbox: %d', len(lmbx.series))
|
||||
+ if not lmbx.series:
|
||||
+ logger.debug('No series found in this mailbox, skipping')
|
||||
+ continue
|
||||
+ lmbxs.append(lmbx)
|
||||
+
|
||||
+ if not lmbxs:
|
||||
+ logger.error('Could not fetch any threads for the matching messages!')
|
||||
+ sys.exit(1)
|
||||
+
|
||||
+ lsers: List[b4.LoreSeries] = list()
|
||||
+ for lmbx in lmbxs:
|
||||
+ maxrev = max(lmbx.series.keys())
|
||||
+ if cmdargs.all_series and len(lmbx.series) < maxrev:
|
||||
+ logger.debug('Fetching prior series')
|
||||
+ # Do we have a change-id in this series?
|
||||
+ lser = lmbx.get_series(codereview_trailers=False)
|
||||
+ fillin_q: str = ''
|
||||
+ if lser and lser.change_id:
|
||||
+ logger.debug('Found change-id %s in the series', lser.change_id)
|
||||
+ fillin_q = f'nq:"change-id:{lser.change_id}"'
|
||||
+ elif lser and lser.subject and lser.fromemail:
|
||||
+ # We're going to match by first patch/cover letter subject and author.
|
||||
+ # It's not perfect, but it's the best we can do without a change-id.
|
||||
+ fillin_q = '(s:"%s" AND f:"%s")' % (lser.subject.replace('"', ''), lser.fromemail)
|
||||
+ if fillin_q:
|
||||
+ fillin_q += f' AND rt:..{cdate}'
|
||||
+ logger.debug('fillin_q=%s', fillin_q)
|
||||
+ q_msgs = b4.get_pi_search_results(fillin_q, nocache=cmdargs.nocache, full_threads=True)
|
||||
+ if q_msgs:
|
||||
+ for q_msg in q_msgs:
|
||||
+ lmbx.add_message(q_msg)
|
||||
+ q_msgid = b4.LoreMessage.get_clean_msgid(q_msg)
|
||||
+ if q_msgid:
|
||||
+ seen_msgids.add(q_msgid)
|
||||
+
|
||||
+ for lser in lmbx.series.values():
|
||||
+ if lser and lser not in lsers:
|
||||
+ lsers.append(lser)
|
||||
+
|
||||
+ if not len(lsers):
|
||||
+ logger.error('Could not find any series containing this patch!')
|
||||
+ if links:
|
||||
+ try_links(links)
|
||||
+ sys.exit(1)
|
||||
+
|
||||
+ lsers.sort(key=lambda r: r.submission_date or 0)
|
||||
+ logger.debug('Number of matching series: %d', len(lsers))
|
||||
+ lmsg: Optional[b4.LoreMessage] = None
|
||||
+ if not cmdargs.all_series:
|
||||
+ # Go backwards in time and find the first matching patch
|
||||
+ for lser in reversed(lsers):
|
||||
+ for lmsg in lser.patches[1:]:
|
||||
+ if lmsg is None:
|
||||
+ continue
|
||||
+ if lmsg.git_patch_id == patch_id:
|
||||
+ logger.debug('matched by exact patch-id')
|
||||
+ print_one_match(lmsg.full_subject, linkmask % lmsg.msgid)
|
||||
+ return
|
||||
+ if lmsg.subject == csubj:
|
||||
+ logger.debug('matched by subject')
|
||||
+ print_one_match(lmsg.full_subject, linkmask % lmsg.msgid)
|
||||
+ return
|
||||
+
|
||||
logger.info('---')
|
||||
- logger.info('This patch is present in the following series:')
|
||||
+ logger.info('This patch belongs in the following series:')
|
||||
logger.info('---')
|
||||
- for rev in revs:
|
||||
+ for lser in lsers:
|
||||
firstmsg: Optional[b4.LoreMessage] = None
|
||||
- pref = f' v{rev}: '
|
||||
- lser = lmbx.series[rev]
|
||||
- lmsg: Optional[b4.LoreMessage] = None
|
||||
+ pref = f' v{lser.revision}: '
|
||||
if lser.has_cover:
|
||||
firstmsg = lser.patches[0]
|
||||
for lmsg in lser.patches[1:]:
|
||||
@@ -179,7 +270,7 @@ def dig_commitish(cmdargs: argparse.Namespace) -> None:
|
||||
if lmsg is None:
|
||||
# Use the first patch in the series as a fallback
|
||||
lmsg = firstmsg
|
||||
- logger.info('%s%s', pref, lmsg.full_subject)
|
||||
+ logger.info('%s%s', pref, firstmsg.full_subject)
|
||||
logger.info('%s%s', ' ' * len(pref), linkmask % lmsg.msgid)
|
||||
|
||||
|
||||
--
|
||||
2.51.0
|
||||
|
||||
3
_scmsync.obsinfo
Normal file
3
_scmsync.obsinfo
Normal file
@@ -0,0 +1,3 @@
|
||||
mtime: 1760505825
|
||||
commit: 1848bfda697b9e55f6d27d18d7b2a4069b7829bc24bf6ddffd46896cf36db548
|
||||
url: https://src.opensuse.org/jirislaby/d-t-b4.git
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba79ca0a67f106f20e3d10db8a63bc8097deb4c33ea1f00a90496ea00b985783
|
||||
size 268868
|
||||
BIN
b4-0.14.3.tar.gz
LFS
Normal file
BIN
b4-0.14.3.tar.gz
LFS
Normal file
Binary file not shown.
27
b4.changes
27
b4.changes
@@ -1,3 +1,30 @@
|
||||
-------------------------------------------------------------------
|
||||
Tue Oct 14 06:11:17 UTC 2025 - Jiri Slaby <jslaby@suse.cz>
|
||||
|
||||
- add dig support
|
||||
* 0001-ez-do-a-patch-id-match-when-pulling-in-trailer-updat.patch
|
||||
* 0001-dig-initial-b4-dig-implementation.patch
|
||||
* 0002-dig-fix-wrong-msgid-output-for-matches.patch
|
||||
* 0003-dig-actually-handle-commitish-strings.patch
|
||||
* 0004-dig-first-round-of-refinement-to-dig.patch
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Wed Oct 1 16:09:50 UTC 2025 - Jiri Slaby <jslaby@suse.cz>
|
||||
|
||||
- update to 0.14.3:
|
||||
* Don't strip trailing blank lines in patches when not needed
|
||||
* fix show-info in another branch
|
||||
* stop generating full object IDs for non-binary diffs
|
||||
* better handle invalid timezone values
|
||||
* Create patches with --default-prefix (when git version supports it)
|
||||
* Add --find-renames to git-show to counteract local configs
|
||||
* Fix misleading error message on cherry-picked patches
|
||||
* be more careful when applying trailers to non-prep branches
|
||||
* Don't quote trailers containing brackets
|
||||
* Support smtpauth=none in sendemail configs
|
||||
* when querying by change-id, don't add author and subject
|
||||
* clean up after git-filter-repo runs in recent git-filter-repo versions
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Wed Sep 18 05:13:21 UTC 2024 - Jiri Slaby <jslaby@suse.cz>
|
||||
|
||||
|
||||
7
b4.spec
7
b4.spec
@@ -24,13 +24,18 @@
|
||||
%global pprefix python311
|
||||
%endif
|
||||
Name: b4
|
||||
Version: 0.14.2
|
||||
Version: 0.14.3
|
||||
Release: 0
|
||||
Summary: Helper scripts for kernel.org patches
|
||||
License: GPL-2.0-or-later
|
||||
Group: Development/Tools/Other
|
||||
URL: https://git.kernel.org/pub/scm/utils/b4/b4.git
|
||||
Source0: https://github.com/mricon/b4/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz
|
||||
Patch0: 0001-ez-do-a-patch-id-match-when-pulling-in-trailer-updat.patch
|
||||
Patch1: 0001-dig-initial-b4-dig-implementation.patch
|
||||
Patch2: 0002-dig-fix-wrong-msgid-output-for-matches.patch
|
||||
Patch3: 0003-dig-actually-handle-commitish-strings.patch
|
||||
Patch4: 0004-dig-first-round-of-refinement-to-dig.patch
|
||||
BuildRequires: %{python_module base >= 3.9}
|
||||
BuildRequires: %{python_module dkimpy >= 1.0.5}
|
||||
BuildRequires: %{python_module patatt >= 0.6}
|
||||
|
||||
3
build.specials.obscpio
Normal file
3
build.specials.obscpio
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:61a6900dd7986707a060c06f6bc4422735c2a66d40741814a9cfd2d858de0467
|
||||
size 256
|
||||
Reference in New Issue
Block a user