chiark / gitweb /
Write to a stack log when stack is modified
authorKarl Hasselström <kha@treskal.com>
Sun, 21 Sep 2008 12:17:40 +0000 (14:17 +0200)
committerKarl Hasselström <kha@treskal.com>
Sun, 21 Sep 2008 12:19:07 +0000 (14:19 +0200)
Create a log branch (called <branchname>.stgit) for each StGit branch,
and write to it whenever the stack is modified.

Commands using the new infrastructure write to the log when they
commit a transaction. Commands using the old infrastructure get a log
entry write written for them when they exit, unless they explicitly
ask for this not to happen.

The only thing you can do with this log at the moment is look at it.

Signed-off-by: Karl Hasselström <kha@treskal.com>
31 files changed:
stgit/commands/branch.py
stgit/commands/clone.py
stgit/commands/common.py
stgit/commands/diff.py
stgit/commands/files.py
stgit/commands/float.py
stgit/commands/fold.py
stgit/commands/hide.py
stgit/commands/imprt.py
stgit/commands/log.py
stgit/commands/mail.py
stgit/commands/patches.py
stgit/commands/pick.py
stgit/commands/pop.py
stgit/commands/pull.py
stgit/commands/push.py
stgit/commands/rebase.py
stgit/commands/refresh.py
stgit/commands/rename.py
stgit/commands/repair.py
stgit/commands/resolved.py
stgit/commands/show.py
stgit/commands/sink.py
stgit/commands/status.py
stgit/commands/sync.py
stgit/commands/unhide.py
stgit/lib/git.py
stgit/lib/log.py [new file with mode: 0644]
stgit/lib/stack.py
stgit/lib/transaction.py
stgit/main.py

index 1b1b98f87954e50bbec47784f8e58ff892ea43b2..ef715476f10b75f868894b335c9ade5ccb466c49 100644 (file)
@@ -21,6 +21,7 @@ from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
 from stgit import stack, git, basedir
+from stgit.lib import log
 
 help = 'Branch operations: switch, list, create, rename, delete, ...'
 kind = 'stack'
@@ -102,7 +103,7 @@ options = [
     opt('--force', action = 'store_true',
         short = 'Force a delete when the series is not empty')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = False)
 
 def __is_current_branch(branch_name):
     return crt_series.get_name() == branch_name
@@ -196,6 +197,7 @@ def func(parser, options, args):
                                    parent_branch = parentbranch)
 
         out.info('Branch "%s" created' % args[0])
+        log.compat_log_entry('branch --create')
         return
 
     elif options.clone:
@@ -216,6 +218,8 @@ def func(parser, options, args):
         crt_series.clone(clone)
         out.done()
 
+        log.copy_log(log.default_repo(), crt_series.get_name(), clone,
+                     'branch --clone')
         return
 
     elif options.delete:
@@ -223,6 +227,7 @@ def func(parser, options, args):
         if len(args) != 1:
             parser.error('incorrect number of arguments')
         __delete_branch(args[0], options.force)
+        log.delete_log(log.default_repo(), args[0])
         return
 
     elif options.list:
@@ -230,13 +235,16 @@ def func(parser, options, args):
         if len(args) != 0:
             parser.error('incorrect number of arguments')
 
-        branches = git.get_heads()
-        branches.sort()
+        branches = set(git.get_heads())
+        for br in set(branches):
+            m = re.match(r'^(.*)\.stgit$', br)
+            if m and m.group(1) in branches:
+                branches.remove(br)
 
         if branches:
             out.info('Available branches:')
             max_len = max([len(i) for i in branches])
-            for i in branches:
+            for i in sorted(branches):
                 __print_branch(i, max_len)
         else:
             out.info('No branches')
@@ -273,7 +281,7 @@ def func(parser, options, args):
         stack.Series(args[0]).rename(args[1])
 
         out.info('Renamed branch "%s" to "%s"' % (args[0], args[1]))
-
+        log.rename_log(log.default_repo(), args[0], args[1], 'branch --rename')
         return
 
     elif options.unprotect:
