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