Don't escape accents in the darcs output.
[darcs2git.git] / darcs2git.py
blob1b864ede1083f4a2d6b14740d9839093b8331334
1 #! /usr/bin/python
3 """
5 darcs2git -- Darcs to git converter.
7 Copyright (c) 2007 Han-Wen Nienhuys <hanwen@xs4all.nl>
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2, or (at your option)
12 any later version.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23 """
25 # TODO:
27 # - time zones
29 # - file modes
31 # - use binary search to find from-patch in case of conflict.
34 import sha
35 from datetime import datetime
36 from time import strptime
37 import urlparse
38 import distutils.version
39 import glob
40 import os
41 import sys
42 import time
43 import xml.dom.minidom
44 import re
45 import gzip
46 import optparse
49 ################################################################
50 # globals
53 log_file = None
54 options = None
55 mail_to_name_dict = {}
56 pending_patches = {}
57 git_commits = {}
58 used_tags = {}
60 ################################################################
61 # utils
63 class PullConflict (Exception):
64 pass
65 class CommandFailed (Exception):
66 pass
68 def progress (s):
69 sys.stderr.write (s + '\n')
71 def get_cli_options ():
72 class MyOP(optparse.OptionParser):
73 def print_help(self):
74 optparse.OptionParser.print_help (self)
75 print '''
76 DESCRIPTION
78 This tool is a conversion utility for Darcs repositories, importing
79 them in chronological order. It requires a Git version that has
80 git fast-import. It does not support incremental updating.
82 BUGS
84 * repositories with skewed timestamps, or different patches with
85 equal timestamps will confuse darcs2git.
86 * does not respect file modes or time zones.
87 * too slow. See source code for instructions to speed it up.
88 * probably doesn\'t work on partial repositories
90 Report new bugs to hanwen@xs4all.nl
92 LICENSE
94 Copyright (c) 2007 Han-Wen Nienhuys <hanwen@xs4all.nl>.
95 Distributed under terms of the GNU General Public License
96 This program comes with NO WARRANTY.
97 '''
99 p = MyOP ()
101 p.usage='''darcs2git [OPTIONS] DARCS-REPO'''
102 p.description='''Convert darcs repo to git.'''
104 def update_map (option, opt, value, parser):
105 for l in open (value).readlines ():
106 (mail, name) = tuple (l.strip ().split ('='))
107 mail_to_name_dict[mail] = name
109 p.add_option ('-a', '--authors', action='callback',
110 callback=update_map,
111 type='string',
112 nargs=1,
113 help='read a text file, containing EMAIL=NAME lines')
115 p.add_option ('--checkpoint-frequency', action='store',
116 dest='checkpoint_frequency',
117 type='int',
118 default=0,
119 help='how often should the git importer be synced?\n'
120 'Default is 0 (no limit)'
123 p.add_option ('-d', '--destination', action='store',
124 type='string',
125 default='',
126 dest='target_git_repo',
127 help='where to put the resulting Git repo.')
129 p.add_option ('--verbose', action='store_true',
130 dest='verbose',
131 default=False,
132 help='show commands as they are invoked')
134 p.add_option ('--history-window', action='store',
135 dest='history_window',
136 type='int',
137 default=0,
138 help='Look back this many patches as conflict ancestors.\n'
139 'Default is 0 (no limit)')
141 p.add_option ('--debug', action='store_true',
142 dest='debug',
143 default=False,
144 help="""add patch numbers to commit messages;
145 don\'t clean conversion repo;
146 test end result.""")
148 p.add_option ('-e', '--encoding', action='store',
149 type='string',
150 default='UTF-8',
151 nargs=1,
152 help='specify encoding, for example ISO-8859-2')
154 global options
155 options, args = p.parse_args ()
156 if not args:
157 p.print_help ()
158 sys.exit (2)
160 if len(urlparse.urlparse(args[0])) == 0:
161 raise NotImplementedError, "We support local DARCS repos only."
163 git_version = distutils.version.LooseVersion(
164 os.popen("git --version","r").read().strip().split(" ")[-1])
165 ideal_version = distutils.version.LooseVersion("1.5.0")
166 if git_version<ideal_version:
167 raise RuntimeError,"You need git >= 1.5.0 for this."
169 options.basename = os.path.basename (
170 os.path.normpath (args[0])).replace ('.darcs', '')
171 if not options.target_git_repo:
172 options.target_git_repo = options.basename + '.git'
174 if options.debug:
175 global log_file
176 name = options.target_git_repo.replace ('.git', '.log')
177 if name == options.target_git_repo:
178 name += '.log'
180 progress ("Shell log to %s" % name)
181 log_file = open (name, 'w')
183 return (options, args)
185 def read_pipe (cmd, ignore_errors=False):
186 if options.verbose:
187 progress ('pipe %s' % cmd)
188 pipe = os.popen (cmd)
190 val = pipe.read ()
191 if pipe.close () and not ignore_errors:
192 raise CommandFailed ("Pipe failed: %s" % cmd)
194 return val
196 def system (c, ignore_error=0, timed=0):
197 if timed:
198 c = "time " + c
199 if options.verbose:
200 progress (c)
202 if log_file:
203 log_file.write ('%s\n' % c)
204 log_file.flush ()
206 if os.system (c) and not ignore_error:
207 raise CommandFailed ("Command failed: %s" % c)
209 def darcs_date_to_git (x):
210 t = time.strptime (x, '%Y%m%d%H%M%S')
211 return '%d' % int (time.mktime (t))
213 def darcs_timezone (x) :
214 time.strptime (x, '%a %b %d %H:%M:%S %Z %Y')
216 # todo
217 return "+0100"
219 ################################################################
220 # darcs
222 ## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/521889
224 PATCH_DATE_FORMAT = '%Y%m%d%H%M%S'
226 patch_pattern = r"""
227 \[ # Patch start indicator
228 (?P<name>[^\n]+)\n # Patch name (rest of same line)
229 (?P<author>[^\*]+) # Patch author
230 \* # Author/date separator
231 (?P<inverted>[-\*]) # Inverted patch indicator
232 (?P<date>\d{14}) # Patch date
233 (?:\n(?P<comment>(?:^\ [^\n]*\n)+))? # Optional long comment
234 \] # Patch end indicator
236 patch_re = re.compile(patch_pattern, re.VERBOSE | re.MULTILINE)
237 tidy_comment_re = re.compile(r'^ ', re.MULTILINE)
239 def parse_inventory(inventory):
241 Given the contents of a darcs inventory file, generates ``Patch``
242 objects representing contained patch details.
244 for match in patch_re.finditer(inventory):
245 attrs = match.groupdict(None)
246 attrs['inverted'] = (attrs['inverted'] == '-')
247 if attrs['comment'] is not None:
248 attrs['comment'] = tidy_comment_re.sub('', attrs['comment']).strip()
249 yield InventoryPatch(**attrs)
251 def fix_braindead_darcs_escapes(s):
252 def insert_hibit(match):
253 return chr(int(match.group(1), 16))
255 return re.sub(r'\[_\\([0-9a-f][0-9a-f])_\]',
256 insert_hibit, str(s))
258 class InventoryPatch:
260 Patch details, as defined in a darcs inventory file.
262 Attribute names match those generated by the
263 ``darcs changes --xml-output`` command.
266 def __init__(self, name, author, date, inverted, comment=None):
267 self.name = name
268 self.author = author
269 self.date = datetime(*strptime(date, PATCH_DATE_FORMAT)[:6])
270 self.inverted = inverted
271 self.comment = comment
273 def __str__(self):
274 return self.name
276 @property
277 def complete_patch_details(self):
278 date_str = self.date.strftime(PATCH_DATE_FORMAT)
279 return '%s%s%s%s%s' % (
280 self.name, self.author, date_str,
281 self.comment and ''.join([l.rstrip() for l in self.comment.split('\n')]) or '',
282 self.inverted and 't' or 'f')
284 def short_id (self):
285 inv = '*'
286 if self.inverted:
287 inv = '-'
289 return unicode('%s%s*%s%s' % (self.name, self.author, inv, self.hash.split ('-')[0]), options.encoding)
291 @property
292 def hash(self):
294 Calculates the filename of the gzipped file containing patch
295 contents in the repository's ``patches`` directory.
297 This consists of the patch date, a partial SHA-1 hash of the
298 patch author and a full SHA-1 hash of the complete patch
299 details.
302 date_str = self.date.strftime(PATCH_DATE_FORMAT)
303 return '%s-%s-%s.gz' % (date_str,
304 sha.new(self.author).hexdigest()[:5],
305 sha.new(self.complete_patch_details).hexdigest())
307 ## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/521889
309 class DarcsConversionRepo:
310 """Representation of a Darcs repo.
312 The repo is thought to be ordered, and supports methods for
313 going back (obliterate) and forward (pull).
317 def __init__ (self, dir, patches):
318 self.dir = os.path.abspath (dir)
319 self.patches = patches
320 self._current_number = -1
321 self._is_valid = -1
322 self._inventory_dict = None
323 self._short_id_dict = dict ((p.short_id (), p) for p in patches)
325 def __del__ (self):
326 if not options.debug:
327 system ('rm -fr %s' % self.dir)
329 def is_contiguous (self):
330 return (len (self.inventory_dict ()) == self._current_number + 1
331 and self.contains_contiguous (self._current_number))
333 def contains_contiguous (self, num):
334 if not self._is_valid:
335 return False
337 darcs_dir = self.dir + '/_darcs'
338 if not os.path.exists (darcs_dir):
339 return False
341 for p in self.patches[:num + 1]:
342 if not self.has_patch (p):
343 return False
345 return True
347 def has_patch (self, p):
348 assert self._is_valid
350 return p.short_id () in self.inventory_dict ()
352 def pristine_tree (self):
353 return self.dir + '/_darcs/pristine'
355 def go_back_to (self, dest):
357 # at 4, len = 5, go to 2: count == 2
358 count = len (self.inventory_dict()) - dest - 1
360 assert self._is_valid
361 assert count > 0
363 self.checkout ()
364 dir = self.dir
366 progress ('Rewinding %d patches' % count)
367 system ('cd %(dir)s && darcs revert --all' % locals())
368 system ('cd %(dir)s && yes|darcs obliterate -a --ignore-times --last %(count)d' % locals ())
369 system ('cd %(dir)s && darcs revert -a' % locals())
370 d = self.inventory_dict ()
371 for p in self.patches[dest+1:self._current_number+1]:
372 try:
373 del d[p.short_id ()]
374 except KeyError:
375 pass
377 self._current_number = dest
379 def clean (self):
380 system ('rm -rf %s' % self.dir)
382 def checkout (self):
383 dir = self.dir
384 system ('rsync -a %(dir)s/_darcs/pristine/ %(dir)s/' % locals ())
386 def pull (self, patch):
387 id = patch.attributes['hash']
388 source_repo = patch.dir
389 dir = self.dir
391 progress ('Pull patch %d' % patch.number)
392 system ('cd %(dir)s && darcs revert --all' % locals())
393 system ('cd %(dir)s && darcs pull --ignore-times --quiet --all --match "hash %(id)s" %(source_repo)s ' % locals ())
395 self._current_number = patch.number
397 ## must reread: the pull may have pulled in others.
398 self._inventory_dict = None
400 def go_forward_to (self, num):
401 d = self.inventory_dict ()
403 pull_me = []
405 ## ugh
406 for p in self.patches[0:num+1]:
407 if not d.has_key (p.short_id ()):
408 pull_me.append (p)
409 d[p.short_id ()] = p
411 pull_str = ' || '.join (['hash %s' % p.id () for p in pull_me])
412 dir = self.dir
413 src = self.patches[0].dir
415 progress ('Pulling %d patches to go to %d' % (len (pull_me), num))
416 system ('darcs revert --repo %(dir)s --all' % locals ())
417 system ('darcs pull --all --repo %(dir)s --match "%(pull_str)s" %(src)s' % locals ())
419 def create_fresh (self):
420 dir = self.dir
421 system ('rm -rf %(dir)s && mkdir %(dir)s && darcs init --repo %(dir)s'
422 % locals ())
423 self._is_valid = True
424 self._current_number = -1
425 self._inventory_dict = {}
427 def inventory (self):
428 darcs_dir = self.dir + '/_darcs'
429 i = ''
430 for f in [darcs_dir + '/inventory'] + glob.glob (darcs_dir + '/inventories/*'):
431 i += open (f).read ()
432 return i
434 def inventory_dict (self):
436 if type (self._inventory_dict) != type ({}):
437 self._inventory_dict = {}
439 for p in parse_inventory(self.inventory()):
440 key = p.short_id()
442 try:
443 self._inventory_dict[key] = self._short_id_dict[key]
444 except KeyError:
445 print 'key not found', key
446 print self._short_id_dict
447 raise
449 return self._inventory_dict
451 def start_at (self, num):
452 """Move the repo to NUM.
454 This uses the fishy technique of writing the inventory and
455 constructing the pristine tree with 'darcs repair'
457 progress ('Starting afresh at %d' % num)
459 self.create_fresh ()
460 dir = self.dir
461 iv = open (dir + '/_darcs/inventory', 'w')
462 if log_file:
463 log_file.write ("# messing with _darcs/inventory")
465 for p in self.patches[:num+1]:
466 os.link (p.filename (), dir + '/_darcs/patches/' + os.path.basename (p.filename ()))
467 iv.write (p.header ())
468 self._inventory_dict[p.short_id ()] = p
469 iv.close ()
471 system ('darcs revert --repo %(dir)s --all' % locals())
472 system ('darcs repair --repo %(dir)s --quiet' % locals ())
473 self.checkout ()
474 self._current_number = num
475 self._is_valid = True
477 def go_to (self, dest):
478 if not self._is_valid:
479 self.start_at (dest)
480 elif dest == self._current_number and self.is_contiguous ():
481 pass
482 elif (self.contains_contiguous (dest)):
483 self.go_back_to (dest)
484 elif dest - len (self.inventory_dict ()) < dest / 100:
485 self.go_forward_to (dest)
486 else:
487 self.start_at (dest)
490 def go_from_to (self, from_patch, to_patch):
492 """Move the repo to FROM_PATCH, then go to TO_PATCH. Raise
493 PullConflict if conflict is detected
496 progress ('Trying %s -> %s' % (from_patch, to_patch))
497 dir = self.dir
498 source = to_patch.dir
500 if from_patch:
501 self.go_to (from_patch.number)
502 else:
503 self.create_fresh ()
505 try:
506 self.pull (to_patch)
507 success = 'No conflicts to resolve' in read_pipe ('cd %(dir)s && echo y|darcs resolve' % locals ())
508 except CommandFailed:
509 self._is_valid = False
510 raise PullConflict ()
512 if not success:
513 raise PullConflict ()
515 class DarcsPatch:
516 def __repr__ (self):
517 return 'patch %d' % self.number
519 def __init__ (self, xml, dir):
520 self.xml = xml
521 self.dir = dir
522 self.number = -1
523 self.attributes = {}
524 self._contents = None
525 for (nm, value) in xml.attributes.items():
526 self.attributes[nm] = value
528 # fixme: ugh attributes vs. methods.
529 self.extract_author ()
530 self.extract_message ()
531 self.extract_time ()
533 def id (self):
534 return self.attributes['hash']
536 def short_id (self):
537 inv = '*'
538 if self.attributes['inverted'] == 'True':
539 inv = '-'
541 return '%s%s*%s%s' % (self.name(), self.attributes['author'], inv, self.attributes['hash'].split ('-')[0])
543 def filename (self):
544 return self.dir + '/_darcs/patches/' + self.attributes['hash']
546 def contents (self):
547 if type (self._contents) != type (''):
548 f = gzip.open (self.filename ())
549 self._contents = f.read ()
551 return self._contents
553 def header (self):
554 lines = self.contents ().split ('\n')
556 name = lines[0]
557 committer = lines[1] + '\n'
558 committer = re.sub ('] {\n$', ']\n', committer)
559 committer = re.sub ('] *\n$', ']\n', committer)
560 comment = ''
561 if not committer.endswith (']\n'):
562 for l in lines[2:]:
563 if l[0] == ']':
564 comment += ']\n'
565 break
566 comment += l + '\n'
568 header = name + '\n' + committer
569 if comment:
570 header += comment
572 assert header[-1] == '\n'
573 return header
575 def extract_author (self):
576 mail = self.attributes['author']
577 name = ''
578 m = re.search ("^(.*) <(.*)>$", mail)
580 if m:
581 name = m.group (1)
582 mail = m.group (2)
583 else:
584 try:
585 name = mail_to_name_dict[mail]
586 except KeyError:
587 name = mail.split ('@')[0]
589 self.author_name = name
591 # mail addresses should be plain strings.
592 self.author_mail = mail.encode('utf-8')
594 def extract_time (self):
595 self.date = darcs_date_to_git (self.attributes['date']) + ' ' + darcs_timezone (self.attributes['local_date'])
597 def name (self):
598 patch_name = ''
599 try:
600 name_elt = self.xml.getElementsByTagName ('name')[0]
601 patch_name = unicode(fix_braindead_darcs_escapes(str(name_elt.childNodes[0].data)), options.encoding)
602 except IndexError:
603 pass
604 return patch_name
606 def extract_message (self):
607 patch_name = self.name ()
608 comment_elts = self.xml.getElementsByTagName ('comment')
609 comment = ''
610 if comment_elts:
611 comment = comment_elts[0].childNodes[0].data
613 if self.attributes['inverted'] == 'True':
614 patch_name = 'UNDO: ' + patch_name
616 self.message = '%s\n\n%s' % (patch_name, comment)
618 def tag_name (self):
619 patch_name = self.name ()
620 if patch_name.startswith ("TAG "):
621 tag = patch_name[4:]
622 tag = re.sub (r'\s', '_', tag).strip ()
623 tag = re.sub (r':', '_', tag).strip ()
624 return tag
625 return ''
627 def get_darcs_patches (darcs_repo):
628 progress ('reading patches.')
630 xml_string = read_pipe ('darcs changes --xml --reverse --repo ' + darcs_repo)
632 dom = xml.dom.minidom.parseString(xml_string)
633 xmls = dom.documentElement.getElementsByTagName('patch')
635 patches = [DarcsPatch (x, darcs_repo) for x in xmls]
637 n = 0
638 for p in patches:
639 p.number = n
640 n += 1
642 return patches
644 ################################################################
645 # GIT export
647 class GitCommit:
648 def __init__ (self, parent, darcs_patch):
649 self.parent = parent
650 self.darcs_patch = darcs_patch
651 if parent:
652 self.depth = parent.depth + 1
653 else:
654 self.depth = 0
656 def number (self):
657 return self.darcs_patch.number
659 def parent_patch (self):
660 if self.parent:
661 return self.parent.darcs_patch
662 else:
663 return None
665 def common_ancestor (a, b):
666 while 1:
667 if a.depth < b.depth:
668 b = b.parent
669 elif a.depth > b.depth:
670 a = a.parent
671 else:
672 break
674 while a and b:
675 if a == b:
676 return a
678 a = a.parent
679 b = b.parent
681 return None
683 def export_checkpoint (gfi):
684 gfi.write ('checkpoint\n\n')
686 def export_tree (tree, gfi):
687 tree = os.path.normpath (tree)
688 gfi.write ('deleteall\n')
689 for (root, dirs, files) in os.walk (tree):
690 for f in files:
691 rf = os.path.normpath (os.path.join (root, f))
692 s = open (rf).read ()
693 rf = rf.replace (tree + '/', '')
695 gfi.write ('M 644 inline %s\n' % rf)
696 gfi.write ('data %d\n%s\n' % (len (s), s))
697 gfi.write ('\n')
700 def export_commit (repo, patch, last_patch, gfi):
701 gfi.write ('commit refs/heads/darcstmp%d\n' % patch.number)
702 gfi.write ('mark :%d\n' % (patch.number + 1))
704 raw_name = patch.author_name
705 gfi.write ('committer %s <%s> %s\n' % (raw_name,
706 patch.author_mail,
707 patch.date))
709 msg = patch.message
710 if options.debug:
711 msg += '\n\n#%d\n' % patch.number
713 msg = msg.encode('utf-8')
714 gfi.write ('data %d\n%s\n' % (len (msg), msg))
715 mergers = []
716 for (n, p) in pending_patches.items ():
717 if repo.has_patch (p):
718 mergers.append (n)
719 del pending_patches[n]
721 if (last_patch
722 and mergers == []
723 and git_commits.has_key (last_patch.number)):
724 mergers = [last_patch.number]
726 if mergers:
727 gfi.write ('from :%d\n' % (mergers[0] + 1))
728 for m in mergers[1:]:
729 gfi.write ('merge :%d\n' % (m + 1))
731 pending_patches[patch.number] = patch
732 export_tree (repo.pristine_tree (), gfi)
734 n = -1
735 if last_patch:
736 n = last_patch.number
737 git_commits[patch.number] = GitCommit (git_commits.get (n, None),
738 patch)
740 def export_pending (gfi):
741 if len (pending_patches.items ()) == 1:
742 gfi.write ('reset refs/heads/master\n')
743 gfi.write ('from :%d\n\n' % (pending_patches.values()[0].number+1))
745 progress ("Creating branch master")
746 return
748 for (n, p) in pending_patches.items ():
749 gfi.write ('reset refs/heads/master%d\n' % n)
750 gfi.write ('from :%d\n\n' % (n+1))
752 progress ("Creating branch master%d" % n)
754 patches = pending_patches.values()
755 patch = patches[0]
756 gfi.write ('commit refs/heads/master\n')
757 gfi.write ('committer %s <%s> %s\n' % (patch.author_name,
758 patch.author_mail,
759 patch.date))
760 msg = 'tie together'
761 gfi.write ('data %d\n%s\n' % (len(msg), msg))
762 gfi.write ('from :%d\n' % (patch.number + 1))
763 for p in patches[1:]:
764 gfi.write ('merge :%d\n' % (p.number + 1))
765 gfi.write ('\n')
767 def export_tag (patch, gfi):
768 gfi.write ('tag %s\n' % patch.tag_name ())
769 gfi.write ('from :%d\n' % (patch.number + 1))
770 gfi.write ('tagger %s <%s> %s\n' % (patch.author_name,
771 patch.author_mail,
772 patch.date))
774 raw_message = patch.message.encode('utf-8')
775 gfi.write ('data %d\n%s\n' % (len (raw_message),
776 raw_message))
778 ################################################################
779 # main.
781 def test_conversion (darcs_repo, git_repo):
782 pristine = '%(darcs_repo)s/_darcs/pristine' % locals ()
783 if not os.path.exists (pristine):
784 progress ("darcs repository does not contain pristine tree?!")
785 return
787 gd = options.basename + '.checkouttmp.git'
788 system ('rm -rf %(gd)s && git clone %(git_repo)s %(gd)s' % locals ())
789 diff_cmd = 'diff --exclude .git -urN %(gd)s %(pristine)s' % locals ()
790 diff = read_pipe (diff_cmd, ignore_errors=True)
791 system ('rm -rf %(gd)s' % locals ())
793 if diff:
794 if len (diff) > 1024:
795 diff = diff[:512] + '\n...\n' + diff[-512:]
797 progress ("Conversion introduced changes: %s" % diff)
798 raise 'fdsa'
799 else:
800 progress ("Checkout matches pristine darcs tree.")
802 def main ():
803 (options, args) = get_cli_options ()
805 darcs_repo = os.path.abspath (args[0])
806 git_repo = os.path.abspath (options.target_git_repo)
808 if os.path.exists (git_repo):
809 system ('rm -rf %(git_repo)s' % locals ())
811 system ('mkdir %(git_repo)s && cd %(git_repo)s && git --bare init' % locals ())
812 system ('git --git-dir %(git_repo)s repo-config core.logAllRefUpdates false' % locals ())
814 os.environ['DARCS_DONT_ESCAPE_8BIT'] = '1'
815 os.environ['GIT_DIR'] = git_repo
817 quiet = ' --quiet'
818 if options.verbose:
819 quiet = ' '
821 gfi = os.popen ('git fast-import %s' % quiet, 'w')
823 patches = get_darcs_patches (darcs_repo)
824 conv_repo = DarcsConversionRepo (options.basename + ".tmpdarcs", patches)
825 conv_repo.start_at (-1)
827 for p in patches:
828 parent_patch = None
829 parent_number = -1
831 combinations = [(v, w) for v in pending_patches.values ()
832 for w in pending_patches.values ()]
833 candidates = [common_ancestor (git_commits[c[0].number], git_commits[c[1].number]) for c in combinations]
834 candidates = sorted ([(-a.darcs_patch.number, a) for a in candidates])
835 for (depth, c) in candidates:
836 q = c.darcs_patch
837 try:
838 conv_repo.go_from_to (q, p)
840 parent_patch = q
841 parent_number = q.number
842 progress ('Found existing common parent as predecessor')
843 break
845 except PullConflict:
846 pass
848 ## no branches found where we could attach.
849 ## try previous commits one by one.
850 if not parent_patch:
851 parent_number = p.number - 2
852 while 1:
853 if parent_number >= 0:
854 parent_patch = patches[parent_number]
856 try:
857 conv_repo.go_from_to (parent_patch, p)
858 break
859 except PullConflict:
861 ## simplistic, may not be enough.
862 progress ('conflict, going one back')
863 parent_number -= 1
865 if parent_number < 0:
866 break
868 if (options.history_window
869 and parent_number < p.number - options.history_window):
871 parent_number = -2
872 break
874 if parent_number >= 0 or p.number == 0:
875 progress ('Export %d -> %d (total %d)' % (parent_number,
876 p.number, len (patches)))
877 export_commit (conv_repo, p, parent_patch, gfi)
878 if p.tag_name ():
879 export_tag (p, gfi)
881 if options.checkpoint_frequency and p.number % options.checkpoint_frequency == 0:
882 export_checkpoint (gfi)
883 else:
884 progress ("Can't import patch %d, need conflict resolution patch?"
885 % p.number)
887 export_pending (gfi)
888 gfi.close ()
889 for f in glob.glob ('%(git_repo)s/refs/heads/darcstmp*' % locals ()):
890 os.unlink (f)
892 test_conversion (darcs_repo, git_repo)
894 if not options.debug:
895 conv_repo.clean ()
897 main ()