#!/usr/bin/env python2
#
# Copyright (c) 2012 Felipe Contreras
#

# Inspired by Rocco Rutte's hg-fast-export

# Just copy to your ~/bin, or anywhere in your $PATH.
# Then you can clone with:
# git clone hg::/path/to/mercurial/repo/
#
# For remote repositories a local clone is stored in
# "$GIT_DIR/hg/origin/clone/.hg/".

from mercurial import hg, ui, bookmarks, context, encoding
from mercurial import node, error, extensions, discovery, util
from mercurial import changegroup

import re
import sys
import os
import posixpath
try:
    import json
except ImportError:
    import simplejson as json 
import shutil
import subprocess
import urllib.request, urllib.parse, urllib.error
import atexit
import urllib.parse
import hashlib
import time as ptime

#
# If you want to see Mercurial revisions as Git commit notes:
# git config core.notesRef refs/notes/hg
#
# If you are not in hg-git-compat mode and want to disable the tracking of
# named branches:
# git config --global remote-hg.track-branches false
#
# If you want the equivalent of hg's clone/pull--insecure option:
# git config --global remote-hg.insecure true
#
# If you want to switch to hg-git compatibility mode:
# git config --global remote-hg.hg-git-compat true
#


# The 'mode' variable indicates whether we are in 'git' or 'hg' compatibility mode.
# This has the following effects:
#
# git:
# Sensible defaults for git.
# hg bookmarks are exported as git branches, hg branches are prefixed
# with 'branches/', HEAD is a special case.
#
# hg:
# Emulate hg-git.
# Only hg bookmarks are exported as git branches.
# Commits are modified to preserve hg information and allow bidirectionality.
#
mode = 'git'

DEBUG_REMOTEHG = os.environ.get("DEBUG_REMOTEHG") != None

NAME_RE = re.compile('^([^<>]+)')
AUTHOR_RE = re.compile('^([^<>]+?)? ?[<>]([^<>]*)(?:$|>)')
EMAIL_RE = re.compile(r'([^ \t<>]+@[^ \t<>]+)')
AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.*))?$')
RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')

VERSION = 2


def die(msg, *args):
    sys.stderr.write('ERROR: %s\n' % (msg % args))
    sys.exit(1)

def warn(msg, *args):
    sys.stderr.write('WARNING: %s\n' % (msg % args))

# convert between git timezone and mercurial timezone data.
# git stores timezone as a string in the form "+HHMM" or "-HHMM".
# Mercurial instead encodes the timezone as an offset to UTC in seconds.
# This offset has the opposite sign from the one used by git.
#
# To convert between the two, we have to be a bit careful, as
# dividing a negativ integer in python 2 rounds differently than
# in C.

# hg->git: convert timezone offset in seconds to string '+HHMM' or '-HHMM'
def gittz(tz):
    if tz < 0:
        tz = -tz
        sign = +1
    else:
        sign = -1
    hours = tz / 3600
    minutes = tz % 3600 / 60
    return '%+03d%02d' % (sign * hours, minutes)

# git->hg: convert timezone string '+HHMM' or '-HHMM' to time offset in seconds
def hgtz(tz):
    tz = int(tz)
    if tz < 0:
        tz = -tz
        sign = +1
    else:
        sign = -1
    tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
    return sign * tz

# hg->git: convert hg flags to git file mode
def gitmode(flags):
    return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'

# git->hg: convert git file mode to hg flags
def hgmode(mode):
    m = { '100755': 'x', '120000': 'l' }
    return m.get(mode, '')

def hghex(n):
    return node.hex(n)

def hgbin(n):
    return node.bin(n)

def hgref(ref):
    return ref.replace('___', ' ')

def gitref(ref):
    return ref.replace(' ', '___')

def check_version(*check):
    if not hg_version:
        return True
    return hg_version >= check

def get_config(config):
    cmd = ['git', 'config', '--get', config]
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    output, _ = process.communicate()
    return output

def get_config_bool(config, default=False):
    value = get_config(config).rstrip('\n')
    if value == "true":
        return True
    elif value == "false":
        return False
    else:
        return default

