From 6f869d56454db777d9bb529acdd6b8d67ef12013 Mon Sep 17 00:00:00 2001 From: brinkflew Date: Sun, 26 Jul 2026 22:12:51 +0200 Subject: [PATCH] [FIX] git: keep the last line of output when pulling worktrees `FetchCommand.run` clears the blank line that `Command.table` appends after the last worktree summary. `PullCommand` overrides `run_hook` with plain log lines and never emitted that trailing blank, so the cleanup erased the summary of the last repository instead. Give the pull hook the same output shape as the fetch one: a section title introducing each worktree and a blank line closing it. The worktree name moves from every log message to that title, and the contract is now documented on `FetchCommand.run_hook` so future overrides keep it. Claude-Session: https://claude.ai/code/session_012tWyDuUYenE92nsBtG19Du --- odev/_version.py | 2 +- odev/commands/git/fetch.py | 7 +++++- odev/commands/git/pull.py | 17 +++++++-------- odev/common/console.py | 22 +++++++++++++------ tests/tests/commands/test_git_and_scripts.py | 23 ++++++++++++++++++++ 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/odev/_version.py b/odev/_version.py index 7306664df..12aa2f183 100644 --- a/odev/_version.py +++ b/odev/_version.py @@ -22,4 +22,4 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "4.29.9" +__version__ = "4.29.10" diff --git a/odev/commands/git/fetch.py b/odev/commands/git/fetch.py index 02598e5a8..7fc7e9b56 100644 --- a/odev/commands/git/fetch.py +++ b/odev/commands/git/fetch.py @@ -37,10 +37,15 @@ def run(self): for worktree in sorted_worktree: self.run_hook(worktree, changes_by_worktree[worktree]) + # Drop the blank line trailing the output of the last worktree. self.console.clear_line() def run_hook(self, worktree: str, changes: list[tuple[str, int, int]]): - """Print a summary of the pending changes for a worktree.""" + """Print a summary of the pending changes for a worktree. + + Overrides must terminate their output with a blank line, separating consecutive worktrees and letting + :meth:`run` clear the last one. + """ self.table( [ TableHeader("Repository", min_width=max(len(repository.name) for repository in self.repositories)), diff --git a/odev/commands/git/pull.py b/odev/commands/git/pull.py index 40d310bae..c95e48d6b 100644 --- a/odev/commands/git/pull.py +++ b/odev/commands/git/pull.py @@ -17,7 +17,9 @@ class PullCommand(FetchCommand): _help = "Pull changes in local worktrees managed by odev." def run_hook(self, name: str, changes: list[tuple[str, int, int]]): - """Print a summary of the pending changes for a worktree.""" + """Pull the pending changes for a worktree and print a summary of the operation.""" + self.console.title_rule(name) + for change in changes: repository, behind, _ = change worktree = next( @@ -33,19 +35,16 @@ def run_hook(self, name: str, changes: list[tuple[str, int, int]]): raise self.error(f"Worktree {name!r} does not exist") if worktree.detached: - logger.info(f"Worktree {name!r} is detached") + logger.info(f"Detached worktree in {repository!r}") continue if not behind: - logger.info( - f"No pending changes for worktree {name!r} in {repository!r} for version {worktree.branch!r}" - ) + logger.info(f"No pending changes in {repository!r} for version {worktree.branch!r}") continue - with progress.spinner( - f"Pulling {behind} commits in {worktree.connector.name!r} for version {worktree.branch!r}" - ): + with progress.spinner(f"Pulling {behind} commits in {repository!r} for version {worktree.branch!r}"): worktree.connector.pull_worktrees([worktree], force=True) - logger.info(f"Pulled {behind} commits in {worktree.connector.name!r} for version {worktree.branch!r}") + logger.info(f"Pulled {behind} commits in {repository!r} for version {worktree.branch!r}") + self.print() self.odev.config.repositories.set_date(name, datetime.today()) diff --git a/odev/common/console.py b/odev/common/console.py index c8987bc52..c848c60f9 100644 --- a/odev/common/console.py +++ b/odev/common/console.py @@ -392,6 +392,20 @@ def print( else: super().print(renderable, *args, **kwargs) + def title_rule(self, title: str) -> None: + """Print a left-aligned rule introducing a section of output. + + :param title: Text to display inside the rule. + """ + rule_char: str = "─" + + self.rule( + f"{rule_char} {string.stylize(title, 'bold color.cyan')}", + align="left", + style="", + characters=rule_char, + ) + def table( self, headers: Sequence[TableHeader], @@ -408,13 +422,7 @@ def table( :param kwargs: Additional keyword arguments to pass to the Rich Table. """ if title is not None: - rule_char: str = "─" - self.rule( - f"{rule_char} {string.stylize(title, 'bold color.cyan')}", - align="left", - style="", - characters=rule_char, - ) + self.title_rule(title) return self.table(headers, rows, totals, show_header=any(header.title for header in headers), box=None) kwargs.setdefault("show_header", True) diff --git a/tests/tests/commands/test_git_and_scripts.py b/tests/tests/commands/test_git_and_scripts.py index d3d25630e..5b8feda39 100644 --- a/tests/tests/commands/test_git_and_scripts.py +++ b/tests/tests/commands/test_git_and_scripts.py @@ -1,6 +1,7 @@ from argparse import Namespace from unittest.mock import MagicMock +from odev.commands.git.pull import PullCommand from odev.commands.scripts.assets import PathfinderCommand as AssetsCommand from odev.commands.scripts.pathfinder import PathfinderCommand from odev.common.connectors.git import GitConnector @@ -33,6 +34,28 @@ def test_04_worktree_name_required_for_create(self): _, stderr = self.dispatch_command("worktree", "--create") self.assertIn("provide a name for the worktree", stderr) + def test_05_pull_output_ends_with_blank_line(self): + """`FetchCommand.run` clears the line trailing the last worktree, so `run_hook` must end on a blank one.""" + worktree = MagicMock(name="17.0", detached=False, branch="17.0") + worktree.name = "17.0" + worktree.connector.name = "odoo/odoo" + + command = PullCommand.__new__(PullCommand) + printed: list[str] = [] + titled: list[str] = [] + + with ( + self.patch_property(PullCommand, "worktrees", [worktree]), + self.patch( + self.odev.console, "print", side_effect=lambda renderable="", *_, **__: printed.append(renderable) + ), + self.patch(self.odev.console, "title_rule", side_effect=titled.append), + ): + command.run_hook("17.0", [("odoo/odoo", 0, 0)]) + + self.assertEqual(titled, ["17.0"]) + self.assertEqual(printed, [""]) + class TestScriptCommands(OdevTestCase): def test_01_assets_script_run_after(self):