a3e8f6fc1b512797297bbe4aa6f45a24be86baa6
[darcs2git.git] / darcs2git.py
bloba3e8f6fc1b512797297bbe4aa6f45a24be86baa6
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 global options
149 options, args = p.parse_args ()
150 if not args:
151 p.print_help ()
152 sys.exit (2)
154 if len(urlparse.urlparse(args[0])) == 0:
155 raise NotImplementedError, "We support local DARCS repos only."
157 git_version = distutils.version.LooseVersion(
158 os.popen("git --version","r").read().strip().split(" ")[-1])
159 ideal_version = distutils.version.LooseVersion("1.5.0")
160 if git_version<ideal_version:
161 raise RuntimeError,"You need git >= 1.5.0 for this."
163 options.basename = os.path.basename (
164 os.path.normpath (args[0])).replace ('.darcs', '')
165 if not options.target_git_repo:
166 options.target_git_repo = options.basename + '.git'
168 if options.debug:
169 global log_file
170 name = options.target_git_repo.replace ('.git', '.log')
171 if name == options.target_git_repo:
172 name += '.log'
174 progress ("Shell log to %s" % name)
175 log_file = open (name, 'w')
177 return (options, args)
179 def read_pipe (cmd, ignore_errors=False):
180 if options.verbose:
181 progress ('pipe %s' % cmd)
182 pipe = os.popen (cmd)
184 val = pipe.read ()
185 if pipe.close () and not ignore_errors:
186 raise CommandFailed ("Pipe failed: %s" % cmd)
188 return val
190 def system (c, ignore_error=0, timed=0):
191 if timed:
192 c = "time " + c
193 if options.verbose:
194 progress (c)
196 if log_file:
197 log_file.write ('%s\n' % c)
198 log_file.flush ()
200 if os.system (c) and not ignore_error:
201 raise CommandFailed ("Command failed: %s" % c)
203 def darcs_date_to_git (x):
204 t = time.strptime (x, '%Y%m%d%H%M%S')
205 return '%d' % int (time.mktime (t))
207 def darcs_timezone (x) :
208 time.strptime (x, '%a %b %d %H:%M:%S %Z %Y')
210 # todo
211 return "+0100"
213 ################################################################
214 # darcs
216 ## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/521889
218 PATCH_DATE_FORMAT = '%Y%m%d%H%M%S'
220 patch_pattern = r"""
221 \[ # Patch start indicator
222 (?P<name>[^\n]+)\n # Patch name (rest of same line)
223 (?P<author>[^\*]+) # Patch author
224 \* # Author/date separator
225 (?P<inverted>[-\*]) # Inverted patch indicator
226 (?P<date>\d{14}) # Patch date
227 (?:\n(?P<comment>(?:^\ [^\n]*\n)+))? # Optional long comment
228 \] # Patch end indicator
230 patch_re = re.compile(patch_pattern, re.VERBOSE | re.MULTILINE)
231 tidy_comment_re = re.compile(r'^ ', re.MULTILINE)
233 def parse_inventory(inventory):
235 Given the contents of a darcs inventory file, generates ``Patch``
236 objects representing contained patch details.
238 for match in patch_re.finditer(inventory):
239 attrs = match.groupdict(None)
240 attrs['inverted'] = (attrs['inverted'] == '-')
241 if attrs['comment'] is not None:
242 attrs['comment'] = tidy_comment_re.sub('', attrs['comment']).strip()
243 yield InventoryPatch(**attrs)
245 def fix_braindead_darcs_escapes(s):
246 def insert_hibit(match):
247 return chr(int(match.group(1), 16))
249 return re.sub(r'\[_\\([0-9a-f][0-9a-f])_\]',
250 insert_hibit, str(s))
252 class InventoryPatch:
254 Patch details, as defined in a darcs inventory file.
256 Attribute names match those generated by the
257 ``darcs changes --xml-output`` command.
260 def __init__(self, name, author, date, inverted, comment=None):
261 self.name = name
262 self.author = author
263 self.date = datetime(*strptime(date, PATCH_DATE_FORMAT)[:6])
264 self.inverted = inverted
265 self.comment = comment
267 def __str__(self):
268 return self.name
270 @property
271 def complete_patch_details(self):
272 date_str = self.date.strftime(PATCH_DATE_FORMAT)
273 return '%s%s%s%s%s' % (
274 self.name, self.author, date_str,
275 self.comment and ''.join([l.rstrip() for l in self.comment.split('\n')]) or '',
276 self.inverted and 't' or 'f')
278 def short_id (self):
279 inv = '*'
280 if self.inverted:
281 inv = '-'
283 return unicode('%s%s*%s%s' % (self.name, self.author, inv, self.hash.split ('-')[0]), 'UTF-8')
285 @property
286 def hash(self):
288 Calculates the filename of the gzipped file containing patch
289 contents in the repository's ``patches`` directory.
291 This consists of the patch date, a partial SHA-1 hash of the
292 patch author and a full SHA-1 hash of the complete patch
293 details.
296 date_str = self.date.strftime(PATCH_DATE_FORMAT)
297 return '%s-%s-%s.gz' % (date_str,
298 sha.new(self.author).hexdigest()[:5],
299 sha.new(self.complete_patch_details).hexdigest())
301 ## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/521889
303 class DarcsConversionRepo:
304 """Representation of a Darcs repo.
306 The repo is thought to be ordered, and supports methods for
307 going back (obliterate) and forward (pull).
311 def __init__ (self, dir, patches):
312 self.dir = os.path.abspath (dir)
313 self.patches = patches
314 self._current_number = -1
315 self._is_valid = -1
316 self._inventory_dict = None
317 self._short_id_dict = dict ((p.short_id (), p) for p in patches)
319 def __del__ (self):
320 if not options.debug:
321 system ('rm -fr %s' % self.dir)
323 def is_contiguous (self):
324 return (len (self.inventory_dict ()) == self._current_number + 1
325 and self.contains_contiguous (self._current_number))
327 def contains_contiguous (self, num):
328 if not self._is_valid:
329 return False
331 darcs_dir = self.dir + '/_darcs'
332 if not os.path.exists (darcs_dir):
333 return False
335 for p in self.patches[:num + 1]:
336 if not self.has_patch (p):
337 return False
339 return True
341 def has_patch (self, p):
342 assert self._is_valid
344 return p.short_id () in self.inventory_dict ()
346 def pristine_tree (self):
347 return self.dir + '/_darcs/pristine'
349 def go_back_to (self, dest):
351 # at 4, len = 5, go to 2: count == 2
352 count = len (self.inventory_dict()) - dest - 1
354 assert self._is_valid
355 assert count > 0
357 self.checkout ()
358 dir = self.dir
360 progress ('Rewinding %d patches' % count)
361 system ('cd %(dir)s && darcs revert --all' % locals())
362 system ('cd %(dir)s && yes|darcs obliterate -a --ignore-times --last %(count)d' % locals ())
363 system ('cd %(dir)s && darcs revert -a' % locals())
364 d = self.inventory_dict ()
365 for p in self.patches[dest+1:self._current_number+1]:
366 try:
367 del d[p.short_id ()]
368 except KeyError:
369 pass
371 self._current_number = dest
373 def clean (self):
374 system ('rm -rf %s' % self.dir)
376 def checkout (self):
377 dir = self.dir
378 system ('rsync -a %(dir)s/_darcs/pristine/ %(dir)s/' % locals ())
380 def pull (self, patch):
381 id = patch.attributes['hash']
382 source_repo = patch.dir
383 dir = self.dir
385 progress ('Pull patch %d' % patch.number)
386 system ('cd %(dir)s && darcs revert --all' % locals())
387 system ('cd %(dir)s && darcs pull --ignore-times --quiet --all --match "hash %(id)s" %(source_repo)s ' % locals ())
389 self._current_number = patch.number
391 ## must reread: the pull may have pulled in others.
392 self._inventory_dict = None
394 def go_forward_to (self, num):
395 d = self.inventory_dict ()
397 pull_me = []
399 ## ugh
400 for p in self.patches[0:num+1]:
401 if not d.has_key (p.short_id ()):
402 pull_me.append (p)
403 d[p.short_id ()] = p
405 pull_str = ' || '.join (['hash %s' % p.id () for p in pull_me])
406 dir = self.dir
407 src = self.patches[0].dir
409 progress ('Pulling %d patches to go to %d' % (len (pull_me), num))
410 system ('darcs revert --repo %(dir)s --all' % locals ())
411 system ('darcs pull --all --repo %(dir)s --match "%(pull_str)s" %(src)s' % locals ())
413 def create_fresh (self):
414 dir = self.dir
415 system ('rm -rf %(dir)s && mkdir %(dir)s && darcs init --repo %(dir)s'
416 % locals ())
417 self._is_valid = True
418 self._current_number = -1
419 self._inventory_dict = {}
421 def inventory (self):
422 darcs_dir = self.dir + '/_darcs'
423 i = ''
424 for f in [darcs_dir + '/inventory'] + glob.glob (darcs_dir + '/inventories/*'):
425 i += open (f).read ()
426 return i
428 def inventory_dict (self):
430 if type (self._inventory_dict) != type ({}):
431 self._inventory_dict = {}
433 for p in parse_inventory(self.inventory()):
434 key = p.short_id()
436 try:
437 self._inventory_dict[key] = self._short_id_dict[key]
438 except KeyError:
439 print 'key not found', key
440 print self._short_id_dict
441 raise
443 return self._inventory_dict
445 def start_at (self, num):
446 """Move the repo to NUM.
448 This uses the fishy technique of writing the inventory and
449 constructing the pristine tree with 'darcs repair'
451 progress ('Starting afresh at %d' % num)
453 self.create_fresh ()
454 dir = self.dir
455 iv = open (dir + '/_darcs/inventory', 'w')
456 if log_file:
457 log_file.write ("# messing with _darcs/inventory")
459 for p in self.patches[:num+1]:
460 os.link (p.filename (), dir + '/_darcs/patches/' + os.path.basename (p.filename ()))
461 iv.write (p.header ())
462 self._inventory_dict[p.short_id ()] = p
463 iv.close ()
465 system ('darcs revert --repo %(dir)s --all' % locals())
466 system ('darcs repair --repo %(dir)s --quiet' % locals ())
467 self.checkout ()
468 self._current_number = num
469 self._is_valid = True
471 def go_to (self, dest):
472 if not self._is_valid:
473 self.start_at (dest)
474 elif dest == self._current_number and self.is_contiguous ():
475 pass
476 elif (self.contains_contiguous (dest)):
477 self.go_back_to (dest)
478 elif dest - len (self.inventory_dict ()) < dest / 100:
479 self.go_forward_to (dest)
480 else:
481 self.start_at (dest)
484 def go_from_to (self, from_patch, to_patch):
486 """Move the repo to FROM_PATCH, then go to TO_PATCH. Raise
487 PullConflict if conflict is detected
490 progress ('Trying %s -> %s' % (from_patch, to_patch))
491 dir = self.dir
492 source = to_patch.dir
494 if from_patch:
495 self.go_to (from_patch.number)
496 else:
497 self.create_fresh ()
499 try:
500 self.pull (to_patch)
501 success = 'No conflicts to resolve' in read_pipe ('cd %(dir)s && echo y|darcs resolve' % locals ())
502 except CommandFailed:
503 self._is_valid = False
504 raise PullConflict ()
506 if not success:
507 raise PullConflict ()
509 class DarcsPatch:
510 def __repr__ (self):
511 return 'patch %d' % self.number
513 def __init__ (self, xml, dir):
514 self.xml = xml
515 self.dir = dir
516 self.number = -1
517 self.attributes = {}
518 self._contents = None
519 for (nm, value) in xml.attributes.items():
520 self.attributes[nm] = value
522 # fixme: ugh attributes vs. methods.
523 self.extract_author ()
524 self.extract_message ()
525 self.extract_time ()
527 def id (self):
528 return self.attributes['hash']
530 def short_id (self):
531 inv = '*'
532 if self.attributes['inverted'] == 'True':
533 inv = '-'
535 return '%s%s*%s%s' % (self.name(), self.attributes['author'], inv, self.attributes['hash'].split ('-')[0])
537 def filename (self):
538 return self.dir + '/_darcs/patches/' + self.attributes['hash']
540 def contents (self):
541 if type (self._contents) != type (''):
542 f = gzip.open (self.filename ())
543 self._contents = f.read ()
545 return self._contents
547 def header (self):
548 lines = self.contents ().split ('\n')
550 name = lines[0]
551 committer = lines[1] + '\n'
552 committer = re.sub ('] {\n$', ']\n', committer)
553 committer = re.sub ('] *\n$', ']\n', committer)
554 comment = ''
555 if not committer.endswith (']\n'):
556 for l in lines[2:]:
557 if l[0] == ']':
558 comment += ']\n'
559 break
560 comment += l + '\n'
562 header = name + '\n' + committer
563 if comment:
564 header += comment
566 assert header[-1] == '\n'
567 return header
569 def extract_author (self):
570 mail = self.attributes['author']
571 name = ''
572 m = re.search ("^(.*) <(.*)>$", mail)
574 if m:
575 name = m.group (1)
576 mail = m.group (2)
577 else:
578 try:
579 name = mail_to_name_dict[mail]
580 except KeyError:
581 name = mail.split ('@')[0]
583 self.author_name = name
585 # mail addresses should be plain strings.
586 self.author_mail = mail.encode('utf-8')
588 def extract_time (self):
589 self.date = darcs_date_to_git (self.attributes['date']) + ' ' + darcs_timezone (self.attributes['local_date'])
591 def name (self):
592 patch_name = ''
593 try:
594 name_elt = self.xml.getElementsByTagName ('name')[0]
595 patch_name = unicode(fix_braindead_darcs_escapes(str(name_elt.childNodes[0].data)), 'UTF-8')
596 except IndexError:
597 pass
598 return patch_name
600 def extract_message (self):
601 patch_name = self.name ()
602 comment_elts = self.xml.getElementsByTagName ('comment')
603 comment = ''
604 if comment_elts:
605 comment = comment_elts[0].childNodes[0].data
607 if self.attributes['inverted'] == 'True':
608 patch_name = 'UNDO: ' + patch_name
610 self.message = '%s\n\n%s' % (patch_name, comment)
612 def tag_name (self):
613 patch_name = self.name ()
614 if patch_name.startswith ("TAG "):
615 tag = patch_name[4:]
616 tag = re.sub (r'\s', '_', tag).strip ()
617 tag = re.sub (r':', '_', tag).strip ()
618 return tag
619 return ''
621 def get_darcs_patches (darcs_repo):
622 progress ('reading patches.')
624 xml_string = read_pipe ('darcs changes --xml --reverse --repo ' + darcs_repo)
626 dom = xml.dom.minidom.parseString(xml_string)
627 xmls = dom.documentElement.getElementsByTagName('patch')
629 patches = [DarcsPatch (x, darcs_repo) for x in xmls]
631 n = 0
632 for p in patches:
633 p.number = n
634 n += 1
636 return patches
638 ################################################################
639 # GIT export
641 class GitCommit:
642 def __init__ (self, parent, darcs_patch):
643 self.parent = parent
644 self.darcs_patch = darcs_patch
645 if parent:
646 self.depth = parent.depth + 1
647 else:
648 self.depth = 0
650 def number (self):
651 return self.darcs_patch.number
653 def parent_patch (self):
654 if self.parent:
655 return self.parent.darcs_patch
656 else:
657 return None
659 def common_ancestor (a, b):
660 while 1:
661 if a.depth < b.depth:
662 b = b.parent
663 elif a.depth > b.depth:
664 a = a.parent
665 else:
666 break
668 while a and b:
669 if a == b:
670 return a
672 a = a.parent
673 b = b.parent
675 return None
677 def export_checkpoint (gfi):
678 gfi.write ('checkpoint\n\n')
680 def export_tree (tree, gfi):
681 tree = os.path.normpath (tree)
682 gfi.write ('deleteall\n')
683 for (root, dirs, files) in os.walk (tree):
684 for f in files:
685 rf = os.path.normpath (os.path.join (root, f))
686 s = open (rf).read ()
687 rf = rf.replace (tree + '/', '')
689 gfi.write ('M 644 inline %s\n' % rf)
690 gfi.write ('data %d\n%s\n' % (len (s), s))
691 gfi.write ('\n')
694 def export_commit (repo, patch, last_patch, gfi):
695 gfi.write ('commit refs/heads/darcstmp%d\n' % patch.number)
696 gfi.write ('mark :%d\n' % (patch.number + 1))
698 raw_name = patch.author_name
699 gfi.write ('committer %s <%s> %s\n' % (raw_name,
700 patch.author_mail,
701 patch.date))
703 msg = patch.message
704 if options.debug:
705 msg += '\n\n#%d\n' % patch.number
707 msg = msg.encode('utf-8')
708 gfi.write ('data %d\n%s\n' % (len (msg), msg))
709 mergers = []
710 for (n, p) in pending_patches.items ():
711 if repo.has_patch (p):
712 mergers.append (n)
713 del pending_patches[n]
715 if (last_patch
716 and mergers == []
717 and git_commits.has_key (last_patch.number)):
718 mergers = [last_patch.number]
720 if mergers:
721 gfi.write ('from :%d\n' % (mergers[0] + 1))
722 for m in mergers[1:]:
723 gfi.write ('merge :%d\n' % (m + 1))
725 pending_patches[patch.number] = patch
726 export_tree (repo.pristine_tree (), gfi)
728 n = -1
729 if last_patch:
730 n = last_patch.number
731 git_commits[patch.number] = GitCommit (git_commits.get (n, None),
732 patch)
734 def export_pending (gfi):
735 if len (pending_patches.items ()) == 1:
736 gfi.write ('reset refs/heads/master\n')
737 gfi.write ('from :%d\n\n' % (pending_patches.values()[0].number+1))
739 progress ("Creating branch master")
740 return
742 for (n, p) in pending_patches.items ():
743 gfi.write ('reset refs/heads/master%d\n' % n)
744 gfi.write ('from :%d\n\n' % (n+1))
746 progress ("Creating branch master%d" % n)
748 patches = pending_patches.values()
749 patch = patches[0]
750 gfi.write ('commit refs/heads/master\n')
751 gfi.write ('committer %s <%s> %s\n' % (patch.author_name,
752 patch.author_mail,
753 patch.date))
754 msg = 'tie together'
755 gfi.write ('data %d\n%s\n' % (len(msg), msg))
756 gfi.write ('from :%d\n' % (patch.number + 1))
757 for p in patches[1:]:
758 gfi.write ('merge :%d\n' % (p.number + 1))
759 gfi.write ('\n')
761 def export_tag (patch, gfi):
762 gfi.write ('tag %s\n' % patch.tag_name ())
763 gfi.write ('from :%d\n' % (patch.number + 1))
764 gfi.write ('tagger %s <%s> %s\n' % (patch.author_name,
765 patch.author_mail,
766 patch.date))
768 raw_message = patch.message.encode('utf-8')
769 gfi.write ('data %d\n%s\n' % (len (raw_message),
770 raw_message))
772 ################################################################
773 # main.
775 def test_conversion (darcs_repo, git_repo):
776 pristine = '%(darcs_repo)s/_darcs/pristine' % locals ()
777 if not os.path.exists (pristine):
778 progress ("darcs repository does not contain pristine tree?!")
779 return
781 gd = options.basename + '.checkouttmp.git'
782 system ('rm -rf %(gd)s && git clone %(git_repo)s %(gd)s' % locals ())
783 diff_cmd = 'diff --exclude .git -urN %(gd)s %(pristine)s' % locals ()
784 diff = read_pipe (diff_cmd, ignore_errors=True)
785 system ('rm -rf %(gd)s' % locals ())
787 if diff:
788 if len (diff) > 1024:
789 diff = diff[:512] + '\n...\n' + diff[-512:]
791 progress ("Conversion introduced changes: %s" % diff)
792 raise 'fdsa'
793 else:
794 progress ("Checkout matches pristine darcs tree.")
796 def main ():
797 (options, args) = get_cli_options ()
799 darcs_repo = os.path.abspath (args[0])
800 git_repo = os.path.abspath (options.target_git_repo)
802 if os.path.exists (git_repo):
803 system ('rm -rf %(git_repo)s' % locals ())
805 system ('mkdir %(git_repo)s && cd %(git_repo)s && git --bare init' % locals ())
806 system ('git --git-dir %(git_repo)s repo-config core.logAllRefUpdates false' % locals ())
808 os.environ['GIT_DIR'] = git_repo
810 quiet = ' --quiet'
811 if options.verbose:
812 quiet = ' '
814 gfi = os.popen ('git fast-import %s' % quiet, 'w')
816 patches = get_darcs_patches (darcs_repo)
817 conv_repo = DarcsConversionRepo (options.basename + ".tmpdarcs", patches)
818 conv_repo.start_at (-1)
820 for p in patches:
821 parent_patch = None
822 parent_number = -1
824 combinations = [(v, w) for v in pending_patches.values ()
825 for w in pending_patches.values ()]
826 candidates = [common_ancestor (git_commits[c[0].number], git_commits[c[1].number]) for c in combinations]
827 candidates = sorted ([(-a.darcs_patch.number, a) for a in candidates])
828 for (depth, c) in candidates:
829 q = c.darcs_patch
830 try:
831 conv_repo.go_from_to (q, p)
833 parent_patch = q
834 parent_number = q.number
835 progress ('Found existing common parent as predecessor')
836 break
838 except PullConflict:
839 pass
841 ## no branches found where we could attach.
842 ## try previous commits one by one.
843 if not parent_patch:
844 parent_number = p.number - 2
845 while 1:
846 if parent_number >= 0:
847 parent_patch = patches[parent_number]
849 try:
850 conv_repo.go_from_to (parent_patch, p)
851 break
852 except PullConflict:
854 ## simplistic, may not be enough.
855 progress ('conflict, going one back')
856 parent_number -= 1
858 if parent_number < 0:
859 break
861 if (options.history_window
862 and parent_number < p.number - options.history_window):
864 parent_number = -2
865 break
867 if parent_number >= 0 or p.number == 0:
868 progress ('Export %d -> %d (total %d)' % (parent_number,
869 p.number, len (patches)))
870 export_commit (conv_repo, p, parent_patch, gfi)
871 if p.tag_name ():
872 export_tag (p, gfi)
874 if options.checkpoint_frequency and p.number % options.checkpoint_frequency == 0:
875 export_checkpoint (gfi)
876 else:
877 progress ("Can't import patch %d, need conflict resolution patch?"
878 % p.number)
880 export_pending (gfi)
881 gfi.close ()
882 for f in glob.glob ('%(git_repo)s/refs/heads/darcstmp*' % locals ()):
883 os.unlink (f)
885 test_conversion (darcs_repo, git_repo)
887 if not options.debug:
888 conv_repo.clean ()
890 main ()