# Handle loading and storing git marks files
class Marks:

    def __init__(self, path, repo):
        self.path = path
        self.repo = repo
        self.clear()
        self.load()

        if self.version < VERSION:
            if self.version == 1:
                self.upgrade_one()

            # upgraded?
            if self.version < VERSION:
                self.clear()
                self.version = VERSION

    def clear(self):
        self.tips = {}
        self.marks = {}
        self.rev_marks = {}
        self.last_mark = 0
        self.version = 0
        self.last_note = 0

    def load(self):
        if not os.path.exists(self.path):
            return

        tmp = json.load(open(self.path))

        self.tips = tmp['tips']
        self.marks = tmp['marks']
        self.last_mark = tmp['last-mark']
        self.version = tmp.get('version', 1)
        self.last_note = tmp.get('last-note', 0)

        for rev, mark in self.marks.items():
            self.rev_marks[mark] = rev

    def upgrade_one(self):
        def get_id(rev):
            return hghex(self.repo.changelog.node(int(rev)))
        self.tips = dict((name, get_id(rev)) for name, rev in self.tips.items())
        self.marks = dict((get_id(rev), mark) for rev, mark in self.marks.items())
        self.rev_marks = dict((mark, get_id(rev)) for mark, rev in self.rev_marks.items())
        self.version = 2

    def dict(self):
        return { 'tips': self.tips, 'marks': self.marks,
                'last-mark': self.last_mark, 'version': self.version,
                'last-note': self.last_note }

    def store(self):
        json.dump(self.dict(), open(self.path, 'w'))

    def __str__(self):
        return str(self.dict())

    def from_rev(self, rev):
        return self.marks[rev]

    def to_rev(self, mark):
        return str(self.rev_marks[mark])

    def next_mark(self):
        self.last_mark += 1
        return self.last_mark

    def get_mark(self, rev):
        self.last_mark += 1
        self.marks[rev] = self.last_mark
        return self.last_mark

    def new_mark(self, rev, mark):
        self.marks[rev] = mark
        self.rev_marks[mark] = rev
        self.last_mark = mark

    def is_marked(self, rev):
        return rev in self.marks

    def get_tip(self, branch):
        return str(self.tips[branch])

    def set_tip(self, branch, tip):
        self.tips[branch] = tip

# Parser class for the input git feeds to us. See
# - http://git-scm.com/docs/gitremote-helpers
# - http://git-scm.com/docs/git-fast-import
class Parser:

    def __init__(self, repo):
        self.repo = repo
        self.line = self.get_line()

    def get_line(self):
        return sys.stdin.readline().strip()

    def __getitem__(self, i):
        return self.line.split()[i]

    def check(self, word):
        return self.line.startswith(word)

    def each_block(self, separator):
        while self.line != separator:
            yield self.line
            self.line = self.get_line()

    def __iter__(self):
        return self.each_block('')

    def __next__(self):
        self.line = self.get_line()
        if self.line == 'done':
            self.line = None

    def get_mark(self):
        i = self.line.index(':') + 1
        return int(self.line[i:])

    def get_data(self):
        if not self.check('data'):
            return None
        i = self.line.index(' ') + 1
        size = int(self.line[i:])
        return sys.stdin.read(size)

    def get_author(self):
        ex = None
        m = RAW_AUTHOR_RE.match(self.line)
        if not m:
            return None
        _, name, email, date, tz = m.groups()
        if name and 'ext:' in name:
            m = re.match('^(.+?) ext:\((.+)\)$', name)
            if m:
                name = m.group(1)
                ex = urllib.parse.unquote(m.group(2))

        if email != bad_mail:
            if name:
                user = '%s <%s>' % (name, email)
            else:
                user = '<%s>' % (email)
        else:
            user = name

        if ex:
            user += ex

        return (user, int(date), hgtz(tz))

# hg->git: helper function for export_files, which modifies the given
# file path, taken from a hg commit, for use in a fast-import stream.
def fix_file_path(path):
    path = posixpath.normpath(path)
    if not os.path.isabs(path):
        return path
    return posixpath.relpath(path, '/')

# hg->git: export files from hg to git. That is, read them from the hg
# repository, and print them into the fast-import stream we are feeding
# to git.
def export_files(files):
    final = []
    for f in files:
        fid = node.hex(f.filenode())

        if fid in filenodes:
            mark = filenodes[fid]
        else:
            mark = marks.next_mark()
            filenodes[fid] = mark
            d = f.data()

            print("blob")
            print("mark :%u" % mark)
            print("data %d" % len(d))
            print(d)

        path = fix_file_path(f.path())
        final.append((gitmode(f.flags()), mark, path))

    return final

# hg->git: determine which files changed on the hg side.
def get_filechanges(repo, ctx, parent):
    modified = set()
    added = set()
    removed = set()

    # load earliest manifest first for caching reasons
    prev = parent.manifest().copy()
    cur = ctx.manifest()

    for fn in cur:
        if fn in prev:
            if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
                modified.add(fn)
            del prev[fn]
        else:
            added.add(fn)
    removed |= set(prev.keys())

    return added | modified, removed

