Commit | Line | Data |
---|---|---|
652b2e67 KH |
1 | """The L{StackTransaction} class makes it possible to make complex |
2 | updates to an StGit stack in a safe and convenient way.""" | |
3 | ||
afa3f9b9 | 4 | import atexit |
1743e459 | 5 | import itertools as it |
afa3f9b9 | 6 | |
f9cc5e69 | 7 | from stgit import exception, utils |
352446d4 | 8 | from stgit.utils import any, all |
cbe4567e | 9 | from stgit.out import * |
117ed129 | 10 | from stgit.lib import git, log |
1de97e5f | 11 | from stgit.config import config |
cbe4567e KH |
12 | |
13 | class TransactionException(exception.StgException): | |
652b2e67 KH |
14 | """Exception raised when something goes wrong with a |
15 | L{StackTransaction}.""" | |
cbe4567e | 16 | |
dcd32afa | 17 | class TransactionHalted(TransactionException): |
652b2e67 KH |
18 | """Exception raised when a L{StackTransaction} stops part-way through. |
19 | Used to make a non-local jump from the transaction setup to the | |
20 | part of the transaction code where the transaction is run.""" | |
dcd32afa KH |
21 | |
22 | def _print_current_patch(old_applied, new_applied): | |
cbe4567e KH |
23 | def now_at(pn): |
24 | out.info('Now at patch "%s"' % pn) | |
25 | if not old_applied and not new_applied: | |
26 | pass | |
27 | elif not old_applied: | |
28 | now_at(new_applied[-1]) | |
29 | elif not new_applied: | |
30 | out.info('No patch applied') | |
31 | elif old_applied[-1] == new_applied[-1]: | |
32 | pass | |
33 | else: | |
34 | now_at(new_applied[-1]) | |
35 | ||
dcd32afa | 36 | class _TransPatchMap(dict): |
652b2e67 | 37 | """Maps patch names to sha1 strings.""" |
dcd32afa KH |
38 | def __init__(self, stack): |
39 | dict.__init__(self) | |
40 | self.__stack = stack | |
41 | def __getitem__(self, pn): | |
42 | try: | |
43 | return dict.__getitem__(self, pn) | |
44 | except KeyError: | |
45 | return self.__stack.patches.get(pn).commit | |
46 | ||
cbe4567e | 47 | class StackTransaction(object): |
652b2e67 KH |
48 | """A stack transaction, used for making complex updates to an StGit |
49 | stack in one single operation that will either succeed or fail | |
50 | cleanly. | |
51 | ||
52 | The basic theory of operation is the following: | |
53 | ||
54 | 1. Create a transaction object. | |
55 | ||
56 | 2. Inside a:: | |
57 | ||
58 | try | |
59 | ... | |
60 | except TransactionHalted: | |
61 | pass | |
62 | ||
63 | block, update the transaction with e.g. methods like | |
64 | L{pop_patches} and L{push_patch}. This may create new git | |
65 | objects such as commits, but will not write any refs; this means | |
66 | that in case of a fatal error we can just walk away, no clean-up | |
67 | required. | |
68 | ||
69 | (Some operations may need to touch your index and working tree, | |
70 | though. But they are cleaned up when needed.) | |
71 | ||
72 | 3. After the C{try} block -- wheher or not the setup ran to | |
73 | completion or halted part-way through by raising a | |
74 | L{TransactionHalted} exception -- call the transaction's L{run} | |
75 | method. This will either succeed in writing the updated state to | |
76 | your refs and index+worktree, or fail without having done | |
77 | anything.""" | |
9690617a | 78 | def __init__(self, stack, msg, discard_changes = False, |
ee11a289 CM |
79 | allow_conflicts = False, allow_bad_head = False, |
80 | check_clean_iw = None): | |
9690617a KH |
81 | """Create a new L{StackTransaction}. |
82 | ||
83 | @param discard_changes: Discard any changes in index+worktree | |
84 | @type discard_changes: bool | |
85 | @param allow_conflicts: Whether to allow pre-existing conflicts | |
86 | @type allow_conflicts: bool or function of L{StackTransaction}""" | |
cbe4567e KH |
87 | self.__stack = stack |
88 | self.__msg = msg | |
dcd32afa | 89 | self.__patches = _TransPatchMap(stack) |
cbe4567e KH |
90 | self.__applied = list(self.__stack.patchorder.applied) |
91 | self.__unapplied = list(self.__stack.patchorder.unapplied) | |
dcd2e3da | 92 | self.__hidden = list(self.__stack.patchorder.hidden) |
dcd32afa KH |
93 | self.__error = None |
94 | self.__current_tree = self.__stack.head.data.tree | |
3b0552a7 | 95 | self.__base = self.__stack.base |
9690617a | 96 | self.__discard_changes = discard_changes |
c70033b4 | 97 | self.__bad_head = None |
a79cd5d5 | 98 | self.__conflicts = None |
781e549a KH |
99 | if isinstance(allow_conflicts, bool): |
100 | self.__allow_conflicts = lambda trans: allow_conflicts | |
101 | else: | |
102 | self.__allow_conflicts = allow_conflicts | |
afa3f9b9 | 103 | self.__temp_index = self.temp_index_tree = None |
f4e6a60e KH |
104 | if not allow_bad_head: |
105 | self.__assert_head_top_equal() | |
ee11a289 CM |
106 | if check_clean_iw: |
107 | self.__assert_index_worktree_clean(check_clean_iw) | |
dcd32afa KH |
108 | stack = property(lambda self: self.__stack) |
109 | patches = property(lambda self: self.__patches) | |
cbe4567e KH |
110 | def __set_applied(self, val): |
111 | self.__applied = list(val) | |
112 | applied = property(lambda self: self.__applied, __set_applied) | |
113 | def __set_unapplied(self, val): | |
114 | self.__unapplied = list(val) | |
115 | unapplied = property(lambda self: self.__unapplied, __set_unapplied) | |
dcd2e3da KH |
116 | def __set_hidden(self, val): |
117 | self.__hidden = list(val) | |
118 | hidden = property(lambda self: self.__hidden, __set_hidden) | |
4ae6b67e KH |
119 | all_patches = property(lambda self: (self.__applied + self.__unapplied |
120 | + self.__hidden)) | |
3b0552a7 | 121 | def __set_base(self, val): |
980bde6a KH |
122 | assert (not self.__applied |
123 | or self.patches[self.applied[0]].data.parent == val) | |
3b0552a7 KH |
124 | self.__base = val |
125 | base = property(lambda self: self.__base, __set_base) | |
afa3f9b9 KH |
126 | @property |
127 | def temp_index(self): | |
128 | if not self.__temp_index: | |
129 | self.__temp_index = self.__stack.repository.temp_index() | |
130 | atexit.register(self.__temp_index.delete) | |
131 | return self.__temp_index | |
c70033b4 KH |
132 | @property |
133 | def top(self): | |
134 | if self.__applied: | |
135 | return self.__patches[self.__applied[-1]] | |
136 | else: | |
137 | return self.__base | |
138 | def __get_head(self): | |
139 | if self.__bad_head: | |
140 | return self.__bad_head | |
141 | else: | |
142 | return self.top | |
143 | def __set_head(self, val): | |
144 | self.__bad_head = val | |
145 | head = property(__get_head, __set_head) | |
f4e6a60e KH |
146 | def __assert_head_top_equal(self): |
147 | if not self.__stack.head_top_equal(): | |
dcd32afa KH |
148 | out.error( |
149 | 'HEAD and top are not the same.', | |
150 | 'This can happen if you modify a branch with git.', | |
151 | '"stg repair --help" explains more about what to do next.') | |
152 | self.__abort() | |
ee11a289 CM |
153 | def __assert_index_worktree_clean(self, iw): |
154 | if not iw.worktree_clean(): | |
155 | self.__halt('Worktree not clean. Use "refresh" or "status --reset"') | |
156 | if not iw.index.is_clean(self.stack.head): | |
157 | self.__halt('Index not clean. Use "refresh" or "status --reset"') | |
f4e6a60e KH |
158 | def __checkout(self, tree, iw, allow_bad_head): |
159 | if not allow_bad_head: | |
160 | self.__assert_head_top_equal() | |
9690617a | 161 | if self.__current_tree == tree and not self.__discard_changes: |
781e549a KH |
162 | # No tree change, but we still want to make sure that |
163 | # there are no unresolved conflicts. Conflicts | |
164 | # conceptually "belong" to the topmost patch, and just | |
165 | # carrying them along to another patch is confusing. | |
166 | if (self.__allow_conflicts(self) or iw == None | |
167 | or not iw.index.conflicts()): | |
168 | return | |
169 | out.error('Need to resolve conflicts first') | |
170 | self.__abort() | |
171 | assert iw != None | |
9690617a KH |
172 | if self.__discard_changes: |
173 | iw.checkout_hard(tree) | |
174 | else: | |
175 | iw.checkout(self.__current_tree, tree) | |
781e549a | 176 | self.__current_tree = tree |
dcd32afa KH |
177 | @staticmethod |
178 | def __abort(): | |
179 | raise TransactionException( | |
180 | 'Command aborted (all changes rolled back)') | |
cbe4567e | 181 | def __check_consistency(self): |
4ae6b67e | 182 | remaining = set(self.all_patches) |
cbe4567e KH |
183 | for pn, commit in self.__patches.iteritems(): |
184 | if commit == None: | |
185 | assert self.__stack.patches.exists(pn) | |
186 | else: | |
187 | assert pn in remaining | |
59032ccd KH |
188 | def abort(self, iw = None): |
189 | # The only state we need to restore is index+worktree. | |
190 | if iw: | |
c70033b4 KH |
191 | self.__checkout(self.__stack.head.data.tree, iw, |
192 | allow_bad_head = True) | |
85aaed81 KH |
193 | def run(self, iw = None, set_head = True, allow_bad_head = False, |
194 | print_current_patch = True): | |
652b2e67 KH |
195 | """Execute the transaction. Will either succeed, or fail (with an |
196 | exception) and do nothing.""" | |
dcd32afa | 197 | self.__check_consistency() |
c70033b4 KH |
198 | log.log_external_mods(self.__stack) |
199 | new_head = self.head | |
cbe4567e KH |
200 | |
201 | # Set branch head. | |
bca31c2f KH |
202 | if set_head: |
203 | if iw: | |
204 | try: | |
c70033b4 | 205 | self.__checkout(new_head.data.tree, iw, allow_bad_head) |
bca31c2f KH |
206 | except git.CheckoutException: |
207 | # We have to abort the transaction. | |
208 | self.abort(iw) | |
209 | self.__abort() | |
210 | self.__stack.set_head(new_head, self.__msg) | |
cbe4567e | 211 | |
dcd32afa | 212 | if self.__error: |
a79cd5d5 CM |
213 | if self.__conflicts: |
214 | out.error(*([self.__error] + self.__conflicts)) | |
215 | else: | |
216 | out.error(self.__error) | |
dcd32afa | 217 | |
cbe4567e | 218 | # Write patches. |
a3d7efcc KH |
219 | def write(msg): |
220 | for pn, commit in self.__patches.iteritems(): | |
221 | if self.__stack.patches.exists(pn): | |
222 | p = self.__stack.patches.get(pn) | |
223 | if commit == None: | |
224 | p.delete() | |
225 | else: | |
226 | p.set_commit(commit, msg) | |
cbe4567e | 227 | else: |
a3d7efcc KH |
228 | self.__stack.patches.new(pn, commit, msg) |
229 | self.__stack.patchorder.applied = self.__applied | |
230 | self.__stack.patchorder.unapplied = self.__unapplied | |
231 | self.__stack.patchorder.hidden = self.__hidden | |
232 | log.log_entry(self.__stack, msg) | |
233 | old_applied = self.__stack.patchorder.applied | |
ba52890d CM |
234 | if not self.__conflicts: |
235 | write(self.__msg) | |
236 | else: | |
a3d7efcc | 237 | write(self.__msg + ' (CONFLICT)') |
85aaed81 KH |
238 | if print_current_patch: |
239 | _print_current_patch(old_applied, self.__applied) | |
dcd32afa | 240 | |
f9cc5e69 KH |
241 | if self.__error: |
242 | return utils.STGIT_CONFLICT | |
243 | else: | |
244 | return utils.STGIT_SUCCESS | |
245 | ||
dcd32afa KH |
246 | def __halt(self, msg): |
247 | self.__error = msg | |
248 | raise TransactionHalted(msg) | |
249 | ||
250 | @staticmethod | |
251 | def __print_popped(popped): | |
252 | if len(popped) == 0: | |
253 | pass | |
254 | elif len(popped) == 1: | |
255 | out.info('Popped %s' % popped[0]) | |
256 | else: | |
257 | out.info('Popped %s -- %s' % (popped[-1], popped[0])) | |
258 | ||
259 | def pop_patches(self, p): | |
260 | """Pop all patches pn for which p(pn) is true. Return the list of | |
652b2e67 KH |
261 | other patches that had to be popped to accomplish this. Always |
262 | succeeds.""" | |
dcd32afa KH |
263 | popped = [] |
264 | for i in xrange(len(self.applied)): | |
265 | if p(self.applied[i]): | |
266 | popped = self.applied[i:] | |
267 | del self.applied[i:] | |
268 | break | |
269 | popped1 = [pn for pn in popped if not p(pn)] | |
270 | popped2 = [pn for pn in popped if p(pn)] | |
271 | self.unapplied = popped1 + popped2 + self.unapplied | |
272 | self.__print_popped(popped) | |
273 | return popped1 | |
274 | ||
85aaed81 | 275 | def delete_patches(self, p, quiet = False): |
dcd32afa | 276 | """Delete all patches pn for which p(pn) is true. Return the list of |
652b2e67 KH |
277 | other patches that had to be popped to accomplish this. Always |
278 | succeeds.""" | |
dcd32afa | 279 | popped = [] |
dcd2e3da | 280 | all_patches = self.applied + self.unapplied + self.hidden |
dcd32afa KH |
281 | for i in xrange(len(self.applied)): |
282 | if p(self.applied[i]): | |
283 | popped = self.applied[i:] | |
284 | del self.applied[i:] | |
285 | break | |
286 | popped = [pn for pn in popped if not p(pn)] | |
287 | self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)] | |
dcd2e3da | 288 | self.hidden = [pn for pn in self.hidden if not p(pn)] |
dcd32afa KH |
289 | self.__print_popped(popped) |
290 | for pn in all_patches: | |
291 | if p(pn): | |
292 | s = ['', ' (empty)'][self.patches[pn].data.is_nochange()] | |
293 | self.patches[pn] = None | |
85aaed81 KH |
294 | if not quiet: |
295 | out.info('Deleted %s%s' % (pn, s)) | |
dcd32afa KH |
296 | return popped |
297 | ||
b4d91eee CM |
298 | def push_patch(self, pn, iw = None, allow_interactive = False, |
299 | already_merged = False): | |
dcd32afa KH |
300 | """Attempt to push the named patch. If this results in conflicts, |
301 | halts the transaction. If index+worktree are given, spill any | |
302 | conflicts to them.""" | |
dcf646ca | 303 | out.start('Pushing patch "%s"' % pn) |
352446d4 KH |
304 | orig_cd = self.patches[pn].data |
305 | cd = orig_cd.set_committer(None) | |
dcd32afa | 306 | oldparent = cd.parent |
c70033b4 | 307 | cd = cd.set_parent(self.top) |
b4d91eee CM |
308 | if already_merged: |
309 | # the resulting patch is empty | |
310 | tree = cd.parent.data.tree | |
311 | else: | |
312 | base = oldparent.data.tree | |
313 | ours = cd.parent.data.tree | |
314 | theirs = cd.tree | |
315 | tree, self.temp_index_tree = self.temp_index.merge( | |
316 | base, ours, theirs, self.temp_index_tree) | |
60a82557 | 317 | s = '' |
dcd32afa KH |
318 | merge_conflict = False |
319 | if not tree: | |
320 | if iw == None: | |
321 | self.__halt('%s does not apply cleanly' % pn) | |
322 | try: | |
c70033b4 | 323 | self.__checkout(ours, iw, allow_bad_head = False) |
dcd32afa KH |
324 | except git.CheckoutException: |
325 | self.__halt('Index/worktree dirty') | |
326 | try: | |
1de97e5f CM |
327 | interactive = (allow_interactive and |
328 | config.get('stgit.autoimerge') == 'yes') | |
329 | iw.merge(base, ours, theirs, interactive = interactive) | |
dcd32afa KH |
330 | tree = iw.index.write_tree() |
331 | self.__current_tree = tree | |
dcf646ca | 332 | s = 'modified' |
a79cd5d5 | 333 | except git.MergeConflictException, e: |
dcd32afa KH |
334 | tree = ours |
335 | merge_conflict = True | |
a79cd5d5 | 336 | self.__conflicts = e.conflicts |
dcf646ca | 337 | s = 'conflict' |
363d432f KH |
338 | except git.MergeException, e: |
339 | self.__halt(str(e)) | |
dcd32afa | 340 | cd = cd.set_tree(tree) |
352446d4 KH |
341 | if any(getattr(cd, a) != getattr(orig_cd, a) for a in |
342 | ['parent', 'tree', 'author', 'message']): | |
a3d7efcc | 343 | comm = self.__stack.repository.commit(cd) |
f4893b17 KH |
344 | if merge_conflict: |
345 | # When we produce a conflict, we'll run the update() | |
346 | # function defined below _after_ having done the | |
347 | # checkout in run(). To make sure that we check out | |
348 | # the real stack top (as it will look after update() | |
349 | # has been run), set it hard here. | |
350 | self.head = comm | |
352446d4 | 351 | else: |
a3d7efcc | 352 | comm = None |
dcf646ca | 353 | s = 'unmodified' |
b4d91eee | 354 | if already_merged: |
dcf646ca | 355 | s = 'merged' |
b4d91eee | 356 | elif not merge_conflict and cd.is_nochange(): |
dcf646ca CM |
357 | s = 'empty' |
358 | out.done(s) | |
ba52890d | 359 | |
dcd32afa | 360 | if merge_conflict: |
781e549a KH |
361 | # We've just caused conflicts, so we must allow them in |
362 | # the final checkout. | |
363 | self.__allow_conflicts = lambda trans: True | |
ba52890d | 364 | self.__patches = _TransPatchMap(self.__stack) |
781e549a | 365 | |
ba52890d CM |
366 | # Update the stack state |
367 | if comm: | |
368 | self.patches[pn] = comm | |
369 | if pn in self.hidden: | |
370 | x = self.hidden | |
a3d7efcc | 371 | else: |
ba52890d CM |
372 | x = self.unapplied |
373 | del x[x.index(pn)] | |
374 | self.applied.append(pn) | |
375 | ||
376 | if merge_conflict: | |
377 | self.__halt("%d merge conflict(s)" % len(self.__conflicts)) | |
1743e459 | 378 | |
051704b1 DK |
379 | def push_tree(self, pn): |
380 | """Push the named patch without updating its tree.""" | |
381 | orig_cd = self.patches[pn].data | |
382 | cd = orig_cd.set_committer(None).set_parent(self.top) | |
383 | ||
384 | s = '' | |
385 | if any(getattr(cd, a) != getattr(orig_cd, a) for a in | |
386 | ['parent', 'tree', 'author', 'message']): | |
387 | self.patches[pn] = self.__stack.repository.commit(cd) | |
388 | else: | |
389 | s = ' (unmodified)' | |
390 | if cd.is_nochange(): | |
391 | s = ' (empty)' | |
392 | out.info('Pushed %s%s' % (pn, s)) | |
393 | ||
394 | if pn in self.hidden: | |
395 | x = self.hidden | |
396 | else: | |
397 | x = self.unapplied | |
398 | del x[x.index(pn)] | |
399 | self.applied.append(pn) | |
400 | ||
d44708ef | 401 | def reorder_patches(self, applied, unapplied, hidden = None, iw = None): |
1743e459 | 402 | """Push and pop patches to attain the given ordering.""" |
d44708ef CM |
403 | if hidden is None: |
404 | hidden = self.hidden | |
1743e459 KH |
405 | common = len(list(it.takewhile(lambda (a, b): a == b, |
406 | zip(self.applied, applied)))) | |
407 | to_pop = set(self.applied[common:]) | |
408 | self.pop_patches(lambda pn: pn in to_pop) | |
409 | for pn in applied[common:]: | |
410 | self.push_patch(pn, iw) | |
411 | assert self.applied == applied | |
412 | assert set(self.unapplied + self.hidden) == set(unapplied + hidden) | |
413 | self.unapplied = unapplied | |
414 | self.hidden = hidden | |
b4d91eee CM |
415 | |
416 | def check_merged(self, patches): | |
417 | """Return a subset of patches already merged.""" | |
3b52f6da | 418 | out.start('Checking for patches merged upstream') |
b4d91eee CM |
419 | merged = [] |
420 | if self.temp_index_tree != self.stack.head.data.tree: | |
421 | self.temp_index.read_tree(self.stack.head.data.tree) | |
422 | self.temp_index_tree = self.stack.head.data.tree | |
423 | for pn in reversed(patches): | |
424 | # check whether patch changes can be reversed in the current index | |
425 | cd = self.patches[pn].data | |
426 | if cd.is_nochange(): | |
427 | continue | |
428 | try: | |
429 | self.temp_index.apply_treediff(cd.tree, cd.parent.data.tree, | |
430 | quiet = True) | |
431 | merged.append(pn) | |
432 | # The self.temp_index was modified by apply_treediff() so | |
433 | # force read_tree() the next time merge() is used. | |
434 | self.temp_index_tree = None | |
435 | except git.MergeException: | |
436 | pass | |
3b52f6da | 437 | out.done('%d found' % len(merged)) |
b4d91eee | 438 | return merged |