Add yt-dlp v2021.09.02
[sunny256-utils.git] / git-remote-hg
blob4ec985291c57a55f826e6d99a118a61ad1c3a313
1 #!/usr/bin/env python2
3 # Copyright (c) 2012 Felipe Contreras
6 # Inspired by Rocco Rutte's hg-fast-export
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/
12 # For remote repositories a local clone is stored in
13 # "$GIT_DIR/hg/origin/clone/.hg/".
15 from mercurial import hg, ui, bookmarks, context, encoding
16 from mercurial import node, error, extensions, discovery, util
17 from mercurial import changegroup
19 import re
20 import sys
21 import os
22 import posixpath
23 try:
24     import json
25 except ImportError:
26     import simplejson as json 
27 import shutil
28 import subprocess
29 import urllib
30 import atexit
31 import urlparse
32 import hashlib
33 import time as ptime
36 # If you want to see Mercurial revisions as Git commit notes:
37 # git config core.notesRef refs/notes/hg
39 # If you are not in hg-git-compat mode and want to disable the tracking of
40 # named branches:
41 # git config --global remote-hg.track-branches false
43 # If you want the equivalent of hg's clone/pull--insecure option:
44 # git config --global remote-hg.insecure true
46 # If you want to switch to hg-git compatibility mode:
47 # git config --global remote-hg.hg-git-compat true
51 # The 'mode' variable indicates whether we are in 'git' or 'hg' compatibility mode.
52 # This has the following effects:
54 # git:
55 # Sensible defaults for git.
56 # hg bookmarks are exported as git branches, hg branches are prefixed
57 # with 'branches/', HEAD is a special case.
59 # hg:
60 # Emulate hg-git.
61 # Only hg bookmarks are exported as git branches.
62 # Commits are modified to preserve hg information and allow bidirectionality.
64 mode = 'git'
66 DEBUG_REMOTEHG = os.environ.get("DEBUG_REMOTEHG") != None
68 NAME_RE = re.compile('^([^<>]+)')
69 AUTHOR_RE = re.compile('^([^<>]+?)? ?[<>]([^<>]*)(?:$|>)')
70 EMAIL_RE = re.compile(r'([^ \t<>]+@[^ \t<>]+)')
71 AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.*))?$')
72 RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
74 VERSION = 2
77 def die(msg, *args):
78     sys.stderr.write('ERROR: %s\n' % (msg % args))
79     sys.exit(1)
81 def warn(msg, *args):
82     sys.stderr.write('WARNING: %s\n' % (msg % args))
84 # convert between git timezone and mercurial timezone data.
85 # git stores timezone as a string in the form "+HHMM" or "-HHMM".
86 # Mercurial instead encodes the timezone as an offset to UTC in seconds.
87 # This offset has the opposite sign from the one used by git.
89 # To convert between the two, we have to be a bit careful, as
90 # dividing a negativ integer in python 2 rounds differently than
91 # in C.
93 # hg->git: convert timezone offset in seconds to string '+HHMM' or '-HHMM'
94 def gittz(tz):
95     if tz < 0:
96         tz = -tz
97         sign = +1
98     else:
99         sign = -1
100     hours = tz / 3600
101     minutes = tz % 3600 / 60
102     return '%+03d%02d' % (sign * hours, minutes)
104 # git->hg: convert timezone string '+HHMM' or '-HHMM' to time offset in seconds
105 def hgtz(tz):
106     tz = int(tz)
107     if tz < 0:
108         tz = -tz
109         sign = +1
110     else:
111         sign = -1
112     tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
113     return sign * tz
115 # hg->git: convert hg flags to git file mode
116 def gitmode(flags):
117     return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
119 # git->hg: convert git file mode to hg flags
120 def hgmode(mode):
121     m = { '100755': 'x', '120000': 'l' }
122     return m.get(mode, '')
124 def hghex(n):
125     return node.hex(n)
127 def hgbin(n):
128     return node.bin(n)
130 def hgref(ref):
131     return ref.replace('___', ' ')
133 def gitref(ref):
134     return ref.replace(' ', '___')
136 def check_version(*check):
137     if not hg_version:
138         return True
139     return hg_version >= check
141 def get_config(config):
142     cmd = ['git', 'config', '--get', config]
143     process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
144     output, _ = process.communicate()
145     return output
147 def get_config_bool(config, default=False):
148     value = get_config(config).rstrip('\n')
149     if value == "true":
150         return True
151     elif value == "false":
152         return False
153     else:
154         return default
156 # Handle loading and storing git marks files
157 class Marks:
159     def __init__(self, path, repo):
160         self.path = path
161         self.repo = repo
162         self.clear()
163         self.load()
165         if self.version < VERSION:
166             if self.version == 1:
167                 self.upgrade_one()
169             # upgraded?
170             if self.version < VERSION:
171                 self.clear()
172                 self.version = VERSION
174     def clear(self):
175         self.tips = {}
176         self.marks = {}
177         self.rev_marks = {}
178         self.last_mark = 0
179         self.version = 0
180         self.last_note = 0
182     def load(self):
183         if not os.path.exists(self.path):
184             return
186         tmp = json.load(open(self.path))
188         self.tips = tmp['tips']
189         self.marks = tmp['marks']
190         self.last_mark = tmp['last-mark']
191         self.version = tmp.get('version', 1)
192         self.last_note = tmp.get('last-note', 0)
194         for rev, mark in self.marks.iteritems():
195             self.rev_marks[mark] = rev
197     def upgrade_one(self):
198         def get_id(rev):
199             return hghex(self.repo.changelog.node(int(rev)))
200         self.tips = dict((name, get_id(rev)) for name, rev in self.tips.iteritems())
201         self.marks = dict((get_id(rev), mark) for rev, mark in self.marks.iteritems())
202         self.rev_marks = dict((mark, get_id(rev)) for mark, rev in self.rev_marks.iteritems())
203         self.version = 2
205     def dict(self):
206         return { 'tips': self.tips, 'marks': self.marks,
207                 'last-mark': self.last_mark, 'version': self.version,
208                 'last-note': self.last_note }
210     def store(self):
211         json.dump(self.dict(), open(self.path, 'w'))
213     def __str__(self):
214         return str(self.dict())
216     def from_rev(self, rev):
217         return self.marks[rev]
219     def to_rev(self, mark):
220         return str(self.rev_marks[mark])
222     def next_mark(self):
223         self.last_mark += 1
224         return self.last_mark
226     def get_mark(self, rev):
227         self.last_mark += 1
228         self.marks[rev] = self.last_mark
229         return self.last_mark
231     def new_mark(self, rev, mark):
232         self.marks[rev] = mark
233         self.rev_marks[mark] = rev
234         self.last_mark = mark
236     def is_marked(self, rev):
237         return rev in self.marks
239     def get_tip(self, branch):
240         return str(self.tips[branch])
242     def set_tip(self, branch, tip):
243         self.tips[branch] = tip
245 # Parser class for the input git feeds to us. See
246 # - http://git-scm.com/docs/gitremote-helpers
247 # - http://git-scm.com/docs/git-fast-import
248 class Parser:
250     def __init__(self, repo):
251         self.repo = repo
252         self.line = self.get_line()
254     def get_line(self):
255         return sys.stdin.readline().strip()
257     def __getitem__(self, i):
258         return self.line.split()[i]
260     def check(self, word):
261         return self.line.startswith(word)
263     def each_block(self, separator):
264         while self.line != separator:
265             yield self.line
266             self.line = self.get_line()
268     def __iter__(self):
269         return self.each_block('')
271     def next(self):
272         self.line = self.get_line()
273         if self.line == 'done':
274             self.line = None
276     def get_mark(self):
277         i = self.line.index(':') + 1
278         return int(self.line[i:])
280     def get_data(self):
281         if not self.check('data'):
282             return None
283         i = self.line.index(' ') + 1
284         size = int(self.line[i:])
285         return sys.stdin.read(size)
287     def get_author(self):
288         ex = None
289         m = RAW_AUTHOR_RE.match(self.line)
290         if not m:
291             return None
292         _, name, email, date, tz = m.groups()
293         if name and 'ext:' in name:
294             m = re.match('^(.+?) ext:\((.+)\)$', name)
295             if m:
296                 name = m.group(1)
297                 ex = urllib.unquote(m.group(2))
299         if email != bad_mail:
300             if name:
301                 user = '%s <%s>' % (name, email)
302             else:
303                 user = '<%s>' % (email)
304         else:
305             user = name
307         if ex:
308             user += ex
310         return (user, int(date), hgtz(tz))
312 # hg->git: helper function for export_files, which modifies the given
313 # file path, taken from a hg commit, for use in a fast-import stream.
314 def fix_file_path(path):
315     path = posixpath.normpath(path)
316     if not os.path.isabs(path):
317         return path
318     return posixpath.relpath(path, '/')
320 # hg->git: export files from hg to git. That is, read them from the hg
321 # repository, and print them into the fast-import stream we are feeding
322 # to git.
323 def export_files(files):
324     final = []
325     for f in files:
326         fid = node.hex(f.filenode())
328         if fid in filenodes:
329             mark = filenodes[fid]
330         else:
331             mark = marks.next_mark()
332             filenodes[fid] = mark
333             d = f.data()
335             print "blob"
336             print "mark :%u" % mark
337             print "data %d" % len(d)
338             print d
340         path = fix_file_path(f.path())
341         final.append((gitmode(f.flags()), mark, path))
343     return final
345 # hg->git: determine which files changed on the hg side.
346 def get_filechanges(repo, ctx, parent):
347     modified = set()
348     added = set()
349     removed = set()
351     # load earliest manifest first for caching reasons
352     prev = parent.manifest().copy()
353     cur = ctx.manifest()
355     for fn in cur:
356         if fn in prev:
357             if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
358                 modified.add(fn)
359             del prev[fn]
360         else:
361             added.add(fn)
362     removed |= set(prev.keys())
364     return added | modified, removed
366 def fixup_user_git(user):
367     name = mail = None
368     user = user.replace('"', '')
369     m = AUTHOR_RE.match(user)
370     if m:
371         name = m.group(1)
372         mail = m.group(2).strip()
373     else:
374         m = EMAIL_RE.match(user)
375         if m:
376             mail = m.group(1)
377         else:
378             m = NAME_RE.match(user)
379             if m:
380                 name = m.group(1).strip()
381     return (name, mail)
383 def fixup_user_hg(user):
384     def sanitize(name):
385         # stole this from hg-git
386         return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
388     m = AUTHOR_HG_RE.match(user)
389     if m:
390         name = sanitize(m.group(1))
391         mail = sanitize(m.group(2))
392         ex = m.group(3)
393         if ex:
394             name += ' ext:(' + urllib.quote(ex) + ')'
395     else:
396         name = sanitize(user)
397         if '@' in user:
398             mail = name
399         else:
400             mail = None
402     return (name, mail)
404 def fixup_user(user):
405     if mode == 'git':
406         name, mail = fixup_user_git(user)
407     else:
408         name, mail = fixup_user_hg(user)
410     if not name:
411         name = bad_name
412     if not mail:
413         mail = bad_mail
415     return '%s <%s>' % (name, mail)
417 # helper function for get_repo
418 def updatebookmarks(repo, peer):
419     remotemarks = peer.listkeys('bookmarks')
420     localmarks = repo._bookmarks
422     if not remotemarks:
423         return
425     for k, v in remotemarks.iteritems():
426         localmarks[k] = hgbin(v)
428     if hasattr(localmarks, 'write'):
429         localmarks.write()
430     else:
431         bookmarks.write(repo)
433 # instantiate a Mercurial "repo" object so that
434 # we can interact with the hg repository.
435 # Also pulls remote changes.
436 def get_repo(url, alias):
437     global peer
439     myui = ui.ui()
440     myui.setconfig('ui', 'interactive', 'off')
441     myui.fout = sys.stderr
443     if get_config_bool('remote-hg.insecure'):
444         myui.setconfig('web', 'cacerts', '')
446     extensions.loadall(myui)
448     if hg.islocal(url) and not os.environ.get('GIT_REMOTE_HG_TEST_REMOTE'):
449         repo = hg.repository(myui, url)
450         if not os.path.exists(dirname):
451             os.makedirs(dirname)
452     else:
453         shared_path = os.path.join(gitdir, 'hg')
455         # check and upgrade old organization
456         hg_path = os.path.join(shared_path, '.hg')
457         if os.path.exists(shared_path) and not os.path.exists(hg_path):
458             repos = os.listdir(shared_path)
459             for x in repos:
460                 local_hg = os.path.join(shared_path, x, 'clone', '.hg')
461                 if not os.path.exists(local_hg):
462                     continue
463                 if not os.path.exists(hg_path):
464                     shutil.move(local_hg, hg_path)
465                 shutil.rmtree(os.path.join(shared_path, x, 'clone'))
467         # setup shared repo (if not there)
468         try:
469             hg.peer(myui, {}, shared_path, create=True)
470         except error.RepoError:
471             pass
473         if not os.path.exists(dirname):
474             os.makedirs(dirname)
476         local_path = os.path.join(dirname, 'clone')
477         if not os.path.exists(local_path):
478             hg.share(myui, shared_path, local_path, update=False)
479         else:
480             # make sure the shared path is always up-to-date
481             util.writefile(os.path.join(local_path, '.hg', 'sharedpath'), hg_path)
483         repo = hg.repository(myui, local_path)
484         peer = hg.peer(repo.ui, {}, url)
486         if check_version(3, 2):
487             from mercurial import exchange
488             exchange.pull(repo, peer, heads=None, force=True)
489         else:
490             repo.pull(peer, heads=None, force=True)
492         updatebookmarks(repo, peer)
494     return repo
496 # hg->git: helper function for export_ref
497 def rev_to_mark(rev):
498     return marks.from_rev(rev.hex())
500 # git->hg: helper function for parse_reset and parse_commit
501 def mark_to_rev(mark):
502     return marks.to_rev(mark)
504 # hg->git: helper function for export_ref
505 # Get a range of hg revisions in the form of a..b (git committish)
506 def gitrange(repo, a, b):
507     positive = []
508     pending = set([int(b)])
509     negative = set([int(a)])
510     for cur in xrange(b, -1, -1):
511         if not pending:
512             break
514         parents = [p for p in repo.changelog.parentrevs(cur) if p >= 0]
516         if cur in pending:
517             positive.append(cur)
518             pending.remove(cur)
519             for p in parents:
520                 if p not in negative:
521                     pending.add(p)
522         elif cur in negative:
523             negative.remove(cur)
524             for p in parents:
525                 if p not in pending:
526                     negative.add(p)
527                 else:
528                     pending.discard(p)
530     positive.reverse()
531     return positive
533 # hg->git: export ref from hg to git
534 def export_ref(repo, name, kind, head):
535     ename = '%s/%s' % (kind, name)
536     try:
537         tip = marks.get_tip(ename)
538         tip = repo[tip]
539     except:
540         tip = repo[-1]
542     revs = gitrange(repo, tip, head)
544     total = len(revs)
545     tip = tip.rev()
547     for rev in revs:
549         c = repo[rev]
550         node = c.node()
552         if marks.is_marked(c.hex()):
553             continue
555         (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
556         rev_branch = extra['branch']
558         author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
559         if 'committer' in extra:
560             try:
561                 cuser, ctime, ctz = extra['committer'].rsplit(' ', 2)
562                 committer = "%s %s %s" % (fixup_user(cuser), ctime, gittz(int(ctz)))
563             except ValueError:
564                 cuser = extra['committer']
565                 committer = "%s %d %s" % (fixup_user(cuser), time, gittz(tz))
566         else:
567             committer = author
569         parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]
571         if len(parents) == 0:
572             modified = c.manifest().keys()
573             removed = []
574         else:
575             modified, removed = get_filechanges(repo, c, parents[0])
577         desc += '\n'
579         if mode == 'hg':
580             extra_msg = ''
582             if rev_branch != 'default':
583                 extra_msg += 'branch : %s\n' % rev_branch
585             renames = []
586             for f in c.files():
587                 if f not in c.manifest():
588                     continue
589                 rename = c.filectx(f).renamed()
590                 if rename:
591                     renames.append((rename[0], f))
593             for e in renames:
594                 extra_msg += "rename : %s => %s\n" % e
596             for key, value in extra.iteritems():
597                 if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
598                     continue
599                 else:
600                     extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
602             if extra_msg:
603                 desc += '\n--HG--\n' + extra_msg
605         if len(parents) == 0 and rev:
606             print 'reset %s/%s' % (prefix, ename)
608         modified_final = export_files(c.filectx(f) for f in modified)
610         print "commit %s/%s" % (prefix, ename)
611         print "mark :%d" % (marks.get_mark(c.hex()))
612         print "author %s" % (author)
613         print "committer %s" % (committer)
614         print "data %d" % (len(desc))
615         print desc
617         if len(parents) > 0:
618             print "from :%s" % (rev_to_mark(parents[0]))
619             if len(parents) > 1:
620                 print "merge :%s" % (rev_to_mark(parents[1]))
622         for f in removed:
623             print "D %s" % (fix_file_path(f))
624         for f in modified_final:
625             print "M %s :%u %s" % f
626         print
628         progress = (rev - tip)
629         if (progress % 100 == 0):
630             print "progress revision %d '%s' (%d/%d)" % (rev, name, progress, total)
632     # make sure the ref is updated
633     print "reset %s/%s" % (prefix, ename)
634     print "from :%u" % rev_to_mark(head)
635     print
637     pending_revs = set(revs) - notes
638     if pending_revs:
639         note_mark = marks.next_mark()
640         ref = "refs/notes/hg"
642         print "commit %s" % ref
643         print "mark :%d" % (note_mark)
644         print "committer remote-hg <> %d %s" % (ptime.time(), gittz(ptime.timezone))
645         desc = "Notes for %s\n" % (name)
646         print "data %d" % (len(desc))
647         print desc
648         if marks.last_note:
649             print "from :%u" % marks.last_note
651         for rev in pending_revs:
652             notes.add(rev)
653             c = repo[rev]
654             print "N inline :%u" % rev_to_mark(c)
655             msg = c.hex()
656             print "data %d" % (len(msg))
657             print msg
658         print
660         marks.last_note = note_mark
662     marks.set_tip(ename, head.hex())
664 # hg->git: export tag ref from hg to git
665 def export_tag(repo, tag):
666     export_ref(repo, tag, 'tags', repo[hgref(tag)])
668 # hg->git: export bookmark ref from hg to git
669 def export_bookmark(repo, bmark):
670     head = bmarks[hgref(bmark)]
671     export_ref(repo, bmark, 'bookmarks', head)
673 # hg->git: export branch ref from hg to git
674 def export_branch(repo, branch):
675     tip = get_branch_tip(repo, branch)
676     head = repo[tip]
677     export_ref(repo, branch, 'branches', head)
679 # hg->git: export HEAD ref from hg to git
680 def export_head(repo):
681     export_ref(repo, g_head[0], 'bookmarks', g_head[1])
683 # Handle the 'capabilities' remote-helper command.
684 def do_capabilities(parser):
685     print "import"
686     print "export"
687     print "refspec refs/heads/branches/*:%s/branches/*" % prefix
688     print "refspec refs/heads/*:%s/bookmarks/*" % prefix
689     print "refspec refs/tags/*:%s/tags/*" % prefix
691     path = os.path.join(dirname, 'marks-git')
693     # capabilities preceded with * are mandatory
694     if os.path.exists(path):
695         print "*import-marks %s" % path
696     print "*export-marks %s" % path
697     print "option"
699     print
701 def branch_tip(branch):
702     return branches[branch][-1]
704 def get_branch_tip(repo, branch):
705     heads = branches.get(hgref(branch), None)
706     if not heads:
707         return None
709     # verify there's only one head
710     if (len(heads) > 1):
711         warn("Branch '%s' has more than one head, consider merging" % branch)
712         return branch_tip(hgref(branch))
714     return heads[0]
716 # helper function for do_list
717 # hg->git: figure out the 'head' of the hg repository
718 def list_head(repo, cur):
719     global g_head, fake_bmark
721     if 'default' not in branches:
722         # empty repo
723         return
725     node = repo[branch_tip('default')]
726     head = 'master' if 'master' not in bmarks else 'default'
727     fake_bmark = head
728     bmarks[head] = node
730     head = gitref(head)
731     print "@refs/heads/%s HEAD" % head
732     g_head = (head, node)
734 # Handle the 'list' remote-helper command.
735 # hg->git: list refs available in the hg repository
736 def do_list(parser):
737     repo = parser.repo
738     for bmark, node in bookmarks.listbookmarks(repo).iteritems():
739         bmarks[bmark] = repo[node]
741     cur = repo.dirstate.branch()
742     orig = peer if peer else repo
744     for branch, heads in orig.branchmap().iteritems():
745         # only open heads
746         heads = [h for h in heads if 'close' not in repo.changelog.read(h)[5]]
747         if heads:
748             branches[branch] = heads
750     list_head(repo, cur)
752     if track_branches:
753         for branch in branches:
754             print "? refs/heads/branches/%s" % gitref(branch)
756     for bmark in bmarks:
757         if bmarks[bmark].hex() == '0' * 40:
758             warn("Ignoring invalid bookmark '%s'", bmark)
759         else:
760             print "? refs/heads/%s" % gitref(bmark)
762     for tag, node in repo.tagslist():
763         if tag == 'tip':
764             continue
765         print "? refs/tags/%s" % gitref(tag)
767     print
769 # Handle the 'import' remote-helper command.
770 # hg->git: read the hg repository and generate a fast-import stream from
771 # it which git then parses.
772 def do_import(parser):
773     repo = parser.repo
775     path = os.path.join(dirname, 'marks-git')
777     print "feature done"
778     if os.path.exists(path):
779         print "feature import-marks=%s" % path
780     print "feature export-marks=%s" % path
781     print "feature force"
782     sys.stdout.flush()
784     tmp = encoding.encoding
785     encoding.encoding = 'utf-8'
787     # lets get all the import lines
788     while parser.check('import'):
789         ref = parser[1]
791         if (ref == 'HEAD'):
792             export_head(repo)
793         elif ref.startswith('refs/heads/branches/'):
794             branch = ref[len('refs/heads/branches/'):]
795             export_branch(repo, branch)
796         elif ref.startswith('refs/heads/'):
797             bmark = ref[len('refs/heads/'):]
798             export_bookmark(repo, bmark)
799         elif ref.startswith('refs/tags/'):
800             tag = ref[len('refs/tags/'):]
801             export_tag(repo, tag)
803         parser.next()
805     encoding.encoding = tmp
807     print 'done'
809 # git->hg: parse 'blob' in the fast-import stream git feeds us
810 def parse_blob(parser):
811     parser.next()
812     mark = parser.get_mark()
813     parser.next()
814     data = parser.get_data()
815     blob_marks[mark] = data
816     parser.next()
818 # git->hg: helper function for parse_commit
819 def get_merge_files(repo, p1, p2, files):
820     for e in repo[p1].files():
821         if e not in files:
822             if e not in repo[p1].manifest():
823                 continue
824             f = { 'ctx': repo[p1][e] }
825             files[e] = f
827 # git->hg: helper function for parse_commit
828 def c_style_unescape(string):
829     if string[0] == string[-1] == '"':
830         return string.decode('string-escape')[1:-1]
831     return string
833 # git->hg: parse 'commit' in the fast-import stream git feeds us
834 def parse_commit(parser):
835     from_mark = merge_mark = None
837     ref = parser[1]
838     parser.next()
840     commit_mark = parser.get_mark()
841     parser.next()
842     author = parser.get_author()
843     parser.next()
844     committer = parser.get_author()
845     parser.next()
846     data = parser.get_data()
847     parser.next()
848     if parser.check('from'):
849         from_mark = parser.get_mark()
850         parser.next()
851     if parser.check('merge'):
852         merge_mark = parser.get_mark()
853         parser.next()
854         if parser.check('merge'):
855             die('octopus merges are not supported yet')
857     # fast-export adds an extra newline
858     if data[-1] == '\n':
859         data = data[:-1]
861     files = {}
863     for line in parser:
864         if parser.check('M'):
865             t, m, mark_ref, path = line.split(' ', 3)
866             if m == '160000':
867                 warn("Git submodules are not supported, ignoring '%s'", path)
868                 continue
869             mark = int(mark_ref[1:])
870             f = { 'mode': hgmode(m), 'data': blob_marks[mark] }
871         elif parser.check('D'):
872             t, path = line.split(' ', 1)
873             f = { 'deleted': True }
874         else:
875             die('Unknown file command: %s' % line)
876         path = c_style_unescape(path)
877         files[path] = f
879     # only export the commits if we are on an internal proxy repo
880     if dry_run and not peer:
881         parsed_refs[ref] = None
882         return
884     def getfilectx(repo, memctx, f):
885         of = files[f]
886         if 'deleted' in of:
887             # the file is not available anymore - was deleted
888             if check_version(3, 2):
889                 return None
890             else:
891                 raise IOError
892         if 'ctx' in of:
893             return of['ctx']
894         is_exec = of['mode'] == 'x'
895         is_link = of['mode'] == 'l'
896         rename = of.get('rename', None)
897         if check_version(3, 1):
898             return context.memfilectx(repo, f, of['data'],
899                     is_link, is_exec, rename)
900         else:
901             return context.memfilectx(f, of['data'],
902                     is_link, is_exec, rename)
904     repo = parser.repo
906     user, date, tz = author
907     extra = {}
909     if committer != author:
910         extra['committer'] = "%s %u %u" % committer
912     if from_mark:
913         p1 = mark_to_rev(from_mark)
914     else:
915         p1 = '0' * 40
917     if merge_mark:
918         p2 = mark_to_rev(merge_mark)
919     else:
920         p2 = '0' * 40
922     #
923     # If files changed from any of the parents, hg wants to know, but in git if
924     # nothing changed from the first parent, nothing changed.
925     #
926     if merge_mark:
927         get_merge_files(repo, p1, p2, files)
929     # Check if the ref is supposed to be a named branch
930     if ref.startswith('refs/heads/branches/'):
931         branch = ref[len('refs/heads/branches/'):]
932         extra['branch'] = hgref(branch)
934     if mode == 'hg':
935         i = data.find('\n--HG--\n')
936         if i >= 0:
937             tmp = data[i + len('\n--HG--\n'):].strip()
938             for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
939                 if k == 'rename':
940                     old, new = v.split(' => ', 1)
941                     files[new]['rename'] = old
942                 elif k == 'branch':
943                     extra[k] = v
944                 elif k == 'extra':
945                     ek, ev = v.split(' : ', 1)
946                     extra[ek] = urllib.unquote(ev)
947             data = data[:i]
949     ctx = context.memctx(repo, (p1, p2), data,
950             files.keys(), getfilectx,
951             user, (date, tz), extra)
953     tmp = encoding.encoding
954     encoding.encoding = 'utf-8'
956     node = hghex(repo.commitctx(ctx))
958     encoding.encoding = tmp
960     parsed_refs[ref] = node
961     marks.new_mark(node, commit_mark)
963 # git->hg: parse 'reset' in the fast-import stream git feeds us
964 def parse_reset(parser):
965     ref = parser[1]
966     parser.next()
967     # ugh
968     if parser.check('commit'):
969         parse_commit(parser)
970         return
971     if not parser.check('from'):
972         return
973     from_mark = parser.get_mark()
974     parser.next()
976     try:
977         rev = mark_to_rev(from_mark)
978     except KeyError:
979         rev = None
980     parsed_refs[ref] = rev
982 # git->hg: parse 'tag' in the fast-import stream git feeds us
983 def parse_tag(parser):
984     name = parser[1]
985     parser.next()
986     from_mark = parser.get_mark()
987     parser.next()
988     tagger = parser.get_author()
989     parser.next()
990     data = parser.get_data()
991     parser.next()
993     parsed_tags[name] = (tagger, data)
995 def write_tag(repo, tag, node, msg, author):
996     branch = repo[node].branch()
997     tip = branch_tip(branch)
998     tip = repo[tip]
1000     def getfilectx(repo, memctx, f):
1001         try:
1002             fctx = tip.filectx(f)
1003             data = fctx.data()
1004         except error.ManifestLookupError:
1005             data = ""
1006         content = data + "%s %s\n" % (node, tag)
1007         if check_version(3, 1):
1008             return context.memfilectx(repo, f, content, False, False, None)
1009         else:
1010             return context.memfilectx(f, content, False, False, None)
1012     p1 = tip.hex()
1013     p2 = '0' * 40
1014     if author:
1015         user, date, tz = author
1016         date_tz = (date, tz)
1017     else:
1018         cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
1019         process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
1020         output, _ = process.communicate()
1021         m = re.match('^.* <.*>', output)
1022         if m:
1023             user = m.group(0)
1024         else:
1025             user = repo.ui.username()
1026         date_tz = None
1028     ctx = context.memctx(repo, (p1, p2), msg,
1029             ['.hgtags'], getfilectx,
1030             user, date_tz, {'branch': branch})
1032     tmp = encoding.encoding
1033     encoding.encoding = 'utf-8'
1035     tagnode = repo.commitctx(ctx)
1037     encoding.encoding = tmp
1039     return (tagnode, branch)
1041 def checkheads_bmark(repo, ref, ctx):
1042     bmark = ref[len('refs/heads/'):]
1043     if bmark not in bmarks:
1044         # new bmark
1045         return True
1047     ctx_old = bmarks[bmark]
1048     ctx_new = ctx
1050     if not ctx.rev():
1051         print "error %s unknown" % ref
1052         return False
1054     if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
1055         if force_push:
1056             print "ok %s forced update" % ref
1057         else:
1058             print "error %s non-fast forward" % ref
1059             return False
1061     return True
1063 def checkheads(repo, remote, p_revs):
1065     remotemap = remote.branchmap()
1066     if not remotemap:
1067         # empty repo
1068         return True
1070     new = {}
1071     ret = True
1073     for node, ref in p_revs.iteritems():
1074         ctx = repo[node]
1075         branch = ctx.branch()
1076         if branch not in remotemap:
1077             # new branch
1078             continue
1079         if not ref.startswith('refs/heads/branches'):
1080             if ref.startswith('refs/heads/'):
1081                 if not checkheads_bmark(repo, ref, ctx):
1082                     ret = False
1084             # only check branches
1085             continue
1086         new.setdefault(branch, []).append(ctx.rev())
1088     for branch, heads in new.iteritems():
1089         old = [repo.changelog.rev(x) for x in remotemap[branch]]
1090         for rev in heads:
1091             if check_version(2, 3):
1092                 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
1093             else:
1094                 ancestors = repo.changelog.ancestors(rev)
1095             found = False
1097             for x in old:
1098                 if x in ancestors:
1099                     found = True
1100                     break
1102             if found:
1103                 continue
1105             node = repo.changelog.node(rev)
1106             ref = p_revs[node]
1107             if force_push:
1108                 print "ok %s forced update" % ref
1109             else:
1110                 print "error %s non-fast forward" % ref
1111                 ret = False
1113     return ret
1115 def push_unsafe(repo, remote, parsed_refs, p_revs):
1117     force = force_push
1119     fci = discovery.findcommonincoming
1120     commoninc = fci(repo, remote, force=force)
1121     common, _, remoteheads = commoninc
1123     if not checkheads(repo, remote, p_revs):
1124         return None
1126     if check_version(3, 2):
1127         cg = changegroup.getchangegroup(repo, 'push', heads=list(p_revs), common=common)
1128     elif check_version(3, 0):
1129         cg = changegroup.getbundle(repo, 'push', heads=list(p_revs), common=common)
1130     else:
1131         cg = repo.getbundle('push', heads=list(p_revs), common=common)
1133     unbundle = remote.capable('unbundle')
1134     if unbundle:
1135         if force:
1136             remoteheads = ['force']
1137         ret = remote.unbundle(cg, remoteheads, 'push')
1138     else:
1139         ret = remote.addchangegroup(cg, 'push', repo.url())
1141     phases = remote.listkeys('phases')
1142     if phases:
1143         for head in p_revs:
1144             # update to public
1145             remote.pushkey('phases', hghex(head), '1', '0')
1147     return ret
1149 def push(repo, remote, parsed_refs, p_revs):
1150     if hasattr(remote, 'canpush') and not remote.canpush():
1151         print "error cannot push"
1153     if not p_revs:
1154         # nothing to push
1155         return
1157     lock = None
1158     unbundle = remote.capable('unbundle')
1159     if not unbundle:
1160         lock = remote.lock()
1161     try:
1162         ret = push_unsafe(repo, remote, parsed_refs, p_revs)
1163     finally:
1164         if lock is not None:
1165             lock.release()
1167     return ret
1169 def check_tip(ref, kind, name, heads):
1170     try:
1171         ename = '%s/%s' % (kind, name)
1172         tip = marks.get_tip(ename)
1173     except KeyError:
1174         return True
1175     else:
1176         return tip in heads
1178 # Handle the 'export' remote-helper command.
1179 # git->hg: parse the fast-import stream git feeds us,
1180 # and add the commits and data in it to the hg repository.
1181 def do_export(parser):
1182     p_bmarks = []
1183     p_revs = {}
1185     parser.next()
1187     # parse the fast-import stream
1188     # see also http://git-scm.com/docs/git-fast-import
1189     for line in parser.each_block('done'):
1190         if parser.check('blob'):
1191             parse_blob(parser)
1192         elif parser.check('commit'):
1193             parse_commit(parser)
1194         elif parser.check('reset'):
1195             parse_reset(parser)
1196         elif parser.check('tag'):
1197             parse_tag(parser)
1198         elif parser.check('feature'):
1199             pass
1200         else:
1201             die('unhandled export command: %s' % line)
1203     need_fetch = False
1205     for ref, node in parsed_refs.iteritems():
1206         bnode = hgbin(node) if node else None
1207         if ref.startswith('refs/heads/branches'):
1208             branch = ref[len('refs/heads/branches/'):]
1209             if branch in branches and bnode in branches[branch]:
1210                 # up to date
1211                 continue
1213             if peer:
1214                 remotemap = peer.branchmap()
1215                 if remotemap and branch in remotemap:
1216                     heads = [hghex(e) for e in remotemap[branch]]
1217                     if not check_tip(ref, 'branches', branch, heads):
1218                         print "error %s fetch first" % ref
1219                         need_fetch = True
1220                         continue
1222             p_revs[bnode] = ref
1223             print "ok %s" % ref
1224         elif ref.startswith('refs/heads/'):
1225             bmark = ref[len('refs/heads/'):]
1226             new = node
1227             old = bmarks[bmark].hex() if bmark in bmarks else ''
1229             if old == new:
1230                 continue
1232             print "ok %s" % ref
1233             if bmark != fake_bmark and \
1234                     not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1235                 p_bmarks.append((ref, bmark, old, new))
1237             if peer:
1238                 remote_old = peer.listkeys('bookmarks').get(bmark)
1239                 if remote_old:
1240                     if not check_tip(ref, 'bookmarks', bmark, remote_old):
1241                         print "error %s fetch first" % ref
1242                         need_fetch = True
1243                         continue
1245             p_revs[bnode] = ref
1246         elif ref.startswith('refs/tags/'):
1247             if dry_run:
1248                 print "ok %s" % ref
1249                 continue
1250             tag = ref[len('refs/tags/'):]
1251             tag = hgref(tag)
1252             author, msg = parsed_tags.get(tag, (None, None))
1253             if mode == 'git':
1254                 if not msg:
1255                     msg = 'Added tag %s for changeset %s' % (tag, node[:12])
1256                 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1257                 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1258             else:
1259                 fp = parser.repo.opener('localtags', 'a')
1260                 fp.write('%s %s\n' % (node, tag))
1261                 fp.close()
1262             p_revs[bnode] = ref
1263             print "ok %s" % ref
1264         else:
1265             # transport-helper/fast-export bugs
1266             continue
1268     if need_fetch:
1269         print
1270         return
1272     if dry_run:
1273         if peer and not force_push:
1274             checkheads(parser.repo, peer, p_revs)
1275         print
1276         return
1278     if peer:
1279         if not push(parser.repo, peer, parsed_refs, p_revs):
1280             # do not update bookmarks
1281             print
1282             return
1284         # update remote bookmarks
1285         remote_bmarks = peer.listkeys('bookmarks')
1286         for ref, bmark, old, new in p_bmarks:
1287             if force_push:
1288                 old = remote_bmarks.get(bmark, '')
1289             if not peer.pushkey('bookmarks', bmark, old, new):
1290                 print "error %s" % ref
1291     else:
1292         # update local bookmarks
1293         for ref, bmark, old, new in p_bmarks:
1294             if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1295                 print "error %s" % ref
1297     print
1299 # Handle the 'option' remote-helper command.
1300 def do_option(parser):
1301     global dry_run, force_push
1302     _, key, value = parser.line.split(' ')
1303     if key == 'dry-run':
1304         dry_run = (value == 'true')
1305         print 'ok'
1306     elif key == 'force':
1307         force_push = (value == 'true')
1308         print 'ok'
1309     else:
1310         print 'unsupported'
1312 def fix_path(alias, repo, orig_url):
1313     url = urlparse.urlparse(orig_url, 'file')
1314     if url.scheme != 'file' or os.path.isabs(os.path.expanduser(url.path)):
1315         return
1316     abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1317     cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1318     subprocess.call(cmd)
1320 def main(args):
1321     global prefix, gitdir, dirname, branches, bmarks
1322     global marks, blob_marks, parsed_refs
1323     global peer, mode, bad_mail, bad_name
1324     global track_branches, force_push, is_tmp
1325     global parsed_tags
1326     global filenodes
1327     global fake_bmark, hg_version
1328     global dry_run
1329     global notes, alias
1331     marks = None
1332     is_tmp = False
1333     gitdir = os.environ.get('GIT_DIR', None)
1335     if len(args) < 3:
1336         die('Not enough arguments.')
1338     if not gitdir:
1339         die('GIT_DIR not set')
1341     alias = args[1]
1342     url = args[2]
1343     peer = None
1345     hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1346     track_branches = get_config_bool('remote-hg.track-branches', True)
1347     force_push = False
1349     if hg_git_compat:
1350         mode = 'hg'
1351         bad_mail = 'none@none'
1352         bad_name = ''
1353     else:
1354         mode = 'git'
1355         bad_mail = 'unknown'
1356         bad_name = 'Unknown'
1358     if alias[4:] == url:
1359         is_tmp = True
1360         alias = hashlib.sha1(alias).hexdigest()
1362     dirname = os.path.join(gitdir, 'hg', alias)
1363     branches = {}
1364     bmarks = {}
1365     blob_marks = {}
1366     parsed_refs = {}
1367     parsed_tags = {}
1368     filenodes = {}
1369     fake_bmark = None
1370     try:
1371         version, _, extra = util.version().partition('+')
1372         version = list(int(e) for e in version.split('.'))
1373         if extra:
1374             version[1] += 1
1375         hg_version = tuple(version)
1376     except:
1377         hg_version = None
1378     dry_run = False
1379     notes = set()
1381     repo = get_repo(url, alias)
1382     prefix = 'refs/hg/%s' % alias
1384     if not is_tmp:
1385         fix_path(alias, peer or repo, url)
1387     marks_path = os.path.join(dirname, 'marks-hg')
1388     marks = Marks(marks_path, repo)
1390     if sys.platform == 'win32':
1391         import msvcrt
1392         msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1394     parser = Parser(repo)
1395     for line in parser:
1396         if parser.check('capabilities'):
1397             do_capabilities(parser)
1398         elif parser.check('list'):
1399             do_list(parser)
1400         elif parser.check('import'):
1401             do_import(parser)
1402         elif parser.check('export'):
1403             do_export(parser)
1404         elif parser.check('option'):
1405             do_option(parser)
1406         else:
1407             die('unhandled command: %s' % line)
1408         sys.stdout.flush()
1410     marks.store()
1412 def bye():
1413     if is_tmp:
1414         shutil.rmtree(dirname)
1416 if __name__ == "__main__":
1417     atexit.register(bye)
1419     try:
1420         sys.exit(main(sys.argv))
1421     except Exception, e:
1422         if DEBUG_REMOTEHG:
1423            raise
1424         die("%s", e)