contrib / remote-helpers / git-remote-hgon commit remote-hg: ensure remote rebasing works (9529cce)
   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(node):
  78    return hg.node.hex(node)
  79
  80def hgbin(node):
  81    return hg.node.bin(node)
  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 get_repo(url, alias):
 360    global dirname, peer
 361
 362    myui = ui.ui()
 363    myui.setconfig('ui', 'interactive', 'off')
 364    myui.fout = sys.stderr
 365
 366    if get_config_bool('remote-hg.insecure'):
 367        myui.setconfig('web', 'cacerts', '')
 368
 369    extensions.loadall(myui)
 370
 371    if hg.islocal(url):
 372        repo = hg.repository(myui, url)
 373        if not os.path.exists(dirname):
 374            os.makedirs(dirname)
 375    else:
 376        shared_path = os.path.join(gitdir, 'hg')
 377        if not os.path.exists(shared_path):
 378            try:
 379                hg.clone(myui, {}, url, shared_path, update=False, pull=True)
 380            except:
 381                die('Repository error')
 382
 383        if not os.path.exists(dirname):
 384            os.makedirs(dirname)
 385
 386        local_path = os.path.join(dirname, 'clone')
 387        if not os.path.exists(local_path):
 388            hg.share(myui, shared_path, local_path, update=False)
 389
 390        repo = hg.repository(myui, local_path)
 391        try:
 392            peer = hg.peer(myui, {}, url)
 393        except:
 394            die('Repository error')
 395        repo.pull(peer, heads=None, force=True)
 396
 397    return repo
 398
 399def rev_to_mark(rev):
 400    global marks
 401    return marks.from_rev(rev.hex())
 402
 403def mark_to_rev(mark):
 404    global marks
 405    return marks.to_rev(mark)
 406
 407def export_ref(repo, name, kind, head):
 408    global prefix, marks, mode
 409
 410    ename = '%s/%s' % (kind, name)
 411    tip = marks.get_tip(ename)
 412    if tip and tip in repo:
 413        tip = repo[tip].rev()
 414    else:
 415        tip = 0
 416
 417    revs = xrange(tip, head.rev() + 1)
 418    count = 0
 419
 420    for rev in revs:
 421
 422        c = repo[rev]
 423        node = c.node()
 424
 425        if marks.is_marked(c.hex()):
 426            count += 1
 427            continue
 428
 429        (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
 430        rev_branch = extra['branch']
 431
 432        author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
 433        if 'committer' in extra:
 434            user, time, tz = extra['committer'].rsplit(' ', 2)
 435            committer = "%s %s %s" % (user, time, gittz(int(tz)))
 436        else:
 437            committer = author
 438
 439        parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]
 440
 441        if len(parents) == 0:
 442            modified = c.manifest().keys()
 443            removed = []
 444        else:
 445            modified, removed = get_filechanges(repo, c, parents[0])
 446
 447        desc += '\n'
 448
 449        if mode == 'hg':
 450            extra_msg = ''
 451
 452            if rev_branch != 'default':
 453                extra_msg += 'branch : %s\n' % rev_branch
 454
 455            renames = []
 456            for f in c.files():
 457                if f not in c.manifest():
 458                    continue
 459                rename = c.filectx(f).renamed()
 460                if rename:
 461                    renames.append((rename[0], f))
 462
 463            for e in renames:
 464                extra_msg += "rename : %s => %s\n" % e
 465
 466            for key, value in extra.iteritems():
 467                if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
 468                    continue
 469                else:
 470                    extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
 471
 472            if extra_msg:
 473                desc += '\n--HG--\n' + extra_msg
 474
 475        if len(parents) == 0 and rev:
 476            print 'reset %s/%s' % (prefix, ename)
 477
 478        modified_final = export_files(c.filectx(f) for f in modified)
 479
 480        print "commit %s/%s" % (prefix, ename)
 481        print "mark :%d" % (marks.get_mark(c.hex()))
 482        print "author %s" % (author)
 483        print "committer %s" % (committer)
 484        print "data %d" % (len(desc))
 485        print desc
 486
 487        if len(parents) > 0:
 488            print "from :%s" % (rev_to_mark(parents[0]))
 489            if len(parents) > 1:
 490                print "merge :%s" % (rev_to_mark(parents[1]))
 491
 492        for f in modified_final:
 493            print "M %s :%u %s" % f
 494        for f in removed:
 495            print "D %s" % (fix_file_path(f))
 496        print
 497
 498        count += 1
 499        if (count % 100 == 0):
 500            print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
 501
 502    # make sure the ref is updated
 503    print "reset %s/%s" % (prefix, ename)
 504    print "from :%u" % rev_to_mark(head)
 505    print
 506
 507    marks.set_tip(ename, head.hex())
 508
 509def export_tag(repo, tag):
 510    export_ref(repo, tag, 'tags', repo[hgref(tag)])
 511
 512def export_bookmark(repo, bmark):
 513    head = bmarks[hgref(bmark)]
 514    export_ref(repo, bmark, 'bookmarks', head)
 515
 516def export_branch(repo, branch):
 517    tip = get_branch_tip(repo, branch)
 518    head = repo[tip]
 519    export_ref(repo, branch, 'branches', head)
 520
 521def export_head(repo):
 522    global g_head
 523    export_ref(repo, g_head[0], 'bookmarks', g_head[1])
 524
 525def do_capabilities(parser):
 526    global prefix, dirname
 527
 528    print "import"
 529    print "export"
 530    print "refspec refs/heads/branches/*:%s/branches/*" % prefix
 531    print "refspec refs/heads/*:%s/bookmarks/*" % prefix
 532    print "refspec refs/tags/*:%s/tags/*" % prefix
 533
 534    path = os.path.join(dirname, 'marks-git')
 535
 536    if os.path.exists(path):
 537        print "*import-marks %s" % path
 538    print "*export-marks %s" % path
 539
 540    print
 541
 542def branch_tip(repo, branch):
 543    # older versions of mercurial don't have this
 544    if hasattr(repo, 'branchtip'):
 545        return repo.branchtip(branch)
 546    else:
 547        return repo.branchtags()[branch]
 548
 549def get_branch_tip(repo, branch):
 550    global branches
 551
 552    heads = branches.get(hgref(branch), None)
 553    if not heads:
 554        return None
 555
 556    # verify there's only one head
 557    if (len(heads) > 1):
 558        warn("Branch '%s' has more than one head, consider merging" % branch)
 559        return branch_tip(repo, hgref(branch))
 560
 561    return heads[0]
 562
 563def list_head(repo, cur):
 564    global g_head, bmarks
 565
 566    head = bookmarks.readcurrent(repo)
 567    if head:
 568        node = repo[head]
 569    else:
 570        # fake bookmark from current branch
 571        head = cur
 572        node = repo['.']
 573        if not node:
 574            node = repo['tip']
 575        if not node:
 576            return
 577        if head == 'default':
 578            head = 'master'
 579        bmarks[head] = node
 580
 581    head = gitref(head)
 582    print "@refs/heads/%s HEAD" % head
 583    g_head = (head, node)
 584
 585def do_list(parser):
 586    global branches, bmarks, track_branches
 587
 588    repo = parser.repo
 589    for bmark, node in bookmarks.listbookmarks(repo).iteritems():
 590        bmarks[bmark] = repo[node]
 591
 592    cur = repo.dirstate.branch()
 593
 594    list_head(repo, cur)
 595
 596    if track_branches:
 597        for branch in repo.branchmap():
 598            heads = repo.branchheads(branch)
 599            if len(heads):
 600                branches[branch] = heads
 601
 602        for branch in branches:
 603            print "? refs/heads/branches/%s" % gitref(branch)
 604
 605    for bmark in bmarks:
 606        print "? refs/heads/%s" % gitref(bmark)
 607
 608    for tag, node in repo.tagslist():
 609        if tag == 'tip':
 610            continue
 611        print "? refs/tags/%s" % gitref(tag)
 612
 613    print
 614
 615def do_import(parser):
 616    repo = parser.repo
 617
 618    path = os.path.join(dirname, 'marks-git')
 619
 620    print "feature done"
 621    if os.path.exists(path):
 622        print "feature import-marks=%s" % path
 623    print "feature export-marks=%s" % path
 624    print "feature force"
 625    sys.stdout.flush()
 626
 627    tmp = encoding.encoding
 628    encoding.encoding = 'utf-8'
 629
 630    # lets get all the import lines
 631    while parser.check('import'):
 632        ref = parser[1]
 633
 634        if (ref == 'HEAD'):
 635            export_head(repo)
 636        elif ref.startswith('refs/heads/branches/'):
 637            branch = ref[len('refs/heads/branches/'):]
 638            export_branch(repo, branch)
 639        elif ref.startswith('refs/heads/'):
 640            bmark = ref[len('refs/heads/'):]
 641            export_bookmark(repo, bmark)
 642        elif ref.startswith('refs/tags/'):
 643            tag = ref[len('refs/tags/'):]
 644            export_tag(repo, tag)
 645
 646        parser.next()
 647
 648    encoding.encoding = tmp
 649
 650    print 'done'
 651
 652def parse_blob(parser):
 653    global blob_marks
 654
 655    parser.next()
 656    mark = parser.get_mark()
 657    parser.next()
 658    data = parser.get_data()
 659    blob_marks[mark] = data
 660    parser.next()
 661
 662def get_merge_files(repo, p1, p2, files):
 663    for e in repo[p1].files():
 664        if e not in files:
 665            if e not in repo[p1].manifest():
 666                continue
 667            f = { 'ctx' : repo[p1][e] }
 668            files[e] = f
 669
 670def parse_commit(parser):
 671    global marks, blob_marks, parsed_refs
 672    global mode
 673
 674    from_mark = merge_mark = None
 675
 676    ref = parser[1]
 677    parser.next()
 678
 679    commit_mark = parser.get_mark()
 680    parser.next()
 681    author = parser.get_author()
 682    parser.next()
 683    committer = parser.get_author()
 684    parser.next()
 685    data = parser.get_data()
 686    parser.next()
 687    if parser.check('from'):
 688        from_mark = parser.get_mark()
 689        parser.next()
 690    if parser.check('merge'):
 691        merge_mark = parser.get_mark()
 692        parser.next()
 693        if parser.check('merge'):
 694            die('octopus merges are not supported yet')
 695
 696    # fast-export adds an extra newline
 697    if data[-1] == '\n':
 698        data = data[:-1]
 699
 700    files = {}
 701
 702    for line in parser:
 703        if parser.check('M'):
 704            t, m, mark_ref, path = line.split(' ', 3)
 705            mark = int(mark_ref[1:])
 706            f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
 707        elif parser.check('D'):
 708            t, path = line.split(' ', 1)
 709            f = { 'deleted' : True }
 710        else:
 711            die('Unknown file command: %s' % line)
 712        files[path] = f
 713
 714    def getfilectx(repo, memctx, f):
 715        of = files[f]
 716        if 'deleted' in of:
 717            raise IOError
 718        if 'ctx' in of:
 719            return of['ctx']
 720        is_exec = of['mode'] == 'x'
 721        is_link = of['mode'] == 'l'
 722        rename = of.get('rename', None)
 723        return context.memfilectx(f, of['data'],
 724                is_link, is_exec, rename)
 725
 726    repo = parser.repo
 727
 728    user, date, tz = author
 729    extra = {}
 730
 731    if committer != author:
 732        extra['committer'] = "%s %u %u" % committer
 733
 734    if from_mark:
 735        p1 = mark_to_rev(from_mark)
 736    else:
 737        p1 = '\0' * 20
 738
 739    if merge_mark:
 740        p2 = mark_to_rev(merge_mark)
 741    else:
 742        p2 = '\0' * 20
 743
 744    #
 745    # If files changed from any of the parents, hg wants to know, but in git if
 746    # nothing changed from the first parent, nothing changed.
 747    #
 748    if merge_mark:
 749        get_merge_files(repo, p1, p2, files)
 750
 751    # Check if the ref is supposed to be a named branch
 752    if ref.startswith('refs/heads/branches/'):
 753        branch = ref[len('refs/heads/branches/'):]
 754        extra['branch'] = hgref(branch)
 755
 756    if mode == 'hg':
 757        i = data.find('\n--HG--\n')
 758        if i >= 0:
 759            tmp = data[i + len('\n--HG--\n'):].strip()
 760            for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
 761                if k == 'rename':
 762                    old, new = v.split(' => ', 1)
 763                    files[new]['rename'] = old
 764                elif k == 'branch':
 765                    extra[k] = v
 766                elif k == 'extra':
 767                    ek, ev = v.split(' : ', 1)
 768                    extra[ek] = urllib.unquote(ev)
 769            data = data[:i]
 770
 771    ctx = context.memctx(repo, (p1, p2), data,
 772            files.keys(), getfilectx,
 773            user, (date, tz), extra)
 774
 775    tmp = encoding.encoding
 776    encoding.encoding = 'utf-8'
 777
 778    node = hghex(repo.commitctx(ctx))
 779
 780    encoding.encoding = tmp
 781
 782    parsed_refs[ref] = node
 783    marks.new_mark(node, commit_mark)
 784
 785def parse_reset(parser):
 786    global parsed_refs
 787
 788    ref = parser[1]
 789    parser.next()
 790    # ugh
 791    if parser.check('commit'):
 792        parse_commit(parser)
 793        return
 794    if not parser.check('from'):
 795        return
 796    from_mark = parser.get_mark()
 797    parser.next()
 798
 799    rev = mark_to_rev(from_mark)
 800    parsed_refs[ref] = rev
 801
 802def parse_tag(parser):
 803    name = parser[1]
 804    parser.next()
 805    from_mark = parser.get_mark()
 806    parser.next()
 807    tagger = parser.get_author()
 808    parser.next()
 809    data = parser.get_data()
 810    parser.next()
 811
 812    parsed_tags[name] = (tagger, data)
 813
 814def write_tag(repo, tag, node, msg, author):
 815    branch = repo[node].branch()
 816    tip = branch_tip(repo, branch)
 817    tip = repo[tip]
 818
 819    def getfilectx(repo, memctx, f):
 820        try:
 821            fctx = tip.filectx(f)
 822            data = fctx.data()
 823        except error.ManifestLookupError:
 824            data = ""
 825        content = data + "%s %s\n" % (node, tag)
 826        return context.memfilectx(f, content, False, False, None)
 827
 828    p1 = tip.hex()
 829    p2 = '\0' * 20
 830    if not author:
 831        author = (None, 0, 0)
 832    user, date, tz = author
 833
 834    ctx = context.memctx(repo, (p1, p2), msg,
 835            ['.hgtags'], getfilectx,
 836            user, (date, tz), {'branch' : branch})
 837
 838    tmp = encoding.encoding
 839    encoding.encoding = 'utf-8'
 840
 841    tagnode = repo.commitctx(ctx)
 842
 843    encoding.encoding = tmp
 844
 845    return tagnode
 846
 847def do_export(parser):
 848    global parsed_refs, bmarks, peer
 849
 850    p_bmarks = []
 851
 852    parser.next()
 853
 854    for line in parser.each_block('done'):
 855        if parser.check('blob'):
 856            parse_blob(parser)
 857        elif parser.check('commit'):
 858            parse_commit(parser)
 859        elif parser.check('reset'):
 860            parse_reset(parser)
 861        elif parser.check('tag'):
 862            parse_tag(parser)
 863        elif parser.check('feature'):
 864            pass
 865        else:
 866            die('unhandled export command: %s' % line)
 867
 868    for ref, node in parsed_refs.iteritems():
 869        bnode = hgbin(node)
 870        if ref.startswith('refs/heads/branches'):
 871            branch = ref[len('refs/heads/branches/'):]
 872            if branch in branches and bnode in branches[branch]:
 873                # up to date
 874                continue
 875            print "ok %s" % ref
 876        elif ref.startswith('refs/heads/'):
 877            bmark = ref[len('refs/heads/'):]
 878            p_bmarks.append((bmark, node))
 879            continue
 880        elif ref.startswith('refs/tags/'):
 881            tag = ref[len('refs/tags/'):]
 882            tag = hgref(tag)
 883            author, msg = parsed_tags.get(tag, (None, None))
 884            if mode == 'git':
 885                if not msg:
 886                    msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
 887                write_tag(parser.repo, tag, node, msg, author)
 888            else:
 889                fp = parser.repo.opener('localtags', 'a')
 890                fp.write('%s %s\n' % (node, tag))
 891                fp.close()
 892            print "ok %s" % ref
 893        else:
 894            # transport-helper/fast-export bugs
 895            continue
 896
 897    if peer:
 898        parser.repo.push(peer, force=force_push, newbranch=True)
 899        remote_bmarks = peer.listkeys('bookmarks')
 900
 901    # handle bookmarks
 902    for bmark, node in p_bmarks:
 903        ref = 'refs/heads/' + bmark
 904        new = node
 905
 906        if bmark in bmarks:
 907            old = bmarks[bmark].hex()
 908        else:
 909            old = ''
 910
 911        if old == new:
 912            continue
 913
 914        if bmark == 'master' and 'master' not in parser.repo._bookmarks:
 915            # fake bookmark
 916            print "ok %s" % ref
 917            continue
 918        elif bookmarks.pushbookmark(parser.repo, bmark, old, new):
 919            # updated locally
 920            pass
 921        else:
 922            print "error %s" % ref
 923            continue
 924
 925        if peer:
 926            old = remote_bmarks.get(bmark, '')
 927            if not peer.pushkey('bookmarks', bmark, old, new):
 928                print "error %s" % ref
 929                continue
 930
 931        print "ok %s" % ref
 932
 933    print
 934
 935def fix_path(alias, repo, orig_url):
 936    url = urlparse.urlparse(orig_url, 'file')
 937    if url.scheme != 'file' or os.path.isabs(url.path):
 938        return
 939    abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
 940    cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
 941    subprocess.call(cmd)
 942
 943def main(args):
 944    global prefix, gitdir, dirname, branches, bmarks
 945    global marks, blob_marks, parsed_refs
 946    global peer, mode, bad_mail, bad_name
 947    global track_branches, force_push, is_tmp
 948    global parsed_tags
 949    global filenodes
 950
 951    alias = args[1]
 952    url = args[2]
 953    peer = None
 954
 955    hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
 956    track_branches = get_config_bool('remote-hg.track-branches', True)
 957    force_push = get_config_bool('remote-hg.force-push')
 958
 959    if hg_git_compat:
 960        mode = 'hg'
 961        bad_mail = 'none@none'
 962        bad_name = ''
 963    else:
 964        mode = 'git'
 965        bad_mail = 'unknown'
 966        bad_name = 'Unknown'
 967
 968    if alias[4:] == url:
 969        is_tmp = True
 970        alias = hashlib.sha1(alias).hexdigest()
 971    else:
 972        is_tmp = False
 973
 974    gitdir = os.environ['GIT_DIR']
 975    dirname = os.path.join(gitdir, 'hg', alias)
 976    branches = {}
 977    bmarks = {}
 978    blob_marks = {}
 979    parsed_refs = {}
 980    marks = None
 981    parsed_tags = {}
 982    filenodes = {}
 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))