From patchwork Sat Apr 4 21:29:02 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 2141 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=1775338374; bh=cH4OmYT6974EI5+74qEIkJXDhvXlKWxnMzZqUmesUwE=; 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=R1QrTSSKXWF2U6mVChviLCDPffznfgu7d5s2+aX5s7LKUdeErf9U+UXsKLoddAJxA gmQ7zY9vH8lLznQK5r5hnb3PdUDZFV9muDuGeT4RfUa4juzaBnA2CKATCVCK2DIyoR vaKPCZ7aIr5m7KKzHCAgOQo1eGlwy+3NMUqHgSKk+E0nQ4BAXmvxtKQ+nCt4tOzqYR ZMooqBPhadjPIS8bJIf8GeyRRmrbAF1Wi/VCTFRDKDXMcoiLqWKZfU+Y0jWD0HkOBn OOCwu6W2xIMQnzpZHGeLSaDTp57jGvobayNkG620uqO3mM5QusSrLfQN3xWlW9U8mf mbgnFIVaG1JNQ== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id CD5A05FC8F for ; Sat, 4 Apr 2026 15:32:54 -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 1UI8hA3cgEPF for ; Sat, 4 Apr 2026 15:32:54 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775338374; bh=cH4OmYT6974EI5+74qEIkJXDhvXlKWxnMzZqUmesUwE=; 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=R1QrTSSKXWF2U6mVChviLCDPffznfgu7d5s2+aX5s7LKUdeErf9U+UXsKLoddAJxA gmQ7zY9vH8lLznQK5r5hnb3PdUDZFV9muDuGeT4RfUa4juzaBnA2CKATCVCK2DIyoR vaKPCZ7aIr5m7KKzHCAgOQo1eGlwy+3NMUqHgSKk+E0nQ4BAXmvxtKQ+nCt4tOzqYR ZMooqBPhadjPIS8bJIf8GeyRRmrbAF1Wi/VCTFRDKDXMcoiLqWKZfU+Y0jWD0HkOBn OOCwu6W2xIMQnzpZHGeLSaDTp57jGvobayNkG620uqO3mM5QusSrLfQN3xWlW9U8mf mbgnFIVaG1JNQ== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id BA7E26833B for ; Sat, 4 Apr 2026 15:32:54 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775338372; bh=PjvGtPjWINKMKuUEy65wksxGiCSRwF+F1ZE5oObO+Xk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=KOlkThyJpLu2pp5q6CEndln+pcGskKN/W6xktnUjrZ6CmUdQCk5z19ahyb+o9X1FE 6dl0nKuqIaBtbmrhbsA55DwSjETE0+UPIBDafpe4iaVhtPNEcHk987L/yZzR9gq6SJ ++pdaVLCqZEjXXeGBMb7BTFSVqkYGmqSo+AxRWaqjaDxElYKQZ04a3uyCEo54p77I1 snFYz9ok/7ww48BrnPBgWuaqYI1YTwQllMpr0UO8AFwuZOC3rjeU9nJW7aTGBepS7U 9USFEHv8n+OWaTm1tYtOlIDLzsoX25SBaeTxnoY03KwGSLzGeTSjIeZ9X8rheG3eH5 oFgO5J2Tc061w== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 870E95E7B4; Sat, 4 Apr 2026 15:32:52 -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 ll5qqaVb_99K; Sat, 4 Apr 2026 15:32:52 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775338368; bh=zZZP7HiyXUiWfA4hkqoAlh+M4jL0VwkwTLiC//D1fyI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=dBa0cPAf6gCQ+PiI3W8yFXgrYjJbf/7mACeMydKjKjshH6+q5n10wFgNM/y6EZkc7 C+IEsttRlOSCVXsG6Ds8zWD14rglSBOxuTDabSzwBGhKd2lwvt+jxP4YWAk7WkrlG0 GqrgMQnONLTocMn1Vf2sq1NvESl3R5yRkRSwnM0LlqwFw/2d+QZfwGBwjIBc/G7j3r ZBEDTIlbQ/jNEYKBAE05X50qXykw9MLzlNaQ2Pdz7rFwe+IILvYuifYtBB6RnWNZid oXpRb0SapoCuU3JeLVpyu3Be7mHf6FL8RU9fY6OC/cKc22IA6UXnCqF3O81lTtDW7y GXGQSn86lS33Q== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 1BD5E6833B; Sat, 4 Apr 2026 15:32:48 -0600 (MDT) From: Simon Glass To: U-Boot Concept Date: Sat, 4 Apr 2026 15:29:02 -0600 Message-ID: <20260404213020.372253-27-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260404213020.372253-1-sjg@u-boot.org> References: <20260404213020.372253-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: EHOSG2OJLKZEJMJE33EZPVMP2HHFX7TV X-Message-ID-Hash: EHOSG2OJLKZEJMJE33EZPVMP2HHFX7TV 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 26/37] patman: Extend database for review draft tracking and notes 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 schema migrations v9 and v10 and supporting methods: - v9: Add draft_id, status, gmail_msg_id and gmail_thread_id columns to the review table for tracking Gmail draft lifecycle - v10: Add notes column to ser_ver for storing review-handling notes New methods: - series_set_source() to mark a series as coming from review - review_set_draft_id/sent/replied/deleted() for draft lifecycle - review_delete_for_version() for force re-review - ser_ver_set_notes() and ser_ver_get_all_notes() for the handle-reviews skill Signed-off-by: Simon Glass --- tools/patman/database.py | 155 +++++++++++++++++++++++++++++++++-- tools/patman/test_cseries.py | 2 +- 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/tools/patman/database.py b/tools/patman/database.py index 7f33137d0b7..8a96030b68f 100644 --- a/tools/patman/database.py +++ b/tools/patman/database.py @@ -20,12 +20,20 @@ from u_boot_pylib import tout from patman.series import Series # Schema version (version 0 means there is no database yet) -LATEST = 8 +LATEST = 10 # Information about a review record +# Review status values +REVIEW_NEW = 'new' # AI review generated, no draft created +REVIEW_DRAFT = 'draft' # Gmail draft created +REVIEW_SENT = 'sent' # Draft was sent +REVIEW_DELETED = 'deleted' # Draft was deleted without sending +REVIEW_REPLIED = 'replied' # Author has replied to our review + Review = namedtuple( 'REVIEW', - 'idnum,svid,seq,body,approved,timestamp') + 'idnum,svid,seq,body,approved,timestamp,draft_id,status,' + 'gmail_msg_id,gmail_thread_id') # Information about a series/version record SerVer = namedtuple( @@ -243,6 +251,19 @@ class Database: # pylint:disable=R0904 'timestamp TEXT, ' 'FOREIGN KEY (svid) REFERENCES ser_ver (id))') + def _migrate_to_v9(self): + """Add draft tracking, status, and Gmail IDs to review table""" + self.cur.execute('ALTER TABLE review ADD COLUMN draft_id') + self.cur.execute('ALTER TABLE review ADD COLUMN status') + self.cur.execute('ALTER TABLE review ADD COLUMN gmail_msg_id') + self.cur.execute('ALTER TABLE review ADD COLUMN gmail_thread_id') + self.cur.execute("UPDATE review SET status = 'new'") + + def _migrate_to_v10(self): + """Add review notes column to ser_ver table""" + self.cur.execute('ALTER TABLE ser_ver ADD COLUMN notes') + + # pylint: disable=R0912 def migrate_to(self, dest_version): """Migrate the database to the selected version @@ -277,6 +298,10 @@ class Database: # pylint:disable=R0904 self._migrate_to_v7() elif version == 8: self._migrate_to_v8() + elif version == 9: + self._migrate_to_v9() + elif version == 10: + self._migrate_to_v10() # Save the new version if we have a schema_version table if version > 1: @@ -566,6 +591,17 @@ class Database: # pylint:disable=R0904 'UPDATE series SET upstream = ? WHERE id = ?', (ups, series_idnum)) + def series_set_source(self, series_idnum, source): + """Update the source field for a series + + Args: + series_idnum (int): ID num of the series + source (str): Source value, e.g. 'review' + """ + self.execute( + 'UPDATE series SET source = ? WHERE id = ?', + (source, series_idnum)) + def series_get_null_upstream(self): """Get a list of series names that have no upstream set @@ -688,6 +724,32 @@ class Database: # pylint:disable=R0904 """ self.execute('UPDATE ser_ver SET desc = ? WHERE id = ?', (desc, svid)) + def ser_ver_set_notes(self, svid, notes): + """Store review-handling notes for a series version + + Args: + svid (int): ser_ver ID num + notes (str): Notes text from review-notes.txt + """ + self.execute( + 'UPDATE ser_ver SET notes = ? WHERE id = ?', (notes, svid)) + + def ser_ver_get_all_notes(self, series_id): + """Get review notes from all versions of a series + + Args: + series_id (int): Series ID + + Return: + list of tuple: (version, notes) for versions that have notes, + ordered by version + """ + res = self.execute( + 'SELECT version, notes FROM ser_ver ' + 'WHERE series_id = ? AND notes IS NOT NULL ' + 'ORDER BY version', (series_id,)) + return [(v, n) for v, n in res.fetchall() if n] + def ser_ver_add(self, series_idnum, version, link=None, desc=None): """Add a new ser_ver record @@ -864,7 +926,7 @@ class Database: # pylint:disable=R0904 # upstream functions - # pylint: disable=R0913 + # pylint: disable=R0913,R0917 def upstream_add(self, name, url, patchwork_url=None, identity=None, series_to=None, no_maintainers=False, no_tags=False): """Add a new upstream record @@ -1224,7 +1286,7 @@ class Database: # pylint:disable=R0904 res = self.execute(query) return res.fetchall() - # pylint: disable=R0913 + # pylint: disable=R0913,R0917 def review_add(self, svid, seq, body, approved, timestamp): """Add a review record @@ -1239,11 +1301,68 @@ class Database: # pylint:disable=R0904 int: ID num of the new review record """ self.execute( - 'INSERT INTO review (svid, seq, body, approved, timestamp) ' - 'VALUES (?, ?, ?, ?, ?)', - (svid, seq, body, 1 if approved else 0, timestamp)) + 'INSERT INTO review (svid, seq, body, approved, timestamp, ' + 'status) VALUES (?, ?, ?, ?, ?, ?)', + (svid, seq, body, 1 if approved else 0, timestamp, + REVIEW_NEW)) return self.lastrowid() + def review_set_draft_id(self, review_id, draft_id): + """Set the Gmail draft ID for a review record + + Args: + review_id (int): Review record ID + draft_id (str or None): Gmail draft ID + """ + status = REVIEW_DRAFT if draft_id else None + self.execute( + 'UPDATE review SET draft_id = ?, status = ? WHERE id = ?', + (draft_id, status, review_id)) + + def review_set_sent(self, review_id, body, gmail_msg_id=None, + gmail_thread_id=None): + """Mark a review as sent and update its body + + Args: + review_id (int): Review record ID + body (str): Sent email body text + gmail_msg_id (str or None): Gmail message ID of sent email + gmail_thread_id (str or None): Gmail thread ID + """ + self.execute( + 'UPDATE review SET body = ?, draft_id = NULL, status = ?, ' + 'gmail_msg_id = ?, gmail_thread_id = ? WHERE id = ?', + (body, REVIEW_SENT, gmail_msg_id, gmail_thread_id, + review_id)) + + def review_set_replied(self, review_id): + """Mark a review as having received a reply + + Args: + review_id (int): Review record ID + """ + self.execute( + 'UPDATE review SET status = ? WHERE id = ?', + (REVIEW_REPLIED, review_id)) + + def review_set_deleted(self, review_id): + """Mark a review draft as deleted (not sent) + + Args: + review_id (int): Review record ID + """ + self.execute( + 'UPDATE review SET draft_id = NULL, status = ? ' + 'WHERE id = ?', (REVIEW_DELETED, review_id)) + + def review_delete_for_version(self, svid): + """Delete all review records for a given series version + + Args: + svid (int): ser_ver ID num + """ + self.execute('DELETE FROM review WHERE svid = ?', (svid,)) + def review_get_for_version(self, svid): """Get review records for a given series version @@ -1254,10 +1373,30 @@ class Database: # pylint:disable=R0904 list of Review: Review records ordered by sequence """ res = self.execute( - 'SELECT id, svid, seq, body, approved, timestamp ' + 'SELECT id, svid, seq, body, approved, timestamp, draft_id, ' + 'status, gmail_msg_id, gmail_thread_id ' 'FROM review WHERE svid = ? ORDER BY seq', (svid,)) return [Review(*row) for row in res.fetchall()] + def review_get_by_status(self, status, need_thread=False): + """Get review records with a given status + + Args: + status (str): Status to filter by (e.g. 'draft', 'sent') + need_thread (bool): If True, only return records with a + gmail_thread_id + + Return: + list of Review: Matching review records + """ + sql = ('SELECT id, svid, seq, body, approved, timestamp, ' + 'draft_id, status, gmail_msg_id, gmail_thread_id ' + 'FROM review WHERE status = ?') + if need_thread: + sql += ' AND gmail_thread_id IS NOT NULL' + res = self.execute(sql, (status,)) + return [Review(*row) for row in res.fetchall()] + def review_get_previous(self, series_id, version): """Get reviews from the previous version of a series diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py index 3b78399d907..e207e8bc173 100644 --- a/tools/patman/test_cseries.py +++ b/tools/patman/test_cseries.py @@ -3590,7 +3590,7 @@ Date: .* self.assertEqual(f'Update database to v{version}', out.getvalue().strip()) self.assertEqual(version, db.get_schema_version()) - self.assertEqual(8, database.LATEST) + self.assertEqual(10, database.LATEST) def test_migrate_future_version(self): """Test that a database newer than patman is rejected"""