#
# This file is part of `gitflow`.
# Copyright (c) 2010-2011 Vincent Driessen
# Copyright (c) 2012 Hartmut Goebel
# Distributed under a BSD-like license. For full terms see the file LICENSE.txt
#

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

from git import GitCommandError, Reference
from gitflow.exceptions import (NoSuchBranchError, BranchExistsError,
                                PrefixNotUniqueError, BaseNotOnBranch,
                                WorkdirIsDirtyError, BranchTypeExistsError,
                                TagExistsError, MergeError, BaseNotFound)

__copyright__ = "2010-2011 Vincent Driessen; 2012 Hartmut Goebel"
__license__ = "BSD"

class BranchManager(object):
    """
    Initializes an instance of :class:`BranchManager`.  A branch
    manager is responsible for listing, creating, merging, deleting,
    finishing (i.e. merging+deleting) branches of a given type.

    :param gitflow:
        The :class:`gitflow.core.GitFlow` instance that this branch
        manager belongs to.

    :param prefix:
        The prefix to use for the type of branches that this branch
        manager manages. If this is not `None`, it supersedes the
        configuration of the `gitflow` object's repository.
    """

    def _get_prefix(self):
        if self._prefix is not None:
            return self._prefix
        try:
            return self.gitflow.get_prefix(self.identifier)
        except:
            return self.DEFAULT_PREFIX
    def _set_prefix(self, value): self._prefix = value
    prefix = property(_get_prefix, _set_prefix)

    def __init__(self, gitflow, prefix=None):
        from gitflow.core import GitFlow
        assert isinstance(gitflow, GitFlow), "Argument 'gitflow' must be a GitFlow instance."
        self.gitflow = gitflow
        if not prefix is None:
            assert isinstance(prefix, basestring), "Argument 'prefix' must be a string."
            self.prefix = prefix
        else:
            self.prefix = None

    def default_base(self):
        """
        :returns:
            The name of branch to use as the default base for branching off from
            in case no explicit base is specified.

        This method can be overriden in a subclass of :class:`BranchManager`.
        If not overriden, the default is to use the 'develop' branch.
        """
        return self.gitflow.develop_name()

    def full_name(self, name):
        return self.prefix + name

    def shorten(self, full_name):
        """
        Returns the friendly (short) name of this branch, without the prefix,
        given the fully qualified branch name.

        :param full_name:
            The full name of the branch as it is known to Git, including the
            prefix.

        :returns:
            The friendly name of the branch.
        """
        if full_name.startswith(self.prefix):
            return full_name[len(self.prefix):]
        else:
            return full_name

    def by_name_prefix(self, nameprefix, remote=False):
        """
        If exactly one branch of the type that this manager manages starts with
        the given name prefix, returns that branch.  Raises
        :exc:`NoSuchBranchError` in case no branches exist with the given
        prefix, or :exc:`PrefixNotUniqueError` in case multiple matches are
        found.

        :param nameprefix:
            The name prefix (or full name) of the short branch name to match.

        :returns:
            The :class:`git.refs.Head` instance of the branch that can be
            uniquely identified by the given name prefix.
        """
        if remote:
            nameprefix = self.gitflow.origin_name(self.full_name(nameprefix))
        else:
            nameprefix = self.full_name(nameprefix)
        matches = [b
                   for b in self.iter(remote)
                   if b.name.startswith(nameprefix)]
        num_matches = len(matches)
        if num_matches == 1:
            return matches[0]
        elif num_matches < 1:
            raise NoSuchBranchError('There is no %s branch matching the '
                    'prefix "%s"' % (self.identifier, nameprefix))
        else:
            raise PrefixNotUniqueError('There are multiple %s branches '
                    'matching the prefix "%s": %s' % (self.identifier,
                        nameprefix, matches))

    def iter(self, remote=False):
        """
        :returns:
            An iterator, iterating over all branches of the type that this
            manager manages.
        """
        if remote:
            refs = self.gitflow.repo.refs
            prefix = self.gitflow.origin_name(self.prefix)
        else:
            refs = self.gitflow.repo.branches
            prefix = self.prefix

        for r in refs:
            if r.name.startswith(prefix):
                yield r

    def list(self, remote=False):
        """
        :returns:
            A list of all branches of the type that this manager manages.  See
            also :meth:`iter`.
        """
        return list(self.iter(remote))

    def create(self, name, base=None, fetch=False,
               must_be_on_default_base=False):
        """
        Creates a branch of the type that this manager manages and checks it
        out.

        :param name:
            The (short) name of the branch to create.

        :param base:
            The base commit or ref to base the branch off from.  If a base is
            not provided explicitly, the default base for this type of branch is
            used.  See also :meth:`default_base`.

        :param fetch:
            If set, update the local repo with remote changes prior to
            creating the new branch.

        :param must_be_on_default_base:
            If set, the `base` must be a valid commit on the branch
            manager `default_base`.

        :returns:
            The newly created :class:`git.refs.Head` reference.
        """
        gitflow = self.gitflow
        repo = gitflow.repo

        if fetch:
            gitflow.origin().fetch()

        # Make sure the branch exists neither locally nor remotely.
        full_name = self.full_name(name)
        origin_name = gitflow.origin_name(full_name)
        for r in repo.refs:
            if r.name in (full_name, origin_name):
                raise BranchExistsError(full_name)

        # Make sure the base exists and is in sync.
        if base is None:
            base = self.default_base()
        elif must_be_on_default_base:
            if not gitflow.is_merged_into(base, self.default_base()):
                raise BaseNotOnBranch(base, self.default_base())

        origin_base = gitflow.origin_name(base)
        if base in repo.refs:
            if origin_base in repo.refs:
                gitflow.require_branches_equal(origin_base, base)
        elif origin_base in repo.refs:
            base = origin_base
        else:
            raise BaseNotFound('Base branch {0} not found.'.format(base))

        # Make sure there are no merge conflicts.
        gitflow.require_no_merge_conflict()
        if gitflow.has_staged_commits():
            raise WorkdirIsDirtyError('Contains local changes checked into '
                                      'the index but not committed.')

        # Finally, create and checkout.
        branch = repo.create_head(full_name, base)
        branch.checkout()
        return branch

    def _is_single_commit_branch(self, from_, to):
        git = self.gitflow.repo.git
        commits = git.rev_list('%s...%s' % (from_, to), n=2).split()
        return len(commits) == 1

    def merge(self, name, into, message=None):
        """
        This merges the branch named :attr:`name` into the branch named
        :attr:`into`, using commit message :attr:`message`.

        :param name:
            The (short) name of the branch that needs merging.  Alternatively
            this is a instance of git.Reference (or subclass).
        :param into:
            The name of the branch to merge into.
        :param message:
            The commit message to use for the merge commit.  If it is not given,
            a default merge message is used.  You can use the following string
            placeholders, which :meth:`merge` will expand::

                %(name)s = The full name of the branch (e.g. 'feature/foo')
                %(short_name)s = The friendly name of the branch (e.g. 'foo')
                %(identifier)s = The type (e.g. 'feature', 'hotfix', etc.)

        You typically don't need to override this method in a subclass.
        """
        if isinstance(name, basestring):
            full_name = self.prefix + name
        else:
            assert isinstance(name, Reference)
            full_name = name
        repo = self.gitflow.repo
        repo.branches[into].checkout()
        if self.gitflow.is_merged_into(full_name, into):
            # already merged, nothing more to do
            return

        kwargs = dict(no_ff=True)
        if message is not None:
            message = (message
                       % dict(name=full_name, identifier=self.identifier,
                              short_name=name))
            kwargs['message'] = message
        # `git merge` does not send the error message to stderr, thus
        # we need to capture stdout manually :-(
        stdout = StringIO()
        try:
            repo.git.merge(full_name, output_stream=stdout, **kwargs)
        except GitCommandError, e:
            txt = stdout.getvalue().rstrip()
            if e.stderr:
                text = text + '\n' + e.stderr
            raise MergeError(txt)

    def delete(self, name, force=False):
        """
        This deletes a branch of the type that this manager manages named
        :attr:`name`.

        :param name:
            The (short) name of the branch to delete.

        :param force:
            Delete the branch, even if this would lead to data loss.
        """
        repo = self.gitflow.repo
        full_name = self.prefix + name
        repo.delete_head(full_name, force=force)


