You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by gj...@apache.org on 2012/10/16 22:06:19 UTC

svn commit: r1398968 [28/28] - in /incubator/bloodhound/trunk/trac: ./ contrib/ doc/ doc/api/ doc/utils/ sample-plugins/ sample-plugins/permissions/ sample-plugins/workflow/ trac/ trac/admin/ trac/admin/templates/ trac/admin/tests/ trac/db/ trac/db/tes...

Modified: incubator/bloodhound/trunk/trac/tracopt/ticket/deleter.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/tracopt/ticket/deleter.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/tracopt/ticket/deleter.py (original)
+++ incubator/bloodhound/trunk/trac/tracopt/ticket/deleter.py Tue Oct 16 20:06:09 2012
@@ -20,6 +20,7 @@ from trac.ticket.model import Ticket
 from trac.ticket.web_ui import TicketModule
 from trac.util import get_reporter_id
 from trac.util.datefmt import from_utimestamp
+from trac.util.presentation import captioned_button
 from trac.util.translation import _
 from trac.web.api import IRequestFilter, IRequestHandler, ITemplateStreamFilter
 from trac.web.chrome import ITemplateProvider, add_notice, add_stylesheet
@@ -67,9 +68,12 @@ class TicketDeleter(Component):
             return tag.form(
                 tag.div(
                     tag.input(type='hidden', name='action', value='delete'),
-                    tag.input(type='submit', value=_('Delete'),
-                              title=_('Delete ticket')),
-                    class_='inlinebuttons'),
+                    tag.input(type='submit',
+                              value=captioned_button(req, u'–', # 'EN DASH'
+                                                     _("Delete")),
+                              title=_('Delete ticket'),
+                              class_="trac-delete"),
+                    class_="inlinebuttons"),
                 action='#', method='get')
         
         def delete_comment():
@@ -81,10 +85,12 @@ class TicketDeleter(Component):
                                   value='delete-comment'),
                         tag.input(type='hidden', name='cnum', value=cnum),
                         tag.input(type='hidden', name='cdate', value=cdate),
-                        tag.input(type='submit', value=_('Delete'),
-                                  title=_('Delete comment %(num)s',
-                                          num=cnum)),
-                        class_='inlinebuttons'),
+                        tag.input(type='submit',
+                                  value=captioned_button(req, u'–', # 'EN DASH'
+                                                         _("Delete")),
+                                  title=_('Delete comment %(num)s', num=cnum),
+                                  class_="trac-delete"),
+                        class_="inlinebuttons"),
                     action='#', method='get')
             
         buffer = StreamBuffer()

Modified: incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py (original)
+++ incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py Tue Oct 16 20:06:09 2012
@@ -31,12 +31,70 @@ import weakref
 
 __all__ = ['GitError', 'GitErrorSha', 'Storage', 'StorageFactory']
 
+
+def terminate(process):
+    """Python 2.5 compatibility method.
+    os.kill is not available on Windows before Python 2.7.
+    In Python 2.6 subprocess.Popen has a terminate method.
+    (It also seems to have some issues on Windows though.)
+    """
+
+    def terminate_win(process):
+        import ctypes
+        PROCESS_TERMINATE = 1
+        handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE,
+                                                    False,
+                                                    process.pid)
+        ctypes.windll.kernel32.TerminateProcess(handle, -1)
+        ctypes.windll.kernel32.CloseHandle(handle)
+
+    def terminate_nix(process):
+        import os
+        import signal
+        return os.kill(process.pid, signal.SIGTERM)
+
+    if sys.platform == 'win32':
+        return terminate_win(process)
+    return terminate_nix(process)
+
+
 class GitError(Exception):
     pass
 
 class GitErrorSha(GitError):
     pass
 
