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