Include name in short_id; fixes multiple patches with same time stamp.
[darcs2git.git] / darcs2git.py
blob5d7c84d84f2c43d88fafe1086860902a59657453
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
579 self.author_mail = mail
581 def extract_time (self):
582 self.date = darcs_date_to_git (self.attributes['date']) + ' ' + darcs_timezone (self.attributes['local_date'])
584 def name (self):
585 patch_name = ''
586 try:
587 name_elt = self.xml.getElementsByTagName ('name')[0]
588 patch_name = unicode(fix_braindead_darcs_escapes(str(name_elt.childNodes[0].data)), 'UTF-8')
589 except IndexError:
590 pass
591 return patch_name
593 def extract_message (self):
594 patch_name = self.name ()
595 comment_elts = self.xml.getElementsByTagName ('comment')
596 comment = ''
597 if comment_elts:
598 comment = comment_elts[0].childNodes[0].data
600 if self.attributes['inverted'] == 'True':
601 patch_name = 'UNDO: ' + patch_name
603 self.message = '%s\n\n%s' % (patch_name, comment)
605 def tag_name (self):
606 patch_name = self.name ()
607 if patch_name.startswith ("TAG "):
608 tag = patch_name[4:]
609 tag = re.sub (r'\s', '_', tag).strip ()
610 tag = re.sub (r':', '_', tag).strip ()
611 return tag
612 return ''
614 def get_darcs_patches (darcs_repo):
615 progress ('reading patches.')
617 xml_string = read_pipe ('darcs changes --xml --reverse --repo ' + darcs_repo)
619 dom = xml.dom.minidom.parseString(xml_string)
620 xmls = dom.documentElement.getElementsByTagName('patch')
622 patches = [DarcsPatch (x, darcs_repo) for x in xmls]
624 n = 0
625 for p in patches:
626 p.number = n
627 n += 1
629 return patches
631 ################################################################
632 # GIT export
634 class GitCommit:
635 def __init__ (self, parent, darcs_patch):
636 self.parent = parent
637 self.darcs_patch = darcs_patch
638 if parent:
639 self.depth = parent.depth + 1
640 else:
641 self.depth = 0
643 def number (self):
644 return self.darcs_patch.number
646 def parent_patch (self):
647 if self.parent:
648 return self.parent.darcs_patch
649 else:
650 return None
652 def common_ancestor (a, b):
653 while 1:
654 if a.depth < b.depth:
655 b = b.parent
656 elif a.depth > b.depth:
657 a = a.parent
658 else:
659 break
661 while a and b:
662 if a == b:
663 return a
665 a = a.parent
666 b = b.parent
668 return None
670 def export_checkpoint (gfi):
671 gfi.write ('checkpoint\n\n')
673 def export_tree (tree, gfi):
674 tree = os.path.normpath (tree)
675 gfi.write ('deleteall\n')
676 for (root, dirs, files) in os.walk (tree):
677 for f in files:
678 rf = os.path.normpath (os.path.join (root, f))
679 s = open (rf).read ()
680 rf = rf.replace (tree + '/', '')
682 gfi.write ('M 644 inline %s\n' % rf)
683 gfi.write ('data %d\n%s\n' % (len (s), s))
684 gfi.write ('\n')
687 def export_commit (repo, patch, last_patch, gfi):
688 gfi.write ('commit refs/heads/darcstmp%d\n' % patch.number)
689 gfi.write ('mark :%d\n' % (patch.number + 1))
690 gfi.write ('committer %s <%s> %s\n' % (patch.author_name,
691 patch.author_mail,
692 patch.date))
694 msg = patch.message
695 if options.debug:
696 msg += '\n\n#%d\n' % patch.number
698 msg = msg.encode('utf-8')
699 gfi.write ('data %d\n%s\n' % (len (msg), msg))
700 mergers = []
701 for (n, p) in pending_patches.items ():
702 if repo.has_patch (p):
703 mergers.append (n)
704 del pending_patches[n]
706 if (last_patch
707 and mergers == []
708 and git_commits.has_key (last_patch.number)):
709 mergers = [last_patch.number]
711 if mergers:
712 gfi.write ('from :%d\n' % (mergers[0] + 1))
713 for m in mergers[1:]:
714 gfi.write ('merge :%d\n' % (m + 1))
716 pending_patches[patch.number] = patch
717 export_tree (repo.pristine_tree (), gfi)
719 n = -1
720 if last_patch:
721 n = last_patch.number
722 git_commits[patch.number] = GitCommit (git_commits.get (n, None),
723 patch)
725 def export_pending (gfi):
726 if len (pending_patches.items ()) == 1:
727 gfi.write ('reset refs/heads/master\n')
728 gfi.write ('from :%d\n\n' % (pending_patches.values()[0].number+1))
730 progress ("Creating branch master")
731 return
733 for (n, p) in pending_patches.items ():
734 gfi.write ('reset refs/heads/master%d\n' % n)
735 gfi.write ('from :%d\n\n' % (n+1))
737 progress ("Creating branch master%d" % n)
739 patches = pending_patches.values()
740 patch = patches[0]
741 gfi.write ('commit refs/heads/master\n')
742 gfi.write ('committer %s <%s> %s\n' % (patch.author_name,
743 patch.author_mail,
744 patch.date))
745 msg = 'tie together'
746 gfi.write ('data %d\n%s\n' % (len(msg), msg))
747 gfi.write ('from :%d\n' % (patch.number + 1))
748 for p in patches[1:]:
749 gfi.write ('merge :%d\n' % (p.number + 1))
750 gfi.write ('\n')
752 def export_tag (patch, gfi):
753 gfi.write ('tag %s\n' % patch.tag_name ())
754 gfi.write ('from :%d\n' % (patch.number + 1))
755 gfi.write ('tagger %s <%s> %s\n' % (patch.author_name,
756 patch.author_mail,
757 patch.date))
759 raw_message = patch.message.encode('utf-8')
760 gfi.write ('data %d\n%s\n' % (len (raw_message),
761 raw_message))
763 ################################################################
764 # main.
766 def test_conversion (darcs_repo, git_repo):
767 pristine = '%(darcs_repo)s/_darcs/pristine' % locals ()
768 if not os.path.exists (pristine):
769 progress ("darcs repository does not contain pristine tree?!")
770 return
772 gd = options.basename + '.checkouttmp.git'
773 system ('rm -rf %(gd)s && git clone %(git_repo)s %(gd)s' % locals ())
774 diff_cmd = 'diff --exclude .git -urN %(gd)s %(pristine)s' % locals ()
775 diff = read_pipe (diff_cmd, ignore_errors=True)
776 system ('rm -rf %(gd)s' % locals ())
778 if diff:
779 if len (diff) > 1024:
780 diff = diff[:512] + '\n...\n' + diff[-512:]
782 progress ("Conversion introduced changes: %s" % diff)
783 raise 'fdsa'
784 else:
785 progress ("Checkout matches pristine darcs tree.")
787 def main ():
788 (options, args) = get_cli_options ()
790 darcs_repo = os.path.abspath (args[0])
791 git_repo = os.path.abspath (options.target_git_repo)
793 if os.path.exists (git_repo):
794 system ('rm -rf %(git_repo)s' % locals ())
796 system ('mkdir %(git_repo)s && cd %(git_repo)s && git --bare init' % locals ())
797 system ('git --git-dir %(git_repo)s repo-config core.logAllRefUpdates false' % locals ())
799 os.environ['GIT_DIR'] = git_repo
801 quiet = ' --quiet'
802 if options.verbose:
803 quiet = ' '
805 gfi = os.popen ('git-fast-import %s' % quiet, 'w')
807 patches = get_darcs_patches (darcs_repo)
808 conv_repo = DarcsConversionRepo (options.basename + ".tmpdarcs", patches)
809 conv_repo.start_at (-1)
811 for p in patches:
812 parent_patch = None
813 parent_number = -1
815 combinations = [(v, w) for v in pending_patches.values ()
816 for w in pending_patches.values ()]
817 candidates = [common_ancestor (git_commits[c[0].number], git_commits[c[1].number]) for c in combinations]
818 candidates = sorted ([(-a.darcs_patch.number, a) for a in candidates])
819 for (depth, c) in candidates:
820 q = c.darcs_patch
821 try:
822 conv_repo.go_from_to (q, p)
824 parent_patch = q
825 parent_number = q.number
826 progress ('Found existing common parent as predecessor')
827 break
829 except PullConflict:
830 pass
832 ## no branches found where we could attach.
833 ## try previous commits one by one.
834 if not parent_patch:
835 parent_number = p.number - 2
836 while 1:
837 if parent_number >= 0:
838 parent_patch = patches[parent_number]
840 try:
841 conv_repo.go_from_to (parent_patch, p)
842 break
843 except PullConflict:
845 ## simplistic, may not be enough.
846 progress ('conflict, going one back')
847 parent_number -= 1
849 if parent_number < 0:
850 break
852 if (options.history_window
853 and parent_number < p.number - options.history_window):
855 parent_number = -2
856 break
858 if parent_number >= 0 or p.number == 0:
859 progress ('Export %d -> %d (total %d)' % (parent_number,
860 p.number, len (patches)))
861 export_commit (conv_repo, p, parent_patch, gfi)
862 if p.tag_name ():
863 export_tag (p, gfi)
865 if options.checkpoint_frequency and p.number % options.checkpoint_frequency == 0:
866 export_checkpoint (gfi)
867 else:
868 progress ("Can't import patch %d, need conflict resolution patch?"
869 % p.number)
871 export_pending (gfi)
872 gfi.close ()
873 for f in glob.glob ('%(git_repo)s/refs/heads/darcstmp*' % locals ()):
874 os.unlink (f)
876 test_conversion (darcs_repo, git_repo)
878 if not options.debug:
879 conv_repo.clean ()
881 main ()