+# Helper functions
+
+def parse_commit(raw):
+    """Parse the raw content of a commit (as given by `git cat-file -p <rev>`).
+
+    Return the commit message and a dict of properties.
+    """
+    if not raw:
+        raise GitErrorSha
+    lines = raw.splitlines()
+    if not lines:
+        raise GitErrorSha
+    line = lines.pop(0)
+    props = {}
+    multiline = multiline_key = None
+    while line:
+        if line[0] == ' ':
+            if not multiline:
+                multiline_key = key
+                multiline = [props[multiline_key][-1]]
+            multiline.append(line[1:])
+        else:
+            key, value = line.split(None, 1)
+            props.setdefault(key, []).append(value.strip())
+        line = lines.pop(0)
+        if multiline and (not line or key != multiline_key):
+            props[multiline_key][-1] = '\n'.join(multiline)
+            multiline = None
+    return '\n'.join(lines), props
+
+
 class GitCore(object):
     """Low-level wrapper around git executable"""
 
@@ -252,7 +310,7 @@ class Storage(object):
         try:
             g = GitCore(git_bin=git_bin)
             [v] = g.version().splitlines()
-            _, _, version = v.strip().split()
+            version = v.strip().split()[2]
             # 'version' has usually at least 3 numeric version
             # components, e.g.::
             #  1.5.4.2
@@ -299,6 +357,19 @@ class Storage(object):
 
         self.logger = log
 
+        self.commit_encoding = None
+
+        # caches
+        self.__rev_cache = None
+        self.__rev_cache_lock = Lock()
+
+        # cache the last 200 commit messages
+        self.__commit_msg_cache = SizedDict(200)
+        self.__commit_msg_lock = Lock()
+
+        self.__cat_file_pipe = None
+        self.__cat_file_pipe_lock = Lock()
+
         if git_fs_encoding is not None:
             # validate encoding name
             codecs.lookup(git_fs_encoding)
@@ -318,31 +389,21 @@ class Storage(object):
             self.logger.error("GIT control files missing in '%s'" % git_dir)
             if os.path.exists(__git_file_path('.git')):
                 self.logger.error("entry '.git' found in '%s'"
-                                  " -- maybe use that folder instead..." 
+                                  " -- maybe use that folder instead..."
                                   % git_dir)
             raise GitError("GIT control files not found, maybe wrong "
                            "directory?")
 
-        self.logger.debug("PyGIT.Storage instance %d constructed" % id(self))
-
         self.repo = GitCore(git_dir, git_bin=git_bin)
 
-        self.commit_encoding = None
-
-        # caches
-        self.__rev_cache = None
-        self.__rev_cache_lock = Lock()
-
-        # cache the last 200 commit messages
-        self.__commit_msg_cache = SizedDict(200)
-        self.__commit_msg_lock = Lock()
-
-        self.__cat_file_pipe = None
+        self.logger.debug("PyGIT.Storage instance %d constructed" % id(self))
 
     def __del__(self):
-        if self.__cat_file_pipe is not None:
-            self.__cat_file_pipe.stdin.close()
-            self.__cat_file_pipe.wait()
+        with self.__cat_file_pipe_lock:
+            if self.__cat_file_pipe is not None:
+                self.__cat_file_pipe.stdin.close()
+                terminate(self.__cat_file_pipe)
+                self.__cat_file_pipe.wait()
 
     #
     # cache handling
@@ -587,20 +648,39 @@ class Storage(object):
         return self.verifyrev('HEAD')
 
     def cat_file(self, kind, sha):
-        if self.__cat_file_pipe is None:
-            self.__cat_file_pipe = self.repo.cat_file_batch()
-
-        self.__cat_file_pipe.stdin.write(sha + '\n')
-        self.__cat_file_pipe.stdin.flush()
-        _sha, _type, _size = self.__cat_file_pipe.stdout.readline().split()
-
-        if _type != kind:
-            raise TracError("internal error (got unexpected object kind "
-                            "'%s')" % k)
-
-        size = int(_size)
-        return self.__cat_file_pipe.stdout.read(size + 1)[:size]
+        with self.__cat_file_pipe_lock:
+            if self.__cat_file_pipe is None:
+                self.__cat_file_pipe = self.repo.cat_file_batch()
 