class FeatureBranchManager(BranchManager):
    identifier = 'feature'
    DEFAULT_PREFIX = 'feature/'

    def create(self, name, base=None, fetch=False):
        """
        Creates a branch of type `feature` and checks it out.

        :param name:
            The (short) name of the branch to create. This will be
            prefixed by `feature/` or whatever is configured in
            `gitflow.prefix.feature`.

        :param base:
            The base commit or ref to base the branch off from.  If no
            base is provided, it defaults to the branch configured in
            `gitflow.branch.develop`.  See also :meth:`default_base`.

        :param fetch:
            If set, update the local repo with remote changes prior to
            creating the new branch.

        :returns:
            The newly created :class:`git.refs.Head` reference.
        """
        # :todo:rethink: feature branch is not required to start at `develop`
        return super(FeatureBranchManager, self).create(
            name, base, fetch=fetch, must_be_on_default_base=False)


    def finish(self, name, fetch=False, upstream='default', rebase=False,
               keep=False, force_delete=False, push=False, tagging_info=None):
        """
        Finishes the branch of type `feature` named :attr:`name`.
        Finishing means that:

        * The `feature`-branch is merged into the `develop`-branch.
        * The `feature`-branch is deleted if all merges are successful.

        :param name:
            The (short) name of the branch to finish.
        :param fetch:
            If set, update the local repo with remote changes prior to
            merging.
        :param rebase:
            Rebase `featuer`-branch prior to merging.
        :param keep:
            Keep `feature`-branch after performing finish.
        :param force_delete:
            Force deleting the `feature`-branch even if merging failed.
        :param push:
            Push changes to the remote repository.
        """
        assert not tagging_info, "FeatureBranchManager does not support tagging"
        gitflow = self.gitflow
        full_name = self.full_name(name)
        gitflow.must_be_uptodate(full_name, fetch=fetch)
        gitflow.must_be_uptodate(upstream, fetch=fetch)

        if rebase:
            gitflow.rebase(self.identifier, name, interactive=False,
                           upstream=upstream)
        else:
            self.merge(name, upstream,
                       'Finished %(identifier)s %(short_name)s.')

        to_push = [upstream]

        if not keep:
            self.delete(name, force=force_delete)
            to_push.append(':'+full_name)
        if push:
            gitflow.origin().push(to_push)


