summary refs log blame commit diff stats
path: root/ganarchy/git.py
blob: f8ccfcd311427aff468009463cee7fd27ec41567 (plain) (tree)
























                                                                             
                          









                                                            







                                                   











                                                                   
                           
                                                                                


                                       

                                                  

























                                                                         














                                                         
                           
                                                                                       


                                       

                                                  
                                           













                                                                    
                                 
                                                                                         



                                            













                                                      
                                  
                                                                               



                                       
                                                  
                                     










                                                         
                                  
                                                                               



                                               
                                                  
                                     
# 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 <https://www.gnu.org/licenses/>.

"""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