# SPDX-FileCopyrightText: 2023 Blender Foundation # # SPDX-License-Identifier: GPL-2.0-or-later # Simple module for inspecting git commits import os import subprocess import datetime from typing import ( List, Union, Optional, Tuple, ) class GitCommit: __slots__ = ( "sha1", # to extract more info "_git_dir", # cached values "_author", "_email", "_date", "_body", "_files", "_files_status", "_diff", ) def __init__(self, sha1: bytes, git_dir: str): self.sha1 = sha1 self._git_dir = git_dir self._author: Optional[str] = None self._email: Optional[str] = None self._date: Optional[datetime.datetime] = None self._body: Optional[str] = None self._files: Optional[List[bytes]] = None self._files_status: Optional[List[List[bytes]]] = None self._diff: Optional[str] = None def cache(self) -> None: """ Cache all properties (except for diff as it's significantly larger than other members). """ self.author self.email self.date self.body self.files self.files_status def _log_format(self, format: str, args: Tuple[Union[str, bytes], ...] = ()) -> bytes: # sha1 = self.sha1.decode('ascii') cmd: Tuple[Union[str, bytes], ...] = ( "git", "--git-dir", self._git_dir, "log", "-1", # only this rev self.sha1, "--format=" + format, *args, ) # print(" ".join(cmd)) with subprocess.Popen( cmd, stdout=subprocess.PIPE, ) as p: assert p is not None and p.stdout is not None return p.stdout.read() @property def sha1_short(self) -> str: cmd = ( "git", "--git-dir", self._git_dir, "rev-parse", "--short", self.sha1, ) with subprocess.Popen( cmd, stdout=subprocess.PIPE, ) as p: assert p is not None and p.stdout is not None return p.stdout.read().strip().decode('ascii') @property def author(self) -> str: ret = self._author if ret is None: content = self._log_format("%an")[:-1] ret = content.decode("utf8", errors="ignore") self._author = ret return ret @property def email(self) -> str: ret = self._email if ret is None: content = self._log_format("%ae")[:-1] ret = content.decode("utf8", errors="ignore") self._email = ret return ret @property def date(self) -> datetime.datetime: ret = self._date if ret is None: import datetime ret = datetime.datetime.fromtimestamp(int(self._log_format("%ct"))) self._date = ret return ret @property def body(self) -> str: ret = self._body if ret is None: content = self._log_format("%B")[:-1] ret = content.decode("utf8", errors="ignore") self._body = ret return ret @property def subject(self) -> str: return self.body.lstrip().partition("\n")[0] @property def files(self) -> List[bytes]: ret = self._files if ret is None: ret = [f for f in self._log_format("format:", args=("--name-only",)).split(b"\n") if f] self._files = ret return ret @property def files_status(self) -> List[List[bytes]]: ret = self._files_status if ret is None: ret = [f.split(None, 1) for f in self._log_format("format:", args=("--name-status",)).split(b"\n") if f] self._files_status = ret return ret @property def diff(self) -> str: ret = self._diff if ret is None: content = self._log_format("", args=("-p",)) ret = content.decode("utf8", errors="ignore") self._diff = ret return ret class GitCommitIter: __slots__ = ( "_path", "_git_dir", "_sha1_range", "_process", ) def __init__(self, path: str, sha1_range: str): self._path = path self._git_dir = os.path.join(path, ".git") self._sha1_range = sha1_range self._process: Optional[subprocess.Popen[bytes]] = None def __iter__(self) -> "GitCommitIter": cmd = ( "git", "--git-dir", self._git_dir, "log", self._sha1_range, "--format=%H", ) # print(" ".join(cmd)) self._process = subprocess.Popen( cmd, stdout=subprocess.PIPE, ) return self def __next__(self) -> GitCommit: assert self._process is not None and self._process.stdout is not None sha1 = self._process.stdout.readline()[:-1] if sha1: return GitCommit(sha1, self._git_dir) else: raise StopIteration class GitRepo: __slots__ = ( "_path", "_git_dir", ) def __init__(self, path: str): self._path = path self._git_dir = os.path.join(path, ".git") @property def branch(self) -> bytes: cmd = ( "git", "--git-dir", self._git_dir, "rev-parse", "--abbrev-ref", "HEAD", ) # print(" ".join(cmd)) p = subprocess.Popen( cmd, stdout=subprocess.PIPE, ) assert p is not None and p.stdout is not None return p.stdout.read()