class ReleaseBranchManager(BranchManager):
    identifier = 'release'
    DEFAULT_PREFIX = 'release/'

    def create(self, version, base=None, fetch=False):
        """
        Creates a branch of type `release` and checks it out.

        :param version:
            The version to be released and for which to create the
            release-branch for. This will be prefixed by `release/`
            or whatever is configured in `gitflow.prefix.release`.

        :param base:
            The base commit or ref to base the branch off from.  If no
            base is provided, it defaults to the branch configured in
            `gitflow.branch.develop`.  See also :meth:`default_base`.

        :param fetch:
            If set, update the local repo with remote changes prior to
            creating the new branch.

        :returns:
            The newly created :class:`git.refs.Head` reference.
        """
        gitflow = self.gitflow
        git = gitflow.git

        # there must be no active `release` branch
        if len(self.list()) > 0:
            raise BranchTypeExistsError(self.identifier)

        # there must be no tag for this version yet
        tagname = gitflow.get('gitflow.prefix.versiontag') + version
        if tagname in gitflow.repo.tags:
            raise TagExistsError(tagname)

        # create the release branch
        b = super(ReleaseBranchManager, self).create(
            version, base, fetch=fetch, must_be_on_default_base=True)

        # push the newly created branch
        git.push(gitflow.origin_name(), self.by_name_prefix(version))

        return b


    def finish(self, name, fetch=False, rebase=False, keep=False,
               force_delete=False, push=False, tagging_info=None):
        assert rebase == False, "Rebasing a release branch does not make any sense."
        # require release branch to exist
        # if flag-fetch: fetch master und develop
        #   diese muessen dann gleich $ORIGIN/master bzw. $ORIGIN/develop sein
        gitflow = self.gitflow
        full_name = self.full_name(name)
        gitflow.must_be_uptodate(full_name, fetch=fetch)
        gitflow.must_be_uptodate(gitflow.develop_name(), fetch=fetch)
        gitflow.must_be_uptodate(gitflow.master_name(), fetch=fetch)

        to_push = [self.gitflow.develop_name(), self.gitflow.master_name()]

        self.merge(name, self.gitflow.master_name(),
                'Finished %s %s.' % (self.identifier, name))

        tag = None
        if tagging_info is not None:
            # try to tag the release
            tagname = self.gitflow.get('gitflow.prefix.versiontag') + name
            # In case a previous attempt to finish this release branch
            # has failed, but the tag was set successful, we skip it
            # now.
            # :todo: check: if tag exists, it must point to the commit
            tag = gitflow.tag(tagname, self.gitflow.master_name(),
                        **tagging_info)
            to_push.append(tagname)

        # merge the master branch back into develop; this makes the
        # master branch - and the new tag (if provided) - a parent of
        # the development branch, which in turn lets you use 'git
        # describe' on either branch
        self.merge(tag or self.gitflow.master(),
                   self.gitflow.develop_name(),
                   'Finished %s %s.' % (self.identifier, name))
        if not keep:
            self.delete(name, force=force_delete)
            to_push.append(':'+full_name)
        if push:
            gitflow.origin().push(to_push)


class HotfixBranchManager(ReleaseBranchManager):
    identifier = 'hotfix'
    DEFAULT_PREFIX = 'hotfix/'

    def default_base(self):
        return self.gitflow.master_name()


class SupportBranchManager(BranchManager):
    identifier = 'support'
    DEFAULT_PREFIX = 'support/'

    def default_base(self):
        return self.gitflow.master_name()

    def create(self, name, base=None, fetch=False):
        """
        Creates a branch of type `support` and checks it out.

        :param name:
            The (short) name of the branch to create. This will be
            prefixed by `support/` or whatever is configured in
            `gitflow.prefix.support`.

        :param base:
            The base commit or ref to base the branch off from.  If no
            base is provided, it defaults to the branch configured in
            `gitflow.branch.develop`.  See also :meth:`default_base`.

        :param fetch:
            If set, update the local repo with remote changes prior to
            creating the new branch.

        :returns:
            The newly created :class:`git.refs.Head` reference.
        """
        return super(SupportBranchManager, self).create(
            name, base, fetch=fetch, must_be_on_default_base=True)

    def finish(self, *args):
        raise NotImplementedError("Finishing support branches does not make "
                "any sense.")
