contrib / remote-helpers / git-remote-hgon commit remote-hg: implement custom push() (4f37bdb)
   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, discovery
  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 push_unsafe(repo, remote, parsed_refs, p_revs):
 858
 859    force = force_push
 860
 861    fci = discovery.findcommonincoming
 862    commoninc = fci(repo, remote, force=force)
 863    common, _, remoteheads = commoninc
 864
 865    # TODO checkheads
 866
 867    cg = repo.getbundle('push', heads=list(p_revs), common=common)
 868
 869    unbundle = remote.capable('unbundle')
 870    if unbundle:
 871        if force:
 872            remoteheads = ['force']
 873        return remote.unbundle(cg, remoteheads, 'push')
 874    else:
 875        return remote.addchangegroup(cg, 'push', repo.url())
 876
 877def push(repo, remote, parsed_refs, p_revs):
 878    if hasattr(remote, 'canpush') and not remote.canpush():
 879        print "error cannot push"
 880
 881    if not p_revs:
 882        # nothing to push
 883        return
 884
 885    lock = None
 886    unbundle = remote.capable('unbundle')
 887    if not unbundle:
 888        lock = remote.lock()
 889    try:
 890        ret = push_unsafe(repo, remote, parsed_refs, p_revs)
 891    finally:
 892        if lock is not None:
 893            lock.release()
 894
 895    return ret
 896
 897def do_export(parser):
 898    global parsed_refs, bmarks, peer
 899
 900    p_bmarks = []
 901    p_revs = set()
 902
 903    parser.next()
 904
 905    for line in parser.each_block('done'):
 906        if parser.check('blob'):
 907            parse_blob(parser)
 908        elif parser.check('commit'):
 909            parse_commit(parser)
 910        elif parser.check('reset'):
 911            parse_reset(parser)
 912        elif parser.check('tag'):
 913            parse_tag(parser)
 914        elif parser.check('feature'):
 915            pass
 916        else:
 917            die('unhandled export command: %s' % line)
 918
 919    for ref, node in parsed_refs.iteritems():
 920        bnode = hgbin(node)
 921        if ref.startswith('refs/heads/branches'):
 922            branch = ref[len('refs/heads/branches/'):]
 923            if branch in branches and bnode in branches[branch]:
 924                # up to date
 925                continue
 926            p_revs.add(bnode)
 927            print "ok %s" % ref
 928        elif ref.startswith('refs/heads/'):
 929            bmark = ref[len('refs/heads/'):]
 930            new = node
 931            old = bmarks[bmark].hex() if bmark in bmarks else ''
 932
 933            if old == new:
 934                continue
 935
 936            print "ok %s" % ref
 937            if bmark != fake_bmark and \
 938                    not (bmark == 'master' and bmark not in parser.repo._bookmarks):
 939                p_bmarks.append((ref, bmark, old, new))
 940
 941            p_revs.add(bnode)
 942        elif ref.startswith('refs/tags/'):
 943            tag = ref[len('refs/tags/'):]
 944            tag = hgref(tag)
 945            author, msg = parsed_tags.get(tag, (None, None))
 946            if mode == 'git':
 947                if not msg:
 948                    msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
 949                tagnode = write_tag(parser.repo, tag, node, msg, author)
 950                p_revs.add(tagnode)
 951            else:
 952                fp = parser.repo.opener('localtags', 'a')
 953                fp.write('%s %s\n' % (node, tag))
 954                fp.close()
 955            p_revs.add(bnode)
 956            print "ok %s" % ref
 957        else:
 958            # transport-helper/fast-export bugs
 959            continue
 960
 961    if peer:
 962        push(parser.repo, peer, parsed_refs, p_revs)
 963
 964        # update remote bookmarks
 965        remote_bmarks = peer.listkeys('bookmarks')
 966        for ref, bmark, old, new in p_bmarks:
 967            if force_push:
 968                old = remote_bmarks.get(bmark, '')
 969            if not peer.pushkey('bookmarks', bmark, old, new):
 970                print "error %s" % ref
 971    else:
 972        # update local bookmarks
 973        for ref, bmark, old, new in p_bmarks:
 974            if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
 975                print "error %s" % ref
 976
 977    print
 978
 979def fix_path(alias, repo, orig_url):
 980    url = urlparse.urlparse(orig_url, 'file')
 981    if url.scheme != 'file' or os.path.isabs(url.path):
 982        return
 983    abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
 984    cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
 985    subprocess.call(cmd)
 986
 987def main(args):
 988    global prefix, gitdir, dirname, branches, bmarks
 989    global marks, blob_marks, parsed_refs
 990    global peer, mode, bad_mail, bad_name
 991    global track_branches, force_push, is_tmp
 992    global parsed_tags
 993    global filenodes
 994    global fake_bmark
 995
 996    alias = args[1]
 997    url = args[2]
 998    peer = None
 999
1000    hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1001    track_branches = get_config_bool('remote-hg.track-branches', True)
1002    force_push = get_config_bool('remote-hg.force-push')
1003
1004    if hg_git_compat:
1005        mode = 'hg'
1006        bad_mail = 'none@none'
1007        bad_name = ''
1008    else:
1009        mode = 'git'
1010        bad_mail = 'unknown'
1011        bad_name = 'Unknown'
1012
1013    if alias[4:] == url:
1014        is_tmp = True
1015        alias = hashlib.sha1(alias).hexdigest()
1016    else:
1017        is_tmp = False
1018
1019    gitdir = os.environ['GIT_DIR']
1020    dirname = os.path.join(gitdir, 'hg', alias)
1021    branches = {}
1022    bmarks = {}
1023    blob_marks = {}
1024    parsed_refs = {}
1025    marks = None
1026    parsed_tags = {}
1027    filenodes = {}
1028    fake_bmark = None
1029
1030    repo = get_repo(url, alias)
1031    prefix = 'refs/hg/%s' % alias
1032
1033    if not is_tmp:
1034        fix_path(alias, peer or repo, url)
1035
1036    marks_path = os.path.join(dirname, 'marks-hg')
1037    marks = Marks(marks_path, repo)
1038
1039    if sys.platform == 'win32':
1040        import msvcrt
1041        msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1042
1043    parser = Parser(repo)
1044    for line in parser:
1045        if parser.check('capabilities'):
1046            do_capabilities(parser)
1047        elif parser.check('list'):
1048            do_list(parser)
1049        elif parser.check('import'):
1050            do_import(parser)
1051        elif parser.check('export'):
1052            do_export(parser)
1053        else:
1054            die('unhandled command: %s' % line)
1055        sys.stdout.flush()
1056
1057def bye():
1058    if not marks:
1059        return
1060    if not is_tmp:
1061        marks.store()
1062    else:
1063        shutil.rmtree(dirname)
1064
1065atexit.register(bye)
1066sys.exit(main(sys.argv))