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