diff --git a/odev/_version.py b/odev/_version.py index 3b405064c..346ddacf8 100644 --- a/odev/_version.py +++ b/odev/_version.py @@ -22,4 +22,4 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "4.30.1" +__version__ = "4.30.2" 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):