index 28500c54eea00ccf0363d90bc8cd1e691d01967c..659712d95133a68f68aca68586adc6d7f43feedc 100644 (file)
@@ -37,7 +37,7 @@ not already exist."""
 
 options = []
 
-directory = DirectoryAnywhere(needs_current_series = False)
+directory = DirectoryAnywhere(needs_current_series = False, log = False)
 
 def func(parser, options, args):
     """Clone the <repository> into the local <dir> and initialises the
index 3cecec074a24880e6dc4e20097a91703b79b4066..fea4dbc67ca9345c37fcbc0234e71c61628060a3 100644 (file)
@@ -27,6 +27,7 @@ from stgit import stack, git, basedir
 from stgit.config import config, file_extensions
 from stgit.lib import stack as libstack
 from stgit.lib import git as libgit
+from stgit.lib import log
 
 # Command exception class
 class CmdException(StgException):
@@ -444,8 +445,9 @@ class DirectoryException(StgException):
     pass
 
 class _Directory(object):
-    def __init__(self, needs_current_series = True):
+    def __init__(self, needs_current_series = True, log = True):
         self.needs_current_series =  needs_current_series
+        self.log = log
     @readonly_constant_property
     def git_dir(self):
         try:
@@ -478,6 +480,9 @@ class _Directory(object):
                        ).output_one_line()]
     def cd_to_topdir(self):
         os.chdir(self.__topdir_path)
+    def write_log(self, msg):
+        if self.log:
+            log.compat_log_entry(msg)
 
 class DirectoryAnywhere(_Directory):
     def setup(self):
@@ -502,6 +507,7 @@ class DirectoryHasRepositoryLib(_Directory):
     """For commands that use the new infrastructure in stgit.lib.*."""
     def __init__(self):
         self.needs_current_series = False
+        self.log = False # stgit.lib.transaction handles logging
     def setup(self):
         # This will throw an exception if we don't have a repository.
         self.repository = libstack.Repository.default()
index e0078f998d811e95c807970e887d8cf92bc4cf85..38de3a1dd6cb31b2e34ca02d5746109b3027b8b9 100644 (file)
@@ -44,7 +44,7 @@ options = [
         short = 'Show the stat instead of the diff'),
     ] + argparse.diff_opts_option()
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
 
 def func(parser, options, args):
     """Show the tree diff
index 318a4a327b16b69cdf483cd07028a90c14224cc5..a7576e9fc252ad72d25d9c8303bd883375553a21 100644 (file)
@@ -40,7 +40,7 @@ options = [
         short = 'Bare file names (useful for scripting)'),
     ] + argparse.diff_opts_option()
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
 
 def func(parser, options, args):
     """Show the files modified by a patch (or the current patch)
