1 from stgit import exception, utils
2 from stgit.out import *
3 from stgit.lib import git
5 class TransactionException(exception.StgException):
8 class TransactionHalted(TransactionException):
11 def _print_current_patch(old_applied, new_applied):
13 out.info('Now at patch "%s"' % pn)
14 if not old_applied and not new_applied:
17 now_at(new_applied[-1])
19 out.info('No patch applied')
20 elif old_applied[-1] == new_applied[-1]:
23 now_at(new_applied[-1])
25 class _TransPatchMap(dict):
26 def __init__(self, stack):
29 def __getitem__(self, pn):
31 return dict.__getitem__(self, pn)
33 return self.__stack.patches.get(pn).commit
35 class StackTransaction(object):
36 def __init__(self, stack, msg):
39 self.__patches = _TransPatchMap(stack)
40 self.__applied = list(self.__stack.patchorder.applied)
41 self.__unapplied = list(self.__stack.patchorder.unapplied)
43 self.__current_tree = self.__stack.head.data.tree
44 self.__base = self.__stack.base
45 stack = property(lambda self: self.__stack)
46 patches = property(lambda self: self.__patches)
47 def __set_applied(self, val):
48 self.__applied = list(val)
49 applied = property(lambda self: self.__applied, __set_applied)
50 def __set_unapplied(self, val):
51 self.__unapplied = list(val)
52 unapplied = property(lambda self: self.__unapplied, __set_unapplied)
53 def __set_base(self, val):
54 assert not self.__applied
56 base = property(lambda self: self.__base, __set_base)
57 def __checkout(self, tree, iw):
58 if not self.__stack.head_top_equal():
60 'HEAD and top are not the same.',
61 'This can happen if you modify a branch with git.',
62 '"stg repair --help" explains more about what to do next.')
64 if self.__current_tree != tree:
66 iw.checkout(self.__current_tree, tree)
67 self.__current_tree = tree
70 raise TransactionException(
71 'Command aborted (all changes rolled back)')
72 def __check_consistency(self):
73 remaining = set(self.__applied + self.__unapplied)
74 for pn, commit in self.__patches.iteritems():
76 assert self.__stack.patches.exists(pn)
78 assert pn in remaining
82 return self.__patches[self.__applied[-1]]
85 def abort(self, iw = None):
86 # The only state we need to restore is index+worktree.
88 self.__checkout(self.__stack.head.data.tree, iw)
89 def run(self, iw = None):
90 self.__check_consistency()
91 new_head = self.__head
95 self.__checkout(new_head.data.tree, iw)
96 except git.CheckoutException:
97 # We have to abort the transaction.
100 self.__stack.set_head(new_head, self.__msg)
103 out.error(self.__error)
106 for pn, commit in self.__patches.iteritems():
107 if self.__stack.patches.exists(pn):
108 p = self.__stack.patches.get(pn)
112 p.set_commit(commit, self.__msg)
114 self.__stack.patches.new(pn, commit, self.__msg)
115 _print_current_patch(self.__stack.patchorder.applied, self.__applied)
116 self.__stack.patchorder.applied = self.__applied
117 self.__stack.patchorder.unapplied = self.__unapplied
120 return utils.STGIT_CONFLICT
122 return utils.STGIT_SUCCESS
124 def __halt(self, msg):
126 raise TransactionHalted(msg)
129 def __print_popped(popped):
132 elif len(popped) == 1:
133 out.info('Popped %s' % popped[0])
135 out.info('Popped %s -- %s' % (popped[-1], popped[0]))
137 def pop_patches(self, p):
138 """Pop all patches pn for which p(pn) is true. Return the list of
139 other patches that had to be popped to accomplish this."""
141 for i in xrange(len(self.applied)):
142 if p(self.applied[i]):
143 popped = self.applied[i:]
146 popped1 = [pn for pn in popped if not p(pn)]
147 popped2 = [pn for pn in popped if p(pn)]
148 self.unapplied = popped1 + popped2 + self.unapplied
149 self.__print_popped(popped)
152 def delete_patches(self, p):
153 """Delete all patches pn for which p(pn) is true. Return the list of
154 other patches that had to be popped to accomplish this."""
156 all_patches = self.applied + self.unapplied
157 for i in xrange(len(self.applied)):
158 if p(self.applied[i]):
159 popped = self.applied[i:]
162 popped = [pn for pn in popped if not p(pn)]
163 self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)]
164 self.__print_popped(popped)
165 for pn in all_patches:
167 s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]
168 self.patches[pn] = None
169 out.info('Deleted %s%s' % (pn, s))
172 def push_patch(self, pn, iw = None):
173 """Attempt to push the named patch. If this results in conflicts,
174 halts the transaction. If index+worktree are given, spill any
175 conflicts to them."""
176 i = self.unapplied.index(pn)
177 cd = self.patches[pn].data
178 s = ['', ' (empty)'][cd.is_nochange()]
179 oldparent = cd.parent
180 cd = cd.set_parent(self.__head)
181 base = oldparent.data.tree
182 ours = cd.parent.data.tree
184 tree = self.__stack.repository.simple_merge(base, ours, theirs)
185 merge_conflict = False
188 self.__halt('%s does not apply cleanly' % pn)
190 self.__checkout(ours, iw)
191 except git.CheckoutException:
192 self.__halt('Index/worktree dirty')
194 iw.merge(base, ours, theirs)
195 tree = iw.index.write_tree()
196 self.__current_tree = tree
198 except git.MergeException:
200 merge_conflict = True
202 cd = cd.set_tree(tree)
203 self.patches[pn] = self.__stack.repository.commit(cd)
204 del self.unapplied[i]
205 self.applied.append(pn)
206 out.info('Pushed %s%s' % (pn, s))
208 self.__halt('Merge conflict')