def fixup_user_git(user):
    name = mail = None
    user = user.replace('"', '')
    m = AUTHOR_RE.match(user)
    if m:
        name = m.group(1)
        mail = m.group(2).strip()
    else:
        m = EMAIL_RE.match(user)
        if m:
            mail = m.group(1)
        else:
            m = NAME_RE.match(user)
            if m:
                name = m.group(1).strip()
    return (name, mail)

def fixup_user_hg(user):
    def sanitize(name):
        # stole this from hg-git
        return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))

    m = AUTHOR_HG_RE.match(user)
    if m:
        name = sanitize(m.group(1))
        mail = sanitize(m.group(2))
        ex = m.group(3)
        if ex:
            name += ' ext:(' + urllib.parse.quote(ex) + ')'
    else:
        name = sanitize(user)
        if '@' in user:
            mail = name
        else:
            mail = None

    return (name, mail)

def fixup_user(user):
    if mode == 'git':
        name, mail = fixup_user_git(user)
    else:
        name, mail = fixup_user_hg(user)

    if not name:
        name = bad_name
    if not mail:
        mail = bad_mail

    return '%s <%s>' % (name, mail)

# helper function for get_repo
def updatebookmarks(repo, peer):
    remotemarks = peer.listkeys('bookmarks')
    localmarks = repo._bookmarks

    if not remotemarks:
        return

    for k, v in remotemarks.items():
        localmarks[k] = hgbin(v)

    if hasattr(localmarks, 'write'):
        localmarks.write()
    else:
        bookmarks.write(repo)

# instantiate a Mercurial "repo" object so that
# we can interact with the hg repository.
# Also pulls remote changes.
def get_repo(url, alias):
    global peer

    myui = ui.ui()
    myui.setconfig('ui', 'interactive', 'off')
    myui.fout = sys.stderr

    if get_config_bool('remote-hg.insecure'):
        myui.setconfig('web', 'cacerts', '')

    extensions.loadall(myui)

    if hg.islocal(url) and not os.environ.get('GIT_REMOTE_HG_TEST_REMOTE'):
        repo = hg.repository(myui, url)
        if not os.path.exists(dirname):
            os.makedirs(dirname)
    else:
        shared_path = os.path.join(gitdir, 'hg')

        # check and upgrade old organization
        hg_path = os.path.join(shared_path, '.hg')
        if os.path.exists(shared_path) and not os.path.exists(hg_path):
            repos = os.listdir(shared_path)
            for x in repos:
                local_hg = os.path.join(shared_path, x, 'clone', '.hg')
                if not os.path.exists(local_hg):
                    continue
                if not os.path.exists(hg_path):
                    shutil.move(local_hg, hg_path)
                shutil.rmtree(os.path.join(shared_path, x, 'clone'))

        # setup shared repo (if not there)
        try:
            hg.peer(myui, {}, shared_path, create=True)
        except error.RepoError:
            pass

        if not os.path.exists(dirname):
            os.makedirs(dirname)

        local_path = os.path.join(dirname, 'clone')
        if not os.path.exists(local_path):
            hg.share(myui, shared_path, local_path, update=False)
        else:
            # make sure the shared path is always up-to-date
            util.writefile(os.path.join(local_path, '.hg', 'sharedpath'), hg_path)

        repo = hg.repository(myui, local_path)
        peer = hg.peer(repo.ui, {}, url)

        if check_version(3, 2):
            from mercurial import exchange
            exchange.pull(repo, peer, heads=None, force=True)
        else:
            repo.pull(peer, heads=None, force=True)

        updatebookmarks(repo, peer)

    return repo

# hg->git: helper function for export_ref
def rev_to_mark(rev):
    return marks.from_rev(rev.hex())

# git->hg: helper function for parse_reset and parse_commit
def mark_to_rev(mark):
    return marks.to_rev(mark)

# hg->git: helper function for export_ref
# Get a range of hg revisions in the form of a..b (git committish)
def gitrange(repo, a, b):
    positive = []
    pending = set([int(b)])
    negative = set([int(a)])
    for cur in range(b, -1, -1):
        if not pending:
            break

        parents = [p for p in repo.changelog.parentrevs(cur) if p >= 0]

        if cur in pending:
            positive.append(cur)
            pending.remove(cur)
            for p in parents:
                if p not in negative:
                    pending.add(p)
        elif cur in negative:
            negative.remove(cur)
            for p in parents:
                if p not in pending:
                    negative.add(p)
                else:
                    pending.discard(p)

    positive.reverse()
    return positive

