Add patch history support
authorCatalin Marinas <catalin.marinas@gmail.com>
Fri, 15 Sep 2006 18:24:02 +0000 (19:24 +0100)
committerCatalin Marinas <catalin.marinas@gmail.com>
Fri, 15 Sep 2006 18:24:02 +0000 (19:24 +0100)
The patch history tracking works by generating a chain of log commits for
the corresponding patch commits. A 'log' command is available to either
list the changelog or invoke gitk.

Signed-off-by: Catalin Marinas <catalin.marinas@gmail.com>
stgit/commands/common.py
stgit/commands/log.py [new file with mode: 0644]
stgit/git.py
stgit/main.py
stgit/stack.py
t/t0001-subdir-branches.sh [changed mode: 0644->0755]
t/t1400-patch-history.sh [new file with mode: 0755]

index a073b29..bf8481e 100644 (file)
@@ -98,6 +98,8 @@ def git_id(rev):
                 return series.get_patch(patch).get_old_top()
             elif patch_id == 'bottom.old':
                 return series.get_patch(patch).get_old_bottom()
+            elif patch_id == 'log':
+                return series.get_patch(patch).get_log()
         if patch == 'base' and patch_id == None:
             return read_string(series.get_base_file())
     except RevParseException:
diff --git a/stgit/commands/log.py b/stgit/commands/log.py
new file mode 100644 (file)
index 0000000..033c797
--- /dev/null
@@ -0,0 +1,106 @@
+__copyright__ = """
+Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import sys, os, time
+from optparse import OptionParser, make_option
+from pydoc import pager
+from stgit.commands.common import *
+from stgit import stack, git
+
+help = 'display the patch changelog'
+usage = """%prog [options] [patch]
+
+List all the current and past commit ids of the given patch. The
+--graphical option invokes gitk instead of printing. The changelog
+commit messages have the form '<action> <new-patch-id>'. The <action>
+can be one of the following:
+
+  new     - new patch created
+  refresh - local changes were added to the patch
+  push    - the patch was cleanly pushed onto the stack
+  push(m) - the patch was pushed onto the stack with a three-way merge
+  push(f) - the patch was fast-forwarded
+  undo    - the patch boundaries were restored to the old values
+
+Note that only the diffs shown in the 'refresh' and 'undo' actions are
+meaningful for the patch changes. The 'push' actions represent the
+changes to the entire base of the current patch. Conflicts reset the
+patch content and a subsequent 'refresh' will show the entire patch."""
+
+options = [make_option('-b', '--branch',
+                       help = 'use BRANCH instead of the default one'),
+           make_option('-p', '--patch',
+                       help = 'show the refresh diffs',
+                       action = 'store_true'),
+           make_option('-g', '--graphical',
+                       help = 'run gitk instead of printing',
+                       action = 'store_true')]
+
+def show_log(log, show_patch):
+    """List the patch changelog
+    """
+    commit = git.get_commit(log)
+    diff_str = ''
+    while commit:
+        descr = commit.get_log().rstrip()
+
+        if show_patch:
+            if descr.startswith('refresh') or descr.startswith('undo'):
+                diff_str = '%s%s\n' % (diff_str,
+                                       git.pretty_commit(commit.get_id_hash()))
+        else:
+            author_name, author_email, author_date = \
+                         name_email_date(commit.get_author())
+            secs, tz = author_date.split()
+            date = '%s %s' % (time.ctime(int(secs)), tz)
+
+            print descr, date
+
+        parent = commit.get_parent()
+        if parent:
+            commit = git.get_commit(parent)
+        else:
+            commit = None
+
+    if show_patch and diff_str:
+        pager(diff_str.rstrip())
+
+def func(parser, options, args):
+    """Show the patch changelog
+    """
+    if len(args) == 0:
+        name = crt_series.get_current()
+        if not name:
+            raise CmdException, 'No patches applied'
+    elif len(args) == 1:
+        name = args[0]
+        if not name in crt_series.get_applied() + crt_series.get_unapplied():
+            raise CmdException, 'Unknown patch "%s"' % name
+    else:
+        parser.error('incorrect number of arguments')
+
+    patch = crt_series.get_patch(name)
+
+    log = patch.get_log()
+    if not log:
+        raise CmdException, 'No changelog for patch "%s"' % name
+
+    if options.graphical:
+        if os.system('gitk %s' % log) != 0:
+            raise CmdException, 'gitk execution failed'
+    else:
+        show_log(log, options.patch)
index 2c73bb8..025d15d 100644 (file)
@@ -59,7 +59,11 @@ class Commit:
         return self.__tree
 
     def get_parent(self):
-        return self.get_parents()[0]
+        parents = self.get_parents()
+        if parents:
+            return parents[0]
+        else:
+            return None
 
     def get_parents(self):
         return _output_lines('git-rev-list --parents --max-count=1 %s'
index e80bd1a..f59bce6 100644 (file)
@@ -43,6 +43,7 @@ import stgit.commands.goto
 import stgit.commands.id
 import stgit.commands.imprt
 import stgit.commands.init
+import stgit.commands.log
 import stgit.commands.mail
 import stgit.commands.new
 import stgit.commands.patches
@@ -81,6 +82,7 @@ commands = {
     'id':       stgit.commands.id,
     'import':   stgit.commands.imprt,
     'init':     stgit.commands.init,
+    'log':      stgit.commands.log,
     'mail':     stgit.commands.mail,
     'new':      stgit.commands.new,
     'patches':  stgit.commands.patches,
@@ -126,6 +128,7 @@ patchcommands = (
     'files',
     'fold',
     'import',
+    'log',
     'mail',
     'new',
     'pick',
index c1071b5..0113a1c 100644 (file)
@@ -128,6 +128,8 @@ class Patch:
         self.__dir = os.path.join(self.__series_dir, self.__name)
         self.__refs_dir = refs_dir
         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
+        self.__log_ref_file = os.path.join(self.__refs_dir,
+                                           self.__name + '.log')
 
     def create(self):
         os.mkdir(self.__dir)
@@ -139,23 +141,33 @@ class Patch:
             os.remove(os.path.join(self.__dir, f))
         os.rmdir(self.__dir)
         os.remove(self.__top_ref_file)
+        if os.path.exists(self.__log_ref_file):
+            os.remove(self.__log_ref_file)
 
     def get_name(self):
         return self.__name
 
     def rename(self, newname):
         olddir = self.__dir
-        old_ref_file = self.__top_ref_file
+        old_top_ref_file = self.__top_ref_file
+        old_log_ref_file = self.__log_ref_file
         self.__name = newname
         self.__dir = os.path.join(self.__series_dir, self.__name)
         self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
+        self.__log_ref_file = os.path.join(self.__refs_dir,
+                                           self.__name + '.log')
 
         os.rename(olddir, self.__dir)
-        os.rename(old_ref_file, self.__top_ref_file)
+        os.rename(old_top_ref_file, self.__top_ref_file)
+        if os.path.exists(old_log_ref_file):
+            os.rename(old_log_ref_file, self.__log_ref_file)
 
     def __update_top_ref(self, ref):
         write_string(self.__top_ref_file, ref)
 
+    def __update_log_ref(self, ref):
+        write_string(self.__log_ref_file, ref)
+
     def update_top_ref(self):
         top = self.get_top()
         if top:
@@ -274,6 +286,13 @@ class Patch:
                 address = os.environ['GIT_COMMITTER_EMAIL']
         self.__set_field('commemail', address)
 
+    def get_log(self):
+        return self.__get_field('log')
+
+    def set_log(self, value, backup = False):
+        self.__set_field('log', value)
+        self.__update_log_ref(value)
+
 
 class Series:
     """Class including the operations on series
