contrib / remote-helpers / git-remote-hgon commit remote-hg: improve email sanitation (a2e462c)
   1#!/usr/bin/env python
   2#
   3# Copyright (c) 2012 Felipe Contreras
   4#
   5
   6# Inspired by Rocco Rutte's hg-fast-export
   7
   8# Just copy to your ~/bin, or anywhere in your $PATH.
   9# Then you can clone with:
  10# git clone hg::/path/to/mercurial/repo/
  11#
  12# For remote repositories a local clone is stored in
  13# "$GIT_DIR/hg/origin/clone/.hg/".
  14
  15from mercurial import hg, ui, bookmarks, context, util, encoding, node, error
  16
  17import re
  18import sys
  19import os
  20import json
  21import shutil
  22import subprocess
  23import urllib
  24import atexit
  25import urlparse
  26
  27#
  28# If you want to switch to hg-git compatibility mode:
  29# git config --global remote-hg.hg-git-compat true
  30#
  31# If you are not in hg-git-compat mode and want to disable the tracking of
  32# named branches:
  33# git config --global remote-hg.track-branches false
  34#
  35# If you don't want to force pushes (and thus risk creating new remote heads):
  36# git config --global remote-hg.force-push false
  37#
  38# If you want the equivalent of hg's clone/pull--insecure option:
  39# git config remote-hg.insecure true
  40#
  41# git:
  42# Sensible defaults for git.
  43# hg bookmarks are exported as git branches, hg branches are prefixed
  44# with 'branches/', HEAD is a special case.
  45#
  46# hg:
  47# Emulate hg-git.
  48# Only hg bookmarks are exported as git branches.
  49# Commits are modified to preserve hg information and allow bidirectionality.
  50#
  51
  52NAME_RE = re.compile('^([^<>]+)')
  53AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
  54EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
  55AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
  56RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
  57
  58def die(msg, *args):
  59    sys.stderr.write('ERROR: %s\n' % (msg % args))
  60    sys.exit(1)
  61
  62def warn(msg, *args):
  63    sys.stderr.write('WARNING: %s\n' % (msg % args))
  64
  65def gitmode(flags):
  66    return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
  67
  68def gittz(tz):
  69    return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
  70
  71def hgmode(mode):
  72    m = { '100755': 'x', '120000': 'l' }
  73    return m.get(mode, '')
  74
  75def hghex(node):
  76    return hg.node.hex(node)
  77
  78def get_config(config):
  79    cmd = ['git', 'config', '--get', config]
  80    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
  81    output, _ = process.communicate()
  82    return output
  83
  84class Marks:
  85
  86    def __init__(self, path):
  87        self.path = path
  88        self.tips = {}
  89        self.marks = {}
  90        self.rev_marks = {}
  91        self.last_mark = 0
  92
  93        self.load()
  94
  95    def load(self):
  96        if not os.path.exists(self.path):
  97            return
  98
  99        tmp = json.load(open(self.path))
 100
 101        self.tips = tmp['tips']
 102        self.marks = tmp['marks']
 103        self.last_mark = tmp['last-mark']
 104
 105        for rev, mark in self.marks.iteritems():
 106            self.rev_marks[mark] = int(rev)
 107
 108    def dict(self):
 109        return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
 110
 111    def store(self):
 112        json.dump(self.dict(), open(self.path, 'w'))
 113
 114    def __str__(self):
 115        return str(self.dict())
 116
 117    def from_rev(self, rev):
 118        return self.marks[str(rev)]
 119
 120    def to_rev(self, mark):
 121        return self.rev_marks[mark]
 122
 123    def get_mark(self, rev):
 124        self.last_mark += 1
 125        self.marks[str(rev)] = self.last_mark
 126        return self.last_mark
 127
 128    def new_mark(self, rev, mark):
 129        self.marks[str(rev)] = mark
 130        self.rev_marks[mark] = rev
 131        self.last_mark = mark
 132
 133    def is_marked(self, rev):
 134        return str(rev) in self.marks
 135
 136    def get_tip(self, branch):
 137        return self.tips.get(branch, 0)
 138
 139    def set_tip(self, branch, tip):
 140        self.tips[branch] = tip
 141
 142class Parser:
 143
 144    def __init__(self, repo):
 145        self.repo = repo
 146        self.line = self.get_line()
 147
 148    def get_line(self):
 149        return sys.stdin.readline().strip()
 150
 151    def __getitem__(self, i):
 152        return self.line.split()[i]
 153
 154    def check(self, word):
 155        return self.line.startswith(word)
 156
 157    def each_block(self, separator):
 158        while self.line != separator:
 159            yield self.line
 160            self.line = self.get_line()
 161
 162    def __iter__(self):
 163        return self.each_block('')
 164
 165    def next(self):
 166        self.line = self.get_line()
 167        if self.line == 'done':
 168            self.line = None
 169
 170    def get_mark(self):
 171        i = self.line.index(':') + 1
 172        return int(self.line[i:])
 173
 174    def get_data(self):
 175        if not self.check('data'):
 176            return None
 177        i = self.line.index(' ') + 1
 178        size = int(self.line[i:])
 179        return sys.stdin.read(size)
 180
 181    def get_author(self):
 182        global bad_mail
 183
 184        ex = None
 185        m = RAW_AUTHOR_RE.match(self.line)
 186        if not m:
 187            return None
 188        _, name, email, date, tz = m.groups()
 189        if name and 'ext:' in name:
 190            m = re.match('^(.+?) ext:\((.+)\)$', name)
 191            if m:
 192                name = m.group(1)
 193                ex = urllib.unquote(m.group(2))
 194
 195        if email != bad_mail:
 196            if name:
 197                user = '%s <%s>' % (name, email)
 198            else:
 199                user = '<%s>' % (email)
 200        else:
 201            user = name
 202
 203        if ex:
 204            user += ex
 205
 206        tz = int(tz)
 207        tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
 208        return (user, int(date), -tz)
 209
 210def fix_file_path(path):
 211    if not os.path.isabs(path):
 212        return path
 213    return os.path.relpath(path, '/')
 214
 215def export_file(fc):
 216    d = fc.data()
 217    path = fix_file_path(fc.path())
 218    print "M %s inline %s" % (gitmode(fc.flags()), path)
 219    print "data %d" % len(d)
 220    print d
 221
 222def get_filechanges(repo, ctx, parent):
 223    modified = set()
 224    added = set()
 225    removed = set()
 226
 227    cur = ctx.manifest()
 228    prev = repo[parent].manifest().copy()
 229
 230    for fn in cur:
 231        if fn in prev:
 232            if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
 233                modified.add(fn)
 234            del prev[fn]
 235        else:
 236            added.add(fn)
 237    removed |= set(prev.keys())
 238
 239    return added | modified, removed
 240
 241def fixup_user_git(user):
 242    name = mail = None
 243    user = user.replace('"', '')
 244    m = AUTHOR_RE.match(user)
 245    if m:
 246        name = m.group(1)
 247        mail = m.group(2).strip()
 248    else:
 249        m = EMAIL_RE.match(user)
 250        if m:
 251            name = m.group(1)
 252            mail = m.group(2)
 253        else:
 254            m = NAME_RE.match(user)
 255            if m:
 256                name = m.group(1).strip()
 257    return (name, mail)
 258
 259def fixup_user_hg(user):
 260    def sanitize(name):
 261        # stole this from hg-git
 262        return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
 263
 264    m = AUTHOR_HG_RE.match(user)
 265    if m:
 266        name = sanitize(m.group(1))
 267        mail = sanitize(m.group(2))
 268        ex = m.group(3)
 269        if ex:
 270            name += ' ext:(' + urllib.quote(ex) + ')'
 271    else:
 272        name = sanitize(user)
 273        if '@' in user:
 274            mail = name
 275        else:
 276            mail = None
 277
 278    return (name, mail)
 279
 280def fixup_user(user):
 281    global mode, bad_mail
 282
 283    if mode == 'git':
 284        name, mail = fixup_user_git(user)
 285    else:
 286        name, mail = fixup_user_hg(user)
 287
 288    if not name:
 289        name = bad_name
 290    if not mail:
 291        mail = bad_mail
 292
 293    return '%s <%s>' % (name, mail)
 294
 295def get_repo(url, alias):
 296    global dirname, peer
 297
 298    myui = ui.ui()
 299    myui.setconfig('ui', 'interactive', 'off')
 300    myui.fout = sys.stderr
 301
 302    try:
 303        if get_config('remote-hg.insecure') == 'true\n':
 304            myui.setconfig('web', 'cacerts', '')
 305    except subprocess.CalledProcessError:
 306        pass
 307
 308    if hg.islocal(url):
 309        repo = hg.repository(myui, url)
 310    else:
 311        local_path = os.path.join(dirname, 'clone')
 312        if not os.path.exists(local_path):
 313            try:
 314                peer, dstpeer = hg.clone(myui, {}, url, local_path, update=True, pull=True)
 315            except:
 316                die('Repository error')
 317            repo = dstpeer.local()
 318        else:
 319            repo = hg.repository(myui, local_path)
 320            try:
 321                peer = hg.peer(myui, {}, url)
 322            except:
 323                die('Repository error')
 324            repo.pull(peer, heads=None, force=True)
 325
 326    return repo
 327
 328def rev_to_mark(rev):
 329    global marks
 330    return marks.from_rev(rev)
 331
 332def mark_to_rev(mark):
 333    global marks
 334    return marks.to_rev(mark)
 335
 336def export_ref(repo, name, kind, head):
 337    global prefix, marks, mode
 338
 339    ename = '%s/%s' % (kind, name)
 340    tip = marks.get_tip(ename)
 341
 342    # mercurial takes too much time checking this
 343    if tip and tip == head.rev():
 344        # nothing to do
 345        return
 346    revs = xrange(tip, head.rev() + 1)
 347    count = 0
 348
 349    revs = [rev for rev in revs if not marks.is_marked(rev)]
 350
 351    for rev in revs:
 352
 353        c = repo[rev]
 354        (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
 355        rev_branch = extra['branch']
 356
 357        author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
 358        if 'committer' in extra:
 359            user, time, tz = extra['committer'].rsplit(' ', 2)
 360            committer = "%s %s %s" % (user, time, gittz(int(tz)))
 361        else:
 362            committer = author
 363
 364        parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
 365
 366        if len(parents) == 0:
 367            modified = c.manifest().keys()
 368            removed = []
 369        else:
 370            modified, removed = get_filechanges(repo, c, parents[0])
 371
 372        desc += '\n'
 373
 374        if mode == 'hg':
 375            extra_msg = ''
 376
 377            if rev_branch != 'default':
 378                extra_msg += 'branch : %s\n' % rev_branch
 379
 380            renames = []
 381            for f in c.files():
 382                if f not in c.manifest():
 383                    continue
 384                rename = c.filectx(f).renamed()
 385                if rename:
 386                    renames.append((rename[0], f))
 387
 388            for e in renames:
 389                extra_msg += "rename : %s => %s\n" % e
 390
 391            for key, value in extra.iteritems():
 392                if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
 393                    continue
 394                else:
 395                    extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
 396
 397            if extra_msg:
 398                desc += '\n--HG--\n' + extra_msg
 399
 400        if len(parents) == 0 and rev:
 401            print 'reset %s/%s' % (prefix, ename)
 402
 403        print "commit %s/%s" % (prefix, ename)
 404        print "mark :%d" % (marks.get_mark(rev))
 405        print "author %s" % (author)
 406        print "committer %s" % (committer)
 407        print "data %d" % (len(desc))
 408        print desc
 409
 410        if len(parents) > 0:
 411            print "from :%s" % (rev_to_mark(parents[0]))
 412            if len(parents) > 1:
 413                print "merge :%s" % (rev_to_mark(parents[1]))
 414
 415        for f in modified:
 416            export_file(c.filectx(f))
 417        for f in removed:
 418            print "D %s" % (fix_file_path(f))
 419        print
 420
 421        count += 1
 422        if (count % 100 == 0):
 423            print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
 424            print "#############################################################"
 425
 426    # make sure the ref is updated
 427    print "reset %s/%s" % (prefix, ename)
 428    print "from :%u" % rev_to_mark(rev)
 429    print
 430
 431    marks.set_tip(ename, rev)
 432
 433def export_tag(repo, tag):
 434    export_ref(repo, tag, 'tags', repo[tag])
 435
 436def export_bookmark(repo, bmark):
 437    head = bmarks[bmark]
 438    export_ref(repo, bmark, 'bookmarks', head)
 439
 440def export_branch(repo, branch):
 441    tip = get_branch_tip(repo, branch)
 442    head = repo[tip]
 443    export_ref(repo, branch, 'branches', head)
 444
 445def export_head(repo):
 446    global g_head
 447    export_ref(repo, g_head[0], 'bookmarks', g_head[1])
 448
 449def do_capabilities(parser):
 450    global prefix, dirname
 451
 452    print "import"
 453    print "export"
 454    print "refspec refs/heads/branches/*:%s/branches/*" % prefix
 455    print "refspec refs/heads/*:%s/bookmarks/*" % prefix
 456    print "refspec refs/tags/*:%s/tags/*" % prefix
 457
 458    path = os.path.join(dirname, 'marks-git')
 459
 460    if os.path.exists(path):
 461        print "*import-marks %s" % path
 462    print "*export-marks %s" % path
 463
 464    print
 465
 466def branch_tip(repo, branch):
 467    # older versions of mercurial don't have this
 468    if hasattr(repo, 'branchtip'):
 469        return repo.branchtip(branch)
 470    else:
 471        return repo.branchtags()[branch]
 472
 473def get_branch_tip(repo, branch):
 474    global branches
 475
 476    heads = branches.get(branch, None)
 477    if not heads:
 478        return None
 479
 480    # verify there's only one head
 481    if (len(heads) > 1):
 482        warn("Branch '%s' has more than one head, consider merging" % branch)
 483        return branch_tip(repo, branch)
 484
 485    return heads[0]
 486
 487def list_head(repo, cur):
 488    global g_head, bmarks
 489
 490    head = bookmarks.readcurrent(repo)
 491    if head:
 492        node = repo[head]
 493    else:
 494        # fake bookmark from current branch
 495        head = cur
 496        node = repo['.']
 497        if not node:
 498            node = repo['tip']
 499        if not node:
 500            return
 501        if head == 'default':
 502            head = 'master'
 503        bmarks[head] = node
 504
 505    print "@refs/heads/%s HEAD" % head
 506    g_head = (head, node)
 507
 508def do_list(parser):
 509    global branches, bmarks, mode, track_branches
 510
 511    repo = parser.repo
 512    for bmark, node in bookmarks.listbookmarks(repo).iteritems():
 513        bmarks[bmark] = repo[node]
 514
 515    cur = repo.dirstate.branch()
 516
 517    list_head(repo, cur)
 518
 519    if track_branches:
 520        for branch in repo.branchmap():
 521            heads = repo.branchheads(branch)
 522            if len(heads):
 523                branches[branch] = heads
 524
 525        for branch in branches:
 526            print "? refs/heads/branches/%s" % branch
 527
 528    for bmark in bmarks:
 529        print "? refs/heads/%s" % bmark
 530
 531    for tag, node in repo.tagslist():
 532        if tag == 'tip':
 533            continue
 534        print "? refs/tags/%s" % tag
 535
 536    print
 537
 538def do_import(parser):
 539    repo = parser.repo
 540
 541    path = os.path.join(dirname, 'marks-git')
 542
 543    print "feature done"
 544    if os.path.exists(path):
 545        print "feature import-marks=%s" % path
 546    print "feature export-marks=%s" % path
 547    sys.stdout.flush()
 548
 549    tmp = encoding.encoding
 550    encoding.encoding = 'utf-8'
 551
 552    # lets get all the import lines
 553    while parser.check('import'):
 554        ref = parser[1]
 555
 556        if (ref == 'HEAD'):
 557            export_head(repo)
 558        elif ref.startswith('refs/heads/branches/'):
 559            branch = ref[len('refs/heads/branches/'):]
 560            export_branch(repo, branch)
 561        elif ref.startswith('refs/heads/'):
 562            bmark = ref[len('refs/heads/'):]
 563            export_bookmark(repo, bmark)
 564        elif ref.startswith('refs/tags/'):
 565            tag = ref[len('refs/tags/'):]
 566            export_tag(repo, tag)
 567
 568        parser.next()
 569
 570    encoding.encoding = tmp
 571
 572    print 'done'
 573
 574def parse_blob(parser):
 575    global blob_marks
 576
 577    parser.next()
 578    mark = parser.get_mark()
 579    parser.next()
 580    data = parser.get_data()
 581    blob_marks[mark] = data
 582    parser.next()
 583
 584def get_merge_files(repo, p1, p2, files):
 585    for e in repo[p1].files():
 586        if e not in files:
 587            if e not in repo[p1].manifest():
 588                continue
 589            f = { 'ctx' : repo[p1][e] }
 590            files[e] = f
 591
 592def parse_commit(parser):
 593    global marks, blob_marks, parsed_refs
 594    global mode
 595
 596    from_mark = merge_mark = None
 597
 598    ref = parser[1]
 599    parser.next()
 600
 601    commit_mark = parser.get_mark()
 602    parser.next()
 603    author = parser.get_author()
 604    parser.next()
 605    committer = parser.get_author()
 606    parser.next()
 607    data = parser.get_data()
 608    parser.next()
 609    if parser.check('from'):
 610        from_mark = parser.get_mark()
 611        parser.next()
 612    if parser.check('merge'):
 613        merge_mark = parser.get_mark()
 614        parser.next()
 615        if parser.check('merge'):
 616            die('octopus merges are not supported yet')
 617
 618    files = {}
 619
 620    for line in parser:
 621        if parser.check('M'):
 622            t, m, mark_ref, path = line.split(' ', 3)
 623            mark = int(mark_ref[1:])
 624            f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
 625        elif parser.check('D'):
 626            t, path = line.split(' ', 1)
 627            f = { 'deleted' : True }
 628        else:
 629            die('Unknown file command: %s' % line)
 630        files[path] = f
 631
 632    def getfilectx(repo, memctx, f):
 633        of = files[f]
 634        if 'deleted' in of:
 635            raise IOError
 636        if 'ctx' in of:
 637            return of['ctx']
 638        is_exec = of['mode'] == 'x'
 639        is_link = of['mode'] == 'l'
 640        rename = of.get('rename', None)
 641        return context.memfilectx(f, of['data'],
 642                is_link, is_exec, rename)
 643
 644    repo = parser.repo
 645
 646    user, date, tz = author
 647    extra = {}
 648
 649    if committer != author:
 650        extra['committer'] = "%s %u %u" % committer
 651
 652    if from_mark:
 653        p1 = repo.changelog.node(mark_to_rev(from_mark))
 654    else:
 655        p1 = '\0' * 20
 656
 657    if merge_mark:
 658        p2 = repo.changelog.node(mark_to_rev(merge_mark))
 659    else:
 660        p2 = '\0' * 20
 661
 662    #
 663    # If files changed from any of the parents, hg wants to know, but in git if
 664    # nothing changed from the first parent, nothing changed.
 665    #
 666    if merge_mark:
 667        get_merge_files(repo, p1, p2, files)
 668
 669    # Check if the ref is supposed to be a named branch
 670    if ref.startswith('refs/heads/branches/'):
 671        extra['branch'] = ref[len('refs/heads/branches/'):]
 672
 673    if mode == 'hg':
 674        i = data.find('\n--HG--\n')
 675        if i >= 0:
 676            tmp = data[i + len('\n--HG--\n'):].strip()
 677            for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
 678                if k == 'rename':
 679                    old, new = v.split(' => ', 1)
 680                    files[new]['rename'] = old
 681                elif k == 'branch':
 682                    extra[k] = v
 683                elif k == 'extra':
 684                    ek, ev = v.split(' : ', 1)
 685                    extra[ek] = urllib.unquote(ev)
 686            data = data[:i]
 687
 688    ctx = context.memctx(repo, (p1, p2), data,
 689            files.keys(), getfilectx,
 690            user, (date, tz), extra)
 691
 692    tmp = encoding.encoding
 693    encoding.encoding = 'utf-8'
 694
 695    node = repo.commitctx(ctx)
 696
 697    encoding.encoding = tmp
 698
 699    rev = repo[node].rev()
 700
 701    parsed_refs[ref] = node
 702    marks.new_mark(rev, commit_mark)
 703
 704def parse_reset(parser):
 705    global parsed_refs
 706
 707    ref = parser[1]
 708    parser.next()
 709    # ugh
 710    if parser.check('commit'):
 711        parse_commit(parser)
 712        return
 713    if not parser.check('from'):
 714        return
 715    from_mark = parser.get_mark()
 716    parser.next()
 717
 718    node = parser.repo.changelog.node(mark_to_rev(from_mark))
 719    parsed_refs[ref] = node
 720
 721def parse_tag(parser):
 722    name = parser[1]
 723    parser.next()
 724    from_mark = parser.get_mark()
 725    parser.next()
 726    tagger = parser.get_author()
 727    parser.next()
 728    data = parser.get_data()
 729    parser.next()
 730
 731    parsed_tags[name] = (tagger, data)
 732
 733def write_tag(repo, tag, node, msg, author):
 734    branch = repo[node].branch()
 735    tip = branch_tip(repo, branch)
 736    tip = repo[tip]
 737
 738    def getfilectx(repo, memctx, f):
 739        try:
 740            fctx = tip.filectx(f)
 741            data = fctx.data()
 742        except error.ManifestLookupError:
 743            data = ""
 744        content = data + "%s %s\n" % (hghex(node), tag)
 745        return context.memfilectx(f, content, False, False, None)
 746
 747    p1 = tip.hex()
 748    p2 = '\0' * 20
 749    if not author:
 750        author = (None, 0, 0)
 751    user, date, tz = author
 752
 753    ctx = context.memctx(repo, (p1, p2), msg,
 754            ['.hgtags'], getfilectx,
 755            user, (date, tz), {'branch' : branch})
 756
 757    tmp = encoding.encoding
 758    encoding.encoding = 'utf-8'
 759
 760    tagnode = repo.commitctx(ctx)
 761
 762    encoding.encoding = tmp
 763
 764    return tagnode
 765
 766def do_export(parser):
 767    global parsed_refs, bmarks, peer
 768
 769    p_bmarks = []
 770
 771    parser.next()
 772
 773    for line in parser.each_block('done'):
 774        if parser.check('blob'):
 775            parse_blob(parser)
 776        elif parser.check('commit'):
 777            parse_commit(parser)
 778        elif parser.check('reset'):
 779            parse_reset(parser)
 780        elif parser.check('tag'):
 781            parse_tag(parser)
 782        elif parser.check('feature'):
 783            pass
 784        else:
 785            die('unhandled export command: %s' % line)
 786
 787    for ref, node in parsed_refs.iteritems():
 788        if ref.startswith('refs/heads/branches'):
 789            branch = ref[len('refs/heads/branches/'):]
 790            if branch in branches and node in branches[branch]:
 791                # up to date
 792                continue
 793            print "ok %s" % ref
 794        elif ref.startswith('refs/heads/'):
 795            bmark = ref[len('refs/heads/'):]
 796            p_bmarks.append((bmark, node))
 797            continue
 798        elif ref.startswith('refs/tags/'):
 799            tag = ref[len('refs/tags/'):]
 800            author, msg = parsed_tags.get(tag, (None, None))
 801            if mode == 'git':
 802                if not msg:
 803                    msg = 'Added tag %s for changeset %s' % (tag, hghex(node[:6]));
 804                write_tag(parser.repo, tag, node, msg, author)
 805            else:
 806                fp = parser.repo.opener('localtags', 'a')
 807                fp.write('%s %s\n' % (hghex(node), tag))
 808                fp.close()
 809            print "ok %s" % ref
 810        else:
 811            # transport-helper/fast-export bugs
 812            continue
 813
 814    if peer:
 815        parser.repo.push(peer, force=force_push)
 816
 817    # handle bookmarks
 818    for bmark, node in p_bmarks:
 819        ref = 'refs/heads/' + bmark
 820        new = hghex(node)
 821
 822        if bmark in bmarks:
 823            old = bmarks[bmark].hex()
 824        else:
 825            old = ''
 826
 827        if bmark == 'master' and 'master' not in parser.repo._bookmarks:
 828            # fake bookmark
 829            pass
 830        elif bookmarks.pushbookmark(parser.repo, bmark, old, new):
 831            # updated locally
 832            pass
 833        else:
 834            print "error %s" % ref
 835            continue
 836
 837        if peer:
 838            rb = peer.listkeys('bookmarks')
 839            old = rb.get(bmark, '')
 840            if not peer.pushkey('bookmarks', bmark, old, new):
 841                print "error %s" % ref
 842                continue
 843
 844        print "ok %s" % ref
 845
 846    print
 847
 848def fix_path(alias, repo, orig_url):
 849    url = urlparse.urlparse(orig_url, 'file')
 850    if url.scheme != 'file' or os.path.isabs(url.path):
 851        return
 852    abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
 853    cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
 854    subprocess.call(cmd)
 855
 856def main(args):
 857    global prefix, dirname, branches, bmarks
 858    global marks, blob_marks, parsed_refs
 859    global peer, mode, bad_mail, bad_name
 860    global track_branches, force_push, is_tmp
 861    global parsed_tags
 862
 863    alias = args[1]
 864    url = args[2]
 865    peer = None
 866
 867    hg_git_compat = False
 868    track_branches = True
 869    force_push = True
 870
 871    try:
 872        if get_config('remote-hg.hg-git-compat') == 'true\n':
 873            hg_git_compat = True
 874            track_branches = False
 875        if get_config('remote-hg.track-branches') == 'false\n':
 876            track_branches = False
 877        if get_config('remote-hg.force-push') == 'false\n':
 878            force_push = False
 879    except subprocess.CalledProcessError:
 880        pass
 881
 882    if hg_git_compat:
 883        mode = 'hg'
 884        bad_mail = 'none@none'
 885        bad_name = ''
 886    else:
 887        mode = 'git'
 888        bad_mail = 'unknown'
 889        bad_name = 'Unknown'
 890
 891    if alias[4:] == url:
 892        is_tmp = True
 893        alias = util.sha1(alias).hexdigest()
 894    else:
 895        is_tmp = False
 896
 897    gitdir = os.environ['GIT_DIR']
 898    dirname = os.path.join(gitdir, 'hg', alias)
 899    branches = {}
 900    bmarks = {}
 901    blob_marks = {}
 902    parsed_refs = {}
 903    marks = None
 904    parsed_tags = {}
 905
 906    repo = get_repo(url, alias)
 907    prefix = 'refs/hg/%s' % alias
 908
 909    if not is_tmp:
 910        fix_path(alias, peer or repo, url)
 911
 912    if not os.path.exists(dirname):
 913        os.makedirs(dirname)
 914
 915    marks_path = os.path.join(dirname, 'marks-hg')
 916    marks = Marks(marks_path)
 917
 918    parser = Parser(repo)
 919    for line in parser:
 920        if parser.check('capabilities'):
 921            do_capabilities(parser)
 922        elif parser.check('list'):
 923            do_list(parser)
 924        elif parser.check('import'):
 925            do_import(parser)
 926        elif parser.check('export'):
 927            do_export(parser)
 928        else:
 929            die('unhandled command: %s' % line)
 930        sys.stdout.flush()
 931
 932def bye():
 933    if not marks:
 934        return
 935    if not is_tmp:
 936        marks.store()
 937    else:
 938        shutil.rmtree(dirname)
 939
 940atexit.register(bye)
 941sys.exit(main(sys.argv))