1 """The L{StackTransaction} class makes it possible to make complex
2 updates to an StGit stack in a safe and convenient way."""
6 from itertools
import takewhile
8 from stgit
import exception
, utils
9 from stgit
.config
import config
10 from stgit
.lib
.git
import CheckoutException
, MergeConflictException
, MergeException
11 from stgit
.lib
.log
import log_external_mods
, log_stack_state
12 from stgit
.out
import out
15 class TransactionException(exception
.StgException
):
16 """Exception raised when something goes wrong with a :class:`StackTransaction`."""
19 class TransactionHalted(TransactionException
):
20 """Exception raised when a :class:`StackTransaction` stops part-way through.
22 Used to make a non-local jump from the transaction setup to the part of the
23 transaction code where the transaction is run.
28 def _print_current_patch(old_applied
, new_applied
):
30 out
.info('Now at patch "%s"' % pn
)
32 if not old_applied
and not new_applied
:
35 now_at(new_applied
[-1])
37 out
.info('No patch applied')
38 elif old_applied
[-1] == new_applied
[-1]:
41 now_at(new_applied
[-1])
44 class _TransPatchMap(dict):
45 """Maps patch names to :class:`Commit` objects."""
47 def __init__(self
, stack
):
51 def __getitem__(self
, pn
):
53 return super().__getitem
__(pn
)
55 return self
._stack
.patches
[pn
]
58 class StackTransaction
:
59 """Atomically perform complex updates to StGit stack state.
61 A stack transaction will either succeed or fail cleanly.
63 The basic theory of operation is the following:
65 1. Create a transaction object.
70 except TransactionHalted:
73 block, update the transaction with e.g. methods like :meth:`pop_patches` and
74 :meth:`push_patch`. This may create new Git objects such as commits, but will not
75 write any refs. This means that in case of a fatal error we can just walk away,
78 Some operations may need to touch the index and/or working tree, though. But they
79 are cleaned up when needed.
81 3. After the ``try`` block--wheher or not the setup ran to completion or halted
82 part-way through by raising a :exc:`TransactionHalted` exception--call the
83 transaction's :meth:`run` method. This will either succeed in writing the updated
84 state to refs and index+worktree, or fail without having done anything.
92 discard_changes
=False,
93 allow_conflicts
=False,
97 """Initialize a new :class:`StackTransaction`.
99 :param discard_changes: Discard any changes in index+worktree
100 :type discard_changes: bool
101 :param allow_conflicts: Whether to allow pre-existing conflicts
102 :type allow_conflicts: bool or function taking a :class:`StackTransaction`
103 instance as its argument
108 self
.patches
= _TransPatchMap(stack
)
109 self
._applied
= list(self
.stack
.patchorder
.applied
)
110 self
._unapplied
= list(self
.stack
.patchorder
.unapplied
)
111 self
._hidden
= list(self
.stack
.patchorder
.hidden
)
113 self
._current
_tree
= self
.stack
.head
.data
.tree
114 self
._base
= self
.stack
.base
115 self
._discard
_changes
= discard_changes
116 self
._bad
_head
= None
117 self
._conflicts
= None
118 if isinstance(allow_conflicts
, bool):
119 self
._allow
_conflicts
= lambda trans
: allow_conflicts
121 self
._allow
_conflicts
= allow_conflicts
122 self
._temp
_index
= self
.temp_index_tree
= None
123 if not allow_bad_head
:
124 self
._assert
_head
_top
_equal
()
126 self
._assert
_index
_worktree
_clean
(check_clean_iw
)
133 def applied(self
, value
):
134 self
._applied
= list(value
)
138 return self
._unapplied
141 def unapplied(self
, value
):
142 self
._unapplied
= list(value
)
149 def hidden(self
, value
):
150 self
._hidden
= list(value
)
153 def all_patches(self
):
154 return self
._applied
+ self
._unapplied
+ self
._hidden
161 def base(self
, value
):
162 assert not self
._applied
or self
.patches
[self
.applied
[0]].data
.parent
== value
166 def temp_index(self
):
167 if not self
._temp
_index
:
168 self
._temp
_index
= self
.stack
.repository
.temp_index()
169 atexit
.register(self
._temp
_index
.delete
)
170 return self
._temp
_index
175 return self
.patches
[self
._applied
[-1]]
182 return self
._bad
_head
187 def head(self
, value
):
188 self
._bad
_head
= value
190 def _assert_head_top_equal(self
):
191 if self
.stack
.head
!= self
.stack
.top
:
193 'HEAD and top are not the same.',
194 'This can happen if you modify a branch with git.',
195 '"stg repair --help" explains more about what to do next.',
199 def _assert_index_worktree_clean(self
, iw
):
200 if not iw
.worktree_clean():
201 self
._halt
('Worktree not clean. Use "refresh" or "reset --hard"')
202 if not iw
.index
.is_clean(self
.stack
.head
):
203 self
._halt
('Index not clean. Use "refresh" or "reset --hard"')
205 def _checkout(self
, tree
, iw
, allow_bad_head
):
206 if not allow_bad_head
:
207 self
._assert
_head
_top
_equal
()
208 if self
._current
_tree
== tree
and not self
._discard
_changes
:
209 # No tree change, but we still want to make sure that
210 # there are no unresolved conflicts. Conflicts
211 # conceptually "belong" to the topmost patch, and just
212 # carrying them along to another patch is confusing.
213 if self
._allow
_conflicts
(self
) or iw
is None or not iw
.index
.conflicts():
215 out
.error('Need to resolve conflicts first')
217 assert iw
is not None
218 if self
._discard
_changes
:
219 iw
.checkout_hard(tree
)
221 iw
.checkout(self
._current
_tree
, tree
)
222 self
._current
_tree
= tree
226 raise TransactionException('Command aborted (all changes rolled back)')
228 def _check_consistency(self
):
229 remaining
= set(self
.all_patches
)
230 for pn
, commit
in self
.patches
.items():
232 assert pn
in self
.stack
.patches
234 assert pn
in remaining
236 def abort(self
, iw
=None):
237 # The only state we need to restore is index+worktree.
239 self
._checkout
(self
.stack
.head
.data
.tree
, iw
, allow_bad_head
=True)
242 self
, iw
=None, set_head
=True, allow_bad_head
=False, print_current_patch
=True
244 """Execute the transaction.
246 Will either succeed, or fail (with an exception) and do nothing.
249 self
._check
_consistency
()
250 log_external_mods(self
.stack
)
257 self
._checkout
(new_head
.data
.tree
, iw
, allow_bad_head
)
258 except CheckoutException
:
259 # We have to abort the transaction.
262 self
.stack
.set_head(new_head
, self
._msg
)
266 out
.error(*([self
._error
] + self
._conflicts
))
268 out
.error(self
._error
)
270 old_applied
= self
.stack
.patchorder
.applied
271 msg
= self
._msg
+ (' (CONFLICT)' if self
._conflicts
else '')
274 for pn
, commit
in self
.patches
.items():
275 if pn
in self
.stack
.patches
:
277 self
.stack
.patches
.delete(pn
)
279 self
.stack
.patches
.update(pn
, commit
, msg
)
281 self
.stack
.patches
.new(pn
, commit
, msg
)
282 self
.stack
.patchorder
.set_order(self
._applied
, self
._unapplied
, self
._hidden
)
283 log_stack_state(self
.stack
, msg
)
285 if print_current_patch
:
286 _print_current_patch(old_applied
, self
._applied
)
289 return utils
.STGIT_CONFLICT
291 return utils
.STGIT_SUCCESS
293 def _halt(self
, msg
):
295 raise TransactionHalted(msg
)
298 def _print_popped(popped
):
301 elif len(popped
) == 1:
302 out
.info('Popped %s' % popped
[0])
304 out
.info('Popped %s -- %s' % (popped
[-1], popped
[0]))
306 def pop_patches(self
, p
):
307 """Pop all patches pn for which p(pn) is true.
311 :returns: list of other patches that had to be popped to accomplish this.
315 for i
in range(len(self
.applied
)):
316 if p(self
.applied
[i
]):
317 popped
= self
.applied
[i
:]
320 popped1
= [pn
for pn
in popped
if not p(pn
)]
321 popped2
= [pn
for pn
in popped
if p(pn
)]
322 self
.unapplied
= popped1
+ popped2
+ self
.unapplied
323 self
._print
_popped
(popped
)
326 def delete_patches(self
, p
, quiet
=False):
327 """Delete all patches pn for which p(pn) is true.
331 :returns: list of other patches that had to be popped to accomplish this.
335 all_patches
= self
.applied
+ self
.unapplied
+ self
.hidden
336 for i
in range(len(self
.applied
)):
337 if p(self
.applied
[i
]):
338 popped
= self
.applied
[i
:]
341 popped
= [pn
for pn
in popped
if not p(pn
)]
342 self
.unapplied
= popped
+ [pn
for pn
in self
.unapplied
if not p(pn
)]
343 self
.hidden
= [pn
for pn
in self
.hidden
if not p(pn
)]
344 self
._print
_popped
(popped
)
345 for pn
in all_patches
:
347 s
= ['', ' (empty)'][self
.patches
[pn
].data
.is_nochange()]
348 self
.patches
[pn
] = None
350 out
.info('Deleted %s%s' % (pn
, s
))
353 def push_patch(self
, pn
, iw
=None, allow_interactive
=False, already_merged
=False):
354 """Attempt to push the named patch.
356 If this results in conflicts, the transaction is halted. If index+worktree are
357 given, spill any conflicts to them.
360 out
.start('Pushing patch "%s"' % pn
)
361 orig_cd
= self
.patches
[pn
].data
362 cd
= orig_cd
.set_committer(None)
363 oldparent
= cd
.parent
364 cd
= cd
.set_parent(self
.top
)
366 # the resulting patch is empty
367 tree
= cd
.parent
.data
.tree
369 base
= oldparent
.data
.tree
370 ours
= cd
.parent
.data
.tree
372 tree
, self
.temp_index_tree
= self
.temp_index
.merge(
373 base
, ours
, theirs
, self
.temp_index_tree
376 merge_conflict
= False
379 self
._halt
('%s does not apply cleanly' % pn
)
381 self
._checkout
(ours
, iw
, allow_bad_head
=False)
382 except CheckoutException
:
383 self
._halt
('Index/worktree dirty')
385 interactive
= allow_interactive
and config
.getbool('stgit.autoimerge')
386 iw
.merge(base
, ours
, theirs
, interactive
=interactive
)
387 tree
= iw
.index
.write_tree()
388 self
._current
_tree
= tree
390 except MergeConflictException
as e
:
392 merge_conflict
= True
393 self
._conflicts
= e
.conflicts
395 except MergeException
as e
:
397 cd
= cd
.set_tree(tree
)
399 getattr(cd
, a
) != getattr(orig_cd
, a
)
400 for a
in ['parent', 'tree', 'author', 'message']
402 comm
= self
.stack
.repository
.commit(cd
)
404 # When we produce a conflict, we'll run the update()
405 # function defined below _after_ having done the
406 # checkout in run(). To make sure that we check out
407 # the real stack top (as it will look after update()
408 # has been run), set it hard here.
415 elif not merge_conflict
and cd
.is_nochange():
420 # We've just caused conflicts, so we must allow them in
421 # the final checkout.
422 self
._allow
_conflicts
= lambda trans
: True
424 # Update the stack state
426 self
.patches
[pn
] = comm
427 if pn
in self
.hidden
:
432 self
.applied
.append(pn
)
435 self
._halt
("%d merge conflict(s)" % len(self
._conflicts
))
437 def push_tree(self
, pn
):
438 """Push the named patch without updating its tree."""
439 orig_cd
= self
.patches
[pn
].data
440 cd
= orig_cd
.set_committer(None).set_parent(self
.top
)
444 getattr(cd
, a
) != getattr(orig_cd
, a
)
445 for a
in ['parent', 'tree', 'author', 'message']
447 self
.patches
[pn
] = self
.stack
.repository
.commit(cd
)
452 out
.info('Pushed %s%s' % (pn
, s
))
454 if pn
in self
.hidden
:
459 self
.applied
.append(pn
)
462 self
, applied
, unapplied
, hidden
=None, iw
=None, allow_interactive
=False
464 """Push and pop patches to attain the given ordering."""
468 list(takewhile(lambda a
: a
[0] == a
[1], zip(self
.applied
, applied
)))
470 to_pop
= set(self
.applied
[common
:])
471 self
.pop_patches(lambda pn
: pn
in to_pop
)
472 for pn
in applied
[common
:]:
473 self
.push_patch(pn
, iw
, allow_interactive
=allow_interactive
)
475 # We only get here if all the pushes succeeded.
476 assert self
.applied
== applied
477 assert set(self
.unapplied
+ self
.hidden
) == set(unapplied
+ hidden
)
478 self
.unapplied
= unapplied
481 def rename_patch(self
, old_name
, new_name
):
482 self
.stack
.rename_patch(old_name
, new_name
)
483 self
.patches
[new_name
] = self
.patches
.pop(old_name
)
485 index
= self
._applied
.index(old_name
)
489 self
._applied
[index
] = new_name
491 index
= self
._unapplied
.index(old_name
)
495 self
._unapplied
[index
] = new_name
497 index
= self
._hidden
.index(old_name
)
501 self
._hidden
[index
] = new_name
503 def check_merged(self
, patches
, tree
=None, quiet
=False):
504 """Return a subset of patches already merged."""
506 out
.start('Checking for patches merged upstream')
509 self
.temp_index
.read_tree(tree
)
510 self
.temp_index_tree
= tree
511 elif self
.temp_index_tree
!= self
.stack
.head
.data
.tree
:
512 self
.temp_index
.read_tree(self
.stack
.head
.data
.tree
)
513 self
.temp_index_tree
= self
.stack
.head
.data
.tree
514 for pn
in reversed(patches
):
515 # check whether patch changes can be reversed in the current index
516 cd
= self
.patches
[pn
].data
520 self
.temp_index
.apply_treediff(
526 # The self.temp_index was modified by apply_treediff() so
527 # force read_tree() the next time merge() is used.
528 self
.temp_index_tree
= None
529 except MergeException
:
532 out
.done('%d found' % len(merged
))