﻿# ---------------------------------------------------------------------------
# |  
# |  SourceControlManagement.py
# |  
# |  <NAME> (<EMAIL>)
# |  
# |  08/26/2015 07:15:21 PM
# |  
# ---------------------------------------------------------------------------
# |  
# |  Copyright <NAME> 2015-18.
# |          
# |  Distributed under the Boost Software License, Version 1.0.
# |  (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
# |  
# ---------------------------------------------------------------------------
import os
import sys
import textwrap

from .CallOnExit import CallOnExit
from . import Enum
from . import FileSystem
from .Interface import Interface, abstractmethod, abstractproperty
from .QuickObject import QuickObject

# ---------------------------------------------------------------------------
_script_fullpath = os.path.abspath(__file__) if "python" in sys.executable.lower() else sys.executable
_script_dir, _script_name = os.path.split(_script_fullpath)
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# |
# |  Public Types
# |
# ---------------------------------------------------------------------------
class UpdateMergeArg(object):
    """\
    Argument passed to an update and/or merge command
    """

    Operation = Enum.Create("Update", "Merge", "Other")

    # ---------------------------------------------------------------------------
    @staticmethod
    def FromCommandLine( revision=None,
                         branch=None,
                         date=None,
                         date_greater=None,
                       ):
        if revision:
            return RevisionUpdateMergeArg(revision)

        if branch and date:
            return BranchAndDateUpdateMergeArg(branch, date, greater=date_greater)

        if branch:
            return BranchUpdateMergeArg(branch)

        if date:
            return DateUpdateMergeArg(date, greater=date_greater)

        return EmptyUpdateMergeArg()

# ---------------------------------------------------------------------------
class EmptyUpdateMergeArg(UpdateMergeArg):
    """\
    By default, update to the most recent change on the current branch.
    """
    def __init__(self):
        pass

    def __str__(self):
        return "<SCM-specific tip>"

# ---------------------------------------------------------------------------
class RevisionUpdateMergeArg(UpdateMergeArg):
    """\
    Change branches based on the branch associated with the revision.
    """
    def __init__(self, revision):
        self.Revision = revision

    def __str__(self):
        return self.Revision

# ---------------------------------------------------------------------------
class DateUpdateMergeArg(UpdateMergeArg):
    """\
    Change nearest to the date on any branch.
    """
    def __init__(self, date, greater=None):
        self.Date = date
        self.Greater = greater

    def __str__(self):
        operator = ''
        if self.Greater is not None:
            operator = '>' if self.Greater else '<'

        return "{}{}".format(operator, self.Date)

# ---------------------------------------------------------------------------
class BranchUpdateMergeArg(UpdateMergeArg):
    """\
    Most recent change on the specified branch.
    """
    def __init__(self, branch):
        self.Branch = branch

    def __str__(self):
        return self.Branch

# ---------------------------------------------------------------------------
class BranchAndDateUpdateMergeArg(DateUpdateMergeArg):
    """\
    Change nearest to the date on the specified branch.
    """
    def __init__(self, branch, date, greater=None):
        self.Branch = branch
        super(BranchAndDateUpdateMergeArg, self).__init__(date, greater=greater)

    def __str__(self):
        return "{} ({})".format( self.Branch, 
                                 super(BranchAndDateUpdateMergeArg, self).__str__(),
                               )

# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
class SourceControlManagementBase(Interface):

    # By convention, all actions return (return_code, output) for all actions.
    # An action is any method without a prefix of "Is", "Has", or "Get".

    # ---------------------------------------------------------------------------
    # |
    # |  Public Properties
    # |
    # ---------------------------------------------------------------------------
    @abstractproperty
    def Name(self):
        raise Exception("Abstract property")

    @abstractproperty
    def DefaultBranch(self):
        raise Exception("Abstract property")

    @abstractproperty
    def Tip(self):
        raise Exception("Abstract property")

    @abstractproperty
    def WorkingDirectories(self):
        raise Exception("Abstract property")

    @abstractproperty
    def IgnoreFilename(self):
        raise Exception("Abstract property")

    IsDistributed                           = False

    # ---------------------------------------------------------------------------
    # |
    # |  Public Methods
    # |
    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def IsAvailable():
        """Returns True if the SCM is available on the current machine"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def IsActive(repo_dir):
        """Returns True if the SCM is active in the current repo dir"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def IsRootDir(cls, repo_dir):
        return cls.IsAvailable() and cls.IsActive(repo_dir) and cls.GetRoot(repo_dir) == repo_dir

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Clone(uri, output_dir, branch=None):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Create(output_dir):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRoot(repo_dir):
        """\
        Returns the root directory of the repo associated with the provided directory using
        conventions specific to the SCM.
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetUniqueName(repo_root):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Who(repo_root):
        """Gets the username associated with the specified repo."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetBranches(repo_root):
        """Gets all local branches"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetCurrentBranch(repo_root):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def GetCurrentNormalizedBranch(cls, repo_root):
        """\
        Some SCMs (such as Git) need to do wonky things to ensure that branches
        stay consistent (e.g. avoid a detached head state). Those wonky things may
        require branch renames that are not exposed to the end user. This method
        will "undo" those changes if necessary.
        """
        return cls.GetCurrentBranch(repo_root)

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetMostRecentBranch(repo_root):
        """Returns the branch associated with the most recent change"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def CreateBranch(repo_root, branch_name):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def SetBranch(repo_root, branch_name):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def SetBranchOrDefault(cls, repo_root, branch_name):
        """Sets the working branch to branch_name if branch_name is a valid branch"""
        return cls.SetBranch( repo_root,
                              branch_name if branch_name in cls.GetBranches(repo_root) else cls.DefaultBranch,
                            )

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRevisionInfo(repo_root, revision):
        """\
        Returns an object that contains information about a specific revision. At the
        very least, the object  will contain the attributes:
            - user
            - date
            - summary
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def AddFiles( cls, 
                  repo_root, 
                  file_filenames_or_recurse_flag,
                  functor=None,                         # def Func(fullpath) -> Bool
                ):
        """\
        Addes the file or files provided, or will search for new files in the 
        current dir (if the flag is set to False) or recursively (if the flag is 
        set to True).
        """
        if isinstance(file_filenames_or_recurse_flag, bool):
            if file_filenames_or_recurse_flag:
                # ---------------------------------------------------------------------------
                def GetFiles():
                    return FileSystem.WalkFiles( repo_root,
                                                 traverse_exclude_dir_names=cls.WorkingDirectories,
                                               )
                # ---------------------------------------------------------------------------
            else:
                # ---------------------------------------------------------------------------
                def GetFiles():
                    for item in os.listdir(repo_root):
                        fullpath = os.path.join(repo_root, item)
                        if os.path.isfile(fullpath):
                            yield fullpath

                # ---------------------------------------------------------------------------
                
            filenames = []

            for filename in GetFiles():
                if not functor or functor(filename):
                    filenames.append(filename)

        elif isinstance(file_filenames_or_recurse_flag, str):
            filenames = [ file_filenames_or_recurse_flag, ]

        else:
            filenames = file_filenames_or_recurse_flag

        for filename in filenames:
            if not os.path.isfile(filename):
                raise Exception("'{}' is not a valid filename".format(filename))

        return cls._AddFilesImpl(repo_root, filenames)

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Clean(repo_root, no_prompt=False):
        """Ensures that the repo is in a clean state (no working changes)"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasWorkingChanges(repo_root):
        """Are there changes to files tracked by the repo."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasUntrackedWorkingChanges(repo_root):
        """Are there changes to files that are not tracked by the repo."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def GetChangeStatus(cls, repo_root):
        return QuickObject( untracked=cls.HasUntrackedWorkingChanges(repo_root),
                            working=cls.HasWorkingChanges(repo_root),
                          )

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def CreatePatch( repo_root,
                     patch_filename,
                     start_change=None,
                     end_change=None,
                   ):
        # Local changes will be used if start_= and end_change are None
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def ApplyPatch(repo_root, patch_filename, commit=False):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Commit(repo_root, description, username=None):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Update(repo_root, update_merge_arg):
        raise Exception("Abstract Method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Merge(repo_root, update_merge_arg):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRevisionsSinceLastMerge(repo_root, dest_branch, source_update_merge_arg):
        """\
        Returns a list of all revisions between the last merge to the dest_branch
        and the given arg.
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetChangedFiles(repo_root, revision_or_revisions_or_none):
        """\
        Get a list of all files that have changed as part of the specified revision(s).
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def EnumBlameInfo(repo_root, filename):
        """\
        Generates (line, revision, text) for each line in the specified file.
        """
        raise Exception("Abstract method")

    # ----------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def EnumTrackedFiles(repo_root):
        """\
        Enumerates the fullpaths of all files tracked under the current repo.
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    # |
    # |  Protected Methods
    # |
    # ---------------------------------------------------------------------------
    @staticmethod
    def AreYouSurePrompt(prompt):
        result = raw_input("{}\nEnter 'y' to continue or anything else to exit: ".format(prompt)).strip() == 'y'
        sys.stdout.write("\n")

        return result

    # ---------------------------------------------------------------------------
    # |
    # |  Private Methods
    # |
    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def _AddFilesImpl(repo_root, filenames):
        raise Exception("Abstract method")

# ---------------------------------------------------------------------------
class DistributedSourceControlManagementBase(SourceControlManagementBase):

    IsDistributed                           = True

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Reset(repo_root, no_prompt=False, no_backup=False):
        """Resets the repo to the remote state, erasing any unpushed (but committed) changes."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasUpdateChanges(repo_root):
        """Are there changes that have been pulled but have yet to be updated."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasLocalChanges(repo_root):
        """Are there changes yet to be pushed"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetLocalChanges(repo_root):
        """Returns a list of changes that have yet to be pushed"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasRemoteChanges(repo_root):
        """Are there changes to be pulled"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRemoteChanges(repo_root):
        """Returns a list of changes that have yet to be pulled"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def GetChangeStatus(cls, repo_root):
        return QuickObject( untracked=cls.HasUntrackedWorkingChanges(repo_root),
                            working=cls.HasWorkingChanges(repo_root),
                            local=cls.HasLocalChanges(repo_root),
                            remote=cls.HasRemoteChanges(repo_root),
                            update=cls.HasUpdateChanges(repo_root),
                            branch=cls.GetMostRecentBranch(repo_root),
                          )

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Push(repo_root, create_remote_branch=False):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Pull(repo_root, branch_or_branches=None):
        raise Exception("Abstract method")

# ---------------------------------------------------------------------------
# |
# |  Methods
# |
# ---------------------------------------------------------------------------
def GetPotentialSCMs():
    from .Impl.SourceControlManagement.MercurialSourceControlManagement import MercurialSourceControlManagement
    from .Impl.SourceControlManagement.GitSourceControlManagement import GitSourceControlManagement
    from .Impl.SourceControlManagement.PerforceSourceControlManagement import PerforceSourceControlManagement
    
    return [ MercurialSourceControlManagement,
             GitSourceControlManagement,
             PerforceSourceControlManagement,
           ]

# ---------------------------------------------------------------------------
def GetSCM(repo_root, throw_on_error=True):
    available = []

    potential = GetPotentialSCMs()

    for scm in potential:
        if scm.IsAvailable():
            available.append(scm)
            if repo_root is not None and scm.IsActive(repo_root):
                return scm

    if not throw_on_error:
        return

    raise Exception(textwrap.dedent(
        """\
        No SCMs are active for '{repo_root}'.

        Available SCMs are:
        {available}

        Potential SCMs are:
        {potential}
        """).format( repo_root=repo_root,
                     available="    NONE" if not available else '\n'.join([ "    - {}".format(scm.Name) for scm in available ]),
                     potential="    NONE" if not potential else '\n'.join([ "    - {}".format(scm.Name) for scm in potential ]),
                   ))
    
# ----------------------------------------------------------------------
def GetSCMEx(path, throw_on_error=True):
    sys.path.insert(0, os.getenv("DEVELOPMENT_ENVIRONMENT_FUNDAMENTAL"))
    with CallOnExit(lambda: sys.path.pop(0)):
        import SourceRepositoryTools        # <Unable to import> pylint: disable = F0401

    root = path
    while True:
        if os.path.isfile(os.path.join(root, SourceRepositoryTools.Constants.REPOSITORY_ID_FILENAME)):
            return GetSCM(root, throw_on_error=throw_on_error)

        potential_root = os.path.dirname(root)
        if potential_root == root:
            break

        root = potential_root

    raise Exception("No repository found in the ancestors of '{}'".format(path))

# ---------------------------------------------------------------------------
def EnumSCMDirectories(path):
    assert os.getenv("DEVELOPMENT_ENVIRONMENT_FUNDAMENTAL")
    sys.path.insert(0, os.getenv("DEVELOPMENT_ENVIRONMENT_FUNDAMENTAL"))
    with CallOnExit(lambda: sys.path.pop(0)):
        from SourceRepositoryTools import Constants

    scms = GetPotentialSCMs()

    for root, directories, _ in os.walk(path):
        for scm in scms:
            if scm.IsRootDir(root):
                yield scm, root
                directories[:] = []
                continue

        if Constants.GENERATED_DIRECTORY_NAME in directories:
            directories.remove(Constants.GENERATED_DIRECTORY_NAME)

﻿# ---------------------------------------------------------------------------
# |  
# |  SourceControlManagement.py
# |  
# |  <NAME> (<EMAIL>)
# |  
# |  08/26/2015 07:15:21 PM
# |  
# ---------------------------------------------------------------------------
# |  
# |  Copyright <NAME> 2015-18.
# |          
# |  Distributed under the Boost Software License, Version 1.0.
# |  (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
# |  
# ---------------------------------------------------------------------------
import os
import sys
import textwrap

from .CallOnExit import CallOnExit
from . import Enum
from . import FileSystem
from .Interface import Interface, abstractmethod, abstractproperty
from .QuickObject import QuickObject

# ---------------------------------------------------------------------------
_script_fullpath = os.path.abspath(__file__) if "python" in sys.executable.lower() else sys.executable
_script_dir, _script_name = os.path.split(_script_fullpath)
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# |
# |  Public Types
# |
# ---------------------------------------------------------------------------
class UpdateMergeArg(object):
    """\
    Argument passed to an update and/or merge command
    """

    Operation = Enum.Create("Update", "Merge", "Other")

    # ---------------------------------------------------------------------------
    @staticmethod
    def FromCommandLine( revision=None,
                         branch=None,
                         date=None,
                         date_greater=None,
                       ):
        if revision:
            return RevisionUpdateMergeArg(revision)

        if branch and date:
            return BranchAndDateUpdateMergeArg(branch, date, greater=date_greater)

        if branch:
            return BranchUpdateMergeArg(branch)

        if date:
            return DateUpdateMergeArg(date, greater=date_greater)

        return EmptyUpdateMergeArg()

# ---------------------------------------------------------------------------
class EmptyUpdateMergeArg(UpdateMergeArg):
    """\
    By default, update to the most recent change on the current branch.
    """
    def __init__(self):
        pass

    def __str__(self):
        return "<SCM-specific tip>"

# ---------------------------------------------------------------------------
class RevisionUpdateMergeArg(UpdateMergeArg):
    """\
    Change branches based on the branch associated with the revision.
    """
    def __init__(self, revision):
        self.Revision = revision

    def __str__(self):
        return self.Revision

# ---------------------------------------------------------------------------
class DateUpdateMergeArg(UpdateMergeArg):
    """\
    Change nearest to the date on any branch.
    """
    def __init__(self, date, greater=None):
        self.Date = date
        self.Greater = greater

    def __str__(self):
        operator = ''
        if self.Greater is not None:
            operator = '>' if self.Greater else '<'

        return "{}{}".format(operator, self.Date)

# ---------------------------------------------------------------------------
class BranchUpdateMergeArg(UpdateMergeArg):
    """\
    Most recent change on the specified branch.
    """
    def __init__(self, branch):
        self.Branch = branch

    def __str__(self):
        return self.Branch

# ---------------------------------------------------------------------------
class BranchAndDateUpdateMergeArg(DateUpdateMergeArg):
    """\
    Change nearest to the date on the specified branch.
    """
    def __init__(self, branch, date, greater=None):
        self.Branch = branch
        super(BranchAndDateUpdateMergeArg, self).__init__(date, greater=greater)

    def __str__(self):
        return "{} ({})".format( self.Branch, 
                                 super(BranchAndDateUpdateMergeArg, self).__str__(),
                               )

# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
class SourceControlManagementBase(Interface):

    # By convention, all actions return (return_code, output) for all actions.
    # An action is any method without a prefix of "Is", "Has", or "Get".

    # ---------------------------------------------------------------------------
    # |
    # |  Public Properties
    # |
    # ---------------------------------------------------------------------------
    @abstractproperty
    def Name(self):
        raise Exception("Abstract property")

    @abstractproperty
    def DefaultBranch(self):
        raise Exception("Abstract property")

    @abstractproperty
    def Tip(self):
        raise Exception("Abstract property")

    @abstractproperty
    def WorkingDirectories(self):
        raise Exception("Abstract property")

    @abstractproperty
    def IgnoreFilename(self):
        raise Exception("Abstract property")

    IsDistributed                           = False

    # ---------------------------------------------------------------------------
    # |
    # |  Public Methods
    # |
    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def IsAvailable():
        """Returns True if the SCM is available on the current machine"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def IsActive(repo_dir):
        """Returns True if the SCM is active in the current repo dir"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def IsRootDir(cls, repo_dir):
        return cls.IsAvailable() and cls.IsActive(repo_dir) and cls.GetRoot(repo_dir) == repo_dir

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Clone(uri, output_dir, branch=None):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Create(output_dir):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRoot(repo_dir):
        """\
        Returns the root directory of the repo associated with the provided directory using
        conventions specific to the SCM.
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetUniqueName(repo_root):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Who(repo_root):
        """Gets the username associated with the specified repo."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetBranches(repo_root):
        """Gets all local branches"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetCurrentBranch(repo_root):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def GetCurrentNormalizedBranch(cls, repo_root):
        """\
        Some SCMs (such as Git) need to do wonky things to ensure that branches
        stay consistent (e.g. avoid a detached head state). Those wonky things may
        require branch renames that are not exposed to the end user. This method
        will "undo" those changes if necessary.
        """
        return cls.GetCurrentBranch(repo_root)

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetMostRecentBranch(repo_root):
        """Returns the branch associated with the most recent change"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def CreateBranch(repo_root, branch_name):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def SetBranch(repo_root, branch_name):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def SetBranchOrDefault(cls, repo_root, branch_name):
        """Sets the working branch to branch_name if branch_name is a valid branch"""
        return cls.SetBranch( repo_root,
                              branch_name if branch_name in cls.GetBranches(repo_root) else cls.DefaultBranch,
                            )

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRevisionInfo(repo_root, revision):
        """\
        Returns an object that contains information about a specific revision. At the
        very least, the object  will contain the attributes:
            - user
            - date
            - summary
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def AddFiles( cls, 
                  repo_root, 
                  file_filenames_or_recurse_flag,
                  functor=None,                         # def Func(fullpath) -> Bool
                ):
        """\
        Addes the file or files provided, or will search for new files in the 
        current dir (if the flag is set to False) or recursively (if the flag is 
        set to True).
        """
        if isinstance(file_filenames_or_recurse_flag, bool):
            if file_filenames_or_recurse_flag:
                # ---------------------------------------------------------------------------
                def GetFiles():
                    return FileSystem.WalkFiles( repo_root,
                                                 traverse_exclude_dir_names=cls.WorkingDirectories,
                                               )
                # ---------------------------------------------------------------------------
            else:
                # ---------------------------------------------------------------------------
                def GetFiles():
                    for item in os.listdir(repo_root):
                        fullpath = os.path.join(repo_root, item)
                        if os.path.isfile(fullpath):
                            yield fullpath

                # ---------------------------------------------------------------------------
                
            filenames = []

            for filename in GetFiles():
                if not functor or functor(filename):
                    filenames.append(filename)

        elif isinstance(file_filenames_or_recurse_flag, str):
            filenames = [ file_filenames_or_recurse_flag, ]

        else:
            filenames = file_filenames_or_recurse_flag

        for filename in filenames:
            if not os.path.isfile(filename):
                raise Exception("'{}' is not a valid filename".format(filename))

        return cls._AddFilesImpl(repo_root, filenames)

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Clean(repo_root, no_prompt=False):
        """Ensures that the repo is in a clean state (no working changes)"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasWorkingChanges(repo_root):
        """Are there changes to files tracked by the repo."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasUntrackedWorkingChanges(repo_root):
        """Are there changes to files that are not tracked by the repo."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def GetChangeStatus(cls, repo_root):
        return QuickObject( untracked=cls.HasUntrackedWorkingChanges(repo_root),
                            working=cls.HasWorkingChanges(repo_root),
                          )

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def CreatePatch( repo_root,
                     patch_filename,
                     start_change=None,
                     end_change=None,
                   ):
        # Local changes will be used if start_= and end_change are None
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def ApplyPatch(repo_root, patch_filename, commit=False):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Commit(repo_root, description, username=None):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Update(repo_root, update_merge_arg):
        raise Exception("Abstract Method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Merge(repo_root, update_merge_arg):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRevisionsSinceLastMerge(repo_root, dest_branch, source_update_merge_arg):
        """\
        Returns a list of all revisions between the last merge to the dest_branch
        and the given arg.
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetChangedFiles(repo_root, revision_or_revisions_or_none):
        """\
        Get a list of all files that have changed as part of the specified revision(s).
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def EnumBlameInfo(repo_root, filename):
        """\
        Generates (line, revision, text) for each line in the specified file.
        """
        raise Exception("Abstract method")

    # ----------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def EnumTrackedFiles(repo_root):
        """\
        Enumerates the fullpaths of all files tracked under the current repo.
        """
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    # |
    # |  Protected Methods
    # |
    # ---------------------------------------------------------------------------
    @staticmethod
    def AreYouSurePrompt(prompt):
        result = raw_input("{}\nEnter 'y' to continue or anything else to exit: ".format(prompt)).strip() == 'y'
        sys.stdout.write("\n")

        return result

    # ---------------------------------------------------------------------------
    # |
    # |  Private Methods
    # |
    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def _AddFilesImpl(repo_root, filenames):
        raise Exception("Abstract method")

# ---------------------------------------------------------------------------
class DistributedSourceControlManagementBase(SourceControlManagementBase):

    IsDistributed                           = True

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Reset(repo_root, no_prompt=False, no_backup=False):
        """Resets the repo to the remote state, erasing any unpushed (but committed) changes."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasUpdateChanges(repo_root):
        """Are there changes that have been pulled but have yet to be updated."""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasLocalChanges(repo_root):
        """Are there changes yet to be pushed"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetLocalChanges(repo_root):
        """Returns a list of changes that have yet to be pushed"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def HasRemoteChanges(repo_root):
        """Are there changes to be pulled"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def GetRemoteChanges(repo_root):
        """Returns a list of changes that have yet to be pulled"""
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @classmethod
    def GetChangeStatus(cls, repo_root):
        return QuickObject( untracked=cls.HasUntrackedWorkingChanges(repo_root),
                            working=cls.HasWorkingChanges(repo_root),
                            local=cls.HasLocalChanges(repo_root),
                            remote=cls.HasRemoteChanges(repo_root),
                            update=cls.HasUpdateChanges(repo_root),
                            branch=cls.GetMostRecentBranch(repo_root),
                          )

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Push(repo_root, create_remote_branch=False):
        raise Exception("Abstract method")

    # ---------------------------------------------------------------------------
    @staticmethod
    @abstractmethod
    def Pull(repo_root, branch_or_branches=None):
        raise Exception("Abstract method")

# ---------------------------------------------------------------------------
# |
# |  Methods
# |
# ---------------------------------------------------------------------------
def GetPotentialSCMs():
    from .Impl.SourceControlManagement.MercurialSourceControlManagement import MercurialSourceControlManagement
    from .Impl.SourceControlManagement.GitSourceControlManagement import GitSourceControlManagement
    from .Impl.SourceControlManagement.PerforceSourceControlManagement import PerforceSourceControlManagement
    
    return [ MercurialSourceControlManagement,
             GitSourceControlManagement,
             PerforceSourceControlManagement,
           ]

# ---------------------------------------------------------------------------
def GetSCM(repo_root, throw_on_error=True):
    available = []

    potential = GetPotentialSCMs()

    for scm in potential:
        if scm.IsAvailable():
            available.append(scm)
            if repo_root is not None and scm.IsActive(repo_root):
                return scm

    if not throw_on_error:
        return

    raise Exception(textwrap.dedent(
        """\
        No SCMs are active for '{repo_root}'.

        Available SCMs are:
        {available}

        Potential SCMs are:
        {potential}
        """).format( repo_root=repo_root,
                     available="    NONE" if not available else '\n'.join([ "    - {}".format(scm.Name) for scm in available ]),
                     potential="    NONE" if not potential else '\n'.join([ "    - {}".format(scm.Name) for scm in potential ]),
                   ))
    
# ----------------------------------------------------------------------
def GetSCMEx(path, throw_on_error=True):
    sys.path.insert(0, os.getenv("DEVELOPMENT_ENVIRONMENT_FUNDAMENTAL"))
    with CallOnExit(lambda: sys.path.pop(0)):
        import SourceRepositoryTools        # <Unable to import> pylint: disable = F0401

    root = path
    while True:
        if os.path.isfile(os.path.join(root, SourceRepositoryTools.Constants.REPOSITORY_ID_FILENAME)):
            return GetSCM(root, throw_on_error=throw_on_error)

        potential_root = os.path.dirname(root)
        if potential_root == root:
            break

        root = potential_root

    raise Exception("No repository found in the ancestors of '{}'".format(path))

# ---------------------------------------------------------------------------
def EnumSCMDirectories(path):
    assert os.getenv("DEVELOPMENT_ENVIRONMENT_FUNDAMENTAL")
    sys.path.insert(0, os.getenv("DEVELOPMENT_ENVIRONMENT_FUNDAMENTAL"))
    with CallOnExit(lambda: sys.path.pop(0)):
        from SourceRepositoryTools import Constants

    scms = GetPotentialSCMs()

    for root, directories, _ in os.walk(path):
        for scm in scms:
            if scm.IsRootDir(root):
                yield scm, root
                directories[:] = []
                continue

        if Constants.GENERATED_DIRECTORY_NAME in directories:
            directories.remove(Constants.GENERATED_DIRECTORY_NAME)

# --------------------------------------------------------------------------- # | # |  SourceControlManagement.py # | # |  <NAME> (<EMAIL>) # | # |  08/26/2015 07:15:21 PM # | # --------------------------------------------------------------------------- # | # |  Copyright <NAME> 2015-18. # | # |  Distributed under the Boost Software License, Version 1.0. # |  (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # | # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # | # |  Public Types # | # --------------------------------------------------------------------------- \
     Argument passed to an update and/or merge command # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- \
     By default, update to the most recent change on the current branch. # --------------------------------------------------------------------------- \
     Change branches based on the branch associated with the revision. # --------------------------------------------------------------------------- \
     Change nearest to the date on any branch. # --------------------------------------------------------------------------- \
     Most recent change on the specified branch. # --------------------------------------------------------------------------- \
     Change nearest to the date on the specified branch. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # By convention, all actions return (return_code, output) for all actions. # An action is any method without a prefix of "Is", "Has", or "Get". # --------------------------------------------------------------------------- # | # |  Public Properties # | # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # | # |  Public Methods # | # --------------------------------------------------------------------------- Returns True if the SCM is available on the current machine # --------------------------------------------------------------------------- Returns True if the SCM is active in the current repo dir # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- \
         Returns the root directory of the repo associated with the provided directory using
         conventions specific to the SCM. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- Gets the username associated with the specified repo. # --------------------------------------------------------------------------- Gets all local branches # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- \
         Some SCMs (such as Git) need to do wonky things to ensure that branches
         stay consistent (e.g. avoid a detached head state). Those wonky things may
         require branch renames that are not exposed to the end user. This method
         will "undo" those changes if necessary. # --------------------------------------------------------------------------- Returns the branch associated with the most recent change # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- Sets the working branch to branch_name if branch_name is a valid branch # --------------------------------------------------------------------------- \
         Returns an object that contains information about a specific revision. At the
         very least, the object  will contain the attributes:
             - user
             - date
             - summary # --------------------------------------------------------------------------- # def Func(fullpath) -> Bool \
         Addes the file or files provided, or will search for new files in the 
         current dir (if the flag is set to False) or recursively (if the flag is 
         set to True). # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- Ensures that the repo is in a clean state (no working changes) # --------------------------------------------------------------------------- Are there changes to files tracked by the repo. # --------------------------------------------------------------------------- Are there changes to files that are not tracked by the repo. # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Local changes will be used if start_= and end_change are None # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- \
         Returns a list of all revisions between the last merge to the dest_branch
         and the given arg. # --------------------------------------------------------------------------- \
         Get a list of all files that have changed as part of the specified revision(s). # --------------------------------------------------------------------------- \
         Generates (line, revision, text) for each line in the specified file. # ---------------------------------------------------------------------- \
         Enumerates the fullpaths of all files tracked under the current repo. # --------------------------------------------------------------------------- # | # |  Protected Methods # | # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # | # |  Private Methods # | # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- Resets the repo to the remote state, erasing any unpushed (but committed) changes. # --------------------------------------------------------------------------- Are there changes that have been pulled but have yet to be updated. # --------------------------------------------------------------------------- Are there changes yet to be pushed # --------------------------------------------------------------------------- Returns a list of changes that have yet to be pushed # --------------------------------------------------------------------------- Are there changes to be pulled # --------------------------------------------------------------------------- Returns a list of changes that have yet to be pulled # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # | # |  Methods # | # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- \
         No SCMs are active for '{repo_root}'.
 
         Available SCMs are:
         {available}
 
         Potential SCMs are:
         {potential} # ---------------------------------------------------------------------- # <Unable to import> pylint: disable = F0401 # ---------------------------------------------------------------------------