index 1ca4ed38e380086016a05331467b13ae0704d8a8..93bb69b0720d994ff67bc71e4875ae03d9a9795f 100644 (file)
@@ -36,7 +36,7 @@ options = [
     opt('-s', '--series', action = 'store_true',
         short = 'Rearrange according to a series file')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pops and pushed to make the named patch the topmost patch
index 0f1486a031b10523766bf77d5dfdc80aece6ef28..165ff52749dd6a72ac5e10070df8b768da30be25 100644 (file)
@@ -39,7 +39,7 @@ options = [
     opt('-b', '--base',
         short = 'Use BASE instead of HEAD applying the patch')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Integrate a GNU diff patch into the current patch
index bee2162a74cb01feb1e1a706b2b2b28fe4912715..1bcb5f1c28d092d3c59701f659a2f6b552c426b1 100644 (file)
@@ -33,7 +33,7 @@ options = [
     opt('-b', '--branch',
         short = 'Use BRANCH instead of the default branch')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Hide a range of patch in the series
index 150d9eecf511050ce0343504af4444aca3dbcb59..103ebb03f2d5656588ffde742c550a656166f176 100644 (file)
@@ -81,7 +81,7 @@ options = [
         short = 'Use COMMEMAIL as the committer e-mail'),
     ] + argparse.sign_options()
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = True)
 
 def __strip_patch_name(name):
     stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
index de210ea9a0d1252d1c48a911c364ae901da505a4..07fcd98d5ce9b16eeb5c56b1d2b056439143183e 100644 (file)
@@ -57,7 +57,7 @@ options = [
     opt('-g', '--graphical', action = 'store_true',
         short = 'Run gitk instead of printing')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
 
 def show_log(log, options):
     """List the patch changelog
index caf8f9eb971d08689b076ca0128743729ab0071f..6948a1ee4f8fc5a6267308f2ff9e601e2e9e821e 100644 (file)
@@ -139,7 +139,7 @@ options = [
         short = 'Generate an mbox file instead of sending')
     ] + argparse.diff_opts_option()
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
 
 def __get_sender():
     """Return the 'authname <authemail>' string as read from the
index 0cbc275c6e98ed677cd57b35ff68758a90a19cae..e877171071b313cc19a40b406be9ef4f12b0045e 100644 (file)
@@ -38,7 +38,7 @@ options = [
     opt('-b', '--branch',
         short = 'Use BRANCH instead of the default branch')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
 
 diff_tmpl = \
           '-------------------------------------------------------------------------------\n' \
index 8a882623ad5bd47fc50ddfca2052d901dae869f3..e1c531d7bc153fb13f6813541234de855b686355 100644 (file)
@@ -52,7 +52,7 @@ options = [
     opt('--unapplied', action = 'store_true',
         short = 'Keep the patch unapplied')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def __pick_commit(commit_id, patchname, options):
     """Pick a commit id.
index cf898465c57cdf6ae2cfe38edc13790a106b69e0..1c56671d8317502015c3f0d05984a63c0cf253ab 100644 (file)
@@ -43,7 +43,7 @@ options = [
     opt('-k', '--keep', action = 'store_true',
         short = 'Keep the local changes')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pop the topmost patch from the stack
index c989b5d087c979a37d0f2b1e301b11b915a3727b..82035c675e60dc66740af93bae7f28f941fdd43d 100644 (file)
@@ -43,7 +43,7 @@ options = [
     opt('-m', '--merged', action = 'store_true',
         short = 'Check for patches merged upstream')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pull the changes from a remote repository
index c6056ccafdda24c12e7abe4a4a4457ff5052e551..c3d553dc51acaeea243ffe9e912d400dabd39606 100644 (file)
@@ -50,7 +50,7 @@ options = [
     opt('--undo', action = 'store_true',
         short = 'Undo the last patch pushing')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Pushes the given patch or all onto the series
index b09204ec2a3565ddca754d364fbbb4af02d5558c..b5a80bb42eb006627f3fd2d1c193840a4d6959f6 100644 (file)
@@ -46,7 +46,7 @@ options = [
     opt('-m', '--merged', action = 'store_true',
         short = 'Check for patches merged upstream')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Rebase the current stack
index a20fcc5c4f6b020fe0bd400487dc8fbaa3054f45..7be94e0013d57d7d0ce12d697910d830c3563be2 100644 (file)
@@ -50,7 +50,7 @@ options = [
     opt('-p', '--patch',
         short = 'Refresh (applied) PATCH instead of the top patch')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Generate a new commit for the current or given patch.
index fdb31ee55981105add3c541b58191e91405b9d75..7e0fbf57d36f36f55b204ebd6d6ead215d0194cc 100644 (file)
@@ -33,7 +33,7 @@ options = [
     opt('-b', '--branch',
         short = 'use BRANCH instead of the default one')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Rename a patch in the series
index 6218caaf443c55e19d713b5d06b0f62eb8da16e3..e06df3a66eb5ae401f1764dc7dfd5d8b00efd215 100644 (file)
@@ -70,7 +70,7 @@ repair" is _not_ what you want. In that case, what you want is option
 
 options = []
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 class Commit(object):
     def __init__(self, id):
index d8dacd6684edd98dea9984aae66849d3b6ef7551..ce8630d3fb86c4aa241a01750e4e071ae4ec32f4 100644 (file)
@@ -40,7 +40,7 @@ options = [
     opt('-i', '--interactive', action = 'store_true',
         short = 'Run the interactive merging tool')]
 
-directory = DirectoryHasRepository(needs_current_series = False)
+directory = DirectoryHasRepository(needs_current_series = False, log = False)
 
 def func(parser, options, args):
     """Mark the conflict as resolved
index 41cb31e2ef7fce370dc825199691f9ab261610a1..e08551b3ccb93235e7a1852529a8e38126c8a13a 100644 (file)
@@ -37,7 +37,7 @@ options = [
         short = 'Show the unapplied patches'),
     ] + argparse.diff_opts_option()
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
 
 def func(parser, options, args):
     """Show commit log and diff
index 95a43e16a7c8681eeebfd258edbc3e1fee30a697..34f81c964fac0d570dd215d4e336998e59f829bb 100644 (file)
@@ -51,7 +51,7 @@ options = [
         Specify a target patch to place the patches below, instead of
         sinking them to the bottom of the stack.""")]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def func(parser, options, args):
     """Sink patches down the stack.
index 7c68ba6380e9d542a8546e08c8b92fb77dc8c18b..c78bc1bff425fb64d2df3116507e6c0b24d64c25 100644 (file)
@@ -55,7 +55,7 @@ options = [
     opt('--reset', action = 'store_true',
         short = 'Reset the current tree changes')]
 
-directory = DirectoryHasRepository(needs_current_series = False)
+directory = DirectoryHasRepository(needs_current_series = False, log = False)
 
 def status(files, modified, new, deleted, conflict, unknown, noexclude):
     """Show the tree status
@@ -96,6 +96,7 @@ def func(parser, options, args):
     directory.cd_to_topdir()
 
     if options.reset:
+        directory.log = True
         if args:
             conflicts = git.get_conflicts()
             git.resolved([fn for fn in args if fn in conflicts])
index 767b4d27cf5d5f07d9c52f6734dcd8f7ee0b8f26..26a49f03f9e058836a600b652721e2fb323f7881 100644 (file)
@@ -45,7 +45,7 @@ options = [
     opt('--undo', action = 'store_true',
         short = 'Undo the synchronisation of the current patch')]
 
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
 
 def __check_all():
     check_local_changes()
index c34bc1d5d0fa013dd7c261f8c68edc946198f948..acfef2927cd078dfb3d818c64385cbc2ad2d49b6 100644 (file)
@@ -33,7 +33,7 @@ options = [
     opt('-b', '--branch',
         short = 'Use BRANCH instead of the default branch')]
 
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = True)
 
 def func(parser, options, args):
     """Unhide a range of patches in the series
index 2386e279f6defd042bb90968ff3087303eac7d5c..ac3e9d0c46dd8339a4a824be5576e756150c5ff8 100644 (file)
@@ -139,6 +139,7 @@ class Person(Immutable, Repr):
         assert isinstance(self.__date, Date) or self.__date in [None, NoValue]
     name = property(lambda self: self.__name)
     email = property(lambda self: self.__email)
+    name_email = property(lambda self: '%s <%s>' % (self.name, self.email))
     date = property(lambda self: self.__date)
     def set_name(self, name):
         return type(self)(name = name, defaults = self)
@@ -147,7 +148,7 @@ class Person(Immutable, Repr):
     def set_date(self, date):
         return type(self)(date = date, defaults = self)
     def __str__(self):
-        return '%s <%s> %s' % (self.name, self.email, self.date)
+        return '%s %s' % (self.name_email, self.date)
     @classmethod
     def parse(cls, s):
         m = re.match(r'^([^<]*)<([^>]*)>\s+(\d+\s+[+-]\d{4})$', s)
diff --git a/stgit/lib/log.py b/stgit/lib/log.py
new file mode 100644 (file)
index 0000000..cb32d3e
--- /dev/null
@@ -0,0 +1,328 @@
+r"""This module contains functions and classes for manipulating
+I{patch stack logs} (or just I{stack logs}).
+
+A stack log is a git branch. Each commit contains the complete state
+of the stack at the moment it was written; the most recent commit has
+the most recent state.
+
+For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.
+Each log entry makes sure to have proper references to everything it
+needs, which means that it is safe against garbage collection -- you
+can even pull it from one repository to another.
+
+Stack log format (version 0)
+============================
+
+Version 0 was an experimental version of the stack log format; it is
+no longer supported.
+
+Stack log format (version 1)
+============================
+
+Commit message
+--------------
+
+The commit message is mostly for human consumption; in most cases it
+is just a subject line: the stg subcommand name and possibly some
+important command-line flag.
+
+An exception to this is log commits for undo and redo. Their subject
+line is "C{undo I{n}}" and "C{redo I{n}}"; the positive integer I{n}
+says how many steps were undone or redone.
+
+Tree
+----
+
+  - One blob, C{meta}, that contains the log data:
+
+      - C{Version:} I{n}
+
+        where I{n} must be 1. (Future versions of StGit might change
+        the log format; when this is done, this version number will be
+        incremented.)
+
+      - C{Previous:} I{sha1 or C{None}}
+
+        The commit of the previous log entry, or C{None} if this is
+        the first entry.
+
+      - C{Head:} I{sha1}
+
+        The current branch head.
+
+      - C{Applied:}
+
+        Marks the start of the list of applied patches. They are
+        listed in order, each on its own line: first one or more
+        spaces, then the patch name, then a colon, space, then the
+        patch's sha1.
+
+      - C{Unapplied:}
+
+        Same as C{Applied:}, but for the unapplied patches.
+
+      - C{Hidden:}
+
+        Same as C{Applied:}, but for the hidden patches.
+
+  - One subtree, C{patches}, that contains one blob per patch::
+
+      Bottom: <sha1 of patch's bottom tree>
+      Top:    <sha1 of patch's top tree>
+      Author: <author name and e-mail>
+      Date:   <patch timestamp>
+
+      <commit message>
+
+      ---
+
+      <patch diff>
+
+Following the message is a newline, three dashes, and another newline.
+Then come, each on its own line,
+
+Parents
+-------
+
+  - The first parent is the I{simplified log}, described below.
+
+  - The rest of the parents are just there to make sure that all the
+    commits referred to in the log entry -- patches, branch head,
+    previous log entry -- are ancestors of the log commit. (This is
+    necessary to make the log safe with regard to garbage collection
+    and pulling.)
+
+Simplified log
+--------------
+
+The simplified log is exactly like the full log, except that its only
+parent is the (simplified) previous log entry, if any. It's purpose is
+mainly ease of visualization."""
+
+from stgit.lib import git, stack as libstack
+from stgit import exception, utils
+from stgit.out import out
+import StringIO
+
+class LogException(exception.StgException):
+    pass
+
+class LogParseException(LogException):
+    pass
+
+def patch_file(repo, cd):
+    return repo.commit(git.BlobData(''.join(s + '\n' for s in [
+                    'Bottom: %s' % cd.parent.data.tree.sha1,
+                    'Top:    %s' % cd.tree.sha1,
+                    'Author: %s' % cd.author.name_email,
+                    'Date:   %s' % cd.author.date,
+                    '',
+                    cd.message,
+                    '',
+                    '---',
+                    '',
+                    repo.diff_tree(cd.parent.data.tree, cd.tree, ['-M']
+                                   ).strip()])))
+
+def log_ref(branch):
+    return 'refs/heads/%s.stgit' % branch
+
+class LogEntry(object):
+    __separator = '\n---\n'
+    __max_parents = 16
+    def __init__(self, repo, prev, head, applied, unapplied, hidden,
+                 patches, message):
+        self.__repo = repo
+        self.__prev = prev
+        self.__simplified = None
+        self.head = head
+        self.applied = applied
+        self.unapplied = unapplied
+        self.hidden = hidden
+        self.patches = patches
+        self.message = message
+    @property
+    def simplified(self):
+        if not self.__simplified:
+            self.__simplified = self.commit.data.parents[0]
+        return self.__simplified
+    @property
+    def prev(self):
+        if self.__prev != None and not isinstance(self.__prev, LogEntry):
+            self.__prev = self.from_commit(self.__repo, self.__prev)
+        return self.__prev
+    @classmethod
+    def from_stack(cls, prev, stack, message):
+        return cls(
+            repo = stack.repository,
+            prev = prev,
+            head = stack.head,
+            applied = list(stack.patchorder.applied),
+            unapplied = list(stack.patchorder.unapplied),
+            hidden = list(stack.patchorder.hidden),
+            patches = dict((pn, stack.patches.get(pn).commit)
+                           for pn in stack.patchorder.all),
+            message = message)
+    @staticmethod
+    def __parse_metadata(repo, metadata):
+        """Parse a stack log metadata string."""
+        if not metadata.startswith('Version:'):
+            raise LogParseException('Malformed log metadata')
+        metadata = metadata.splitlines()
+        version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
+        try:
+            version = int(version_str)
+        except ValueError:
+            raise LogParseException(
+                'Malformed version number: %r' % version_str)
+        if version < 1:
+            raise LogException('Log is version %d, which is too old' % version)
+        if version > 1:
+            raise LogException('Log is version %d, which is too new' % version)
+        parsed = {}
+        for line in metadata:
+            if line.startswith(' '):
+                parsed[key].append(line.strip())
+            else:
+                key, val = [x.strip() for x in line.split(':', 1)]
+                if val:
+                    parsed[key] = val
+                else:
+                    parsed[key] = []
+        prev = parsed['Previous']
+        if prev == 'None':
+            prev = None
+        else:
+            prev = repo.get_commit(prev)
+        head = repo.get_commit(parsed['Head'])
+        lists = { 'Applied': [], 'Unapplied': [], 'Hidden': [] }
+        patches = {}
+        for lst in lists.keys():
+            for entry in parsed[lst]:
+                pn, sha1 = [x.strip() for x in entry.split(':')]
+                lists[lst].append(pn)
+                patches[pn] = repo.get_commit(sha1)
+        return (prev, head, lists['Applied'], lists['Unapplied'],
+                lists['Hidden'], patches)
+    @classmethod
+    def from_commit(cls, repo, commit):
+        """Parse a (full or simplified) stack log commit."""
+        message = commit.data.message
+        try:
+            perm, meta = commit.data.tree.data.entries['meta']
+        except KeyError:
+            raise LogParseException('Not a stack log')
+        (prev, head, applied, unapplied, hidden, patches
+         ) = cls.__parse_metadata(repo, meta.data.str)
+        lg = cls(repo, prev, head, applied, unapplied, hidden, patches, message)
+        lg.commit = commit
+        return lg
+    def __metadata_string(self):
+        e = StringIO.StringIO()
+        e.write('Version: 1\n')
+        if self.prev == None:
+            e.write('Previous: None\n')
+        else:
+            e.write('Previous: %s\n' % self.prev.commit.sha1)
+        e.write('Head: %s\n' % self.head.sha1)
+        for lst, title in [(self.applied, 'Applied'),
+                           (self.unapplied, 'Unapplied'),
+                           (self.hidden, 'Hidden')]:
+            e.write('%s:\n' % title)
+            for pn in lst:
+                e.write('  %s: %s\n' % (pn, self.patches[pn].sha1))
+        return e.getvalue()
+    def __parents(self):
+        """Return the set of parents this log entry needs in order to be a
+        descendant of all the commits it refers to."""
+        xp = set([self.head]) | set(self.patches[pn]
+                                    for pn in self.unapplied + self.hidden)
+        if self.applied:
+            xp.add(self.patches[self.applied[-1]])
+        if self.prev != None:
+            xp.add(self.prev.commit)
+            xp -= set(self.prev.patches.values())
+        return xp
+    def __tree(self, metadata):
+        if self.prev == None:
+            def pf(c):
+                return patch_file(self.__repo, c.data)
+        else:
+            prev_top_tree = self.prev.commit.data.tree
+            perm, prev_patch_tree = prev_top_tree.data.entries['patches']
+            # Map from Commit object to patch_file() results taken
+            # from the previous log entry.
+            c2b = dict((self.prev.patches[pn], pf) for pn, pf
+                       in prev_patch_tree.data.entries.iteritems())
+            def pf(c):
+                r = c2b.get(c, None)
+                if not r:
+                    r = patch_file(self.__repo, c.data)
+                return r
+        patches = dict((pn, pf(c)) for pn, c in self.patches.iteritems())
+        return self.__repo.commit(git.TreeData({
+                    'meta': self.__repo.commit(git.BlobData(metadata)),
+                    'patches': self.__repo.commit(git.TreeData(patches)) }))
+    def write_commit(self):
+        metadata = self.__metadata_string()
+        tree = self.__tree(metadata)
+        self.__simplified = self.__repo.commit(git.CommitData(
+                tree = tree, message = self.message,
+                parents = [prev.simplified for prev in [self.prev]
+                           if prev != None]))
+        parents = list(self.__parents())
+        while len(parents) >= self.__max_parents:
+            g = self.__repo.commit(git.CommitData(
+                    tree = tree, parents = parents[-self.__max_parents:],
+                    message = 'Stack log parent grouping'))
+            parents[-self.__max_parents:] = [g]
+        self.commit = self.__repo.commit(git.CommitData(
+                tree = tree, message = self.message,
+                parents = [self.simplified] + parents))
+
+def log_entry(stack, msg):
+    """Write a new log entry for the stack."""
+    ref = log_ref(stack.name)
+    try:
+        last_log = stack.repository.refs.get(ref)
+    except KeyError:
+        last_log = None
+    try:
+        new_log = LogEntry.from_stack(last_log, stack, msg)
+    except LogException, e:
+        out.warn(str(e), 'No log entry written.')
+        return
+    new_log.write_commit()
+    stack.repository.refs.set(ref, new_log.commit, msg)
+
+def compat_log_entry(msg):
+    """Write a new log entry. (Convenience function intended for use by
+    code not yet converted to the new infrastructure.)"""
+    repo = default_repo()
+    try:
+        stack = repo.get_stack(repo.current_branch_name)
+    except libstack.StackException, e:
+        out.warn(str(e), 'Could not write to stack log')
+    else:
+        log_entry(stack, msg)
+
+def delete_log(repo, branch):
+    ref = log_ref(branch)
+    if repo.refs.exists(ref):
+        repo.refs.delete(ref)
+
+def rename_log(repo, old_branch, new_branch, msg):
+    old_ref = log_ref(old_branch)
+    new_ref = log_ref(new_branch)
+    if repo.refs.exists(old_ref):
+        repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
+        repo.refs.delete(old_ref)
+
+def copy_log(repo, src_branch, dst_branch, msg):
+    src_ref = log_ref(src_branch)
+    dst_ref = log_ref(dst_branch)
+    if repo.refs.exists(src_ref):
+        repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
+
+def default_repo():
+    return libstack.Repository.default()
index 1059955ae6c11e61dad9fa0e98c32f37c6988fcf..31960e619f314f3031d61da9f99dfeb1e89230c9 100644 (file)
@@ -169,6 +169,15 @@ class Stack(git.Branch):
                                     ).commit.data.parent
         else:
             return self.head
+    @property
+    def top(self):
+        """Commit of the topmost patch, or the stack base if no patches are
+        applied."""
+        if self.patchorder.applied:
+            return self.patches.get(self.patchorder.applied[-1]).commit
+        else:
+            # When no patches are applied, base == head.
+            return self.head
     def head_top_equal(self):
         if not self.patchorder.applied:
             return True
index cbfca559a25c6014366a6f1a8bb2f8c5d0aad724..9b45729ee78cf786ab6b70e6b95d367c9667428b 100644 (file)
@@ -7,7 +7,7 @@ import itertools as it
 from stgit import exception, utils
 from stgit.utils import any, all
 from stgit.out import *
-from stgit.lib import git
+from stgit.lib import git, log
 
 class TransactionException(exception.StgException):
     """Exception raised when something goes wrong with a
@@ -186,6 +186,7 @@ class StackTransaction(object):
         self.__stack.patchorder.applied = self.__applied
         self.__stack.patchorder.unapplied = self.__unapplied
         self.__stack.patchorder.hidden = self.__hidden
+        log.log_entry(self.__stack, self.__msg)
 
         if self.__error:
             return utils.STGIT_CONFLICT
index 48d8dbb608d06e70a4d0a4aa341d75e7173ebc8a..e3241797e0bfaddd813b3f2f8d5ab410e455b76d 100644 (file)
@@ -151,6 +151,7 @@ def _main():
 
         ret = command.func(parser, options, args)
     except (StgException, IOError, ParsingError, NoSectionError), err:
+        directory.write_log(cmd)
         out.error(str(err), title = '%s %s' % (prog, cmd))
         if debug_level > 0:
             traceback.print_exc()
@@ -166,6 +167,7 @@ def _main():
         traceback.print_exc()
         sys.exit(utils.STGIT_BUG_ERROR)
 
+    directory.write_log(cmd)
     sys.exit(ret or utils.STGIT_SUCCESS)
 
 def main():