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