82fd90e55a759ee66c2cc8a75564854f24a1d54f
[darcs2git.git] / darcs2git.py
blob82fd90e55a759ee66c2cc8a75564854f24a1d54f
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 && echo ay|darcs obliterate --ignore-times --last %(count)d' % locals ())
362 d = self.inventory_dict ()
363 for p in self.patches[dest+1:self._current_number+1]:
364 try:
365 del d[p.short_id ()]
366 except KeyError:
367 pass
369 self._current_number = dest
371 def clean (self):
372 system ('rm -rf %s' % self.dir)
374 def checkout (self):
375 dir = self.dir
376 system ('rsync -a %(dir)s/_darcs/pristine/ %(dir)s/' % locals ())
378 def pull (self, patch):
379 id = patch.attributes['hash']
380 source_repo = patch.dir
381 dir = self.dir
383 progress ('Pull patch %d' % patch.number)
384 system ('cd %(dir)s && darcs pull --ignore-times --quiet --all --match "hash %(id)s" %(source_repo)s ' % locals ())
386 self._current_number = patch.number
388 ## must reread: the pull may have pulled in others.
389 self._inventory_dict = None
391 def go_forward_to (self, num):
392 d = self.inventory_dict ()
394 pull_me = []
396 ## ugh
397 for p in self.patches[0:num+1]:
398 if not d.has_key (p.short_id ()):
399 pull_me.append (p)
400 d[p.short_id ()] = p
402 pull_str = ' || '.join (['hash %s' % p.id () for p in pull_me])
403 dir = self.dir
404 src = self.patches[0].dir
406 progress ('Pulling %d patches to go to %d' % (len (pull_me), num))
407 system ('darcs pull --all --repo %(dir)s --match "%(pull_str)s" %(src)s' % locals ())
409 def create_fresh (self):
410 dir = self.dir
411 system ('rm -rf %(dir)s && mkdir %(dir)s && darcs init --repo %(dir)s'
412 % locals ())
413 self._is_valid = True
414 self._current_number = -1
415 self._inventory_dict = {}
417 def inventory (self):
418 darcs_dir = self.dir + '/_darcs'
419 i = ''
420 for f in [darcs_dir + '/inventory'] + glob.glob (darcs_dir + '/inventories/*'):
421 i += open (f).read ()
422 return i
424 def inventory_dict (self):
426 if type (self._inventory_dict) != type ({}):
427 self._inventory_dict = {}
429 for p in parse_inventory(self.inventory()):
430 key = p.short_id()
432 try:
433 self._inventory_dict[key] = self._short_id_dict[key]
434 except KeyError:
435 print 'key not found', key
436 print self._short_id_dict
437 raise
439 return self._inventory_dict
441 def start_at (self, num):
442 """Move the repo to NUM.
444 This uses the fishy technique of writing the inventory and
445 constructing the pristine tree with 'darcs repair'
447 progress ('Starting afresh at %d' % num)
449 self.create_fresh ()
450 dir = self.dir
451 iv = open (dir + '/_darcs/inventory', 'w')
452 if log_file:
453 log_file.write ("# messing with _darcs/inventory")
455 for p in self.patches[:num+1]:
456 os.link (p.filename (), dir + '/_darcs/patches/' + os.path.basename (p.filename ()))
457 iv.write (p.header ())
458 self._inventory_dict[p.short_id ()] = p
459 iv.close ()
461 system ('darcs repair --repo %(dir)s --quiet' % locals ())
462 self.checkout ()
463 self._current_number = num
464 self._is_valid = True
466 def go_to (self, dest):
467 if not self._is_valid:
468 self.start_at (dest)
469 elif dest == self._current_number and self.is_contiguous ():
470 pass
471 elif (self.contains_contiguous (dest)):
472 self.go_back_to (dest)
473 elif dest - len (self.inventory_dict ()) < dest / 100:
474 self.go_forward_to (dest)
475 else:
476 self.start_at (dest)
479 def go_from_to (self, from_patch, to_patch):
481 """Move the repo to FROM_PATCH, then go to TO_PATCH. Raise
482 PullConflict if conflict is detected
485 progress ('Trying %s -> %s' % (from_patch, to_patch))
486 dir = self.dir
487 source = to_patch.dir
489 if from_patch:
490 self.go_to (from_patch.number)
491 else:
492 self.create_fresh ()
494 try:
495 self.pull (to_patch)
496 success = 'No conflicts to resolve' in read_pipe ('cd %(dir)s && echo y|darcs resolve' % locals ())
497 except CommandFailed:
498 self._is_valid = False
499 raise PullConflict ()
501 if not success:
502 raise PullConflict ()
504 class DarcsPatch:
505 def __repr__ (self):
506 return 'patch %d' % self.number
508 def __init__ (self, xml, dir):
509 self.xml = xml
510 self.dir = dir
511 self.number = -1
512 self.attributes = {}
513 self._contents = None
514 for (nm, value) in xml.attributes.items():
515 self.attributes[nm] = value
517 # fixme: ugh attributes vs. methods.
518 self.extract_author ()
519 self.extract_message ()
520 self.extract_time ()
522 def id (self):
523 return self.attributes['hash']
525 def short_id (self):
526 inv = '*'
527 if self.attributes['inverted'] == 'True':
528 inv = '-'
530 return '%s%s*%s%s' % (self.name(), self.attributes['author'], inv, self.attributes['hash'].split ('-')[0])
532 def filename (self):
533 return self.dir + '/_darcs/patches/' + self.attributes['hash']
535 def contents (self):
536 if type (self._contents) != type (''):
537 f = gzip.open (self.filename ())
538 self._contents = f.read ()
540 return self._contents
542 def header (self):
543 lines = self.contents ().split ('\n')
545 name = lines[0]
546 committer = lines[1] + '\n'
547 committer = re.sub ('] {\n$', ']\n', committer)
548 committer = re.sub ('] *\n$', ']\n', committer)
549 comment = ''
550 if not committer.endswith (']\n'):
551 for l in lines[2:]:
552 if l[0] == ']':
553 comment += ']\n'
554 break
555 comment += l + '\n'
557 header = name + '\n' + committer
558 if comment:
559 header += comment
561 assert header[-1] == '\n'
562 return header
564 def extract_author (self):
565 mail = self.attributes['author']
566 name = ''
567 m = re.search ("^(.*) <(.*)>$", mail)
569 if m:
570 name = m.group (1)
571 mail = m.group (2)
572 else:
573 try:
574 name = mail_to_name_dict[mail]
575 except KeyError:
576 name = mail.split ('@')[0]
578 self.author_name = name
580 # mail addresses should be plain strings.
581 self.author_mail = mail.encode('utf-8')
583 def extract_time (self):
584 self.date = darcs_date_to_git (self.attributes['date']) + ' ' + darcs_timezone (self.attributes['local_date'])
586 def name (self):
587 patch_name = ''
588 try:
589 name_elt = self.xml.getElementsByTagName ('name')[0]
590 patch_name = unicode(fix_braindead_darcs_escapes(str(name_elt.childNodes[0].data)), 'UTF-8')
591 except IndexError:
592 pass
593 return patch_name
595 def extract_message (self):
596 patch_name = self.name ()
597 comment_elts = self.xml.getElementsByTagName ('comment')
598 comment = ''
599 if comment_elts:
600 comment = comment_elts[0].childNodes[0].data
602 if self.attributes['inverted'] == 'True':
603 patch_name = 'UNDO: ' + patch_name
605 self.message = '%s\n\n%s' % (patch_name, comment)
607 def tag_name (self):
608 patch_name = self.name ()
609 if patch_name.startswith ("TAG "):
610 tag = patch_name[4:]
611 tag = re.sub (r'\s', '_', tag).strip ()
612 tag = re.sub (r':', '_', tag).strip ()
613 return tag
614 return ''
616 def get_darcs_patches (darcs_repo):
617 progress ('reading patches.')
619 xml_string = read_pipe ('darcs changes --xml --reverse --repo ' + darcs_repo)
621 dom = xml.dom.minidom.parseString(xml_string)
622 xmls = dom.documentElement.getElementsByTagName('patch')
624 patches = [DarcsPatch (x, darcs_repo) for x in xmls]
626 n = 0
627 for p in patches:
628 p.number = n
629 n += 1
631 return patches
633 ################################################################
634 # GIT export
636 class GitCommit:
637 def __init__ (self, parent, darcs_patch):
638 self.parent = parent
639 self.darcs_patch = darcs_patch
640 if parent:
641 self.depth = parent.depth + 1
642 else:
643 self.depth = 0
645 def number (self):
646 return self.darcs_patch.number
648 def parent_patch (self):
649 if self.parent:
650 return self.parent.darcs_patch
651 else:
652 return None
654 def common_ancestor (a, b):
655 while 1:
656 if a.depth < b.depth:
657 b = b.parent
658 elif a.depth > b.depth:
659 a = a.parent
660 else:
661 break
663 while a and b:
664 if a == b:
665 return a
667 a = a.parent
668 b = b.parent
670 return None
672 def export_checkpoint (gfi):
673 gfi.write ('checkpoint\n\n')
675 def export_tree (tree, gfi):
676 tree = os.path.normpath (tree)
677 gfi.write ('deleteall\n')
678 for (root, dirs, files) in os.walk (tree):
679 for f in files:
680 rf = os.path.normpath (os.path.join (root, f))
681 s = open (rf).read ()
682 rf = rf.replace (tree + '/', '')
684 gfi.write ('M 644 inline %s\n' % rf)
685 gfi.write ('data %d\n%s\n' % (len (s), s))
686 gfi.write ('\n')
689 def export_commit (repo, patch, last_patch, gfi):
690 gfi.write ('commit refs/heads/darcstmp%d\n' % patch.number)
691 gfi.write ('mark :%d\n' % (patch.number + 1))
693 raw_name = patch.author_name
694 gfi.write ('committer %s <%s> %s\n' % (raw_name,
695 patch.author_mail,
696 patch.date))
698 msg = patch.message
699 if options.debug:
700 msg += '\n\n#%d\n' % patch.number
702 msg = msg.encode('utf-8')
703 gfi.write ('data %d\n%s\n' % (len (msg), msg))
704 mergers = []
705 for (n, p) in pending_patches.items ():
706 if repo.has_patch (p):
707 mergers.append (n)
708 del pending_patches[n]
710 if (last_patch
711 and mergers == []
712 and git_commits.has_key (last_patch.number)):
713 mergers = [last_patch.number]
715 if mergers:
716 gfi.write ('from :%d\n' % (mergers[0] + 1))
717 for m in mergers[1:]:
718 gfi.write ('merge :%d\n' % (m + 1))
720 pending_patches[patch.number] = patch
721 export_tree (repo.pristine_tree (), gfi)
723 n = -1
724 if last_patch:
725 n = last_patch.number
726 git_commits[patch.number] = GitCommit (git_commits.get (n, None),
727 patch)
729 def export_pending (gfi):
730 if len (pending_patches.items ()) == 1:
731 gfi.write ('reset refs/heads/master\n')
732 gfi.write ('from :%d\n\n' % (pending_patches.values()[0].number+1))
734 progress ("Creating branch master")
735 return
737 for (n, p) in pending_patches.items ():
738 gfi.write ('reset refs/heads/master%d\n' % n)
739 gfi.write ('from :%d\n\n' % (n+1))
741 progress ("Creating branch master%d" % n)
743 patches = pending_patches.values()
744 patch = patches[0]
745 gfi.write ('commit refs/heads/master\n')
746 gfi.write ('committer %s <%s> %s\n' % (patch.author_name,
747 patch.author_mail,
748 patch.date))
749 msg = 'tie together'
750 gfi.write ('data %d\n%s\n' % (len(msg), msg))
751 gfi.write ('from :%d\n' % (patch.number + 1))
752 for p in patches[1:]:
753 gfi.write ('merge :%d\n' % (p.number + 1))
754 gfi.write ('\n')
756 def export_tag (patch, gfi):
757 gfi.write ('tag %s\n' % patch.tag_name ())
758 gfi.write ('from :%d\n' % (patch.number + 1))
759 gfi.write ('tagger %s <%s> %s\n' % (patch.author_name,
760 patch.author_mail,
761 patch.date))
763 raw_message = patch.message.encode('utf-8')
764 gfi.write ('data %d\n%s\n' % (len (raw_message),
765 raw_message))
767 ################################################################
768 # main.
770 def test_conversion (darcs_repo, git_repo):
771 pristine = '%(darcs_repo)s/_darcs/pristine' % locals ()
772 if not os.path.exists (pristine):
773 progress ("darcs repository does not contain pristine tree?!")
774 return
776 gd = options.basename + '.checkouttmp.git'
777 system ('rm -rf %(gd)s && git clone %(git_repo)s %(gd)s' % locals ())
778 diff_cmd = 'diff --exclude .git -urN %(gd)s %(pristine)s' % locals ()
779 diff = read_pipe (diff_cmd, ignore_errors=True)
780 system ('rm -rf %(gd)s' % locals ())
782 if diff:
783 if len (diff) > 1024:
784 diff = diff[:512] + '\n...\n' + diff[-512:]
786 progress ("Conversion introduced changes: %s" % diff)
787 raise 'fdsa'
788 else:
789 progress ("Checkout matches pristine darcs tree.")
791 def main ():
792 (options, args) = get_cli_options ()
794 darcs_repo = os.path.abspath (args[0])
795 git_repo = os.path.abspath (options.target_git_repo)
797 if os.path.exists (git_repo):
798 system ('rm -rf %(git_repo)s' % locals ())
800 system ('mkdir %(git_repo)s && cd %(git_repo)s && git --bare init' % locals ())
801 system ('git --git-dir %(git_repo)s repo-config core.logAllRefUpdates false' % locals ())
803 os.environ['GIT_DIR'] = git_repo
805 quiet = ' --quiet'
806 if options.verbose:
807 quiet = ' '
809 gfi = os.popen ('git-fast-import %s' % quiet, 'w')
811 patches = get_darcs_patches (darcs_repo)
812 conv_repo = DarcsConversionRepo (options.basename + ".tmpdarcs", patches)
813 conv_repo.start_at (-1)
815 for p in patches:
816 parent_patch = None
817 parent_number = -1
819 combinations = [(v, w) for v in pending_patches.values ()
820 for w in pending_patches.values ()]
821 candidates = [common_ancestor (git_commits[c[0].number], git_commits[c[1].number]) for c in combinations]
822 candidates = sorted ([(-a.darcs_patch.number, a) for a in candidates])
823 for (depth, c) in candidates:
824 q = c.darcs_patch
825 try:
826 conv_repo.go_from_to (q, p)
828 parent_patch = q
829 parent_number = q.number
830 progress ('Found existing common parent as predecessor')
831 break
833 except PullConflict:
834 pass
836 ## no branches found where we could attach.
837 ## try previous commits one by one.
838 if not parent_patch:
839 parent_number = p.number - 2
840 while 1:
841 if parent_number >= 0:
842 parent_patch = patches[parent_number]
844 try:
845 conv_repo.go_from_to (parent_patch, p)
846 break
847 except PullConflict:
849 ## simplistic, may not be enough.
850 progress ('conflict, going one back')
851 parent_number -= 1
853 if parent_number < 0:
854 break
856 if (options.history_window
857 and parent_number < p.number - options.history_window):
859 parent_number = -2
860 break
862 if parent_number >= 0 or p.number == 0:
863 progress ('Export %d -> %d (total %d)' % (parent_number,
864 p.number, len (patches)))
865 export_commit (conv_repo, p, parent_patch, gfi)
866 if p.tag_name ():
867 export_tag (p, gfi)
869 if options.checkpoint_frequency and p.number % options.checkpoint_frequency == 0:
870 export_checkpoint (gfi)
871 else:
872 progress ("Can't import patch %d, need conflict resolution patch?"
873 % p.number)
875 export_pending (gfi)
876 gfi.close ()
877 for f in glob.glob ('%(git_repo)s/refs/heads/darcstmp*' % locals ()):
878 os.unlink (f)
880 test_conversion (darcs_repo, git_repo)
882 if not options.debug:
883 conv_repo.clean ()
885 main ()