# hg->git: export ref from hg to git
def export_ref(repo, name, kind, head):
    ename = '%s/%s' % (kind, name)
    try:
        tip = marks.get_tip(ename)
        tip = repo[tip]
    except:
        tip = repo[-1]

    revs = gitrange(repo, tip, head)

    total = len(revs)
    tip = tip.rev()

    for rev in revs:

        c = repo[rev]
        node = c.node()

        if marks.is_marked(c.hex()):
            continue

        (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
        rev_branch = extra['branch']

        author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
        if 'committer' in extra:
            try:
                cuser, ctime, ctz = extra['committer'].rsplit(' ', 2)
                committer = "%s %s %s" % (fixup_user(cuser), ctime, gittz(int(ctz)))
            except ValueError:
                cuser = extra['committer']
                committer = "%s %d %s" % (fixup_user(cuser), time, gittz(tz))
        else:
            committer = author

        parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]

        if len(parents) == 0:
            modified = list(c.manifest().keys())
            removed = []
        else:
            modified, removed = get_filechanges(repo, c, parents[0])

        desc += '\n'

        if mode == 'hg':
            extra_msg = ''

            if rev_branch != 'default':
                extra_msg += 'branch : %s\n' % rev_branch

            renames = []
            for f in c.files():
                if f not in c.manifest():
                    continue
                rename = c.filectx(f).renamed()
                if rename:
                    renames.append((rename[0], f))

            for e in renames:
                extra_msg += "rename : %s => %s\n" % e

            for key, value in extra.items():
                if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
                    continue
                else:
                    extra_msg += "extra : %s : %s\n" % (key, urllib.parse.quote(value))

            if extra_msg:
                desc += '\n--HG--\n' + extra_msg

        if len(parents) == 0 and rev:
            print('reset %s/%s' % (prefix, ename))

        modified_final = export_files(c.filectx(f) for f in modified)

        print("commit %s/%s" % (prefix, ename))
        print("mark :%d" % (marks.get_mark(c.hex())))
        print("author %s" % (author))
        print("committer %s" % (committer))
        print("data %d" % (len(desc)))
        print(desc)

        if len(parents) > 0:
            print("from :%s" % (rev_to_mark(parents[0])))
            if len(parents) > 1:
                print("merge :%s" % (rev_to_mark(parents[1])))

        for f in removed:
            print("D %s" % (fix_file_path(f)))
        for f in modified_final:
            print("M %s :%u %s" % f)
        print()

        progress = (rev - tip)
        if (progress % 100 == 0):
            print("progress revision %d '%s' (%d/%d)" % (rev, name, progress, total))

    # make sure the ref is updated
    print("reset %s/%s" % (prefix, ename))
    print("from :%u" % rev_to_mark(head))
    print()

    pending_revs = set(revs) - notes
    if pending_revs:
        note_mark = marks.next_mark()
        ref = "refs/notes/hg"

        print("commit %s" % ref)
        print("mark :%d" % (note_mark))
        print("committer remote-hg <> %d %s" % (ptime.time(), gittz(ptime.timezone)))
        desc = "Notes for %s\n" % (name)
        print("data %d" % (len(desc)))
        print(desc)
        if marks.last_note:
            print("from :%u" % marks.last_note)

        for rev in pending_revs:
            notes.add(rev)
            c = repo[rev]
            print("N inline :%u" % rev_to_mark(c))
            msg = c.hex()
            print("data %d" % (len(msg)))
            print(msg)
        print()

        marks.last_note = note_mark

    marks.set_tip(ename, head.hex())

# hg->git: export tag ref from hg to git
def export_tag(repo, tag):
    export_ref(repo, tag, 'tags', repo[hgref(tag)])

# hg->git: export bookmark ref from hg to git
def export_bookmark(repo, bmark):
    head = bmarks[hgref(bmark)]
    export_ref(repo, bmark, 'bookmarks', head)

# hg->git: export branch ref from hg to git
def export_branch(repo, branch):
    tip = get_branch_tip(repo, branch)
    head = repo[tip]
    export_ref(repo, branch, 'branches', head)

# hg->git: export HEAD ref from hg to git
def export_head(repo):
    export_ref(repo, g_head[0], 'bookmarks', g_head[1])

# Handle the 'capabilities' remote-helper command.
def do_capabilities(parser):
    print("import")
    print("export")
    print("refspec refs/heads/branches/*:%s/branches/*" % prefix)
    print("refspec refs/heads/*:%s/bookmarks/*" % prefix)
    print("refspec refs/tags/*:%s/tags/*" % prefix)

    path = os.path.join(dirname, 'marks-git')

    # capabilities preceded with * are mandatory
    if os.path.exists(path):
        print("*import-marks %s" % path)
    print("*export-marks %s" % path)
    print("option")

    print()

def branch_tip(branch):
    return branches[branch][-1]

def get_branch_tip(repo, branch):
    heads = branches.get(hgref(branch), None)
    if not heads:
        return None

    # verify there's only one head
    if (len(heads) > 1):
        warn("Branch '%s' has more than one head, consider merging" % branch)
        return branch_tip(hgref(branch))

    return heads[0]

