# Source code for hgapi.hgapi

    # -*- coding: utf-8 -*-
    from __future__ import print_function, unicode_literals, with_statement
    from subprocess import Popen, PIPE

    try:
        from urllib import unquote
    except ImportError:  # python 3
        from urllib.parse import unquote

    import re
    import os
    import sys

    try:
        import json  # for reading logs
    except ImportError:
        import simplejson as json

    class HgException(Exception):
        """
            Exception class allowing a exit_code parameter and member
            to be used when calling Mercurial to return exit code.
        """

        def __init__(self, msg, exit_code=None):
            super(HgException, self).__init__(msg)
            self.exit_code = exit_code

    class Revision(object):
        """
            A representation of a revision.
            Available fields are::

                node, rev, author, branch, parents, date, tags, desc

            A Revision object is equal to any other object with the
            same value for node.
        """

        def __init__(self, json_log):
            """Create a Revision object from a JSON representation"""
            rev = json.loads(json_log)

            for key in rev.keys():
                if sys.version_info.major < 3:
                    _value = unquote(rev[key].encode("ascii")).decode("utf-8")
                else:
                    _value = unquote(rev[key])
                self.__setattr__(key, _value)
            self.rev = int(self.rev)
            if not self.branch:
                self.branch = 'default'
            if not self.parents:
                self.parents = [int(self.rev) - 1]
            else:
                self.parents = [int(p.split(':')[0]) for p in self.parents.split()]

        def __iter__(self):
            return self

        def __eq__(self, other):
            """Returns true if self.node == other.node."""
            return self.node == other.node

    class Repo(object):
        """A representation of a Mercurial repository."""

        def __init__(self, path, user=None):
            """Create a Repo object from the repository at path."""
            self.path = path
            self.cfg = False
            self.user = user

        _env = os.environ.copy()
        _env[str('LANG')] = str('en_US')

        @classmethod
        def command(cls, path, env, *args):
            """
                Run a hg command in path and return the result.

                Raise on error.
            """
            cmd = ["hg", "--cwd", path, "--encoding", "UTF-8"] + list(args)
            proc = Popen(cmd,
                         stdout=PIPE, stderr=PIPE, env=env)

            out, err = [x.decode("utf-8", "replace") for x in proc.communicate()]

            if proc.returncode:
                cmd = " ".join(cmd)
                raise HgException("Error running %s:\n"
                                  "\tErr: %s\n"
                                  "\tOut: %s\n"
                                  "\tExit: %s"
                                  % (cmd, err, out, proc.returncode),
                                  exit_code=proc.returncode)

            return out

        def __getitem__(self, rev=slice(0, 'tip')):
            """
                Get a Revision object for the revision identified by rev.

                rev can be a range (6c31a9f7be7ac58686f0610dd3c4ba375db2472c:tip)
                a single changeset id or it can be left blank to indicate
                the entire history.
            """
            if isinstance(rev, slice):
                return self.revisions(rev)
            return self.revision(rev)

        def hg_command(self, *args):
            """Run a hg command."""
            return Repo.command(self.path, self._env, *args)

        def hg_init(self):
            """Initialize a new repo."""
            self.hg_command("init")

        def hg_id(self):
            """Get the output of the hg id command (truncated node)."""
            res = self.hg_command("id", "-i")
            return res.strip("\n +")

        def hg_rev(self):
            """Get the revision number of the current revision."""
            res = self.hg_command("id", "-n")
            str_rev = res.strip("\n +")
            return int(str_rev)

        def hg_add(self, filepath=None):
            """
                Add a file to the repo.

                when no filepath is given, all files are added to the repo.
            """
            if filepath is None:
                self.hg_command("add")
            else:
                self.hg_command("add", filepath)

        def hg_addremove(self, filepath=None):
            """
                Add a file to the repo.

                When no filepath is given, all files are added and removed
                to and respectively from the repo.
            """
            if filepath is None:
                self.hg_command("addremove")
            else:
                self.hg_command("addremove", filepath)

        def hg_remove(self, filepath):
            """Remove a file from the repo"""
            self.hg_command("remove", filepath)

        def hg_move(self, source, destination):
            """Move a file in the repo."""
            self.hg_command("move", source, destination)

        def hg_rename(self, source, destination):
            """
                Move a file in the repo.
                This is hg_more.
            """
            return self.hg_move(source, destination)

        def hg_update(self, reference, clean=False):
            """Update to the revision identified by reference."""
            cmd = ["update", str(reference)]
            if clean:
                cmd.append("--clean")
            self.hg_command(*cmd)

        def hg_tag(self, *tags, **kwargs):
            """
                Add one or more tags to the current revision.

                Add one or more tags to the current revision, or revision given by
                passing 'rev' as a keyword argument::

              >>> repo.hg_tag('mytag', rev=3)
            """
            rev = kwargs.get('rev')
            cmd = ['tag'] + list(tags)
            if rev:
                cmd += ['-r', str(rev)]
            self.hg_command(*cmd)

        def hg_tags(self):
            """
                Get all tags from the repo.

                Returns a dict containing tag: shortnode mapping
            """
            cmd = ['tags']
            output = self.hg_command(*cmd)
            res = {}
            reg_expr = "(?P<tag>.+\S)\s+(?P<rev>\d+):(?P<changeset>\w+)"
            pattern = re.compile(reg_expr)
            for row in output.strip().split('\n'):
                match = pattern.match(row)
                tag = match.group("tag")
                changeset = match.group("changeset")
                res[tag] = changeset
            return res

        def hg_heads(self, short=False):
            """
                Get a list with the node identifiers of all open heads.
                If short is given and is not False, return the short
                form of the node id.
            """
            template = "{node}\n" if not short else "{node|short}\n"
            res = self.hg_command("heads", "--template", template)
            return [head for head in res.split("\n") if head]

        def hg_merge(self, reference, preview=False):
            """
                Merge reference to current.

                With 'preview' set to True get a list of revision numbers
                containing all revisions that would have been merged.
            """
            if not preview:
                return self.hg_command("merge", reference)
            else:
                revno_re = re.compile('^changeset: (\d+):\w+$')
                out = self.hg_command("merge", "-P", reference)
                revs = []
                for row in out:
                    match = revno_re.match(row)
                    if match:
                        revs.append(match.group(1))
                return revs

        def hg_revert(self, all=False, *files):
            """Revert repository."""
            if all:
                cmd = ["revert", "--all"]
            else:
                cmd = ["revert"] + list(files)
            self.hg_command(*cmd)

        def hg_node(self):
            """Get the full node id of the current revision."""
            res = self.hg_command("log", "-r", self.hg_id(),
                                  "--template", "{node}")
            return res.strip()

        def hg_commit(self, message, user=None, date=None, files=[],
                      close_branch=False, amend=False, message_file=None):
            """Commit changes to the repository."""
            userspec = "-u" + user if user \
                else "-u" + self.user if self.user else ""
            datespec = "-d" + date if date else ""
            close = "--close-branch" if close_branch else ""
            amendspec = "--amend" if amend else ""
            msg = ("-m", message)
            if message_file is not None:
                msg = ("-l", message_file)
            args = [amendspec, close, userspec, datespec] + files
            # don't send a "" arg for userspec or close, which HG will
            # consider the files arg, committing all files instead of what
            # was passed in files kwarg
            args = [arg for arg in args if arg]
            self.hg_command("commit", msg[0], msg[1], *args)

        def hg_push(self, destination=None):
            """Push changes from this repo."""
            if destination is None:
                self.hg_command("push")
            else:
                self.hg_command("push", destination)

        def hg_pull(self, source=None):
            """Pull changes to this repo."""
            if source is None:
                self.hg_command("pull")
            else:
                self.hg_command("pull", source)

        def hg_paths(self):
            """Get remote repositories."""
            remotes = self.hg_command("paths").split("\n")
            remotes_list = [line.split(" = ") for line in remotes if line != ""]

            return dict(remotes_list)

        def __get_remote_changes(self, command, remote):
            if remote not in self.hg_paths().keys():
                raise HgException("No such remote repository")

            try:
                result = self.hg_command(
                    command,
                    remote,
                    "--template",
                    self.rev_log_tpl
                ).split("\n")
            except HgException:
                return []

            changesets = [change for change in result if change.startswith("{")]
            return list(map(lambda revision: Revision(revision), changesets))

        def hg_outgoing(self, remote="default"):
            """Get outgoing changesets for a certain remote."""
            return self.__get_remote_changes("outgoing", remote)

        def hg_incoming(self, remote="default"):
            """Get incoming changesets for a certain remote."""
            return self.__get_remote_changes("incoming", remote)

        def hg_log(self, identifier=None, limit=None, template=None,
                   branch=None, **kwargs):
            """Get repositiory log."""
            cmds = ["log"]
            if identifier:
                cmds += ['-r', str(identifier)]
            if branch:
                cmds += ['-b', str(branch)]
            if limit:
                cmds += ['-l', str(limit)]
            if template:
                cmds += ['--template', str(template)]
            if kwargs:
                for key in kwargs:
                    cmds += [key, kwargs[key]]
            log = self.hg_command(*cmds)
            return log

        def hg_branch(self, branch_name=None):
            """
                Create a branch or get a branch name.

                If branch_name is not None, the branch is created.
                Otherwise the current branch name is returned.
            """
            args = []
            if branch_name:
                args.append(branch_name)
            branch = self.hg_command("branch", *args)
            return branch.strip()

        def get_branches(self):
            """
                Returns a list of branches from the repo, including versions.

                If get_active_only is True, then only return active branches.
            """
            branches = self.hg_command("branches")
            branch_list = branches.strip().split("\n")
            values = []
            for branch in branch_list:
                b = re.split('(\d+:[A-Za-z0-9]+)', branch)
                if not b:
                    continue
                values.append({'name': b[0].strip(), 'version': b[1].strip()})
            return values

        def get_branch_names(self):
            """ Returns a list of branch names from the repo. """
            branches = self.hg_command("branches")
            branch_list = branches.strip().split("\n")
            values = []
            for branch in branch_list:
                b = re.split('(\d+:[A-Za-z0-9]+)', branch)
                if not b:
                    continue
                name = b[0]
                if name:
                    name = name.strip()
                    values.append(name)
            return values

        BOOKMARK_LIST = 0
        BOOKMARK_CREATE = 1
        BOOKMARK_DELETE = 2
        BOOKMARK_RENAME = 3
        BOOKMARK_INACTIVE = 4

        def hg_bookmarks(self, action=BOOKMARK_LIST, name=None, newname=None,
                         revision=None, force=False):
            cmds = ['bookmarks']
            if force:
                cmds += ['--force']
            if revision:
                cmds += ['--rev', str(revision)]
            if action == Repo.BOOKMARK_LIST:
                out = self.hg_command(*cmds)
                bookmarks = []
                if out.startswith(" "):  # handles "no bookmarks set" reply
                    for line in out.split('\n'):
                        if line:
                            # active/inactive
                            if line.strip()[0] == '*':
                                bookmark = [True]
                                line = line[3:]
                            else:
                                bookmark = [False]
                            # name and identifier
                            line.split()
                            bookmark += [line.split()[0].strip(), line.split()[1]]
                            bookmarks += [bookmark]
                return bookmarks
            elif action == Repo.BOOKMARK_INACTIVE:
                cmds += ['--inactive']
                if name:
                    cmds += [name]
                return self.hg_command(*cmds)
            elif name is not None:
                if action == Repo.BOOKMARK_DELETE:
                    cmds += ['--delete', name]
                    return self.hg_command(*cmds)
                elif action == Repo.BOOKMARK_RENAME and newname is not None:
                    cmds += ['--rename', name, newname]
                    return self.hg_command(*cmds)
                elif action == Repo.BOOKMARK_CREATE:
                    cmds += [name]
                    return self.hg_command(*cmds)

        def hg_diff(self, rev_a=None, rev_b=None, filenames=None):
            """
                Get a unified diff as returned by 'hg diff'.

                rev_a and rev_b are passed as -r <rev> arguments to the call,
                filenames are expected to be an iterable of file names.

                Returns a list of dicts where every dict has a 'filename'
                and 'diff' field, where with diff being the complete diff
                for the file including header (diff -r xxxx -r xxx...).
            """
            cmds = ['diff']
            for rev in (rev_a, rev_b):
                if rev is not None:
                    cmds += ['-r', rev]

            if filenames is not None:
                cmds += list(filenames)

            result = self.hg_command(*cmds)
            diffs = []
            if result:
                filere = re.compile("^diff .* (\S+)$")
                for line in result.split('\n'):
                    match = filere.match(line)
                    if match:
                        diffs.append({'filename': match.groups()[0], 'diff': ''})
                    diffs[-1]['diff'] += line + '\n'
            return diffs

        def hg_status(self, empty=False, clean=False):
            """
                Get repository status.

                Returns a dict containing a *change char* -> *file list*
                mapping, where change char is in::

                 A, M, R, !, ?

                Example after adding one.txt, modifying a_folder/two.txt
                and three.txt::

                 {'A': ['one.txt'], 'M': ['a_folder/two.txt', 'three.txt'],
                 '!': [], '?': [], 'R': []}

                If empty is set to non-False value, don't add empty lists.
                If clean is set to non-False value, add clean files as well (-A)
            """
            cmds = ['status']
            if clean:
                cmds.append('-A')
            out = self.hg_command(*cmds).strip()
            # default empty set
            if empty:
                changes = {}
            else:
                changes = {'A': [], 'M': [], '!': [], '?': [], 'R': []}
                if clean:
                    changes['C'] = []

            if not out:
                return changes
            lines = out.split("\n")
            status_split = re.compile("^(.) (.*)$")

            for change, path in [status_split.match(x).groups() for x in lines]:
                changes.setdefault(change, []).append(path)
            return changes

        def hg_archive(self, destination, revision=None, archive_type=None):
            """
                Archive a repository.

                Creates an archive of a single revision in the specified
                destination.

                If revision is not supplied the default is the parent of the
                repository's working directory (tip).

                If archive_type is not supplied mercurial will determine the
                type based on the file extension. If there is no file extension
                the default is "files".
            """
            cmds = ['archive']

            if archive_type is not None:
                cmds.extend(('-t', archive_type))

            if revision is not None and revision != "tip":
                cmds.extend(('-r', revision))

            cmds.append(destination)

            self.hg_command(*cmds)

        rev_log_tpl = (
            '\{"node":"{node|short}","rev":"{rev}","author":"{author|urlescape}",'
            '"branch":"{branches}","parents":"{parents}","date":"{date|isodate}",'
            '"tags":"{tags}","desc":"{desc|urlescape}\"}\n'
        )

        def revision(self, identifier):
            """Get the identified revision as a Revision object."""
            out = self.hg_log(identifier=str(identifier),
                              template=self.rev_log_tpl)
            return Revision(out)

        def revisions(self, slice_):
            """Returns a list of Revision objects for the given slice"""
            id = ":".join([str(x) for x in (slice_.start, slice_.stop)])
            out = self.hg_log(identifier=id,
                              template=self.rev_log_tpl)

            revs = []
            for entry in out.split('\n')[:-1]:
                revs.append(Revision(entry))

            return revs

        def read_config(self):
            """
                Read the configuration as seen with 'hg showconfig'.

                Is called by __init__ - only needs to be called explicitly
                to reflect changes made since instantiation.
            """
            res = self.hg_command("showconfig")
            cfg = {}
            for row in res.split("\n"):
                section, ign, value = row.partition("=")
                main, ign, sub = section.partition(".")
                sect_cfg = cfg.setdefault(main, {})
                sect_cfg[sub] = value.strip()
            self.cfg = cfg
            return cfg

        def config(self, section, key):
            """Return the value of a configuration variable."""
            if not self.cfg:
                self.cfg = self.read_config()
            return self.cfg.get(section, {}).get(key, None)

        def configbool(self, section, key):
            """
                Return a config value as a boolean value.

                Empty values, the string 'false' (any capitalization),
                and '0' are considered False, anything else is True
            """
            if not self.cfg:
                self.cfg = self.read_config()
            value = self.cfg.get(section, {}).get(key, None)
            if not value:
                return False
            if value == "0" or value.upper() == "FALSE" or value.upper() == "None":
                return False
            return True

        def configlist(self, section, key):
            """
                Return a config value as a list.

                Will try to create a list delimited by commas, or whitespace if
                no commas are present.
            """
            if not self.cfg:
                self.cfg = self.read_config()
            value = self.cfg.get(section, {}).get(key, None)
            if not value:
                return []
            if value.count(","):
                return value.split(",")
            else:
                return value.split()

        @classmethod
        def hg_version(cls):
            """Return the version number of Mercurial."""
            out = Repo.command(".", os.environ, "version")
            match = re.search('\s\(version (.*)\)$', out.split("\n")[0])
            return match.group(1)

        @classmethod
        def hg_clone(cls, url, path, *args):
            """
                Clone repository at given `url` to `path`, then return
                repo object to `path`.
            """
            Repo.command(".", os.environ, "clone", url, path, *args)
            return Repo(path)

        @classmethod
        def hg_root(self, path):
            """
                Return the root (top) of the path.

                When no path is given, current working directory is used.
                Raises HgException when no repo is available.
            """
            if path is None:
                path = os.getcwd()
            return Repo.command(path, os.environ, "root").strip("\n +")