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