diff --git a/sopel/builtins/version.py b/sopel/builtins/version.py index 536eaa83f..81b18c69d 100644 --- a/sopel/builtins/version.py +++ b/sopel/builtins/version.py @@ -9,27 +9,70 @@ from __future__ import annotations import datetime +import logging import os import platform from sopel import __version__ as release, plugin +logger = logging.getLogger(__name__) + PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) GIT_DIR = os.path.join(PROJECT_DIR, '.git') -def git_info(): - head = os.path.join(GIT_DIR, 'HEAD') +def _read_commit(gitdir: str, head: str) -> str | None: + """Given paths to ``.git/`` and ``HEAD``, determine the associated commit hash + + :param gitdir: path to a ``.git/`` directory + :param head: path to ``HEAD`` file + """ + result = None + if os.path.isfile(head): with open(head) as h: head_loc = h.readline()[5:-1] # strip ref: and \n - head_file = os.path.join(GIT_DIR, head_loc) + head_file = os.path.join(gitdir, head_loc) if os.path.isfile(head_file): with open(head_file) as h: - sha = h.readline() + sha = h.readline().strip() if sha: - return sha + result = sha + + return result + + +def _resolve_git_dirs(path: str) -> tuple[str, str]: + """Resolve a ``.git`` path to its 'true' ``.git/`` and `HEAD` + + This helper is useful for dealing with the ``.git`` file stored in a + git worktree. + + :param path: path to a .git directory or worktree reference file + """ + # default to the old behavior: assume `path` is a valid .git/ to begin with + gitdir = path + head = os.path.join(path, "HEAD") + + if os.path.isfile(path): + # this may be a worktree, let's override the result properly if so + with open(path, 'r') as f: + first, rest = next(f).strip().split(maxsplit=1) + if first == "gitdir:": + # line is "gitdir: /path/to/parentrepo/.git/worktrees/thispath" + gitdir = os.path.dirname(os.path.dirname(rest)) + head = os.path.join(rest, "HEAD") + # else: we can't make sense of this file, stick to the default + + return gitdir, head + + +def sopel_git_commit() -> str | None: + """Determine the git commit hash of this Sopel, if applicable + """ + gitdir, head = _resolve_git_dirs(GIT_DIR) + return _read_commit(gitdir, head) @plugin.command('version') @@ -65,7 +108,12 @@ def version(bot, trigger): 'Sopel v%s' % release, 'Python: %s' % platform.python_version() ] - sha = git_info() + try: + sha = sopel_git_commit() + except OSError: + logger.warning("Failed to retrieve git commit hash", exc_info=True) + sha = None + if sha: parts.append('Commit: %s' % sha)