+            try:
+                self.__cat_file_pipe.stdin.write(sha + '\n')
+                self.__cat_file_pipe.stdin.flush()
+            
+                split_stdout_line = self.__cat_file_pipe.stdout.readline() \
+                                                               .split()
+                if len(split_stdout_line) != 3:
+                    raise GitError("internal error (could not split line "
+                                   "'%s')" % (split_stdout_line,))
+                    
+                _sha, _type, _size = split_stdout_line
+
+                if _type != kind:
+                    raise GitError("internal error (got unexpected object "
+                                   "kind '%s', expected '%s')"
+                                   % (_type, kind))
+
+                size = int(_size)
+                return self.__cat_file_pipe.stdout.read(size + 1)[:size]
+            except:
+                # There was an error, we should close the pipe to get to a
+                # consistent state (Otherwise it happens that next time we
+                # call cat_file we get payload from previous call)
+                self.logger.debug("closing cat_file pipe")
+                self.__cat_file_pipe.stdin.close()
+                terminate(self.__cat_file_pipe)
+                self.__cat_file_pipe.wait()
+                self.__cat_file_pipe = None
+        
     def verifyrev(self, rev):
         """verify/lookup given revision object and return a sha id or None
         if lookup failed
@@ -737,19 +817,7 @@ class Storage(object):
             # cache miss
             raw = self.cat_file('commit', commit_id)
             raw = unicode(raw, self.get_commit_encoding(), 'replace')
-            lines = raw.splitlines()
-
-            if not lines:
-                raise GitErrorSha
-
-            line = lines.pop(0)
-            props = {}
-            while line:
-                key, value = line.split(None, 1)
-                props.setdefault(key, []).append(value.strip())
-                line = lines.pop(0)
-
-            result = ('\n'.join(lines), props)
+            result = parse_commit(raw)
 
             self.__commit_msg_cache[commit_id] = result
 
@@ -821,31 +889,6 @@ class Storage(object):
         change = {}
         next_path = []
 
-        def terminate(process):
-            """Python 2.5 compatibility method.
-            os.kill is not available on Windows before Python 2.7.
-            In Python 2.6 subprocess.Popen has a terminate method.
-            (It also seems to have some issues on Windows though.)
-            """
-
-            def terminate_win(process):
-                import ctypes
-                PROCESS_TERMINATE = 1
-                handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE,
-                                                            False,
-                                                            process.pid)
-                ctypes.windll.kernel32.TerminateProcess(handle, -1)
-                ctypes.windll.kernel32.CloseHandle(handle)
-
-            def terminate_nix(process):
-                import os
-                import signal
-                return os.kill(process.pid, signal.SIGTERM)
-
-            if sys.platform == 'win32':
-                return terminate_win(process)
-            return terminate_nix(process)
-
         def name_status_gen():
             p[:] = [self.repo.log_pipe('--pretty=format:%n%H',
                                        '--name-status', sha, '--', base_path)]

Modified: incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py (original)
+++ incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py Tue Oct 16 20:06:09 2012
@@ -37,7 +37,7 @@ from tracopt.versioncontrol.git import P
 
 
 class GitCachedRepository(CachedRepository):
-    """Git-specific cached repository
+    """Git-specific cached repository.
 
     Passes through {display,short,normalize}_rev
     """
