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