@@ -582,7 +601,7 @@ class Series:
                       author_name = None, author_email = None,
                       author_date = None,
                       committer_name = None, committer_email = None,
-                      backup = False):
+                      backup = False, log = 'refresh'):
         """Generates a new commit for the given patch
         """
         name = self.get_current()
@@ -635,6 +654,9 @@ class Series:
         patch.set_commname(committer_name)
         patch.set_commemail(committer_email)
 
+        if log:
+            self.log_patch(patch, log)
+
         return commit_id
 
     def undo_refresh(self):
@@ -654,7 +676,8 @@ class Series:
             raise StackException, 'No refresh undo information available'
 
         git.reset(tree_id = old_top, check_out = False)
-        patch.restore_old_boundaries()
+        if patch.restore_old_boundaries():
+            self.log_patch(patch, 'undo')
 
     def new_patch(self, name, message = None, can_edit = True,
                   unapplied = False, show_patch = False,
@@ -698,21 +721,24 @@ class Series:
         patch.set_commemail(committer_email)
 
         if unapplied:
+            self.log_patch(patch, 'new')
+
             patches = [patch.get_name()] + self.get_unapplied()
 
             f = file(self.__unapplied_file, 'w+')
             f.writelines([line + '\n' for line in patches])
             f.close()
-        else:
-            if before_existing:
-                insert_string(self.__applied_file, patch.get_name())
-                if not self.get_current():
-                    self.__set_current(name)
-            else:
-                append_string(self.__applied_file, patch.get_name())
+        elif before_existing:
+            self.log_patch(patch, 'new')
+
+            insert_string(self.__applied_file, patch.get_name())
+            if not self.get_current():
                 self.__set_current(name)
+        else:
+            append_string(self.__applied_file, patch.get_name())
+            self.__set_current(name)
 
-                self.refresh_patch(cache_update = False)
+            self.refresh_patch(cache_update = False, log = 'new')
 
     def delete_patch(self, name):
         """Deletes a patch
@@ -759,7 +785,8 @@ class Series:
 
             # top != bottom always since we have a commit for each patch
             if head == bottom:
-                # reset the backup information
+                # reset the backup information. No logging since the
+                # patch hasn't changed
                 patch.set_bottom(head, backup = True)
                 patch.set_top(top, backup = True)
 
@@ -790,6 +817,8 @@ class Series:
 
                     patch.set_bottom(head, backup = True)
                     patch.set_top(top, backup = True)
+
+                    self.log_patch(patch, 'push(f)')
                 else:
                     top = head
                     # stop the fast-forwarding, must do a real merge
@@ -860,7 +889,7 @@ class Series:
             patch.set_top(head, backup = True)
             modified = True
         elif head == bottom:
-            # reset the backup information
+            # reset the backup information. No need for logging
             patch.set_bottom(bottom, backup = True)
             patch.set_top(top, backup = True)
 
@@ -900,7 +929,11 @@ class Series:
             if not ex:
                 # if the merge was OK and no conflicts, just refresh the patch
                 # The GIT cache was already updated by the merge operation
-                self.refresh_patch(cache_update = False)
+                if modified:
+                    log = 'push(m)'
+                else:
+                    log = 'push'
+                self.refresh_patch(cache_update = False, log = log)
             else:
                 raise StackException, str(ex)
 
@@ -923,7 +956,11 @@ class Series:
 
         git.reset()
         self.pop_patch(name)
-        return patch.restore_old_boundaries()
+        ret = patch.restore_old_boundaries()
+        if ret:
+            self.log_patch(patch, 'undo')
+
+        return ret
 
     def pop_patch(self, name, keep = False):
         """Pops the top patch from the stack
@@ -1005,3 +1042,20 @@ class Series:
             f.close()
         else:
             raise StackException, 'Unknown patch "%s"' % oldname
+
+    def log_patch(self, patch, message):
+        """Generate a log commit for a patch
+        """
+        top = git.get_commit(patch.get_top())
+        msg = '%s\t%s' % (message, top.get_id_hash())
+
+        old_log = patch.get_log()
+        if old_log:
+            parents = [old_log]
+        else:
+            parents = []
+
+        log = git.commit(message = msg, parents = parents,
+                         cache_update = False, tree_id = top.get_tree(),
+                         allowempty = True)
+        patch.set_log(log)
old mode 100644 (file)
new mode 100755 (executable)
diff --git a/t/t1400-patch-history.sh b/t/t1400-patch-history.sh
new file mode 100755 (executable)
index 0000000..cabd5e8
--- /dev/null
@@ -0,0 +1,85 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Catalin Marinas
+#
+
+test_description='Test the patch history generation.
+
+'
+
+. ./test-lib.sh
+
+test_expect_success \
+       'Initialize the StGIT repository' \
+       '
+       stg init
+       '
+
+test_expect_success \
+       'Create the first patch' \
+       '
+       stg new foo -m "Foo Patch" &&
+       echo foo > test && echo foo2 >> test &&
+       stg add test &&
+       stg refresh
+       '
+
+test_expect_success \
+       'Create the second patch' \
+       '
+       stg new bar -m "Bar Patch" &&
+       echo bar >> test &&
+       stg refresh
+       '
+
+test_expect_success \
+       'Check the "new" and "refresh" logs' \
+       '
+       stg log foo | grep -q -e "^new" &&
+       stg log foo | grep -q -e "^refresh" &&
+       stg log | grep -q -e "^new" &&
+       stg log | grep -q -e "^refresh"
+       '
+
+test_expect_success \
+       'Check the "push" log' \
+       '
+       stg pop &&
+       echo foo > test2 && stg add test2 && stg refresh &&
+       stg push &&
+       stg log | grep -q -e "^push     "
+       '
+
+test_expect_success \
+       'Check the "push(f)" log' \
+       '
+       stg pop &&
+       stg refresh -m "Foo2 Patch" &&
+       stg push &&
+       stg log | grep -q -e "^push(f)  "
+       '
+
+test_expect_success \
+       'Check the "push(m)" log' \
+       '
+       stg pop &&
+       echo foo2 > test && stg refresh &&
+       stg push &&
+       stg log | grep -q -e "^push(m)  "
+       '
+
+test_expect_success \
+       'Check the push "undo" log' \
+       '
+       stg push --undo &&
+       stg log bar | grep -q -e "^undo "
+       '
+
+test_expect_success \
+       'Check the refresh "undo" log' \
+       '
+       stg refresh --undo &&
+       stg log | grep -q -e "^undo     "
+       '
+
+test_done