@@ -61,7 +61,7 @@ class GitCachedRepository(CachedReposito
 
 
 class GitCachedChangeset(CachedChangeset):
-    """Git-specific cached changeset
+    """Git-specific cached changeset.
 
     Handles get_branches()
     """
@@ -85,7 +85,7 @@ def intersperse(sep, iterable):
     """The 'intersperse' generator takes an element and an iterable and
     intersperses that element between the elements of the iterable.
 
-    inspired by Haskell's Data.List.intersperse
+    inspired by Haskell's ``Data.List.intersperse``
     """
 
     for i, item in enumerate(iterable):
@@ -94,12 +94,12 @@ def intersperse(sep, iterable):
 
 # helper
 def _parse_user_time(s):
-    """parse author/committer attribute lines and return
-    (user,timestamp)
+    """Parse author or committer attribute lines and return
+    corresponding ``(user, timestamp)`` pair.
     """
 
     user, time, tz_str = s.rsplit(None, 2)
-    tz = FixedOffset((int(tz_str)*6)/10, tz_str)
+    tz = FixedOffset((int(tz_str) * 6) / 10, tz_str)
     time = datetime.fromtimestamp(float(time), tz)
     return user, time
 
@@ -112,7 +112,7 @@ class GitConnector(Component):
         self._version = None
 
         try:
-            self._version = PyGIT.Storage.git_version(git_bin=self._git_bin)
+            self._version = PyGIT.Storage.git_version(git_bin=self.git_bin)
         except PyGIT.GitError, e:
             self.log.error("GitError: " + str(e))
 
@@ -155,7 +155,7 @@ class GitConnector(Component):
                          title=to_unicode(e), rel='nofollow')
 
     def get_wiki_syntax(self):
