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