From: Karl Hasselström Date: Sun, 21 Sep 2008 12:17:41 +0000 (+0200) Subject: Log and undo external modifications X-Git-Tag: v0.15-rc1~139 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/commitdiff_plain/c70033b45deac916730549bb915231f59895caf9 Log and undo external modifications At the beginning of every StGit command, quickly check if the branch head recorded in the log is the same as the actual branch head; if it's not, conclude that some non-StGit tool has modified the stack, and record a log entry that says so. (Additionally, if the log doesn't exist yet, create it.) This introduces the possibility that a log entry specifies a head and a top that aren't equal. So teach undo, redo, and reset to deal with that case. Signed-off-by: Karl Hasselström --- diff --git a/stgit/commands/common.py b/stgit/commands/common.py index fea4dbc..2bf6ca5 100644 --- a/stgit/commands/common.py +++ b/stgit/commands/common.py @@ -491,6 +491,7 @@ class DirectoryAnywhere(_Directory): class DirectoryHasRepository(_Directory): def setup(self): self.git_dir # might throw an exception + log.compat_log_external_mods() class DirectoryInWorktree(DirectoryHasRepository): def setup(self): diff --git a/stgit/commands/redo.py b/stgit/commands/redo.py index 8952680..23478f6 100644 --- a/stgit/commands/redo.py +++ b/stgit/commands/redo.py @@ -48,7 +48,7 @@ def func(parser, options, args): trans = transaction.StackTransaction(stack, 'redo %d' % options.number, discard_changes = options.hard) try: - log.reset_stack(trans, stack.repository.default_iw, state, []) + log.reset_stack(trans, stack.repository.default_iw, state) except transaction.TransactionHalted: pass - return trans.run(stack.repository.default_iw) + return trans.run(stack.repository.default_iw, allow_bad_head = True) diff --git a/stgit/commands/reset.py b/stgit/commands/reset.py index 7ee8fb7..eb1ab4e 100644 --- a/stgit/commands/reset.py +++ b/stgit/commands/reset.py @@ -61,7 +61,11 @@ def func(parser, options, args): trans = transaction.StackTransaction(stack, 'reset', discard_changes = options.hard) try: - log.reset_stack(trans, stack.repository.default_iw, state, patches) + if patches: + log.reset_stack_partially(trans, stack.repository.default_iw, + state, patches) + else: + log.reset_stack(trans, stack.repository.default_iw, state) except transaction.TransactionHalted: pass - return trans.run(stack.repository.default_iw) + return trans.run(stack.repository.default_iw, allow_bad_head = not patches) diff --git a/stgit/commands/undo.py b/stgit/commands/undo.py index 4f782a0..12f5f6d 100644 --- a/stgit/commands/undo.py +++ b/stgit/commands/undo.py @@ -45,7 +45,7 @@ def func(parser, options, args): trans = transaction.StackTransaction(stack, 'undo %d' % options.number, discard_changes = options.hard) try: - log.reset_stack(trans, stack.repository.default_iw, state, []) + log.reset_stack(trans, stack.repository.default_iw, state) except transaction.TransactionHalted: pass - return trans.run(stack.repository.default_iw) + return trans.run(stack.repository.default_iw, allow_bad_head = True) diff --git a/stgit/lib/log.py b/stgit/lib/log.py index 448532d..dfadd51 100644 --- a/stgit/lib/log.py +++ b/stgit/lib/log.py @@ -164,6 +164,8 @@ class LogEntry(object): return self.patches[self.applied[-1]] else: return self.head + all_patches = property(lambda self: (self.applied + self.unapplied + + self.hidden)) @classmethod def from_stack(cls, prev, stack, message): return cls( @@ -390,34 +392,34 @@ def copy_log(repo, src_branch, dst_branch, msg): def default_repo(): return libstack.Repository.default() -def reset_stack(trans, iw, state, only_patches): - """Reset the stack to a given previous state. If C{only_patches} is - not empty, touch only patches whose names appear in it. +def reset_stack(trans, iw, state): + """Reset the stack to a given previous state.""" + for pn in trans.all_patches: + trans.patches[pn] = None + for pn in state.all_patches: + trans.patches[pn] = state.patches[pn] + trans.applied = state.applied + trans.unapplied = state.unapplied + trans.hidden = state.hidden + trans.base = state.base + trans.head = state.head + +def reset_stack_partially(trans, iw, state, only_patches): + """Reset the stack to a given previous state -- but only the given + patches, not anything else. - @param only_patches: Reset only these patches + @param only_patches: Touch only these patches @type only_patches: iterable""" only_patches = set(only_patches) - def mask(s): - if only_patches: - return s & only_patches - else: - return s - patches_to_reset = mask(set(state.applied + state.unapplied + state.hidden)) + patches_to_reset = set(state.all_patches) & only_patches existing_patches = set(trans.all_patches) original_applied_order = list(trans.applied) - to_delete = mask(existing_patches - patches_to_reset) - - # If we have to change the stack base, we need to pop all patches - # first. - if not only_patches and trans.base != state.base: - trans.pop_patches(lambda pn: True) - out.info('Setting stack base to %s' % state.base.sha1) - trans.base = state.base + to_delete = (existing_patches - patches_to_reset) & only_patches # In one go, do all the popping we have to in order to pop the # patches we're going to delete or modify. def mod(pn): - if only_patches and not pn in only_patches: + if not pn in only_patches: return False if pn in to_delete: return True @@ -443,17 +445,12 @@ def reset_stack(trans, iw, state, only_patches): out.info('Resurrecting %s' % pn) trans.patches[pn] = state.patches[pn] - # Push/pop patches as necessary. - if only_patches: - # Push all the patches that we've popped, if they still - # exist. - pushable = set(trans.unapplied) - for pn in original_applied_order: - if pn in pushable: - trans.push_patch(pn, iw) - else: - # Recreate the exact order specified by the goal state. - trans.reorder_patches(state.applied, state.unapplied, state.hidden, iw) + # Push all the patches that we've popped, if they still + # exist. + pushable = set(trans.unapplied + trans.hidden) + for pn in original_applied_order: + if pn in pushable: + trans.push_patch(pn, iw) def undo_state(stack, undo_steps): """Find the log entry C{undo_steps} steps in the past. (Successive @@ -492,3 +489,36 @@ def undo_state(stack, undo_steps): raise LogException('Not enough undo information available') log = log.prev return log + +def log_external_mods(stack): + ref = log_ref(stack.name) + try: + log_commit = stack.repository.refs.get(ref) + except KeyError: + # No log exists yet. + log_entry(stack, 'start of log') + return + try: + log = get_log_entry(stack.repository, ref, log_commit) + except LogException: + # Something's wrong with the log, so don't bother. + return + if log.head == stack.head: + # No external modifications. + return + log_entry(stack, '\n'.join([ + 'external modifications', '', + 'Modifications by tools other than StGit (e.g. git).'])) + +def compat_log_external_mods(): + try: + repo = default_repo() + except git.RepositoryException: + # No repository, so we couldn't log even if we wanted to. + return + try: + stack = repo.get_stack(repo.current_branch_name) + except exception.StgException: + # Stack doesn't exist, so we can't log. + return + log_external_mods(stack) diff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py index 1ab5f8b..7c7139c 100644 --- a/stgit/lib/transaction.py +++ b/stgit/lib/transaction.py @@ -93,6 +93,7 @@ class StackTransaction(object): self.__current_tree = self.__stack.head.data.tree self.__base = self.__stack.base self.__discard_changes = discard_changes + self.__bad_head = None if isinstance(allow_conflicts, bool): self.__allow_conflicts = lambda trans: allow_conflicts else: @@ -122,8 +123,22 @@ class StackTransaction(object): self.__temp_index = self.__stack.repository.temp_index() atexit.register(self.__temp_index.delete) return self.__temp_index - def __checkout(self, tree, iw): - if not self.__stack.head_top_equal(): + @property + def top(self): + if self.__applied: + return self.__patches[self.__applied[-1]] + else: + return self.__base + def __get_head(self): + if self.__bad_head: + return self.__bad_head + else: + return self.top + def __set_head(self, val): + self.__bad_head = val + head = property(__get_head, __set_head) + def __checkout(self, tree, iw, allow_bad_head): + if not (allow_bad_head or self.__stack.head_top_equal()): out.error( 'HEAD and top are not the same.', 'This can happen if you modify a branch with git.', @@ -156,27 +171,23 @@ class StackTransaction(object): assert self.__stack.patches.exists(pn) else: assert pn in remaining - @property - def __head(self): - if self.__applied: - return self.__patches[self.__applied[-1]] - else: - return self.__base def abort(self, iw = None): # The only state we need to restore is index+worktree. if iw: - self.__checkout(self.__stack.head.data.tree, iw) - def run(self, iw = None, set_head = True): + self.__checkout(self.__stack.head.data.tree, iw, + allow_bad_head = True) + def run(self, iw = None, set_head = True, allow_bad_head = False): """Execute the transaction. Will either succeed, or fail (with an exception) and do nothing.""" self.__check_consistency() - new_head = self.__head + log.log_external_mods(self.__stack) + new_head = self.head # Set branch head. if set_head: if iw: try: - self.__checkout(new_head.data.tree, iw) + self.__checkout(new_head.data.tree, iw, allow_bad_head) except git.CheckoutException: # We have to abort the transaction. self.abort(iw) @@ -273,7 +284,7 @@ class StackTransaction(object): cd = orig_cd.set_committer(None) s = ['', ' (empty)'][cd.is_nochange()] oldparent = cd.parent - cd = cd.set_parent(self.__head) + cd = cd.set_parent(self.top) base = oldparent.data.tree ours = cd.parent.data.tree theirs = cd.tree @@ -284,7 +295,7 @@ class StackTransaction(object): if iw == None: self.__halt('%s does not apply cleanly' % pn) try: - self.__checkout(ours, iw) + self.__checkout(ours, iw, allow_bad_head = False) except git.CheckoutException: self.__halt('Index/worktree dirty') try: diff --git a/t/t3105-undo-external-mod.sh b/t/t3105-undo-external-mod.sh new file mode 100755 index 0000000..f5aad64 --- /dev/null +++ b/t/t3105-undo-external-mod.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +test_description='Undo external modifications of the stack' + +. ./test-lib.sh + +# Ignore our own output files. +cat > .git/info/exclude <> a && + git add a && + git commit -m p0 && + echo 111 >> a && + git add a && + git commit -m p1 && + stg uncommit -n 1 +' + +cat > expected.txt < head0.txt && + echo 222 >> a && + git add a && + git commit -m p2 && + git rev-parse HEAD > head1.txt && + stg repair && + test "$(echo $(stg series))" = "+ p1 > p2" && + test_cmp expected.txt a +' + +cat > expected.txt < head2.txt && + test_cmp head1.txt head2.txt && + test "$(echo $(stg series))" = "> p1" && + test_cmp expected.txt a +' + +cat > expected.txt < head3.txt && + test_cmp head0.txt head3.txt && + test "$(echo $(stg series))" = "> p1" && + test_cmp expected.txt a +' + +test_done