From patchwork Sun Mar 29 15:01:35 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 2084 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=1774796548; bh=n17GeSJJWdQCcKarjtpFen8+jHDDqOCzaw67jDHS9rA=; 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=Gmmks4jtH/fZgCxrGgfFr7hB+me7nrcmJDt62+imGBWiLW83hF+ZProrHg1VdmCqJ w5kZviXPUDHpjKa56qHn/KfEfD0G2OyXG19wb6Mus3EQI4oxLs7Voc5vqTuItNBr6H llPc4x6xWNqjD3NFIjjAzmmMEAR6mQq/I0by6PfEdQ5q40DoQvQHiDrkYd4Fdr4tFC dZ2nnxwno4Qt4qTyqvyGMynjOMdiQ71H8wNcuWo1YMzOLjkEQwkL8Mpi9KUEy41HrP ra9XQJpPZWMvVOO+yajaV4MbqZDdRjE0TmM9eVBtiL4eYm0HIovRBR9pequGbyB4Nm iVH8FU+Faus2Q== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 1411D6A2E4 for ; Sun, 29 Mar 2026 09:02:28 -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 aOoEhuawnsL1 for ; Sun, 29 Mar 2026 09:02:28 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1774796547; bh=n17GeSJJWdQCcKarjtpFen8+jHDDqOCzaw67jDHS9rA=; 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=H5gBZqDeSTuObMxx6O/Eodn0EQ2/CKsNq9z73HvgReUISRuOgInl0+bXjBl754nsI sQa1EsYk3et2bnXODLOE6dUva0noyfY9+ZQp16gZAp3Bnqn5aAN789cEeh8arIESSu HNS2r/QUknXM10Khx1b0QCwYzfuMsHp5KfAOBhfi16/RTE6IWS8hjMk/9fx33FX9s9 lU3qXOxJiSHLwZ8VVtNPtD6q7nkr7a5uJwY/WL01G4YPsN2ns5Dm3YwBE/hWkvtjTV zvF9MgzVHNT7gfumWJLH+2uem6IToUfdb6WepQBOFMFPSdadJzc2QQKvrPh0gh+fbO 2O4of/GpoQb4A== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 0D28F6A2AB for ; Sun, 29 Mar 2026 09:02:27 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1774796545; bh=jsXxjrNavvy2PBgJQgu04+nrZG7J6KfJOWHYNBnw0I0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=JR/aIm3Uqm5xA0jY8Oyc5BdE0CAQK/6ey0VC7MVNTtREiE769/cQlsN1fBkUal10/ tGyFjyW47Wib+mjuUwVumSex60/3ismqEJkD/wy0TKxsnCJ7GeLjfJ64tXohMJ5Mcg rniwHRldpcICv+CSTdlZhOXQUc1Tc4Ti0hEC3oMCZI7YSFNI3NCg+D/uuV+nt09m1N BfPB5MWbW3/605jLmqOAczHQQojxgM4oVk32CasO18aEzE2Fba99vK8+RK3YDVUzdu pBpN1wjDQvQi0F3hZkWjpSYca/ZZYihE1C6HgKXd4BoY7gpCgqA6LYyr/57vCu8l4m k40Fh7lycQh7Q== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id BC6E26A2D3; Sun, 29 Mar 2026 09:02:25 -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 JaRS5m_4Hr26; Sun, 29 Mar 2026 09:02:25 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1774796541; bh=LQe7tZ/l8zQ5BNf3unZTw/DdSeBZ6MqfgtpFjeCsTeM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=L1CSI2mtVjnFQXEfzkm/UOAICCRbE7fY3DozbfVzaYPLlJHY2+8bvLsLJNtCZ1z8k TWJg8j8DyUuexVRE7qmZDo1U0pDVdyU9ydja2IKkFX/pvfhmhkYMJx9yr+466hAspP yxwxWJAEVEOCJIavxbtpfbgsbFkPPLmrHSO87eVYx1dhDE+glOBsilqnnzKmve6oc2 uPUrG0KPd1pVLFrH4XD7YWoAN6VYCXv/wfpxE19DQae7YMWwfIbXcczcXjGH1nkU9w D0NvCz/mUHPQD+oP62EglCttqQLXf+FvCnCezdaBqpmOcMJglt5l2iRBfeQLjo0xvN ERkrTvvjNTXYA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 39A1F6A2D0; Sun, 29 Mar 2026 09:02:21 -0600 (MDT) From: Simon Glass To: U-Boot Concept Date: Sun, 29 Mar 2026 09:01:35 -0600 Message-ID: <20260329150140.4095446-11-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: FHWKAA3IVEDZJHBZTMM4J7WG5BE6O7JA X-Message-ID-Hash: FHWKAA3IVEDZJHBZTMM4J7WG5BE6O7JA 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 10/11] patman: Add 'workflow list' command to show workflow history 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 'list' subcommand (aliases: 'ls', 'wl') to the workflow command that shows all workflow entries ordered by timestamp. Use -a/--all to include archived entries, which are marked with '*' in an extra column. Timestamps are shown in a friendly format: time only for today, day and time for the past week, 'Nd ago' up to two weeks, and 'Nw ago' beyond that. Future dates use the same pattern. Signed-off-by: Simon Glass --- tools/patman/cmdline.py | 7 ++++ tools/patman/control.py | 2 + tools/patman/database.py | 23 ++++++++++++ tools/patman/test_cseries.py | 72 ++++++++++++++++++++++++++++++++++++ tools/patman/workflow.py | 59 +++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+) diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py index 3628c12459c..26626f5b441 100644 --- a/tools/patman/cmdline.py +++ b/tools/patman/cmdline.py @@ -37,6 +37,7 @@ ALIASES = { 'progress': ['p', 'pr', 'prog'], 'rm-version': ['rmv'], 'todo-list': ['tl'], + 'workflow-list': ['wl'], 'unarchive': ['unar'], } @@ -501,6 +502,12 @@ def add_workflow_subparser(subparsers): aliases=ALIASES['todo-list']) tlist.add_argument('--all', action='store_true', dest='show_all', help='Show all scheduled todos, not just due ones') + + wlist = workflow_subparsers.add_parser('list', + aliases=[*ALIASES['workflow-list'], + 'ls']) + wlist.add_argument('-a', '--all', action='store_true', dest='show_all', + help='Include archived entries') return workflow diff --git a/tools/patman/control.py b/tools/patman/control.py index fe012f1a21b..0c3a3097967 100644 --- a/tools/patman/control.py +++ b/tools/patman/control.py @@ -365,6 +365,8 @@ def do_workflow(args, test_db=None): workflow.todo(cser, args.series, args.days) elif args.subcmd == 'todo-list': workflow.todo_list(cser, args.show_all) + elif args.subcmd in ['list', 'wl', 'ls']: + workflow.list_entries(cser, args.show_all) else: raise ValueError(f"Unknown workflow subcommand '{args.subcmd}'") finally: diff --git a/tools/patman/database.py b/tools/patman/database.py index edb7d116c33..3133a320694 100644 --- a/tools/patman/database.py +++ b/tools/patman/database.py @@ -1130,3 +1130,26 @@ class Database: # pylint:disable=R0904 query += ' ORDER BY w.timestamp' res = self.execute(query, params) return res.fetchall() + + def workflow_list(self, include_archived=False): + """Get workflow entries joined with series info + + Args: + include_archived (bool): True to include archived entries + + Return: + list of tuple: + str: workflow type + str: series name + str: series description + str: timestamp + int: archived flag (0 or 1) + """ + query = ('SELECT w.type, s.name, s.desc, w.timestamp, w.archived ' + 'FROM workflow w ' + 'JOIN series s ON w.series_id = s.id') + if not include_archived: + query += ' WHERE w.archived = 0' + query += ' ORDER BY w.timestamp' + res = self.execute(query) + return res.fetchall() diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py index 01f4f4a133d..4eea5922c84 100644 --- a/tools/patman/test_cseries.py +++ b/tools/patman/test_cseries.py @@ -4323,3 +4323,75 @@ Date: .* ts = cser.db.workflow_get('todo', ser.idnum) self.assertEqual('2025-03-12 12:00:00', ts) + + def test_workflow_list(self): + """Test listing all workflow entries""" + 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) + + # Record a send (creates SENT + TODO) + ser = cser.get_series_by_name('first') + wf.sent(cser, ser.idnum) + + # Default list shows only active entries + with terminal.capture() as (out, _): + wf.list_entries(cser, show_all=False) + lines = out.getvalue().splitlines() + self.assertEqual(4, len(lines)) + self.assertIn('sent', lines[2]) + self.assertIn('todo', lines[3]) + + # Archive the todo + wf.todo_clear(cser, 'first') + + # Without --all, only SENT is active; no 'A' column + with terminal.capture() as (out, _): + wf.list_entries(cser, show_all=False) + lines = out.getvalue().splitlines() + self.assertEqual(3, len(lines)) + self.assertIn('sent', lines[2]) + self.assertNotIn('A', lines[0]) + + # With --all, archived entries appear with '*' marker + with terminal.capture() as (out, _): + wf.list_entries(cser, show_all=True) + lines = out.getvalue().splitlines() + self.assertGreater(len(lines), 3) + self.assertIn(' A ', lines[0]) + has_archived = any('*' in line for line in lines[2:]) + self.assertTrue(has_archived) + + def test_friendly_time(self): + """Test friendly timestamp formatting""" + now = datetime(2025, 3, 10, 15, 0, 0) # Monday + + # Same day + when = datetime(2025, 3, 10, 9, 30, 0) + self.assertEqual('09:30', wf.friendly_time(now, when)) + + # Earlier this week (3 days ago = Friday) + when = datetime(2025, 3, 7, 14, 20, 0) + self.assertEqual('Fri 14:20', wf.friendly_time(now, when)) + + # 10 days ago + when = datetime(2025, 2, 28, 10, 0, 0) + self.assertEqual('10d ago', wf.friendly_time(now, when)) + + # 3 weeks ago + when = datetime(2025, 2, 17, 10, 0, 0) + self.assertEqual('3w ago', wf.friendly_time(now, when)) + + # Future within a week (3 days from now = Thursday) + when = datetime(2025, 3, 13, 16, 0, 0) + self.assertEqual('Thu 16:00', wf.friendly_time(now, when)) + + # Future 10 days + when = datetime(2025, 3, 20, 10, 0, 0) + self.assertEqual('in 10d', wf.friendly_time(now, when)) + + # Future 3 weeks + when = datetime(2025, 3, 31, 10, 0, 0) + self.assertEqual('in 3w', wf.friendly_time(now, when)) diff --git a/tools/patman/workflow.py b/tools/patman/workflow.py index 42e38db56f6..d123e3068a2 100644 --- a/tools/patman/workflow.py +++ b/tools/patman/workflow.py @@ -15,6 +15,34 @@ class Wtype(enum.StrEnum): TODO = 'todo' +def friendly_time(now, when): + """Format a timestamp in a human-friendly way + + Args: + now (datetime): Current time + when (datetime): Timestamp to format + + Return: + str: Friendly string, e.g. 'Tue 15:34', '3d ago', '2w ago' + """ + delta = now - when + days = delta.days + if days < 0: + days = -days + if days >= 14: + return f'in {days // 7}w' + if days >= 7: + return f'in {days}d' + return when.strftime('%a %H:%M') + if days == 0: + return when.strftime('%H:%M') + if days < 7: + return when.strftime('%a %H:%M') + if days < 14: + return f'{days}d ago' + return f'{days // 7}w ago' + + def sent(cser, series_id): """Record that a series was sent and create a follow-up todo @@ -92,3 +120,34 @@ def todo_list(cser, show_all): else: due = f'in {days}d' print(f"{name:17} {due:>14} {desc}") + + +def list_entries(cser, show_all): + """List all workflow entries + + Args: + cser (CseriesHelper): Series helper with open database + show_all (bool): True to include archived entries + """ + entries = cser.db.workflow_list(include_archived=show_all) + if not entries: + print('No workflow entries') + return + hdr = f"{'Type':6} {'Series':17} {'When':>10}" + div = f"{'-' * 6} {'-' * 17} {'-' * 10}" + if show_all: + hdr += ' A' + div += ' -' + hdr += ' Description' + div += ' ' + '-' * 30 + print(hdr) + print(div) + now = cser.get_now() + for wtype, name, desc, ts, archived in entries: + when = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S') + friendly = friendly_time(now, when) + line = f"{wtype:6} {name:17} {friendly:>10}" + if show_all: + line += f" {'*' if archived else ' '}" + line += f" {desc}" + print(line)