# helper function for do_list
# hg->git: figure out the 'head' of the hg repository
def list_head(repo, cur):
    global g_head, fake_bmark

    if 'default' not in branches:
        # empty repo
        return

    node = repo[branch_tip('default')]
    head = 'master' if 'master' not in bmarks else 'default'
    fake_bmark = head
    bmarks[head] = node

    head = gitref(head)
    print("@refs/heads/%s HEAD" % head)
    g_head = (head, node)

# Handle the 'list' remote-helper command.
# hg->git: list refs available in the hg repository
def do_list(parser):
    repo = parser.repo
    for bmark, node in bookmarks.listbookmarks(repo).items():
        bmarks[bmark] = repo[node]

    cur = repo.dirstate.branch()
    orig = peer if peer else repo

    for branch, heads in orig.branchmap().items():
        # only open heads
        heads = [h for h in heads if 'close' not in repo.changelog.read(h)[5]]
        if heads:
            branches[branch] = heads

    list_head(repo, cur)

    if track_branches:
        for branch in branches:
            print("? refs/heads/branches/%s" % gitref(branch))

    for bmark in bmarks:
        if bmarks[bmark].hex() == '0' * 40:
            warn("Ignoring invalid bookmark '%s'", bmark)
        else:
            print("? refs/heads/%s" % gitref(bmark))

    for tag, node in repo.tagslist():
        if tag == 'tip':
            continue
        print("? refs/tags/%s" % gitref(tag))

    print()

# Handle the 'import' remote-helper command.
# hg->git: read the hg repository and generate a fast-import stream from
# it which git then parses.
def do_import(parser):
    repo = parser.repo

    path = os.path.join(dirname, 'marks-git')

    print("feature done")
    if os.path.exists(path):
        print("feature import-marks=%s" % path)
    print("feature export-marks=%s" % path)
    print("feature force")
    sys.stdout.flush()

    tmp = encoding.encoding
    encoding.encoding = 'utf-8'

    # lets get all the import lines
    while parser.check('import'):
        ref = parser[1]

        if (ref == 'HEAD'):
            export_head(repo)
        elif ref.startswith('refs/heads/branches/'):
            branch = ref[len('refs/heads/branches/'):]
            export_branch(repo, branch)
        elif ref.startswith('refs/heads/'):
            bmark = ref[len('refs/heads/'):]
            export_bookmark(repo, bmark)
        elif ref.startswith('refs/tags/'):
            tag = ref[len('refs/tags/'):]
            export_tag(repo, tag)

        next(parser)

    encoding.encoding = tmp

    print('done')

# git->hg: parse 'blob' in the fast-import stream git feeds us
def parse_blob(parser):
    next(parser)
    mark = parser.get_mark()
    next(parser)
    data = parser.get_data()
    blob_marks[mark] = data
    next(parser)

# git->hg: helper function for parse_commit
def get_merge_files(repo, p1, p2, files):
    for e in repo[p1].files():
        if e not in files:
            if e not in repo[p1].manifest():
                continue
            f = { 'ctx': repo[p1][e] }
            files[e] = f

# git->hg: helper function for parse_commit
def c_style_unescape(string):
    if string[0] == string[-1] == '"':
        return string.decode('string-escape')[1:-1]
    return string

