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