Include name in short_id; fixes multiple patches with same time stamp.
authorHan-Wen Nienhuys <hanwen@lilypond.org>
Mon, 5 Nov 2007 21:10:18 +0000 (5 19:10 -0200)
committerHan-Wen Nienhuys <hanwen@lilypond.org>
Mon, 5 Nov 2007 21:10:18 +0000 (5 19:10 -0200)
Add unicode fixes

darcs2git.py

index 847e1fc..5d7c84d 100644 (file)
@@ -22,7 +22,6 @@
 
 """
 
-
 # TODO:
 #
 # - time zones
@@ -32,6 +31,9 @@
 # - use binary search to find from-patch in case of conflict.
 #
 
+import sha
+from datetime import datetime
+from time import strptime
 import urlparse
 import distutils.version
 import glob
@@ -43,6 +45,7 @@ import re
 import gzip
 import optparse
 
+
 ################################################################
 # globals
 
@@ -149,14 +152,16 @@ test end result.""")
         sys.exit (2)
 
     if len(urlparse.urlparse(args[0])) == 0:
-        raise NotImplementedError,"We support local DARCS repos only."
+        raise NotImplementedError, "We support local DARCS repos only."
 
-    git_version = distutils.version.LooseVersion(os.popen("git --version","r").read().strip().split(" ")[-1])
+    git_version = distutils.version.LooseVersion(
+        os.popen("git --version","r").read().strip().split(" ")[-1])
     ideal_version = distutils.version.LooseVersion("1.5.0")
     if git_version<ideal_version:
         raise RuntimeError,"You need git >= 1.5.0 for this."
     
-    options.basename = os.path.basename (os.path.normpath (args[0])).replace ('.darcs', '')
+    options.basename = os.path.basename (
+        os.path.normpath (args[0])).replace ('.darcs', '')
     if not options.target_git_repo:
         options.target_git_repo = options.basename + '.git'
         
@@ -208,6 +213,93 @@ def darcs_timezone (x) :
 ################################################################
 # darcs
 
+## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/521889
+
+PATCH_DATE_FORMAT = '%Y%m%d%H%M%S'
+
+patch_pattern = r"""
+   \[                                   # Patch start indicator
+   (?P<name>[^\n]+)\n                   # Patch name (rest of same line)
+   (?P<author>[^\*]+)                   # Patch author
+   \*                                   # Author/date separator 
+   (?P<inverted>[-\*])                  # Inverted patch indicator
+   (?P<date>\d{14})                     # Patch date
+   (?:\n(?P<comment>(?:^\ [^\n]*\n)+))? # Optional long comment
+   \]                                   # Patch end indicator
+   """
+patch_re = re.compile(patch_pattern, re.VERBOSE | re.MULTILINE)
+tidy_comment_re = re.compile(r'^ ', re.MULTILINE)
+
+def parse_inventory(inventory):
+    """
+    Given the contents of a darcs inventory file, generates ``Patch``
+    objects representing contained patch details.
+    """
+    for match in patch_re.finditer(inventory):
+        attrs = match.groupdict(None)
+        attrs['inverted'] = (attrs['inverted'] == '-')
+        if attrs['comment'] is not None:
+            attrs['comment'] = tidy_comment_re.sub('', attrs['comment']).strip()
+        yield InventoryPatch(**attrs)
+
+def fix_braindead_darcs_escapes(s):
+    def insert_hibit(match):
+        return chr(int(match.group(1), 16))
+        
+    return re.sub(r'\[_\\([0-9a-f][0-9a-f])_\]',
+           insert_hibit, str(s))
+
+class InventoryPatch:
+    """
+    Patch details, as defined in a darcs inventory file.
+
+    Attribute names match those generated by the
+    ``darcs changes --xml-output`` command.
+    """
+
+    def __init__(self, name, author, date, inverted, comment=None):
+        self.name = name
+        self.author = author
+        self.date = datetime(*strptime(date, PATCH_DATE_FORMAT)[:6])
+        self.inverted = inverted
+        self.comment = comment
+
+    def __str__(self):
+        return self.name
+
+    @property
+    def complete_patch_details(self):
+        date_str = self.date.strftime(PATCH_DATE_FORMAT)
+        return '%s%s%s%s%s' % (
+            self.name, self.author, date_str,
+            self.comment and ''.join([l.rstrip() for l in self.comment.split('\n')]) or '',
+            self.inverted and 't' or 'f')
+
+    def short_id (self):
+        inv = '*'
+        if self.inverted:
+            inv = '-'
+        
+        return unicode('%s%s*%s%s' % (self.name, self.author, inv, self.hash.split ('-')[0]), 'UTF-8')
+    
+    @property 
+    def hash(self):
+        """
+        Calculates the filename of the gzipped file containing patch
+        contents in the repository's ``patches`` directory.
+
+        This consists of the patch date, a partial SHA-1 hash of the
+        patch author and a full SHA-1 hash of the complete patch
+        details.
+        """
+   
+        date_str = self.date.strftime(PATCH_DATE_FORMAT)
+        return '%s-%s-%s.gz' % (date_str,
+                                sha.new(self.author).hexdigest()[:5],
+                                sha.new(self.complete_patch_details).hexdigest())
+
+## http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/521889
+        
 class DarcsConversionRepo:
     """Representation of a Darcs repo.
 
@@ -222,7 +314,6 @@ going back (obliterate) and forward (pull).
         self._current_number = -1
         self._is_valid = -1
         self._inventory_dict = None
-
         self._short_id_dict = dict ((p.short_id (), p) for p in patches)
 
     def __del__ (self):
@@ -230,7 +321,7 @@ going back (obliterate) and forward (pull).
             system ('rm -fr %s' % self.dir)
         
     def is_contiguous (self):
-        return (len (self.inventory_dict ()) == self._current_number+1
+        return (len (self.inventory_dict ()) == self._current_number + 1
                 and self.contains_contiguous (self._current_number))
 
     def contains_contiguous (self, num):
@@ -244,13 +335,13 @@ going back (obliterate) and forward (pull).
         for p in self.patches[:num + 1]:
             if not self.has_patch (p):
                 return False
-
+        
         return True
     
     def has_patch (self, p):
         assert self._is_valid
         
-        return self.inventory_dict ().has_key (p.short_id ())
+        return p.short_id () in self.inventory_dict ()
     
     def pristine_tree (self):
         return self.dir + '/_darcs/pristine'
@@ -331,17 +422,23 @@ going back (obliterate) and forward (pull).
         return i
 
     def inventory_dict (self):
+        
         if type (self._inventory_dict) != type ({}):
             self._inventory_dict = {}
 
-            def note_patch (m):
-                self._inventory_dict[m.group (1)] = self._short_id_dict[m.group(1)]
+            for p in parse_inventory(self.inventory()):
+                key = p.short_id()
+                
+                try:
+                    self._inventory_dict[key] = self._short_id_dict[key]
+                except KeyError:
+                    print 'key not found', key
+                    print self._short_id_dict
+                    raise
 
-            re.sub (r'\n([^*\n]+\*[*-][0-9]+)', note_patch, self.inventory ())
         return self._inventory_dict
 
     def start_at (self, num):
-    
         """Move the repo to NUM.
 
         This uses the fishy technique of writing the inventory and
