# This file is part of GAnarchy - decentralized project hub # Copyright (C) 2020 Soni L. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Git abstraction. """ # Errors are raised when we can't provide an otherwise valid result. # For example, we return 0 for counts instead of raising, but raise # instead of returning empty strings for commit hashes and messages. import subprocess class GitError(Exception): """Raised when a git operation fails, generally due to a missing commit or branch, or network connection issues. """ pass class Git: def __init__(self, path): self.path = path self.base = ("git", "-C", path) def create(self): """Creates the local repo. Can safely be called on an existing repo. """ subprocess.call(self.base + ("init", "-q")) def check_history(self, local_head, commit): """Checks if the local head contains commit in its history. Raises if it doesn't. Args: local_head (str): Name of local head. commit (str): Commit hash. Raises: GitError: If an error occurs. """ try: subprocess.run( self.base + ("merge-base", "--is-ancestor", commit, local_head), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) except subprocess.CalledProcessError as e: raise GitError("check history") from e def check_branchname(self, branchname): """Checks if the given branchname is a valid branch name. Raises if it isn't. Args: branchname (str): Name of branch. Raises: GitError: If an error occurs. """ try: # TODO check that this rstrip is safe out = subprocess.run( self.base + ("check-ref-format", "--branch", branchname), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).stdout.decode("utf-8").rstrip('\r\n') # protect against @{-1}/@{-n} ("previous checkout operation") # is also fairly future-proofed, I hope? if out != branchname: raise GitError("check branchname", out, branchname) except subprocess.CalledProcessError as e: raise GitError("check branchname") from e def force_fetch(self, url, remote_head, local_head): """Fetches a remote head into a local head. If the local head already exists, it is replaced. Args: url (str): Remote url. remote_head (str): Name of remote head. local_head (str): Name of local head. Raises: GitError: If an error occurs. """ try: subprocess.run( self.base + ("fetch", "-q", url, "+" + remote_head + ":" + local_head), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) except subprocess.CalledProcessError as e: raise GitError(e.output) from e def get_count(self, first_hash, last_hash): """Returns a count of the commits added since ``first_hash`` up to ``last_hash``. Args: first_hash (str): A commit. last_hash (str): Another commit. Returns: int: A count of commits added between the hashes, or 0 if an error occurs. """ try: res = subprocess.run( self.base + ("rev-list", "--count", first_hash + ".." + last_hash, "--"), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).stdout.decode("utf-8").strip() return int(res) except subprocess.CalledProcessError as e: return 0 def get_hash(self, target): """Returns the commit hash for a given target. Args: target (str): a refspec. Raises: GitError: If an error occurs. """ try: return subprocess.run( self.base + ("show", target, "-s", "--format=format:%H", "--"), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).stdout.decode("utf-8") except subprocess.CalledProcessError as e: raise GitError("") from e def get_commit_message(self, target): """Returns the commit message for a given target. Args: target (str): a refspec. Raises: GitError: If an error occurs. """ try: return subprocess.run( self.base + ("show", target, "-s", "--format=format:%B", "--"), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).stdout.decode("utf-8", "replace") except subprocess.CalledProcessError as e: raise GitError("") from e