Show changes to 15 files +298 −17
stgit/commands/branch.py, stgit/commands/common.py, stgit/commands/diff.py, stgit/commands/files.py, stgit/commands/id.py, stgit/commands/log.py, stgit/commands/mail.py, stgit/commands/patches.py, stgit/commands/show.py, stgit/commands/status.py, stgit/lib/git.py, stgit/lib/log.py, stgit/lib/stack.py, stgit/lib/transaction.py, stgit/main.py
diff --git a/stgit/commands/branch.py b/stgit/commands/branch.py
index 50684bb..edbb01c 100644
--- a/stgit/commands/branch.py
+++ b/stgit/commands/branch.py
@@ -25,7 +25,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 = 'manage patch stacks'
usage = """%prog [options] branch-name [commit-id]
@@ -40,7 +40,7 @@ When displaying the branches, the names can be prefixed with
If not given any options, switch to the named branch."""
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = False)
options = [make_option('-c', '--create',
help = 'create a new development branch',
action = 'store_true'),
@@ -161,6 +161,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:
@@ -181,6 +182,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:
@@ -188,6 +191,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:
@@ -195,13 +199,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')
@@ -238,7 +245,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:
diff --git a/stgit/commands/common.py b/stgit/commands/common.py
index 029ec65..fd02398 100644
--- a/stgit/commands/common.py
+++ b/stgit/commands/common.py
@@ -28,6 +28,7 @@ from stgit.run import *
from stgit import stack, git, basedir
from stgit.config import config, file_extensions
from stgit.lib import stack as libstack
+from stgit.lib import log
# Command exception class
class CmdException(StgException):
@@ -478,8 +479,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:
@@ -512,6 +514,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):
@@ -536,6 +541,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()
diff --git a/stgit/commands/diff.py b/stgit/commands/diff.py
index fd6be34..8966642 100644
--- a/stgit/commands/diff.py
+++ b/stgit/commands/diff.py
@@ -42,7 +42,7 @@ rev = '([patch][//[bottom | top]]) | <tree-ish> | base'
If neither bottom nor top are given but a '//' is present, the command
shows the specified patch (defaulting to the current one)."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-r', '--range',
metavar = 'rev1[..[rev2]]', dest = 'revs',
help = 'show the diff between revisions'),
diff --git a/stgit/commands/files.py b/stgit/commands/files.py
index b43b12f..7844f8d 100644
--- a/stgit/commands/files.py
+++ b/stgit/commands/files.py
@@ -34,7 +34,7 @@ given patch. Note that this command doesn't show the files modified in
the working tree and not yet included in the patch by a 'refresh'
command. Use the 'diff' or 'status' commands for these files."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-s', '--stat',
help = 'show the diff stat',
action = 'store_true'),
diff --git a/stgit/commands/id.py b/stgit/commands/id.py
index 94b0229..5bb1ad2 100644
--- a/stgit/commands/id.py
+++ b/stgit/commands/id.py
@@ -33,7 +33,7 @@ the standard GIT id's like heads and tags, this command also accepts
'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'
will be used by default."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one')]
diff --git a/stgit/commands/log.py b/stgit/commands/log.py
index 52d55a5..13e0baa 100644
--- a/stgit/commands/log.py
+++ b/stgit/commands/log.py
@@ -44,7 +44,7 @@ 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."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one'),
make_option('-p', '--patch',
diff --git a/stgit/commands/mail.py b/stgit/commands/mail.py
index b4d4e18..4027170 100644
--- a/stgit/commands/mail.py
+++ b/stgit/commands/mail.py
@@ -90,7 +90,7 @@ the following:
%(prefix)s - 'prefix ' string passed on the command line
%(shortdescr)s - the first line of the patch description"""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-a', '--all',
help = 'e-mail all the applied patches',
action = 'store_true'),
diff --git a/stgit/commands/patches.py b/stgit/commands/patches.py
index 140699d..c95c40f 100644
--- a/stgit/commands/patches.py
+++ b/stgit/commands/patches.py
@@ -33,7 +33,7 @@ it shows the patches affected by the local tree modifications. The
'--diff' option also lists the patch log and the diff for the given
files."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-d', '--diff',
help = 'show the diff for the given files',
action = 'store_true'),
diff --git a/stgit/commands/show.py b/stgit/commands/show.py
index b77a9c8..dd2a3a3 100644
--- a/stgit/commands/show.py
+++ b/stgit/commands/show.py
@@ -30,7 +30,7 @@ Show the commit log and the diff corresponding to the given
patches. The output is similar to that generated by the 'git show'
command."""
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
options = [make_option('-b', '--branch',
help = 'use BRANCH instead of the default one'),
make_option('-a', '--applied',
diff --git a/stgit/commands/status.py b/stgit/commands/status.py
index a5b2f88..4d13112 100644
--- a/stgit/commands/status.py
+++ b/stgit/commands/status.py
@@ -40,7 +40,7 @@ under revision control. The files are prefixed as follows:
A 'refresh' command clears the status of the modified, new and deleted
files."""
-directory = DirectoryHasRepository(needs_current_series = False)
+directory = DirectoryHasRepository(needs_current_series = False, log = False)
options = [make_option('-m', '--modified',
help = 'show modified files only',
action = 'store_true'),
@@ -106,6 +106,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])
diff --git a/stgit/lib/git.py b/stgit/lib/git.py
index a8881f4..35e03d2 100644
--- a/stgit/lib/git.py
+++ b/stgit/lib/git.py
@@ -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
index 0000000..8646e08
--- /dev/null
+++ b/stgit/lib/log.py
@@ -0,0 +1,254 @@
+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}.
+
+Stack log format (version 4)
+============================
+
+I{This describes version 4 of the stack log format. Versions 0 throgh
+3 were earlier development versions that are no longer supported.}
+
+Commit message
+--------------
+
+The commit message is meant for human consumption; in most cases it is
+just a subject line: the stg subcommand name and possibly some
+important command-line flag.
+
+The exception 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
+----
+
+The top-level tree object has the following entries:
+
+ - C{version}: a blob containing the string "C{5\n}".
+
+ - C{head}: a blob containing the ASCII hex sha1 of the current HEAD,
+ followed by a newline.
+
+ - C{applied}, C{unapplied}: blobs containing one line for each
+ applied or unapplied patch, in order. Each line consists of
+
+ - the ASCII hex sha1 of the patch's commit object,
+
+ - a space, and
+
+ - the patch's name.
+
+ - C{patches}: a tree containing one subtree for each patch, named
+ after that patch. Each such subtree contains:
+
+ - C{a}, C{b}: the patch's I{bottom} and I{top} trees.
+
+ - C{info}: a blob containing::
+
+ Author: <author name and e-mail>
+ Date: <patch timestamp>
+
+ <commit message>
+
+Parents
+-------
+
+ - The first parent is the I{simplified log}, described below.
+
+ - The second and third parents are the branch head and the stack
+ top, in any order; if they are identical (which is always the case
+ unless StGit's metadata is messed up by use of a non-StGit tool),
+ just one of them is listed.
+
+ - The last parent is the previous log entry. (If there is no
+ previous log entry, it is omitted.) This log entry must be version
+ E{4}.
+
+Simplified log
+--------------
+
+The simplified log looks exactly like the normal, or I{full}, log,
+except for the following:
+
+ - Instead of having a tree per patch in the C{patches} subtree, it
+ has a blob per patch. This blob contains::
+
+ 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's diff>
+
+ - Its only parent is the simplified version of the previous log
+ entry.
+
+The simplified log contains no information not in the full log; its
+purpose is ease of visualization."""
+
+from stgit.lib import git, stack
+from stgit import exception
+from stgit.out import out
+
+class LogException(exception.StgException):
+ pass
+
+_current_version = 4
+
+def commit_info(cd):
+ return 'Author: %s\nDate: %s\n\n%s' % (cd.author.name_email,
+ cd.author.date, cd.message)
+
+def patch_tree(repository, cd):
+ return repository.commit(git.TreeData({
+ 'a': cd.parent.data.tree, 'b': cd.tree,
+ 'info': repository.commit(git.BlobData(commit_info(cd))) }))
+
+def patch_file(repository, cd):
+ return repository.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,
+ '',
+ '---',
+ '',
+ repository.diff_tree(cd.parent.data.tree, cd.tree, ['-M']
+ ).strip()])))
+
+def order_blob(repository, stack, kind):
+ return repository.commit(git.BlobData(''.join(
+ '%s %s\n' % (stack.patches.get(pn).commit.sha1, pn)
+ for pn in getattr(stack.patchorder, kind))))
+
+def log_entry_trees(repository, stack):
+ full_patches, short_patches = [repository.commit(git.TreeData(dict(
+ (pn, x(repository, stack.patches.get(pn).commit.data))
+ for pn in stack.patchorder.all)))
+ for x in [patch_tree, patch_file]]
+ version = repository.commit(git.BlobData('%d\n' % _current_version))
+ head = repository.commit(git.BlobData('%s\n' % stack.head.sha1))
+ applied = order_blob(repository, stack, 'applied')
+ unapplied = order_blob(repository, stack, 'unapplied')
+ return [repository.commit(git.TreeData({
+ 'version': version,
+ 'head': head,
+ 'patches': patches,
+ 'applied': applied,
+ 'unapplied': unapplied,
+ })) for patches in [full_patches, short_patches]]
+
+def log_ref(branch):
+ return 'refs/heads/%s.stgit' % branch
+
+def log_entry(stack, msg):
+ """Write a new log entry for the stack."""
+ ref = log_ref(stack.name)
+ try:
+ last_log = [FullLog(stack.repository, ref,
+ stack.repository.refs.get(ref))]
+ except KeyError:
+ last_log = []
+ except LogException, e:
+ out.warn(str(e), 'No log entry written.')
+ return
+ full_log_tree, short_log_tree = log_entry_trees(stack.repository, stack)
+ stack_log = stack.repository.commit(
+ git.CommitData(tree = short_log_tree, message = msg,
+ parents = [ll.stack_log for ll in last_log]))
+ refs = list(set([stack.head, stack.top]))
+ full_log = stack.repository.commit(
+ git.CommitData(tree = full_log_tree, message = msg,
+ parents = ([stack_log] + refs
+ + [ll.full_log for ll in last_log])))
+ stack.repository.refs.set(ref, full_log, 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()
+ stack = repo.get_stack(repo.current_branch_name)
+ log_entry(stack, msg)
+
+class Log(object):
+ """Class used to read the stack log. Each instance represents one log
+ entry."""
+ def __init__(self, repo, ref, commit):
+ """Create a L{Log} object representing the log entry C{commit}. In any
+ error messages, say C{ref} when we mean C{commit}."""
+ self.commit = commit
+ mode, vblob = self.commit.data.tree.data.entries.get(
+ 'version', (None, None))
+ if not isinstance(vblob, git.Blob):
+ raise LogException('%s does not contain a valid log' % ref)
+ try:
+ version = int(vblob.data.str)
+ except ValueError:
+ raise LogException('"%s": invalid version number' % vblob.data.str)
+ if version < _current_version:
+ raise LogException(
+ '%s contains a stack log older than version %d;'
+ ' please delete it' % (ref, _current_version))
+ elif version > _current_version:
+ raise LogException(
+ 'Log contains a stack log newer than version %d;'
+ ' this version of StGit cannot read it' % _current_version)
+
+ # TODO: read the rest of the log lazily.
+
+ def pl(name):
+ patches = [x.split() for x in
+ self.commit.data.tree.data.entries[name][1]
+ .data.str.strip().split('\n') if x]
+ # TODO: handle case where we don't have the commit object.
+ return ([pn for sha1, pn in patches],
+ dict((pn, repo.get_commit(sha1)) for sha1, pn in patches))
+ self.patches = {}
+ self.applied, patches = pl('applied')
+ self.patches.update(patches)
+ self.unapplied, patches = pl('unapplied')
+ self.patches.update(patches)
+ self.head = repo.get_commit(
+ self.commit.data.tree.data.entries['head'][1].data.str.strip())
+ if self.applied:
+ self.base = self.patches[self.applied[0]].data.parent
+ else:
+ self.base = self.head
+
+class FullLog(Log):
+ full_log = property(lambda self: self.commit)
+ """Commit of the full log."""
+ stack_log = property(lambda self: self.commit.data.parents[0])
+ """Commit of the simplified log."""
+
+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 stack.Repository.default()
diff --git a/stgit/lib/stack.py b/stgit/lib/stack.py
index 9cb3967..62a1ec2 100644
--- a/stgit/lib/stack.py
+++ b/stgit/lib/stack.py
@@ -165,6 +165,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
diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py
index e47997e..4c4da1a 100644
--- a/stgit/lib/transaction.py
+++ b/stgit/lib/transaction.py
@@ -4,7 +4,7 @@ updates to an StGit stack in a safe and convenient way."""
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
@@ -170,6 +170,7 @@ class StackTransaction(object):
_print_current_patch(self.__stack.patchorder.applied, self.__applied)
self.__stack.patchorder.applied = self.__applied
self.__stack.patchorder.unapplied = self.__unapplied
+ log.log_entry(self.__stack, self.__msg)
if self.__error:
return utils.STGIT_CONFLICT
diff --git a/stgit/main.py b/stgit/main.py
index aa1f8ef..ec0e840 100644
--- a/stgit/main.py
+++ b/stgit/main.py
@@ -277,6 +277,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()
@@ -292,4 +293,5 @@ def main():
traceback.print_exc()
sys.exit(utils.STGIT_BUG_ERROR)
+ directory.write_log(cmd)
sys.exit(ret or utils.STGIT_SUCCESS)