@@ -367,11 +464,9 @@ going back (obliterate) and forward (pull).
         self._is_valid = True
 
     def go_to (self, dest):
-        contiguous = self.is_contiguous ()
-
         if not self._is_valid:
             self.start_at (dest)
-        elif dest == self._current_number and contiguous:
+        elif dest == self._current_number and self.is_contiguous ():
             pass
         elif (self.contains_contiguous (dest)):
             self.go_back_to (dest)
@@ -432,7 +527,7 @@ class DarcsPatch:
         if self.attributes['inverted'] == 'True':
             inv = '-'
             
-        return '%s*%s%s' % (self.attributes['author'], inv, self.attributes['hash'].split ('-')[0])
+        return '%s%s*%s%s' % (self.name(), self.attributes['author'], inv, self.attributes['hash'].split ('-')[0])
 
     def filename (self):
         return self.dir + '/_darcs/patches/' + self.attributes['hash']
@@ -487,10 +582,10 @@ class DarcsPatch:
         self.date = darcs_date_to_git (self.attributes['date']) + ' ' + darcs_timezone (self.attributes['local_date'])
 
     def name (self):
-        patch_name = '(no comment)'
+        patch_name = ''
         try:
             name_elt = self.xml.getElementsByTagName ('name')[0]
-            patch_name = name_elt.childNodes[0].data
+            patch_name = unicode(fix_braindead_darcs_escapes(str(name_elt.childNodes[0].data)), 'UTF-8')
         except IndexError:
             pass
         return patch_name
@@ -600,9 +695,8 @@ def export_commit (repo, patch, last_patch, gfi):
     if options.debug:
         msg += '\n\n#%d\n' % patch.number
         
+    msg = msg.encode('utf-8')
     gfi.write ('data %d\n%s\n' % (len (msg), msg))
-
-    
     mergers = []
     for (n, p) in pending_patches.items ():
         if repo.has_patch (p):
@@ -622,7 +716,6 @@ def export_commit (repo, patch, last_patch, gfi):
     pending_patches[patch.number] = patch
     export_tree (repo.pristine_tree (), gfi)
 
-
     n = -1
     if last_patch:
         n = last_patch.number
@@ -662,8 +755,10 @@ def export_tag (patch, gfi):
     gfi.write ('tagger %s <%s> %s\n' % (patch.author_name,
                                     patch.author_mail,
                                     patch.date))
-    gfi.write ('data %d\n%s\n' % (len (patch.message),
-                                  patch.message))
+
+    raw_message = patch.message.encode('utf-8')
+    gfi.write ('data %d\n%s\n' % (len (raw_message),
+                                  raw_message))
 
 ################################################################
 # main.
@@ -703,7 +798,6 @@ def main ():
 
     os.environ['GIT_DIR'] = git_repo
 
-
     quiet = ' --quiet'
     if options.verbose:
         quiet = ' '
@@ -715,10 +809,9 @@ def main ():
     conv_repo.start_at (-1)
 
     for p in patches:
-        
         parent_patch = None
         parent_number = -1
-
+        
         combinations = [(v, w) for v in pending_patches.values ()
                         for w in pending_patches.values ()]
         candidates = [common_ancestor (git_commits[c[0].number], git_commits[c[1].number]) for c in combinations]
@@ -772,7 +865,8 @@ def main ():
             if options.checkpoint_frequency and p.number % options.checkpoint_frequency == 0:
                 export_checkpoint (gfi)
         else:
-            progress ("Can't import patch %d, need conflict resolution patch?" % p.number)
+            progress ("Can't import patch %d, need conflict resolution patch?"
+                      % p.number)
 
     export_pending (gfi)
     gfi.close ()