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