contrib / remote-helpers / git-remote-hgon commit remote-hg: add tests for 'master' bookmark (9f36d61)
   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, fake_bmark
 564
 565    if 'default' not in repo:
 566        # empty repo
 567        return
 568
 569    node = repo['default']
 570    head = 'master' if not 'master' in bmarks else 'default'
 571    fake_bmark = head
 572    bmarks[head] = node
 573
 574    head = gitref(head)
 575    print "@refs/heads/%s HEAD" % head
 576    g_head = (head, node)
 577
 578def do_list(parser):
 579    global branches, bmarks, track_branches
 580
 581    repo = parser.repo
 582    for bmark, node in bookmarks.listbookmarks(repo).iteritems():
 583        bmarks[bmark] = repo[node]
 584
 585    cur = repo.dirstate.branch()
 586
 587    list_head(repo, cur)
 588
 589    if track_branches:
 590        for branch in repo.branchmap():
 591            heads = repo.branchheads(branch)
 592            if len(heads):
 593                branches[branch] = heads
 594
 595        for branch in branches:
 596            print "? refs/heads/branches/%s" % gitref(branch)
 597
 598    for bmark in bmarks:
 599        print "? refs/heads/%s" % gitref(bmark)
 600
 601    for tag, node in repo.tagslist():
 602        if tag == 'tip':
 603            continue
 604        print "? refs/tags/%s" % gitref(tag)
 605
 606    print
 607
 608def do_import(parser):
 609    repo = parser.repo
 610
 611    path = os.path.join(dirname, 'marks-git')
 612
 613    print "feature done"
 614    if os.path.exists(path):
 615        print "feature import-marks=%s" % path
 616    print "feature export-marks=%s" % path
 617    print "feature force"
 618    sys.stdout.flush()
 619
 620    tmp = encoding.encoding
 621    encoding.encoding = 'utf-8'
 622
 623    # lets get all the import lines
 624    while parser.check('import'):
 625        ref = parser[1]
 626
 627        if (ref == 'HEAD'):
 628            export_head(repo)
 629        elif ref.startswith('refs/heads/branches/'):
 630            branch = ref[len('refs/heads/branches/'):]
 631            export_branch(repo, branch)
 632        elif ref.startswith('refs/heads/'):
 633            bmark = ref[len('refs/heads/'):]
 634            export_bookmark(repo, bmark)
 635        elif ref.startswith('refs/tags/'):
 636            tag = ref[len('refs/tags/'):]
 637            export_tag(repo, tag)
 638
 639        parser.next()
 640
 641    encoding.encoding = tmp
 642
 643    print 'done'
 644
 645def parse_blob(parser):
 646    global blob_marks
 647
 648    parser.next()
 649    mark = parser.get_mark()
 650    parser.next()
 651    data = parser.get_data()
 652    blob_marks[mark] = data
 653    parser.next()
 654
 655def get_merge_files(repo, p1, p2, files):
 656    for e in repo[p1].files():
 657        if e not in files:
 658            if e not in repo[p1].manifest():
 659                continue
 660            f = { 'ctx' : repo[p1][e] }
 661            files[e] = f
 662
 663def parse_commit(parser):
 664    global marks, blob_marks, parsed_refs
 665    global mode
 666
 667    from_mark = merge_mark = None
 668
 669    ref = parser[1]
 670    parser.next()
 671
 672    commit_mark = parser.get_mark()
 673    parser.next()
 674    author = parser.get_author()
 675    parser.next()
 676    committer = parser.get_author()
 677    parser.next()
 678    data = parser.get_data()
 679    parser.next()
 680    if parser.check('from'):
 681        from_mark = parser.get_mark()
 682        parser.next()
 683    if parser.check('merge'):
 684        merge_mark = parser.get_mark()
 685        parser.next()
 686        if parser.check('merge'):
 687            die('octopus merges are not supported yet')
 688
 689    # fast-export adds an extra newline
 690    if data[-1] == '\n':
 691        data = data[:-1]
 692
 693    files = {}
 694
 695    for line in parser:
 696        if parser.check('M'):
 697            t, m, mark_ref, path = line.split(' ', 3)
 698            mark = int(mark_ref[1:])
 699            f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
 700        elif parser.check('D'):
 701            t, path = line.split(' ', 1)
 702            f = { 'deleted' : True }
 703        else:
 704            die('Unknown file command: %s' % line)
 705        files[path] = f
 706
 707    def getfilectx(repo, memctx, f):
 708        of = files[f]
 709        if 'deleted' in of:
 710            raise IOError
 711        if 'ctx' in of:
 712            return of['ctx']
 713        is_exec = of['mode'] == 'x'
 714        is_link = of['mode'] == 'l'
 715        rename = of.get('rename', None)
 716        return context.memfilectx(f, of['data'],
 717                is_link, is_exec, rename)
 718
 719    repo = parser.repo
 720
 721    user, date, tz = author
 722    extra = {}
 723
 724    if committer != author:
 725        extra['committer'] = "%s %u %u" % committer
 726
 727    if from_mark:
 728        p1 = mark_to_rev(from_mark)
 729    else:
 730        p1 = '0' * 40
 731
 732    if merge_mark:
 733        p2 = mark_to_rev(merge_mark)
 734    else:
 735        p2 = '0' * 40
 736
 737    #
 738    # If files changed from any of the parents, hg wants to know, but in git if
 739    # nothing changed from the first parent, nothing changed.
 740    #
 741    if merge_mark:
 742        get_merge_files(repo, p1, p2, files)
 743
 744    # Check if the ref is supposed to be a named branch
 745    if ref.startswith('refs/heads/branches/'):
 746        branch = ref[len('refs/heads/branches/'):]
 747        extra['branch'] = hgref(branch)
 748
 749    if mode == 'hg':
 750        i = data.find('\n--HG--\n')
 751        if i >= 0:
 752            tmp = data[i + len('\n--HG--\n'):].strip()
 753            for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
 754                if k == 'rename':
 755                    old, new = v.split(' => ', 1)
 756                    files[new]['rename'] = old
 757                elif k == 'branch':
 758                    extra[k] = v
 759                elif k == 'extra':
 760                    ek, ev = v.split(' : ', 1)
 761                    extra[ek] = urllib.unquote(ev)
 762            data = data[:i]
 763
 764    ctx = context.memctx(repo, (p1, p2), data,
 765            files.keys(), getfilectx,
 766            user, (date, tz), extra)
 767
 768    tmp = encoding.encoding
 769    encoding.encoding = 'utf-8'
 770
 771    node = hghex(repo.commitctx(ctx))
 772
 773    encoding.encoding = tmp
 774
 775    parsed_refs[ref] = node
 776    marks.new_mark(node, commit_mark)
 777
 778def parse_reset(parser):
 779    global parsed_refs
 780
 781    ref = parser[1]
 782    parser.next()
 783    # ugh
 784    if parser.check('commit'):
 785        parse_commit(parser)
 786        return
 787    if not parser.check('from'):
 788        return
 789    from_mark = parser.get_mark()
 790    parser.next()
 791
 792    rev = mark_to_rev(from_mark)
 793    parsed_refs[ref] = rev
 794
 795def parse_tag(parser):
 796    name = parser[1]
 797    parser.next()
 798    from_mark = parser.get_mark()
 799    parser.next()
 800    tagger = parser.get_author()
 801    parser.next()
 802    data = parser.get_data()
 803    parser.next()
 804
 805    parsed_tags[name] = (tagger, data)
 806
 807def write_tag(repo, tag, node, msg, author):
 808    branch = repo[node].branch()
 809    tip = branch_tip(repo, branch)
 810    tip = repo[tip]
 811
 812    def getfilectx(repo, memctx, f):
 813        try:
 814            fctx = tip.filectx(f)
 815            data = fctx.data()
 816        except error.ManifestLookupError:
 817            data = ""
 818        content = data + "%s %s\n" % (node, tag)
 819        return context.memfilectx(f, content, False, False, None)
 820
 821    p1 = tip.hex()
 822    p2 = '0' * 40
 823    if not author:
 824        author = (None, 0, 0)
 825    user, date, tz = author
 826
 827    ctx = context.memctx(repo, (p1, p2), msg,
 828            ['.hgtags'], getfilectx,
 829            user, (date, tz), {'branch' : branch})
 830
 831    tmp = encoding.encoding
 832    encoding.encoding = 'utf-8'
 833
 834    tagnode = repo.commitctx(ctx)
 835
 836    encoding.encoding = tmp
 837
 838    return tagnode
 839
 840def do_export(parser):
 841    global parsed_refs, bmarks, peer
 842
 843    p_bmarks = []
 844
 845    parser.next()
 846
 847    for line in parser.each_block('done'):
 848        if parser.check('blob'):
 849            parse_blob(parser)
 850        elif parser.check('commit'):
 851            parse_commit(parser)
 852        elif parser.check('reset'):
 853            parse_reset(parser)
 854        elif parser.check('tag'):
 855            parse_tag(parser)
 856        elif parser.check('feature'):
 857            pass
 858        else:
 859            die('unhandled export command: %s' % line)
 860
 861    for ref, node in parsed_refs.iteritems():
 862        bnode = hgbin(node)
 863        if ref.startswith('refs/heads/branches'):
 864            branch = ref[len('refs/heads/branches/'):]
 865            if branch in branches and bnode in branches[branch]:
 866                # up to date
 867                continue
 868            print "ok %s" % ref
 869        elif ref.startswith('refs/heads/'):
 870            bmark = ref[len('refs/heads/'):]
 871            p_bmarks.append((bmark, node))
 872            continue
 873        elif ref.startswith('refs/tags/'):
 874            tag = ref[len('refs/tags/'):]
 875            tag = hgref(tag)
 876            author, msg = parsed_tags.get(tag, (None, None))
 877            if mode == 'git':
 878                if not msg:
 879                    msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
 880                write_tag(parser.repo, tag, node, msg, author)
 881            else:
 882                fp = parser.repo.opener('localtags', 'a')
 883                fp.write('%s %s\n' % (node, tag))
 884                fp.close()
 885            print "ok %s" % ref
 886        else:
 887            # transport-helper/fast-export bugs
 888            continue
 889
 890    if peer:
 891        parser.repo.push(peer, force=force_push, newbranch=True)
 892        remote_bmarks = peer.listkeys('bookmarks')
 893
 894    # handle bookmarks
 895    for bmark, node in p_bmarks:
 896        ref = 'refs/heads/' + bmark
 897        new = node
 898
 899        if bmark in bmarks:
 900            old = bmarks[bmark].hex()
 901        else:
 902            old = ''
 903
 904        if old == new:
 905            continue
 906
 907        if bmark == fake_bmark or \
 908                bmark == 'master' and 'master' not in parser.repo._bookmarks:
 909            print "ok %s" % ref
 910            continue
 911        elif bookmarks.pushbookmark(parser.repo, bmark, old, new):
 912            # updated locally
 913            pass
 914        else:
 915            print "error %s" % ref
 916            continue
 917
 918        if peer:
 919            old = remote_bmarks.get(bmark, '')
 920            if not peer.pushkey('bookmarks', bmark, old, new):
 921                print "error %s" % ref
 922                continue
 923
 924        print "ok %s" % ref
 925
 926    print
 927
 928def fix_path(alias, repo, orig_url):
 929    url = urlparse.urlparse(orig_url, 'file')
 930    if url.scheme != 'file' or os.path.isabs(url.path):
 931        return
 932    abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
 933    cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
 934    subprocess.call(cmd)
 935
 936def main(args):
 937    global prefix, gitdir, dirname, branches, bmarks
 938    global marks, blob_marks, parsed_refs
 939    global peer, mode, bad_mail, bad_name
 940    global track_branches, force_push, is_tmp
 941    global parsed_tags
 942    global filenodes
 943    global fake_bmark
 944
 945    alias = args[1]
 946    url = args[2]
 947    peer = None
 948
 949    hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
 950    track_branches = get_config_bool('remote-hg.track-branches', True)
 951    force_push = get_config_bool('remote-hg.force-push')
 952
 953    if hg_git_compat:
 954        mode = 'hg'
 955        bad_mail = 'none@none'
 956        bad_name = ''
 957    else:
 958        mode = 'git'
 959        bad_mail = 'unknown'
 960        bad_name = 'Unknown'
 961
 962    if alias[4:] == url:
 963        is_tmp = True
 964        alias = hashlib.sha1(alias).hexdigest()
 965    else:
 966        is_tmp = False
 967
 968    gitdir = os.environ['GIT_DIR']
 969    dirname = os.path.join(gitdir, 'hg', alias)
 970    branches = {}
 971    bmarks = {}
 972    blob_marks = {}
 973    parsed_refs = {}
 974    marks = None
 975    parsed_tags = {}
 976    filenodes = {}
 977    fake_bmark = None
 978
 979    repo = get_repo(url, alias)
 980    prefix = 'refs/hg/%s' % alias
 981
 982    if not is_tmp:
 983        fix_path(alias, peer or repo, url)
 984
 985    marks_path = os.path.join(dirname, 'marks-hg')
 986    marks = Marks(marks_path, repo)
 987
 988    if sys.platform == 'win32':
 989        import msvcrt
 990        msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
 991
 992    parser = Parser(repo)
 993    for line in parser:
 994        if parser.check('capabilities'):
 995            do_capabilities(parser)
 996        elif parser.check('list'):
 997            do_list(parser)
 998        elif parser.check('import'):
 999            do_import(parser)
1000        elif parser.check('export'):
1001            do_export(parser)
1002        else:
1003            die('unhandled command: %s' % line)
1004        sys.stdout.flush()
1005
1006def bye():
1007    if not marks:
1008        return
1009    if not is_tmp:
1010        marks.store()
1011    else:
1012        shutil.rmtree(dirname)
1013
1014atexit.register(bye)
1015sys.exit(main(sys.argv))