diff --git a/odev/_version.py b/odev/_version.py index 73cdd71..4226fa7 100644 --- a/odev/_version.py +++ b/odev/_version.py @@ -22,4 +22,4 @@ # or merged change. # ------------------------------------------------------------------------------ -__version__ = "4.25.0" +__version__ = "4.26.0" diff --git a/odev/commands/git/fetch.py b/odev/commands/git/fetch.py index 846fb55..6da14ba 100644 --- a/odev/commands/git/fetch.py +++ b/odev/commands/git/fetch.py @@ -64,6 +64,10 @@ def grouped_changes(self) -> dict[str, list[tuple[str, int, int]]]: changes: dict[str, list[tuple[str, int, int]]] = {} for repository in self.repositories: + try: + repository.prune_worktrees() + except Exception as e: # noqa: BLE001 + logger.debug(f"Failed to prune worktrees for {repository.name!r}: {e}") repository.fetch(detached=False) for name, worktrees in self.grouped_worktrees.items(): diff --git a/odev/common/commands/git.py b/odev/common/commands/git.py index 7a7b75f..c657050 100644 --- a/odev/common/commands/git.py +++ b/odev/common/commands/git.py @@ -4,9 +4,13 @@ from odev.common import args from odev.common.commands import Command from odev.common.connectors import GitConnector, GitWorktree +from odev.common.logging import logging from odev.common.odoobin import odoo_repositories +logger = logging.getLogger(__name__) + + class GitCommand(Command, ABC): """Base command class for interacting with git repositories and worktrees.""" @@ -21,7 +25,13 @@ def repositories(self) -> Generator[GitConnector, None, None]: def worktrees(self) -> Generator[GitWorktree, None, None]: """Iterate over worktrees in Odoo repositories.""" for repository in self.repositories: - yield from repository.worktrees() + for worktree in repository.worktrees(): + if not worktree.path.exists(): + logger.debug(f"Skipping missing worktree {worktree.name!r} at {worktree.path!s}") + continue + if hasattr(self, "args") and self.args.version and worktree.name != self.args.version: + continue + yield worktree @property def grouped_worktrees(self) -> dict[str, list[GitWorktree]]: diff --git a/odev/common/connectors/git.py b/odev/common/connectors/git.py index 4401021..eba5afe 100644 --- a/odev/common/connectors/git.py +++ b/odev/common/connectors/git.py @@ -213,10 +213,18 @@ def pending_changes(self) -> tuple[int, int]: :return: A tuple of commits behind and ahead. :rtype: Tuple[int, int] """ + if self.detached: + return 0, 0 repo = Repo(self.path) - rev_list: str = repo.git.rev_list("--left-right", "--count", "@{u}...HEAD") - commits_behind, commits_ahead = (int(commits) for commits in rev_list.split("\t")) - return commits_behind, commits_ahead + try: + rev_list: str = repo.git.rev_list("--left-right", "--count", "@{u}...HEAD") + commits_behind, commits_ahead = (int(commits) for commits in rev_list.split("\t")) + except GitCommandError as e: + if "no upstream configured" in str(e) or "does not point to a branch" in str(e): + return 0, 0 + raise + else: + return commits_behind, commits_ahead class GitConnector(Connector): diff --git a/odev/common/store/tables/secrets.py b/odev/common/store/tables/secrets.py index 1fb12ce..e794adc 100644 --- a/odev/common/store/tables/secrets.py +++ b/odev/common/store/tables/secrets.py @@ -1,3 +1,4 @@ +import os from base64 import b64decode, b64encode from collections.abc import Sequence from dataclasses import dataclass @@ -69,9 +70,13 @@ class SecretStore(PostgresTable): @classmethod def _list_ssh_keys(cls) -> list[AgentKey]: """List all SSH keys available in the ssh-agent.""" - keys = list(SSHAgent().get_keys()) + try: + keys = list(SSHAgent().get_keys()) + except (SSHException, ConnectionError) as e: + logger.warning(f"Failed to communicate with ssh-agent: {e}") + keys = [] - if not keys: + if not keys and not os.environ.get("ODEV_NO_SSH_AGENT"): raise OdevError("No SSH keys found in ssh-agent, or ssh-agent is not running.") fingerprint = cls.config.security.encryption_key @@ -266,7 +271,13 @@ def _get( return None logger.debug(f"Secret '{name}:{scope}:{platform}' retrieved from storage") - return Secret(name, result[0][0], SecretStore.decrypt(result[0][1]), scope, platform) + try: + password = SecretStore.decrypt(result[0][1]) + except OdevError: + logger.debug(f"Failed to decrypt secret '{name}:{scope}:{platform}', treating as missing") + return None + + return Secret(name, result[0][0], password, scope, platform) def _set(self, secret: Secret): """Save a secret to the vault.