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'
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
parent_branch = parentbranch)
out.info('Branch "%s" created' % args[0])
+ log.compat_log_entry('branch --create')
return
elif options.clone:
crt_series.clone(clone)
out.done()
+ log.copy_log(log.default_repo(), crt_series.get_name(), clone,
+ 'branch --clone')
return
elif options.delete:
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:
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')
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:
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
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):
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:
).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):
"""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()
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
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)
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
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
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
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)
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
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
opt('-b', '--branch',
short = 'Use BRANCH instead of the default branch')]
-directory = DirectoryHasRepository()
+directory = DirectoryHasRepository(log = False)
diff_tmpl = \
'-------------------------------------------------------------------------------\n' \
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.
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
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
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
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
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.
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
options = []
-directory = DirectoryGotoToplevel()
+directory = DirectoryGotoToplevel(log = True)
class Commit(object):
def __init__(self, id):
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
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
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.
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
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])
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()
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
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)
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)
--- /dev/null
+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()
).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
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
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
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()
traceback.print_exc()
sys.exit(utils.STGIT_BUG_ERROR)
+ directory.write_log(cmd)
sys.exit(ret or utils.STGIT_SUCCESS)
def main():