# git->hg: parse 'commit' in the fast-import stream git feeds us
def parse_commit(parser):
    from_mark = merge_mark = None

    ref = parser[1]
    next(parser)

    commit_mark = parser.get_mark()
    next(parser)
    author = parser.get_author()
    next(parser)
    committer = parser.get_author()
    next(parser)
    data = parser.get_data()
    next(parser)
    if parser.check('from'):
        from_mark = parser.get_mark()
        next(parser)
    if parser.check('merge'):
        merge_mark = parser.get_mark()
        next(parser)
        if parser.check('merge'):
            die('octopus merges are not supported yet')

    # fast-export adds an extra newline
    if data[-1] == '\n':
        data = data[:-1]

    files = {}

    for line in parser:
        if parser.check('M'):
            t, m, mark_ref, path = line.split(' ', 3)
            if m == '160000':
                warn("Git submodules are not supported, ignoring '%s'", path)
                continue
            mark = int(mark_ref[1:])
            f = { 'mode': hgmode(m), 'data': blob_marks[mark] }
        elif parser.check('D'):
            t, path = line.split(' ', 1)
            f = { 'deleted': True }
        else:
            die('Unknown file command: %s' % line)
        path = c_style_unescape(path)
        files[path] = f

    # only export the commits if we are on an internal proxy repo
    if dry_run and not peer:
        parsed_refs[ref] = None
        return

    def getfilectx(repo, memctx, f):
        of = files[f]
        if 'deleted' in of:
            # the file is not available anymore - was deleted
            if check_version(3, 2):
                return None
            else:
                raise IOError
        if 'ctx' in of:
            return of['ctx']
        is_exec = of['mode'] == 'x'
        is_link = of['mode'] == 'l'
        rename = of.get('rename', None)
        if check_version(3, 1):
            return context.memfilectx(repo, f, of['data'],
                    is_link, is_exec, rename)
        else:
            return context.memfilectx(f, of['data'],
                    is_link, is_exec, rename)

    repo = parser.repo

    user, date, tz = author
    extra = {}

    if committer != author:
        extra['committer'] = "%s %u %u" % committer

    if from_mark:
        p1 = mark_to_rev(from_mark)
    else:
        p1 = '0' * 40

    if merge_mark:
        p2 = mark_to_rev(merge_mark)
    else:
        p2 = '0' * 40

    #
    # If files changed from any of the parents, hg wants to know, but in git if
    # nothing changed from the first parent, nothing changed.
    #
    if merge_mark:
        get_merge_files(repo, p1, p2, files)

    # Check if the ref is supposed to be a named branch
    if ref.startswith('refs/heads/branches/'):
        branch = ref[len('refs/heads/branches/'):]
        extra['branch'] = hgref(branch)

    if mode == 'hg':
        i = data.find('\n--HG--\n')
        if i >= 0:
            tmp = data[i + len('\n--HG--\n'):].strip()
            for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
                if k == 'rename':
                    old, new = v.split(' => ', 1)
                    files[new]['rename'] = old
                elif k == 'branch':
                    extra[k] = v
                elif k == 'extra':
                    ek, ev = v.split(' : ', 1)
                    extra[ek] = urllib.parse.unquote(ev)
            data = data[:i]

    ctx = context.memctx(repo, (p1, p2), data,
            list(files.keys()), getfilectx,
            user, (date, tz), extra)

    tmp = encoding.encoding
    encoding.encoding = 'utf-8'

    node = hghex(repo.commitctx(ctx))

    encoding.encoding = tmp

    parsed_refs[ref] = node
    marks.new_mark(node, commit_mark)

# git->hg: parse 'reset' in the fast-import stream git feeds us
def parse_reset(parser):
    ref = parser[1]
    next(parser)
    # ugh
    if parser.check('commit'):
        parse_commit(parser)
        return
    if not parser.check('from'):
        return
    from_mark = parser.get_mark()
    next(parser)

    try:
        rev = mark_to_rev(from_mark)
    except KeyError:
        rev = None
    parsed_refs[ref] = rev

# git->hg: parse 'tag' in the fast-import stream git feeds us
def parse_tag(parser):
    name = parser[1]
    next(parser)
    from_mark = parser.get_mark()
    next(parser)
    tagger = parser.get_author()
    next(parser)
    data = parser.get_data()
    next(parser)

    parsed_tags[name] = (tagger, data)

def write_tag(repo, tag, node, msg, author):
    branch = repo[node].branch()
    tip = branch_tip(branch)
    tip = repo[tip]

    def getfilectx(repo, memctx, f):
        try:
            fctx = tip.filectx(f)
            data = fctx.data()
        except error.ManifestLookupError:
            data = ""
        content = data + "%s %s\n" % (node, tag)
        if check_version(3, 1):
            return context.memfilectx(repo, f, content, False, False, None)
        else:
            return context.memfilectx(f, content, False, False, None)

    p1 = tip.hex()
    p2 = '0' * 40
    if author:
        user, date, tz = author
        date_tz = (date, tz)
    else:
        cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        output, _ = process.communicate()
        m = re.match('^.* <.*>', output)
        if m:
            user = m.group(0)
        else:
            user = repo.ui.username()
        date_tz = None

    ctx = context.memctx(repo, (p1, p2), msg,
            ['.hgtags'], getfilectx,
            user, date_tz, {'branch': branch})

    tmp = encoding.encoding
    encoding.encoding = 'utf-8'

    tagnode = repo.commitctx(ctx)

    encoding.encoding = tmp

    return (tagnode, branch)

def checkheads_bmark(repo, ref, ctx):
    bmark = ref[len('refs/heads/'):]
    if bmark not in bmarks:
        # new bmark
        return True

    ctx_old = bmarks[bmark]
    ctx_new = ctx

    if not ctx.rev():
        print("error %s unknown" % ref)
        return False

    if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
        if force_push:
            print("ok %s forced update" % ref)
        else:
            print("error %s non-fast forward" % ref)
            return False

    return True

