Merge branch 'stable'
[stgit] / stgit / lib / transaction.py
1 """The L{StackTransaction} class makes it possible to make complex
2 updates to an StGit stack in a safe and convenient way."""
3
4 import atexit
5 import itertools as it
6
7 from stgit import exception, utils
8 from stgit.utils import any, all
9 from stgit.out import *
10 from stgit.lib import git
11
12 class TransactionException(exception.StgException):
13 """Exception raised when something goes wrong with a
14 L{StackTransaction}."""
15
16 class TransactionHalted(TransactionException):
17 """Exception raised when a L{StackTransaction} stops part-way through.
18 Used to make a non-local jump from the transaction setup to the
19 part of the transaction code where the transaction is run."""
20
21 def _print_current_patch(old_applied, new_applied):
22 def now_at(pn):
23 out.info('Now at patch "%s"' % pn)
24 if not old_applied and not new_applied:
25 pass
26 elif not old_applied:
27 now_at(new_applied[-1])
28 elif not new_applied:
29 out.info('No patch applied')
30 elif old_applied[-1] == new_applied[-1]:
31 pass
32 else:
33 now_at(new_applied[-1])
34
35 class _TransPatchMap(dict):
36 """Maps patch names to sha1 strings."""
37 def __init__(self, stack):
38 dict.__init__(self)
39 self.__stack = stack
40 def __getitem__(self, pn):
41 try:
42 return dict.__getitem__(self, pn)
43 except KeyError:
44 return self.__stack.patches.get(pn).commit
45
46 class StackTransaction(object):
47 """A stack transaction, used for making complex updates to an StGit
48 stack in one single operation that will either succeed or fail
49 cleanly.
50
51 The basic theory of operation is the following:
52
53 1. Create a transaction object.
54
55 2. Inside a::
56
57 try
58 ...
59 except TransactionHalted:
60 pass
61
62 block, update the transaction with e.g. methods like
63 L{pop_patches} and L{push_patch}. This may create new git
64 objects such as commits, but will not write any refs; this means
65 that in case of a fatal error we can just walk away, no clean-up
66 required.
67
68 (Some operations may need to touch your index and working tree,
69 though. But they are cleaned up when needed.)
70
71 3. After the C{try} block -- wheher or not the setup ran to
72 completion or halted part-way through by raising a
73 L{TransactionHalted} exception -- call the transaction's L{run}
74 method. This will either succeed in writing the updated state to
75 your refs and index+worktree, or fail without having done
76 anything."""
77 def __init__(self, stack, msg, allow_conflicts = False):
78 self.__stack = stack
79 self.__msg = msg
80 self.__patches = _TransPatchMap(stack)
81 self.__applied = list(self.__stack.patchorder.applied)
82 self.__unapplied = list(self.__stack.patchorder.unapplied)
83 self.__hidden = list(self.__stack.patchorder.hidden)
84 self.__error = None
85 self.__current_tree = self.__stack.head.data.tree
86 self.__base = self.__stack.base
87 if isinstance(allow_conflicts, bool):
88 self.__allow_conflicts = lambda trans: allow_conflicts
89 else:
90 self.__allow_conflicts = allow_conflicts
91 self.__temp_index = self.temp_index_tree = None
92 stack = property(lambda self: self.__stack)
93 patches = property(lambda self: self.__patches)
94 def __set_applied(self, val):
95 self.__applied = list(val)
96 applied = property(lambda self: self.__applied, __set_applied)
97 def __set_unapplied(self, val):
98 self.__unapplied = list(val)
99 unapplied = property(lambda self: self.__unapplied, __set_unapplied)
100 def __set_hidden(self, val):
101 self.__hidden = list(val)
102 hidden = property(lambda self: self.__hidden, __set_hidden)
103 def __set_base(self, val):
104 assert (not self.__applied
105 or self.patches[self.applied[0]].data.parent == val)
106 self.__base = val
107 base = property(lambda self: self.__base, __set_base)
108 @property
109 def temp_index(self):
110 if not self.__temp_index:
111 self.__temp_index = self.__stack.repository.temp_index()
112 atexit.register(self.__temp_index.delete)
113 return self.__temp_index
114 def __checkout(self, tree, iw):
115 if not self.__stack.head_top_equal():
116 out.error(
117 'HEAD and top are not the same.',
118 'This can happen if you modify a branch with git.',
119 '"stg repair --help" explains more about what to do next.')
120 self.__abort()
121 if self.__current_tree == tree:
122 # No tree change, but we still want to make sure that
123 # there are no unresolved conflicts. Conflicts
124 # conceptually "belong" to the topmost patch, and just
125 # carrying them along to another patch is confusing.
126 if (self.__allow_conflicts(self) or iw == None
127 or not iw.index.conflicts()):
128 return
129 out.error('Need to resolve conflicts first')
130 self.__abort()
131 assert iw != None
132 iw.checkout(self.__current_tree, tree)
133 self.__current_tree = tree
134 @staticmethod
135 def __abort():
136 raise TransactionException(
137 'Command aborted (all changes rolled back)')
138 def __check_consistency(self):
139 remaining = set(self.__applied + self.__unapplied + self.__hidden)
140 for pn, commit in self.__patches.iteritems():
141 if commit == None:
142 assert self.__stack.patches.exists(pn)
143 else:
144 assert pn in remaining
145 @property
146 def __head(self):
147 if self.__applied:
148 return self.__patches[self.__applied[-1]]
149 else:
150 return self.__base
151 def abort(self, iw = None):
152 # The only state we need to restore is index+worktree.
153 if iw:
154 self.__checkout(self.__stack.head.data.tree, iw)
155 def run(self, iw = None, set_head = True):
156 """Execute the transaction. Will either succeed, or fail (with an
157 exception) and do nothing."""
158 self.__check_consistency()
159 new_head = self.__head
160
161 # Set branch head.
162 if set_head:
163 if iw:
164 try:
165 self.__checkout(new_head.data.tree, iw)
166 except git.CheckoutException:
167 # We have to abort the transaction.
168 self.abort(iw)
169 self.__abort()
170 self.__stack.set_head(new_head, self.__msg)
171
172 if self.__error:
173 out.error(self.__error)
174
175 # Write patches.
176 for pn, commit in self.__patches.iteritems():
177 if self.__stack.patches.exists(pn):
178 p = self.__stack.patches.get(pn)
179 if commit == None:
180 p.delete()
181 else:
182 p.set_commit(commit, self.__msg)
183 else:
184 self.__stack.patches.new(pn, commit, self.__msg)
185 _print_current_patch(self.__stack.patchorder.applied, self.__applied)
186 self.__stack.patchorder.applied = self.__applied
187 self.__stack.patchorder.unapplied = self.__unapplied
188 self.__stack.patchorder.hidden = self.__hidden
189
190 if self.__error:
191 return utils.STGIT_CONFLICT
192 else:
193 return utils.STGIT_SUCCESS
194
195 def __halt(self, msg):
196 self.__error = msg
197 raise TransactionHalted(msg)
198
199 @staticmethod
200 def __print_popped(popped):
201 if len(popped) == 0:
202 pass
203 elif len(popped) == 1:
204 out.info('Popped %s' % popped[0])
205 else:
206 out.info('Popped %s -- %s' % (popped[-1], popped[0]))
207
208 def pop_patches(self, p):
209 """Pop all patches pn for which p(pn) is true. Return the list of
210 other patches that had to be popped to accomplish this. Always
211 succeeds."""
212 popped = []
213 for i in xrange(len(self.applied)):
214 if p(self.applied[i]):
215 popped = self.applied[i:]
216 del self.applied[i:]
217 break
218 popped1 = [pn for pn in popped if not p(pn)]
219 popped2 = [pn for pn in popped if p(pn)]
220 self.unapplied = popped1 + popped2 + self.unapplied
221 self.__print_popped(popped)
222 return popped1
223
224 def delete_patches(self, p):
225 """Delete all patches pn for which p(pn) is true. Return the list of
226 other patches that had to be popped to accomplish this. Always
227 succeeds."""
228 popped = []
229 all_patches = self.applied + self.unapplied + self.hidden
230 for i in xrange(len(self.applied)):
231 if p(self.applied[i]):
232 popped = self.applied[i:]
233 del self.applied[i:]
234 break
235 popped = [pn for pn in popped if not p(pn)]
236 self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)]
237 self.hidden = [pn for pn in self.hidden if not p(pn)]
238 self.__print_popped(popped)
239 for pn in all_patches:
240 if p(pn):
241 s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]
242 self.patches[pn] = None
243 out.info('Deleted %s%s' % (pn, s))
244 return popped
245
246 def push_patch(self, pn, iw = None):
247 """Attempt to push the named patch. If this results in conflicts,
248 halts the transaction. If index+worktree are given, spill any
249 conflicts to them."""
250 orig_cd = self.patches[pn].data
251 cd = orig_cd.set_committer(None)
252 s = ['', ' (empty)'][cd.is_nochange()]
253 oldparent = cd.parent
254 cd = cd.set_parent(self.__head)
255 base = oldparent.data.tree
256 ours = cd.parent.data.tree
257 theirs = cd.tree
258 tree, self.temp_index_tree = self.temp_index.merge(
259 base, ours, theirs, self.temp_index_tree)
260 merge_conflict = False
261 if not tree:
262 if iw == None:
263 self.__halt('%s does not apply cleanly' % pn)
264 try:
265 self.__checkout(ours, iw)
266 except git.CheckoutException:
267 self.__halt('Index/worktree dirty')
268 try:
269 iw.merge(base, ours, theirs)
270 tree = iw.index.write_tree()
271 self.__current_tree = tree
272 s = ' (modified)'
273 except git.MergeConflictException:
274 tree = ours
275 merge_conflict = True
276 s = ' (conflict)'
277 except git.MergeException, e:
278 self.__halt(str(e))
279 cd = cd.set_tree(tree)
280 if any(getattr(cd, a) != getattr(orig_cd, a) for a in
281 ['parent', 'tree', 'author', 'message']):
282 self.patches[pn] = self.__stack.repository.commit(cd)
283 else:
284 s = ' (unmodified)'
285 if pn in self.hidden:
286 x = self.hidden
287 else:
288 x = self.unapplied
289 del x[x.index(pn)]
290 self.applied.append(pn)
291 out.info('Pushed %s%s' % (pn, s))
292 if merge_conflict:
293 # We've just caused conflicts, so we must allow them in
294 # the final checkout.
295 self.__allow_conflicts = lambda trans: True
296
297 self.__halt('Merge conflict')
298
299 def reorder_patches(self, applied, unapplied, hidden, iw = None):
300 """Push and pop patches to attain the given ordering."""
301 common = len(list(it.takewhile(lambda (a, b): a == b,
302 zip(self.applied, applied))))
303 to_pop = set(self.applied[common:])
304 self.pop_patches(lambda pn: pn in to_pop)
305 for pn in applied[common:]:
306 self.push_patch(pn, iw)
307 assert self.applied == applied
308 assert set(self.unapplied + self.hidden) == set(unapplied + hidden)
309 self.unapplied = unapplied
310 self.hidden = hidden