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