def checkheads(repo, remote, p_revs):

    remotemap = remote.branchmap()
    if not remotemap:
        # empty repo
        return True

    new = {}
    ret = True

    for node, ref in p_revs.items():
        ctx = repo[node]
        branch = ctx.branch()
        if branch not in remotemap:
            # new branch
            continue
        if not ref.startswith('refs/heads/branches'):
            if ref.startswith('refs/heads/'):
                if not checkheads_bmark(repo, ref, ctx):
                    ret = False

            # only check branches
            continue
        new.setdefault(branch, []).append(ctx.rev())

    for branch, heads in new.items():
        old = [repo.changelog.rev(x) for x in remotemap[branch]]
        for rev in heads:
            if check_version(2, 3):
                ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
            else:
                ancestors = repo.changelog.ancestors(rev)
            found = False

            for x in old:
                if x in ancestors:
                    found = True
                    break

            if found:
                continue

            node = repo.changelog.node(rev)
            ref = p_revs[node]
            if force_push:
                print("ok %s forced update" % ref)
            else:
                print("error %s non-fast forward" % ref)
                ret = False

    return ret

def push_unsafe(repo, remote, parsed_refs, p_revs):

    force = force_push

    fci = discovery.findcommonincoming
    commoninc = fci(repo, remote, force=force)
    common, _, remoteheads = commoninc

    if not checkheads(repo, remote, p_revs):
        return None

    if check_version(3, 2):
        cg = changegroup.getchangegroup(repo, 'push', heads=list(p_revs), common=common)
    elif check_version(3, 0):
        cg = changegroup.getbundle(repo, 'push', heads=list(p_revs), common=common)
    else:
        cg = repo.getbundle('push', heads=list(p_revs), common=common)

    unbundle = remote.capable('unbundle')
    if unbundle:
        if force:
            remoteheads = ['force']
        ret = remote.unbundle(cg, remoteheads, 'push')
    else:
        ret = remote.addchangegroup(cg, 'push', repo.url())

    phases = remote.listkeys('phases')
    if phases:
        for head in p_revs:
            # update to public
            remote.pushkey('phases', hghex(head), '1', '0')

    return ret

def push(repo, remote, parsed_refs, p_revs):
    if hasattr(remote, 'canpush') and not remote.canpush():
        print("error cannot push")

    if not p_revs:
        # nothing to push
        return

    lock = None
    unbundle = remote.capable('unbundle')
    if not unbundle:
        lock = remote.lock()
    try:
        ret = push_unsafe(repo, remote, parsed_refs, p_revs)
    finally:
        if lock is not None:
            lock.release()

    return ret

def check_tip(ref, kind, name, heads):
    try:
        ename = '%s/%s' % (kind, name)
        tip = marks.get_tip(ename)
    except KeyError:
        return True
    else:
        return tip in heads

# Handle the 'export' remote-helper command.
# git->hg: parse the fast-import stream git feeds us,
# and add the commits and data in it to the hg repository.
def do_export(parser):
    p_bmarks = []
    p_revs = {}

    next(parser)

    # parse the fast-import stream
    # see also http://git-scm.com/docs/git-fast-import
    for line in parser.each_block('done'):
        if parser.check('blob'):
            parse_blob(parser)
        elif parser.check('commit'):
            parse_commit(parser)
        elif parser.check('reset'):
            parse_reset(parser)
        elif parser.check('tag'):
            parse_tag(parser)
        elif parser.check('feature'):
            pass
        else:
            die('unhandled export command: %s' % line)

    need_fetch = False

    for ref, node in parsed_refs.items():
        bnode = hgbin(node) if node else None
        if ref.startswith('refs/heads/branches'):
            branch = ref[len('refs/heads/branches/'):]
            if branch in branches and bnode in branches[branch]:
                # up to date
                continue

            if peer:
                remotemap = peer.branchmap()
                if remotemap and branch in remotemap:
                    heads = [hghex(e) for e in remotemap[branch]]
                    if not check_tip(ref, 'branches', branch, heads):
                        print("error %s fetch first" % ref)
                        need_fetch = True
                        continue

            p_revs[bnode] = ref
            print("ok %s" % ref)
        elif ref.startswith('refs/heads/'):
            bmark = ref[len('refs/heads/'):]
            new = node
            old = bmarks[bmark].hex() if bmark in bmarks else ''

            if old == new:
                continue

            print("ok %s" % ref)
            if bmark != fake_bmark and \
                    not (bmark == 'master' and bmark not in parser.repo._bookmarks):
                p_bmarks.append((ref, bmark, old, new))

            if peer:
                remote_old = peer.listkeys('bookmarks').get(bmark)
                if remote_old:
                    if not check_tip(ref, 'bookmarks', bmark, remote_old):
                        print("error %s fetch first" % ref)
                        need_fetch = True
                        continue

            p_revs[bnode] = ref
        elif ref.startswith('refs/tags/'):
            if dry_run:
                print("ok %s" % ref)
                continue
            tag = ref[len('refs/tags/'):]
            tag = hgref(tag)
            author, msg = parsed_tags.get(tag, (None, None))
            if mode == 'git':
                if not msg:
                    msg = 'Added tag %s for changeset %s' % (tag, node[:12])
                tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
                p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
            else:
                fp = parser.repo.opener('localtags', 'a')
                fp.write('%s %s\n' % (node, tag))
                fp.close()
            p_revs[bnode] = ref
            print("ok %s" % ref)
        else:
            # transport-helper/fast-export bugs
            continue

    if need_fetch:
        print()
        return

    if dry_run:
        if peer and not force_push:
            checkheads(parser.repo, peer, p_revs)
        print()
        return

    if peer:
        if not push(parser.repo, peer, parsed_refs, p_revs):
            # do not update bookmarks
            print()
            return

        # update remote bookmarks
        remote_bmarks = peer.listkeys('bookmarks')
        for ref, bmark, old, new in p_bmarks:
            if force_push:
                old = remote_bmarks.get(bmark, '')
            if not peer.pushkey('bookmarks', bmark, old, new):
                print("error %s" % ref)
    else:
        # update local bookmarks
        for ref, bmark, old, new in p_bmarks:
            if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
                print("error %s" % ref)

    print()