-        yield (r'(?:\b|!)r?[0-9a-fA-F]{%d,40}\b' % self._wiki_shortrev_len,
+        yield (r'(?:\b|!)r?[0-9a-fA-F]{%d,40}\b' % self.wiki_shortrev_len,
                lambda fmt, sha, match:
                     self._format_sha_link(fmt, sha.startswith('r')
                                           and sha[1:] or sha, sha))
@@ -166,40 +166,42 @@ class GitConnector(Component):
 
     # IRepositoryConnector methods
 
-    _persistent_cache = BoolOption('git', 'persistent_cache', 'false',
-        """enable persistent caching of commit tree""")
+    persistent_cache = BoolOption('git', 'persistent_cache', 'false',
+        """Enable persistent caching of commit tree.""")
 
-    _cached_repository = BoolOption('git', 'cached_repository', 'false',
-        """wrap `GitRepository` in `CachedRepository`""")
+    cached_repository = BoolOption('git', 'cached_repository', 'false',
+        """Wrap `GitRepository` in `CachedRepository`.""")
 
-    _shortrev_len = IntOption('git', 'shortrev_len', 7,
-        """length rev sha sums should be tried to be abbreviated to
-        (must be >= 4 and <= 40)
+    shortrev_len = IntOption('git', 'shortrev_len', 7,
+        """The length at which a sha1 should be abbreviated to (must
+        be >= 4 and <= 40).
         """)
 
-    _wiki_shortrev_len = IntOption('git', 'wiki_shortrev_len', 40,
-        """minimum length of hex-string for which auto-detection as sha id is
-        performed.
-       (must be >= 4 and <= 40)
-       """)
-
-    _trac_user_rlookup = BoolOption('git', 'trac_user_rlookup', 'false',
-        """enable reverse mapping of git email addresses to trac user ids""")
+    wiki_shortrev_len = IntOption('git', 'wikishortrev_len', 40,
+        """The minimum length of an hex-string for which
+        auto-detection as sha1 is performed (must be >= 4 and <= 40).
+        """)
 
-    _use_committer_id = BoolOption('git', 'use_committer_id', 'true',
-        """use git-committer id instead of git-author id as changeset owner
+    trac_user_rlookup = BoolOption('git', 'trac_user_rlookup', 'false',
+        """Enable reverse mapping of git email addresses to trac user ids
+        (costly if you have many users).""")
+
+    use_committer_id = BoolOption('git', 'use_committer_id', 'true',
+        """Use git-committer id instead of git-author id for the
+        changeset ''Author'' field.
         """)
 
-    _use_committer_time = BoolOption('git', 'use_committer_time', 'true',
-        """use git-committer-author timestamp instead of git-author timestamp
-        as changeset timestamp
+    use_committer_time = BoolOption('git', 'use_committer_time', 'true',
+        """Use git-committer timestamp instead of git-author timestamp
+        for the changeset ''Timestamp'' field.
         """)
 
-    _git_fs_encoding = Option('git', 'git_fs_encoding', 'utf-8',
-        """define charset encoding of paths within git repository""")
+    git_fs_encoding = Option('git', 'git_fs_encoding', 'utf-8',
+        """Define charset encoding of paths within git repositories.""")
 
-    _git_bin = PathOption('git', 'git_bin', '/usr/bin/git',
-        """path to git executable (relative to trac project folder!)""")
+    git_bin = PathOption('git', 'git_bin', '/usr/bin/git',
+        """Path to git executable (relative to the Trac configuration folder,
+        so better use an absolute path here).""")
 
 
     def get_supported_types(self):
@@ -209,11 +211,11 @@ class GitConnector(Component):
         """GitRepository factory method"""
         assert type == 'git'
 
-        if not (4 <= self._shortrev_len <= 40):
-            raise TracError("shortrev_len must be withing [4..40]")
+        if not (4 <= self.shortrev_len <= 40):
+            raise TracError("[git] shortrev_len setting must be within [4..40]")
 
-        if not (4 <= self._wiki_shortrev_len <= 40):
-            raise TracError("wiki_shortrev_len must be withing [4..40]")
+        if not (4 <= self.wiki_shortrev_len <= 40):
+            raise TracError("[git] wikishortrev_len must be within [4..40]")
 
         if not self._version:
             raise TracError("GIT backend not available")
@@ -223,12 +225,12 @@ class GitConnector(Component):
                             (self._version['v_str'],
                              self._version['v_min_str']))
 
-        if self._trac_user_rlookup:
+        if self.trac_user_rlookup:
             def rlookup_uid(email):
-                """reverse map 'real name <us...@domain.tld>' addresses to trac
-                user ids
+                """Reverse map 'real name <us...@domain.tld>' addresses to trac
+                user ids.
 
-                returns None if lookup failed
+                :return: `None` if lookup failed
                 """
 
                 try:
@@ -250,16 +252,16 @@ class GitConnector(Component):
                 return None
 
         repos = GitRepository(dir, params, self.log,
-                              persistent_cache=self._persistent_cache,
-                              git_bin=self._git_bin,
-                              git_fs_encoding=self._git_fs_encoding,
-                              shortrev_len=self._shortrev_len,
+                              persistent_cache=self.persistent_cache,
+                              git_bin=self.git_bin,
+                              git_fs_encoding=self.git_fs_encoding,
+                              shortrev_len=self.shortrev_len,
                               rlookup_uid=rlookup_uid,
-                              use_committer_id=self._use_committer_id,
-                              use_committer_time=self._use_committer_time,
+                              use_committer_id=self.use_committer_id,
+                              use_committer_time=self.use_committer_time,
                               )
 
-        if self._cached_repository:
+        if self.cached_repository:
             repos = GitCachedRepository(self.env, repos, self.log)
             self.log.debug("enabled CachedRepository for '%s'" % dir)
         else:
@@ -369,15 +371,19 @@ class GitRepository(Repository):
         self.logger = log
         self.gitrepo = path
         self.params = params
-        self._shortrev_len = max(4, min(shortrev_len, 40))
+        self.shortrev_len = max(4, min(shortrev_len, 40))
         self.rlookup_uid = rlookup_uid
-        self._use_committer_time = use_committer_time
-        self._use_committer_id = use_committer_id
+        self.use_committer_time = use_committer_time
+        self.use_committer_id = use_committer_id
 
-        self.git = PyGIT.StorageFactory(path, log, not persistent_cache,
-                                        git_bin=git_bin,
-                                        git_fs_encoding=git_fs_encoding) \
-                        .getInstance()
+        try:
+            self.git = PyGIT.StorageFactory(path, log, not persistent_cache,
+                                            git_bin=git_bin,
+                                            git_fs_encoding=git_fs_encoding) \
+                            .getInstance()
+        except PyGIT.GitError, e:
+            raise TracError("%s does not appear to be a Git "
+                            "repository." % path)
 
         Repository.__init__(self, 'git:'+path, self.params, log)
 
@@ -406,7 +412,7 @@ class GitRepository(Repository):
 
     def short_rev(self, rev):
         return self.git.shortrev(self.normalize_rev(rev),
-                                 min_len=self._shortrev_len)
+                                 min_len=self.shortrev_len)
 
     def get_node(self, path, rev=None, historian=None):
         return GitNode(self, path, rev, self.log, None, historian)
@@ -646,22 +652,37 @@ class GitChangeset(Changeset):
         if _children:
             props['children'] = _children
 
+        committer, author = self._get_committer_and_author()
         # use 1st author/committer as changeset owner/timestamp
-        if repos._use_committer_time:
-            _, time_ = _parse_user_time(props['committer'][0])
+        c_user = a_user = c_time = a_time = None
+        if committer:
+            c_user, c_time = _parse_user_time(committer)
+        if author:
+            a_user, a_time = _parse_user_time(author)
+
+        if repos.use_committer_time:
+            time = c_time or a_time
         else:
-            _, time_ = _parse_user_time(props['author'][0])
+            time = a_time or c_time
 
-        if repos._use_committer_id:
-            user_, _ = _parse_user_time(props['committer'][0])
+        if repos.use_committer_id:
+            user = c_user or a_user
         else:
-            user_, _ = _parse_user_time(props['author'][0])
+            user = a_user or c_user
 
         # try to resolve email address to trac uid
-        user_ = repos.rlookup_uid(user_) or user_
+        user = repos.rlookup_uid(user) or user
 
-        Changeset.__init__(self, repos, rev=sha, message=msg, author=user_,
-                           date=time_)
+        Changeset.__init__(self, repos, rev=sha, message=msg, author=user,
+                           date=time)
+
+    def _get_committer_and_author(self):
+        committer = author = None
+        if 'committer' in self.props:
+            committer = self.props['committer'][0]
+        if 'author' in self.props:
+            author = self.props['author'][0]
+        return committer, author
 
     def get_properties(self):
         properties = {}
@@ -672,13 +693,10 @@ class GitChangeset(Changeset):
         if 'children' in self.props:
             properties['Children'] = self.props['children']
 
-        if 'committer' in self.props:
-            properties['git-committer'] = \
-                    _parse_user_time(self.props['committer'][0])
-
-        if 'author' in self.props:
-            properties['git-author'] = \
-                    _parse_user_time(self.props['author'][0])
+        committer, author = self._get_committer_and_author()
+        if author != committer:
+            properties['git-committer'] = _parse_user_time(committer)
+            properties['git-author'] = _parse_user_time(author)
 
         branches = list(self.repos.git.get_branch_contains(self.rev,
                                                            resolve=True))

Modified: incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/tests/PyGIT.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/tests/PyGIT.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/tests/PyGIT.py (original)
+++ incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/tests/PyGIT.py Tue Oct 16 20:06:09 2012
@@ -14,7 +14,7 @@
 import unittest
 
 from trac.test import locate
-from tracopt.versioncontrol.git.PyGIT import GitCore, Storage
+from tracopt.versioncontrol.git.PyGIT import GitCore, Storage, parse_commit
 
 
 class GitTestCase(unittest.TestCase):
@@ -32,6 +32,111 @@ class GitTestCase(unittest.TestCase):
         self.assertTrue(v['v_compatible'])
 
 
+class TestParseCommit(unittest.TestCase):
+    commit2240a7b = '''\
+tree b19535236cfb6c64b798745dd3917dafc27bcd0a
+parent 30aaca4582eac20a52ac7b2ec35bdb908133e5b1
+parent 5a0dc7365c240795bf190766eba7a27600be3b3e
+author Linus Torvalds <to...@linux-foundation.org> 1323915958 -0800
+committer Linus Torvalds <to...@linux-foundation.org> 1323915958 -0800
+mergetag object 5a0dc7365c240795bf190766eba7a27600be3b3e
+ type commit
+ tag tytso-for-linus-20111214A
+ tagger Theodore Ts'o <ty...@mit.edu> 1323890113 -0500
+ 
+ tytso-for-linus-20111214
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v1.4.10 (GNU/Linux)
+ 
+ iQIcBAABCAAGBQJO6PXBAAoJENNvdpvBGATwpuEP/2RCxmdWYZ8/6Z6pmTh3hHN5
+ fx6HckTdvLQOvbQs72wzVW0JKyc25QmW2mQc5z3MjSymjf/RbEKihPUITRNbHrTD
+ T2sP/lWu09AKLioEg4ucAKn/A7Do3UDIkXTszvVVP/t2psVPzLeJ1njQKra14Nyz
+ o0+gSlnwuGx9WaxfR+7MYNs2ikdSkXIeYsiFAOY4YOxwwC99J/lZ0YaNkbI7UBtC
+ yu2XLIvPboa5JZXANq2G3VhVIETMmOyRTCC76OAXjqkdp9nLFWDG0ydqQh0vVZwL
+ xQGOmAj+l3BNTE0QmMni1w7A0SBU3N6xBA5HN6Y49RlbsMYG27aN54Fy5K2R41I3
+ QXVhBL53VD6b0KaITcoz7jIGIy6qk9Wx+2WcCYtQBSIjL2YwlaJq0PL07+vRamex
+ sqHGDejcNY87i6AV0DP6SNuCFCi9xFYoAoMi9Wu5E9+T+Vck0okFzW/luk/FvsSP
+ YA5Dh+vISyBeCnWQvcnBmsUQyf8d9MaNnejZ48ath+GiiMfY8USAZ29RAG4VuRtS
+ 9DAyTTIBA73dKpnvEV9u4i8Lwd8hRVMOnPyOO785NwEXk3Ng08pPSSbMklW6UfCY
+ 4nr5UNB13ZPbXx4uoAvATMpCpYxMaLEdxmeMvgXpkekl0hHBzpVDey1Vu9fb/a5n
+ dQpo6WWG9HIJ23hOGAGR
+ =n3Lm
+ -----END PGP SIGNATURE-----
+
+Merge tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4
+
+* tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4:
+  ext4: handle EOF correctly in ext4_bio_write_page()
+  ext4: remove a wrong BUG_ON in ext4_ext_convert_to_initialized
+  ext4: correctly handle pages w/o buffers in ext4_discard_partial_buffers()
+  ext4: avoid potential hang in mpage_submit_io() when blocksize < pagesize
+  ext4: avoid hangs in ext4_da_should_update_i_disksize()
+  ext4: display the correct mount option in /proc/mounts for [no]init_itable
+  ext4: Fix crash due to getting bogus eh_depth value on big-endian systems
+  ext4: fix ext4_end_io_dio() racing against fsync()
+
+.. using the new signed tag merge of git that now verifies the gpg
+signature automatically.  Yay.  The branchname was just 'dev', which is
+prettier.  I'll tell Ted to use nicer tag names for future cases.
+'''
+
+    def test_parse(self):
+        msg, props = parse_commit(self.commit2240a7b)
+        self.assertTrue(msg)
+        self.assertTrue(props)
+        self.assertEquals(
+            ['30aaca4582eac20a52ac7b2ec35bdb908133e5b1',
+             '5a0dc7365c240795bf190766eba7a27600be3b3e'],
+            props['parent'])
+        self.assertEquals(
+            ['Linus Torvalds <to...@linux-foundation.org> 1323915958 -0800'],
+            props['author'])
+        self.assertEquals(props['author'], props['committer'])
+
+        # Merge tag
+        self.assertEquals(['''\
+object 5a0dc7365c240795bf190766eba7a27600be3b3e
+type commit
+tag tytso-for-linus-20111214A
+tagger Theodore Ts\'o <ty...@mit.edu> 1323890113 -0500
+
+tytso-for-linus-20111214
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+iQIcBAABCAAGBQJO6PXBAAoJENNvdpvBGATwpuEP/2RCxmdWYZ8/6Z6pmTh3hHN5
+fx6HckTdvLQOvbQs72wzVW0JKyc25QmW2mQc5z3MjSymjf/RbEKihPUITRNbHrTD
+T2sP/lWu09AKLioEg4ucAKn/A7Do3UDIkXTszvVVP/t2psVPzLeJ1njQKra14Nyz
+o0+gSlnwuGx9WaxfR+7MYNs2ikdSkXIeYsiFAOY4YOxwwC99J/lZ0YaNkbI7UBtC
+yu2XLIvPboa5JZXANq2G3VhVIETMmOyRTCC76OAXjqkdp9nLFWDG0ydqQh0vVZwL
+xQGOmAj+l3BNTE0QmMni1w7A0SBU3N6xBA5HN6Y49RlbsMYG27aN54Fy5K2R41I3
+QXVhBL53VD6b0KaITcoz7jIGIy6qk9Wx+2WcCYtQBSIjL2YwlaJq0PL07+vRamex
+sqHGDejcNY87i6AV0DP6SNuCFCi9xFYoAoMi9Wu5E9+T+Vck0okFzW/luk/FvsSP
+YA5Dh+vISyBeCnWQvcnBmsUQyf8d9MaNnejZ48ath+GiiMfY8USAZ29RAG4VuRtS
+9DAyTTIBA73dKpnvEV9u4i8Lwd8hRVMOnPyOO785NwEXk3Ng08pPSSbMklW6UfCY
+4nr5UNB13ZPbXx4uoAvATMpCpYxMaLEdxmeMvgXpkekl0hHBzpVDey1Vu9fb/a5n
+dQpo6WWG9HIJ23hOGAGR
+=n3Lm
+-----END PGP SIGNATURE-----'''], props['mergetag'])
+
+        # Message
+        self.assertEquals("""Merge tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4
+
+* tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4:
+  ext4: handle EOF correctly in ext4_bio_write_page()
+  ext4: remove a wrong BUG_ON in ext4_ext_convert_to_initialized
+  ext4: correctly handle pages w/o buffers in ext4_discard_partial_buffers()
+  ext4: avoid potential hang in mpage_submit_io() when blocksize < pagesize
+  ext4: avoid hangs in ext4_da_should_update_i_disksize()
+  ext4: display the correct mount option in /proc/mounts for [no]init_itable
+  ext4: Fix crash due to getting bogus eh_depth value on big-endian systems
+  ext4: fix ext4_end_io_dio() racing against fsync()
+
+.. using the new signed tag merge of git that now verifies the gpg
+signature automatically.  Yay.  The branchname was just 'dev', which is
+prettier.  I'll tell Ted to use nicer tag names for future cases.""", msg)
+
+
 #class GitPerformanceTestCase(unittest.TestCase):
 #    """Performance test. Not really a unit test.
 #    Not self-contained: Needs a git repository and prints performance result
@@ -185,6 +290,7 @@ def suite():
     git = locate("git")
     if git:
         suite.addTest(unittest.makeSuite(GitTestCase, 'test'))
+        suite.addTest(unittest.makeSuite(TestParseCommit, 'test'))
     else:
         print("SKIP: tracopt/versioncontrol/git/tests/PyGIT.py (git cli "
               "binary, 'git', not found)")