From patchwork Fri May 1 11:00:12 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 2263 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1777633321; bh=aP/1ZRhzb/TXkc0F9bTi6Jlg6BRC/5VMX7fI0VhX/Eg=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=HVcz1laUqvUMDnsuLXApSgbQAVzsDE7Afjt8YO8ZJ/x8PHYlx0Z3/vkU01nwrGwNN LB6FfTjxk9WiNRtuWfVxfYrHsXsm0oIUUpQcLfbsPkY38F/N6msYBgTlsoN0jxjYnM lubQUdwQDUw5Pojovy9si11lvzVueA4idWtyHeXQ= Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 7E2E86A837 for ; Fri, 1 May 2026 05:02:01 -0600 (MDT) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id weCAnxBObvXY for ; Fri, 1 May 2026 05:02:01 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1777633321; bh=aP/1ZRhzb/TXkc0F9bTi6Jlg6BRC/5VMX7fI0VhX/Eg=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=HVcz1laUqvUMDnsuLXApSgbQAVzsDE7Afjt8YO8ZJ/x8PHYlx0Z3/vkU01nwrGwNN LB6FfTjxk9WiNRtuWfVxfYrHsXsm0oIUUpQcLfbsPkY38F/N6msYBgTlsoN0jxjYnM lubQUdwQDUw5Pojovy9si11lvzVueA4idWtyHeXQ= Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 6A1F86A833 for ; Fri, 1 May 2026 05:02:01 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1777633318; bh=dMu9N8BuzJt5g7gKaxTaGkt5lZSCjAYRMWZSjj9LIuk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=VHknyf+/B9+uSYtorFzvGsMGK1HkeTdZlM1Kwj36E7JAmg/K1qZmVnbNIucZdfpsl 9cbdyFqN03bVhhDq5EEkfkRhArgwfZzyZlu6BPR3L6paD1QwpNOTB3lBtplZWlmMJk MnIaOgGK4C8MsZB/iilXSbKGQa8tIsHuY3JFHfEc= Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id D678E6A834; Fri, 1 May 2026 05:01:58 -0600 (MDT) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 0ADl68AZzXI3; Fri, 1 May 2026 05:01:58 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1777633313; bh=BcqUr8WsfdXTvOBysNW8Qfc0dXDsnekRGaeWA4ud4h8=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=kMHac/TtFm6cXqcFA9s7sJDCL+By60WxRvvjxuFmSYKMYEF0sGASzviGQ1UCczN6e /bss84YI1Sst4qDKM8t9mHlL1u3yLyaEHG63DxepofvPMqvl1xeTiv7UT4XF2FBRIy QeyH2cE+EZoW/6LVsrkyYIDefim78+gneEf0pGJo= Received: from u-boot.org (unknown [174.51.25.52]) by mail.u-boot.org (Postfix) with ESMTPSA id 1C26C6A848; Fri, 1 May 2026 05:01:53 -0600 (MDT) From: Simon Glass To: U-Boot Concept Date: Fri, 1 May 2026 05:00:12 -0600 Message-ID: <20260501110040.1874719-21-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260501110040.1874719-1-sjg@u-boot.org> References: <20260501110040.1874719-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: CQZERDY7K5KV55T2LDDE6Q2E5DUSKZRR X-Message-ID-Hash: CQZERDY7K5KV55T2LDDE6Q2E5DUSKZRR X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 20/29] patman: Add 'series find' to search by subject fragment List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add a database-only command that searches for series whose cover letter title, per-version description, or patch subjects match a given substring. Multiple matches are shown with the series name, description, and which version/patch matched. This helps locate a series when only a fragment of the subject is remembered, without needing to query patchwork. Signed-off-by: Simon Glass --- tools/patman/cmdline.py | 6 +++++ tools/patman/control.py | 2 ++ tools/patman/cseries.py | 43 +++++++++++++++++++++++++++++ tools/patman/database.py | 40 +++++++++++++++++++++++++++ tools/patman/patman.rst | 16 ++++++++++- tools/patman/review.py | 4 +-- tools/patman/test_cseries.py | 52 +++++++++++++++++++++++++++++++++++- 7 files changed, 158 insertions(+), 5 deletions(-) diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py index fa7493bbfa8..dbd0309090d 100644 --- a/tools/patman/cmdline.py +++ b/tools/patman/cmdline.py @@ -229,6 +229,7 @@ def add_series_subparser(subparsers): help='Manage series of patches') series.defaults_cmds = [ ['set-link', 'fred'], + ['find', 'dummy'], ] series.add_argument( '-n', '--dry-run', action='store_true', dest='dry_run', default=False, @@ -292,6 +293,11 @@ def add_series_subparser(subparsers): _add_show_comments(sall) _add_show_cover_comments(sall) + find = series_subparsers.add_parser( + 'find', help='Search for series by subject fragment') + find.add_argument('query', help='Text to search for') + _add_archived(find) + series_subparsers.add_parser('get-link') series_subparsers.add_parser('inc') info = series_subparsers.add_parser('info') diff --git a/tools/patman/control.py b/tools/patman/control.py index 14d74028ae9..ebe47f96481 100644 --- a/tools/patman/control.py +++ b/tools/patman/control.py @@ -201,6 +201,8 @@ def do_series(args, test_db=None, pwork=None, cser=None): dry_run=args.dry_run, show_summary=True) elif args.subcmd == 'dec': cser.decrement(args.series, args.dry_run) + elif args.subcmd == 'find': + cser.series_find(args.query, args.include_archived) elif args.subcmd == 'info': cser.show_info(args.series, show_reviews=getattr(args, 'reviews', None)) diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py index 893e1b4d212..443fe324c37 100644 --- a/tools/patman/cseries.py +++ b/tools/patman/cseries.py @@ -492,6 +492,49 @@ class Cseries(cser_helper.CseriesHelper): f'{ups:2} {vlist}') print(border) + def series_find(self, query, include_archived=False): + """Search for series by subject fragment + + Args: + query (str): Text to search for in series/version/patch + subjects + include_archived (bool): True to include archived series + """ + col = self.col + rows = self.db.series_search(query, include_archived) + if not rows: + tout.notice(f"No series match '{query}'") + return + + # Deduplicate: for each (series_id, version), keep the best match + # priority: series > version > patch + priority = {'series': 0, 'version': 1, 'patch': 2} + best = {} + for sid, name, desc, version, link, mtype, mtext in rows: + key = (sid, version) + prev = best.get(key) + if prev is None or priority[mtype] < priority[prev[5]]: + best[key] = (sid, name, desc, version, link, mtype, mtext) + + with terminal.pager(): + terminal.tprint(f"{len(best)} match(es) for '{query}':", + colour=col.WHITE, col=col) + last_sid = None + for key in sorted(best, + key=lambda k: (best[k][1], best[k][3])): + sid, name, desc, version, link, mtype, mtext = best[key] + if sid != last_sid: + terminal.tprint('') + terminal.tprint(f'{name}', colour=col.YELLOW, + col=col) + terminal.tprint(f' {desc or "(no description)"}', + col=col) + last_sid = sid + link_str = link or '(no link)' + terminal.tprint(f' v{version} [{link_str}]', + colour=col.BLUE, col=col, newline=False) + terminal.tprint(f' {mtype}: {mtext}', col=col) + def list_patches(self, series, version, show_commit=False, show_patch=False): """List patches in a series diff --git a/tools/patman/database.py b/tools/patman/database.py index 7f1771cbf11..2d6e27ef6e7 100644 --- a/tools/patman/database.py +++ b/tools/patman/database.py @@ -1452,6 +1452,46 @@ class Database: # pylint:disable=R0904 'WHERE sv.link = ?', (str(link),)) return res.fetchone() + def series_search(self, query, include_archived=False): + """Search for series by subject fragment + + Matches series against the given query string in three places: + - Series description (cover letter title) + - ser_ver description (per-version title) + - Patch subjects (pcommit.subject) + + Args: + query (str): Text to search for (case-insensitive substring) + include_archived (bool): True to include archived series + + Return: + list of tuple: (series_id, name, desc, version, link, match_type, + match_text) where match_type is 'series', 'version' or + 'patch', ordered by series name and version + """ + pat = f'%{query}%' + cond = '' if include_archived else ' AND s.archived = 0' + + sql = ( + "SELECT s.id, s.name, s.desc, sv.version, sv.link, " + "'series' AS mtype, s.desc AS mtext " + 'FROM series s JOIN ser_ver sv ON sv.series_id = s.id ' + f'WHERE s.desc LIKE ?{cond} ' + 'UNION ' + "SELECT s.id, s.name, s.desc, sv.version, sv.link, " + "'version' AS mtype, sv.desc AS mtext " + 'FROM series s JOIN ser_ver sv ON sv.series_id = s.id ' + f'WHERE sv.desc LIKE ?{cond} ' + 'UNION ' + "SELECT s.id, s.name, s.desc, sv.version, sv.link, " + "'patch' AS mtype, pc.subject AS mtext " + 'FROM series s JOIN ser_ver sv ON sv.series_id = s.id ' + 'JOIN pcommit pc ON pc.svid = sv.id ' + f'WHERE pc.subject LIKE ?{cond} ' + 'ORDER BY 2, 4') + res = self.execute(sql, (pat, pat, pat)) + return res.fetchall() + def series_find_review_by_name(self, name): """Find a review series by its description diff --git a/tools/patman/patman.rst b/tools/patman/patman.rst index 532db22745f..718078cb882 100644 --- a/tools/patman/patman.rst +++ b/tools/patman/patman.rst @@ -745,11 +745,25 @@ Here is a short overview of the available subcommands: removing that version from the data. If you use this comment on branch 'video3' Patman will delete version 3 and branch 'video3'. + find + Search the database for series matching a subject fragment. + Matches against the series description, per-version description, + and individual patch subjects. Use ``-A`` to include archived + series. Example:: + + patman series find 'fs loader' + get-link Shows the Patchwork link for a series/version + info + Show detailed information about a series, including each + version's link, description, patches and any stored reviews. + Use ``-r`` to include review text. + ls - Lists the series in the database + Lists the series in the database. Use ``-r`` to show only + review series (series fetched by ``patman review``). mark Mark a series with 'Change-Id' tags so that Patman can track patches diff --git a/tools/patman/review.py b/tools/patman/review.py index 76f3dbe274c..056333b5fe3 100644 --- a/tools/patman/review.py +++ b/tools/patman/review.py @@ -1144,10 +1144,8 @@ def search_patch(pwork, title): async def _query(): query = quote_plus(title, safe=':') - subpath = (f'patches/?project={pwork.proj_id}&q={query}' - '&order=-date&per_page=20') async with aiohttp.ClientSession() as client: - return await pwork._request(client, subpath) + return await pwork.search_patches(client, query) loop = asyncio.get_event_loop() results = loop.run_until_complete(_query()) diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py index ea1ac32badc..cb5724631ad 100644 --- a/tools/patman/test_cseries.py +++ b/tools/patman/test_cseries.py @@ -4461,7 +4461,6 @@ Date: .* svid2 = cser.db.ser_ver_add(series_id, 2, desc='Second version desc') # Add patches to v1 - from patman.database import Pcommit cser.db.pcommit_add_list(svid1, [ Pcommit(idnum=None, seq=0, subject='Fix the widget', svid=svid1, change_id=None, state=None, @@ -4491,6 +4490,57 @@ Date: .* self.assertIn('Second version desc', output) self.assertIn('Notes: Fixed review feedback', output) + def test_series_find(self): + """Test the series find command""" + cser = self.get_database() + + # Create two series: one with patches matching 'widget', one with + # matching cover description, one with neither + alpha_id = cser.db.series_add('alpha', 'Widget subsystem refresh') + alpha_svid = cser.db.ser_ver_add(alpha_id, 1) + cser.db.pcommit_add_list(alpha_svid, [ + Pcommit(idnum=None, seq=0, subject='Fix the widget', + svid=alpha_svid, change_id=None, state=None, + patch_id=None, num_comments=0)]) + + beta_id = cser.db.series_add('beta', 'Unrelated cleanup') + beta_svid = cser.db.ser_ver_add(beta_id, 1, + desc='Touch up the widget driver') + cser.db.pcommit_add_list(beta_svid, [ + Pcommit(idnum=None, seq=0, subject='cleanup', + svid=beta_svid, change_id=None, state=None, + patch_id=None, num_comments=0)]) + + gamma_id = cser.db.series_add('gamma', 'Something different') + gamma_svid = cser.db.ser_ver_add(gamma_id, 1) + cser.db.pcommit_add_list(gamma_svid, [ + Pcommit(idnum=None, seq=0, subject='other work', + svid=gamma_svid, change_id=None, state=None, + patch_id=None, num_comments=0)]) + cser.commit() + + # Match on cover-letter description and per-version description + with terminal.capture() as (out, _): + cser.series_find('widget') + output = out.getvalue() + self.assertIn('2 match(es)', output) + self.assertIn('alpha', output) + self.assertIn('beta', output) + self.assertNotIn('gamma', output) + + # Match only on patch subject + with terminal.capture() as (out, _): + cser.series_find('Fix the') + output = out.getvalue() + self.assertIn('1 match(es)', output) + self.assertIn('alpha', output) + + # No matches + with terminal.capture() as (out, _): + cser.series_find('nonexistent') + output = out.getvalue() + self.assertIn("No series match 'nonexistent'", output) + # Series link used by the review tests REVIEW_LINK = 497923 REVIEW_LINK_V2 = 497924