From patchwork Sun Mar 29 15:01:32 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 2081 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=1774796536; bh=YQdic/hRB1EqzEQO7jDcOhHy3J4wJqcx6huuYJ3a6og=; 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=A3AjbWWfFN51SrBsY+yRlA0yRE/Vmhe1AX+3KKliFX9IkWcdKpSHp3EYPjvWFuUMX 5dIgOJd9KvF34kF/nE4+trTM8+rPQEMOOyI0LjDlA3+v4+Ujond4BoRkMSg18IBXg6 N+q776c4LpPvZsPvPOFTjeYaozgth+c4Mb6pj75rExECZxofPhXs1fDHAsnkzOPJNd EzW4PaGitvtNS5fSh2ORlZ7KlsvVs1d8tvYMsRSZUDLcnoWgunvYXoo/Qo7NxDKhSS 2liKwfHuARyNxXL9t16Xp02qtL8zYS0gRDX7dcOyfFEITIL5M5aKch3iWQ0Bx82geO 0bT9h82xh4byQ== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 1A03B6A02E for ; Sun, 29 Mar 2026 09:02:16 -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 kDEU39b0P1bY for ; Sun, 29 Mar 2026 09:02:16 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1774796536; bh=YQdic/hRB1EqzEQO7jDcOhHy3J4wJqcx6huuYJ3a6og=; 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=A3AjbWWfFN51SrBsY+yRlA0yRE/Vmhe1AX+3KKliFX9IkWcdKpSHp3EYPjvWFuUMX 5dIgOJd9KvF34kF/nE4+trTM8+rPQEMOOyI0LjDlA3+v4+Ujond4BoRkMSg18IBXg6 N+q776c4LpPvZsPvPOFTjeYaozgth+c4Mb6pj75rExECZxofPhXs1fDHAsnkzOPJNd EzW4PaGitvtNS5fSh2ORlZ7KlsvVs1d8tvYMsRSZUDLcnoWgunvYXoo/Qo7NxDKhSS 2liKwfHuARyNxXL9t16Xp02qtL8zYS0gRDX7dcOyfFEITIL5M5aKch3iWQ0Bx82geO 0bT9h82xh4byQ== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 057026A2AB for ; Sun, 29 Mar 2026 09:02:16 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1774796534; bh=apTv5mD/Ad4Nel6jSxR7AMs/zATLCzdWjxM2iV/Zdt0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=SUi9mt75WNiTLI3cWvXJA6ZhmXSFgqUokYBXAl8uOmilP0Gz6aQxwpipZGTGcx4UO eBDplqmtqReNbRHn4LOI7BS4UJg7ligYHIhegcWymxSOoC6du0A5/hf0jMNXbwUy5j HFj5kCuANT0NC0hZ/6SL0y2LRZ/dRQ7xsd3VcduSrreyRsNtREAglWBk/botEBnRoQ L/zXE56mPw0peYWrj86tVaX9RCFUq3AlTOFSRbTewFIWgdMSVYOq/WCt2A5kcSjqqc HY3sgA+UqdrHn57xuvBnJMwmaHIZp/LiqNS/luniv1WWr9lINB95yP72zFiJck9IsP E1YQQ8HEF3Gtg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 853786A2AB; Sun, 29 Mar 2026 09:02:14 -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 mzPdx3un_h1B; Sun, 29 Mar 2026 09:02:14 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1774796528; bh=PgYH/AR3CcX2VbbHpS0DvsMS/ufEJBIc5nVRcb+eWJw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=JsWxoSgM5D2uE9Nr/vDJIwMkcAirBKZrO1/pBeUSpDPBZ2xX8D5D2KFcvj4xYBsHl covrwkulQt9fUHqwOQW7BloSHhncvEeQTvwpXto2yS1GdjGoWu1WyMNtvDXDPvXfPQ 6kW4BNlIHG9beINZ/dQzrsKouI5AHNwG+uoZAig0Gcl9+3mj6J0l2bAboho+hLSDro ktlA2oIfXSL/rO+56JmM9LHnDsHIXoy2vGfi6gFM87QlWsAoLixFtCilFHSg24OoJ1 VeYP/d/lXPFp1CiyXzuSSIS74xqE9iDc9xsN+qzXY/Tr0hXJaji3vsjpVpSW8CQfvz kv4jV/T46ofhA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id CB8056A02E; Sun, 29 Mar 2026 09:02:07 -0600 (MDT) From: Simon Glass To: U-Boot Concept Date: Sun, 29 Mar 2026 09:01:32 -0600 Message-ID: <20260329150140.4095446-8-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260329150140.4095446-1-sjg@u-boot.org> References: <20260329150140.4095446-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: 6QUR7CFFFH6RQ4EVGYHKHYNH4TDLIKAR X-Message-ID-Hash: 6QUR7CFFFH6RQ4EVGYHKHYNH4TDLIKAR 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 07/11] patman: Add series-todo workflow feature 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 functions to mark a series for attention after a given number of days, clear the marker, and list which series are due. This is useful for tracking when to follow up on sent patch series. The todo list shows relative due dates (e.g. '3d overdue', 'in 7d', 'today') and a [todo] marker appears in series summary output when a series is due. Signed-off-by: Simon Glass --- tools/patman/cser_helper.py | 7 ++- tools/patman/cseries.py | 1 - tools/patman/test_cseries.py | 97 ++++++++++++++++++++++++++++++++++++ tools/patman/workflow.py | 63 +++++++++++++++++++++++ 4 files changed, 166 insertions(+), 2 deletions(-) diff --git a/tools/patman/cser_helper.py b/tools/patman/cser_helper.py index 0b9f670ca25..7979deda6dc 100644 --- a/tools/patman/cser_helper.py +++ b/tools/patman/cser_helper.py @@ -28,6 +28,7 @@ from patman.database import Database, Pcommit, SerVer from patman import patchwork from patman.series import Series from patman import status +from patman.workflow import Wtype # Tag to use for Change IDs @@ -1552,7 +1553,11 @@ class CseriesHelper: if val in states: state = val state_str, pad = self._build_col(state, base_str=name) - print(f"{state_str}{pad} {stats.rjust(6)} {desc}") + marker = '' + ts = self.db.workflow_get(Wtype.TODO, ser.idnum) + if ts and ts <= self.get_now().strftime('%Y-%m-%d %H:%M:%S'): + marker = ' [todo]' + print(f"{state_str}{pad} {stats.rjust(6)} {desc}{marker}") def _series_max_version(self, idnum): """Find the latest version of a series diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py index 716d3c7aa88..8f159a8308f 100644 --- a/tools/patman/cseries.py +++ b/tools/patman/cseries.py @@ -7,7 +7,6 @@ import asyncio from collections import OrderedDict, defaultdict - import pygit2 from u_boot_pylib import cros_subprocess diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py index 798673e09cb..b4dce4cb853 100644 --- a/tools/patman/test_cseries.py +++ b/tools/patman/test_cseries.py @@ -4176,3 +4176,100 @@ Date: .* 'SELECT archived FROM workflow WHERE series_id = ?', (ser.idnum,)) self.assertEqual(1, res.fetchone()[0]) + + def test_workflow_todo(self): + """Test setting and clearing a todo""" + cser = self.get_cser() + with terminal.capture(): + cser.add('first', 'my description', allow_unmarked=True) + + cser.fake_now = datetime(2025, 3, 1, 12, 0, 0) + ser = cser.get_series_by_name('first') + + # Set a todo for 7 days + with terminal.capture() as (out, _): + wf.todo(cser,'first', 7) + self.assertIn('2025-03-08 12:00:00', out.getvalue()) + + # Check the DB entry + ts = cser.db.workflow_get('todo', ser.idnum) + self.assertEqual('2025-03-08 12:00:00', ts) + + # Replacing the todo should work + with terminal.capture() as (out, _): + wf.todo(cser,'first', 14) + self.assertIn('2025-03-15 12:00:00', out.getvalue()) + ts = cser.db.workflow_get('todo', ser.idnum) + self.assertEqual('2025-03-15 12:00:00', ts) + + # Clear it + with terminal.capture() as (out, _): + wf.todo_clear(cser,'first') + self.assertIn('Todo cleared', out.getvalue()) + self.assertIsNone(cser.db.workflow_get('todo', ser.idnum)) + + def test_workflow_todo_list(self): + """Test listing todos""" + cser = self.get_cser() + with terminal.capture(): + cser.add('first', 'my description', allow_unmarked=True) + cser.add('second', 'board stuff', allow_unmarked=True) + + cser.fake_now = datetime(2025, 3, 10, 12, 0, 0) + + # Set todos: first is due, second is in the future + with terminal.capture(): + wf.todo(cser,'first', 0) + wf.todo(cser,'second', 7) + + # Default list shows only due entries + with terminal.capture() as (out, _): + wf.todo_list(cser,show_all=False) + lines = out.getvalue().splitlines() + self.assertEqual(3, len(lines)) + self.assertIn('first', lines[2]) + self.assertIn('today', lines[2]) + + # --all shows all entries + with terminal.capture() as (out, _): + wf.todo_list(cser,show_all=True) + lines = out.getvalue().splitlines() + self.assertEqual(4, len(lines)) + self.assertIn('first', lines[2]) + self.assertIn('today', lines[2]) + self.assertIn('second', lines[3]) + self.assertIn('in 7d', lines[3]) + + # No todos + with terminal.capture(): + wf.todo_clear(cser,'first') + wf.todo_clear(cser,'second') + with terminal.capture() as (out, _): + wf.todo_list(cser,show_all=False) + self.assertIn('No todos due', out.getvalue()) + + def test_workflow_summary_marker(self): + """Test that [todo] shows in series summary""" + cser = self.get_cser() + with terminal.capture(): + cser.add('first', 'my description', allow_unmarked=True) + + cser.fake_now = datetime(2025, 3, 10, 12, 0, 0) + + # Set a todo that is already due + with terminal.capture(): + wf.todo(cser,'first', 0) + + # Summary should show [todo] + with terminal.capture() as (out, _): + cser.summary(None) + self.assertIn('[todo]', out.getvalue()) + + # Set a todo in the future + with terminal.capture(): + wf.todo(cser,'first', 14) + + # Summary should NOT show [todo] + with terminal.capture() as (out, _): + cser.summary(None) + self.assertNotIn('[todo]', out.getvalue()) diff --git a/tools/patman/workflow.py b/tools/patman/workflow.py index 37644a5de88..029d63cf6a4 100644 --- a/tools/patman/workflow.py +++ b/tools/patman/workflow.py @@ -5,9 +5,72 @@ """Workflow types and operations for patman series management""" +from datetime import datetime, timedelta import enum class Wtype(enum.StrEnum): """Types of workflow entry""" TODO = 'todo' + + +def todo(cser, series, days): + """Mark a series as a todo item after a number of days + + Args: + cser (CseriesHelper): Series helper with open database + series (str): Name of series to use, or None for current branch + days (int): Number of days from now to mark as due + """ + ser = cser._parse_series(series) + cser.db.workflow_archive(Wtype.TODO, ser.idnum) + when = cser.get_now() + timedelta(days=days) + ts = when.strftime('%Y-%m-%d %H:%M:%S') + cser.db.workflow_add(Wtype.TODO, ser.idnum, ts) + cser.commit() + print(f"Series '{ser.name}' marked for todo on {ts}") + + +def todo_clear(cser, series): + """Clear the todo marker for a series + + Args: + cser (CseriesHelper): Series helper with open database + series (str): Name of series to use, or None for current branch + """ + ser = cser._parse_series(series) + cser.db.workflow_archive(Wtype.TODO, ser.idnum) + cser.commit() + print(f"Todo cleared for series '{ser.name}'") + + +def todo_list(cser, show_all): + """List series that are due (or scheduled) for attention + + Args: + cser (CseriesHelper): Series helper with open database + show_all (bool): True to show all scheduled todos, not just + those that are due + """ + now = cser.get_now().strftime('%Y-%m-%d %H:%M:%S') + before = None if show_all else now + entries = cser.db.workflow_get_by_type(Wtype.TODO, before=before) + if not entries: + if show_all: + print('No todos scheduled') + else: + print('No todos due') + return + print(f"{'Series':17} {'Due':>14} Description") + print(f"{'-' * 17} {'-' * 14} {'-' * 30}") + for _sid, name, desc, ts in entries: + when = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S') + delta = when - cser.get_now() + days = delta.days + if days < 0: + due = f'{-days}d overdue' + elif days == 0: + due = 'today' + else: + due = f'in {days}d' + print(f"{name:17} {due:>14} {desc}")