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 1b1b98f..ef71547 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 28500c5..659712d 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 3cecec0..fea4dbc 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 e0078f9..38de3a1 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 318a4a3..a7576e9 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 1ca4ed3..93bb69b 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 0f1486a..165ff52 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 bee2162..1bcb5f1 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 150d9ee..103ebb0 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 de210ea..07fcd98 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 caf8f9e..6948a1e 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 0cbc275..e877171 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 8a88262..e1c531d 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 cf89846..1c56671 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 c989b5d..82035c6 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 c6056cc..c3d553d 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 b09204e..b5a80bb 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 a20fcc5..7be94e0 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 fdb31ee..7e0fbf5 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 6218caa..e06df3a 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 d8dacd6..ce8630d 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 41cb31e..e08551b 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 95a43e1..34f81c9 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 7c68ba6..c78bc1b 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 767b4d2..26a49f0 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 c34bc1d..acfef29 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 2386e27..ac3e9d0 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 1059955..31960e6 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 cbfca55..9b45729 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 48d8dbb..e324179 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():