# Handle the 'option' remote-helper command.
def do_option(parser):
    global dry_run, force_push
    _, key, value = parser.line.split(' ')
    if key == 'dry-run':
        dry_run = (value == 'true')
        print('ok')
    elif key == 'force':
        force_push = (value == 'true')
        print('ok')
    else:
        print('unsupported')

def fix_path(alias, repo, orig_url):
    url = urllib.parse.urlparse(orig_url, 'file')
    if url.scheme != 'file' or os.path.isabs(os.path.expanduser(url.path)):
        return
    abs_url = urllib.parse.urljoin("%s/" % os.getcwd(), orig_url)
    cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
    subprocess.call(cmd)

def main(args):
    global prefix, gitdir, dirname, branches, bmarks
    global marks, blob_marks, parsed_refs
    global peer, mode, bad_mail, bad_name
    global track_branches, force_push, is_tmp
    global parsed_tags
    global filenodes
    global fake_bmark, hg_version
    global dry_run
    global notes, alias

    marks = None
    is_tmp = False
    gitdir = os.environ.get('GIT_DIR', None)

    if len(args) < 3:
        die('Not enough arguments.')

    if not gitdir:
        die('GIT_DIR not set')

    alias = args[1]
    url = args[2]
    peer = None

    hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
    track_branches = get_config_bool('remote-hg.track-branches', True)
    force_push = False

    if hg_git_compat:
        mode = 'hg'
        bad_mail = 'none@none'
        bad_name = ''
    else:
        mode = 'git'
        bad_mail = 'unknown'
        bad_name = 'Unknown'

    if alias[4:] == url:
        is_tmp = True
        alias = hashlib.sha1(alias).hexdigest()

    dirname = os.path.join(gitdir, 'hg', alias)
    branches = {}
    bmarks = {}
    blob_marks = {}
    parsed_refs = {}
    parsed_tags = {}
    filenodes = {}
    fake_bmark = None
    try:
        version, _, extra = util.version().partition('+')
        version = list(int(e) for e in version.split('.'))
        if extra:
            version[1] += 1
        hg_version = tuple(version)
    except:
        hg_version = None
    dry_run = False
    notes = set()

    repo = get_repo(url, alias)
    prefix = 'refs/hg/%s' % alias

    if not is_tmp:
        fix_path(alias, peer or repo, url)

    marks_path = os.path.join(dirname, 'marks-hg')
    marks = Marks(marks_path, repo)

    if sys.platform == 'win32':
        import msvcrt
        msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)

    parser = Parser(repo)
    for line in parser:
        if parser.check('capabilities'):
            do_capabilities(parser)
        elif parser.check('list'):
            do_list(parser)
        elif parser.check('import'):
            do_import(parser)
        elif parser.check('export'):
            do_export(parser)
        elif parser.check('option'):
            do_option(parser)
        else:
            die('unhandled command: %s' % line)
        sys.stdout.flush()

    marks.store()

def bye():
    if is_tmp:
        shutil.rmtree(dirname)

if __name__ == "__main__":
    atexit.register(bye)

    try:
        sys.exit(main(sys.argv))
    except Exception as e:
        if DEBUG_REMOTEHG:
           raise
        die("%s", e)
