stg import now extracts Message-ID header
[stgit.git] / stgit / lib / transaction.py
blob7e2222008984c576fb15e4b8f88d7299c513f680
1 """The L{StackTransaction} class makes it possible to make complex
2 updates to an StGit stack in a safe and convenient way."""
5 import atexit
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.
25 """
28 def _print_current_patch(old_applied, new_applied):
29 def now_at(pn):
30 out.info('Now at patch "%s"' % pn)
32 if not old_applied and not new_applied:
33 pass
34 elif not old_applied:
35 now_at(new_applied[-1])
36 elif not new_applied:
37 out.info('No patch applied')
38 elif old_applied[-1] == new_applied[-1]:
39 pass
40 else:
41 now_at(new_applied[-1])
44 class _TransPatchMap(dict):
45 """Maps patch names to :class:`Commit` objects."""
47 def __init__(self, stack):
48 super().__init__()
49 self._stack = stack
51 def __getitem__(self, pn):
52 try:
53 return super().__getitem__(pn)
54 except KeyError:
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.
66 2. Inside a::
68 try:
69 ...
70 except TransactionHalted:
71 pass
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,
76 no clean-up required.
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.
86 """
88 def __init__(
89 self,
90 stack,
91 msg,
92 discard_changes=False,
93 allow_conflicts=False,
94 allow_bad_head=False,
95 check_clean_iw=None,
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
106 self.stack = stack
107 self._msg = msg
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)
112 self._error = None
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
120 else:
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()
125 if check_clean_iw:
126 self._assert_index_worktree_clean(check_clean_iw)
128 @property
129 def applied(self):
130 return self._applied
132 @applied.setter
133 def applied(self, value):
134 self._applied = list(value)
136 @property
137 def unapplied(self):
138 return self._unapplied
140 @unapplied.setter
141 def unapplied(self, value):
142 self._unapplied = list(value)
144 @property
145 def hidden(self):
146 return self._hidden
148 @hidden.setter
149 def hidden(self, value):
150 self._hidden = list(value)
152 @property
153 def all_patches(self):
154 return self._applied + self._unapplied + self._hidden
156 @property
157 def base(self):
158 return self._base
160 @base.setter
161 def base(self, value):
162 assert not self._applied or self.patches[self.applied[0]].data.parent == value
163 self._base = value
165 @property
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
172 @property
173 def top(self):
174 if self._applied:
175 return self.patches[self._applied[-1]]
176 else:
177 return self._base
179 @property
180 def head(self):
181 if self._bad_head:
182 return self._bad_head
183 else:
184 return self.top
186 @head.setter
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:
192 out.error(
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.',
197 self._abort()
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():
214 return
215 out.error('Need to resolve conflicts first')
216 self._abort()
217 assert iw is not None
218 if self._discard_changes:
219 iw.checkout_hard(tree)
220 else:
221 iw.checkout(self._current_tree, tree)
222 self._current_tree = tree
224 @staticmethod
225 def _abort():
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():
231 if commit is None:
232 assert pn in self.stack.patches
233 else:
234 assert pn in remaining
236 def abort(self, iw=None):
237 # The only state we need to restore is index+worktree.
238 if iw:
239 self._checkout(self.stack.head.data.tree, iw, allow_bad_head=True)
241 def run(
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)
251 new_head = self.head
253 # Set branch head.
254 if set_head:
255 if iw:
256 try:
257 self._checkout(new_head.data.tree, iw, allow_bad_head)
258 except CheckoutException:
259 # We have to abort the transaction.
260 self.abort(iw)
261 self._abort()
262 self.stack.set_head(new_head, self._msg)
264 if self._error:
265 if self._conflicts:
266 out.error(*([self._error] + self._conflicts))
267 else:
268 out.error(self._error)
270 old_applied = self.stack.patchorder.applied
271 msg = self._msg + (' (CONFLICT)' if self._conflicts else '')
273 # Write patches.
274 for pn, commit in self.patches.items():
275 if pn in self.stack.patches:
276 if commit is None:
277 self.stack.patches.delete(pn)
278 else:
279 self.stack.patches.update(pn, commit, msg)
280 else:
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)
288 if self._error:
289 return utils.STGIT_CONFLICT
290 else:
291 return utils.STGIT_SUCCESS
293 def _halt(self, msg):
294 self._error = msg
295 raise TransactionHalted(msg)
297 @staticmethod
298 def _print_popped(popped):
299 if len(popped) == 0:
300 pass
301 elif len(popped) == 1:
302 out.info('Popped %s' % popped[0])
303 else:
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.
309 Always succeeds.
311 :returns: list of other patches that had to be popped to accomplish this.
314 popped = []
315 for i in range(len(self.applied)):
316 if p(self.applied[i]):
317 popped = self.applied[i:]
318 del self.applied[i:]
319 break
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)
324 return popped1
326 def delete_patches(self, p, quiet=False):
327 """Delete all patches pn for which p(pn) is true.
329 Always succeeds.
331 :returns: list of other patches that had to be popped to accomplish this.
334 popped = []
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:]
339 del self.applied[i:]
340 break
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:
346 if p(pn):
347 s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]
348 self.patches[pn] = None
349 if not quiet:
350 out.info('Deleted %s%s' % (pn, s))
351 return popped
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)
365 if already_merged:
366 # the resulting patch is empty
367 tree = cd.parent.data.tree
368 else:
369 base = oldparent.data.tree
370 ours = cd.parent.data.tree
371 theirs = cd.tree
372 tree, self.temp_index_tree = self.temp_index.merge(
373 base, ours, theirs, self.temp_index_tree
375 s = ''
376 merge_conflict = False
377 if not tree:
378 if iw is None:
379 self._halt('%s does not apply cleanly' % pn)
380 try:
381 self._checkout(ours, iw, allow_bad_head=False)
382 except CheckoutException:
383 self._halt('Index/worktree dirty')
384 try:
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
389 s = 'modified'
390 except MergeConflictException as e:
391 tree = ours
392 merge_conflict = True
393 self._conflicts = e.conflicts
394 s = 'conflict'
395 except MergeException as e:
396 self._halt(str(e))
397 cd = cd.set_tree(tree)
398 if any(
399 getattr(cd, a) != getattr(orig_cd, a)
400 for a in ['parent', 'tree', 'author', 'message']
402 comm = self.stack.repository.commit(cd)
403 if merge_conflict:
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.
409 self.head = comm
410 else:
411 comm = None
412 s = 'unmodified'
413 if already_merged:
414 s = 'merged'
415 elif not merge_conflict and cd.is_nochange():
416 s = 'empty'
417 out.done(s)
419 if merge_conflict:
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
425 if comm:
426 self.patches[pn] = comm
427 if pn in self.hidden:
428 x = self.hidden
429 else:
430 x = self.unapplied
431 del x[x.index(pn)]
432 self.applied.append(pn)
434 if merge_conflict:
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)
442 s = ''
443 if any(
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)
448 else:
449 s = ' (unmodified)'
450 if cd.is_nochange():
451 s = ' (empty)'
452 out.info('Pushed %s%s' % (pn, s))
454 if pn in self.hidden:
455 x = self.hidden
456 else:
457 x = self.unapplied
458 del x[x.index(pn)]
459 self.applied.append(pn)
461 def reorder_patches(
462 self, applied, unapplied, hidden=None, iw=None, allow_interactive=False
464 """Push and pop patches to attain the given ordering."""
465 if hidden is None:
466 hidden = self.hidden
467 common = len(
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
479 self.hidden = hidden
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)
484 try:
485 index = self._applied.index(old_name)
486 except ValueError:
487 pass
488 else:
489 self._applied[index] = new_name
490 try:
491 index = self._unapplied.index(old_name)
492 except ValueError:
493 pass
494 else:
495 self._unapplied[index] = new_name
496 try:
497 index = self._hidden.index(old_name)
498 except ValueError:
499 pass
500 else:
501 self._hidden[index] = new_name
503 def check_merged(self, patches, tree=None, quiet=False):
504 """Return a subset of patches already merged."""
505 if not quiet:
506 out.start('Checking for patches merged upstream')
507 merged = []
508 if tree:
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
517 if cd.is_nochange():
518 continue
519 try:
520 self.temp_index.apply_treediff(
521 cd.tree,
522 cd.parent.data.tree,
523 quiet=True,
525 merged.append(pn)
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:
530 pass
531 if not quiet:
532 out.done('%d found' % len(merged))
533 return merged