Print conflict details with the new infrastructure (bug #11181)
[stgit] / stgit / lib / transaction.py
CommitLineData
652b2e67
KH
1"""The L{StackTransaction} class makes it possible to make complex
2updates to an StGit stack in a safe and convenient way."""
3
afa3f9b9 4import atexit
1743e459 5import itertools as it
afa3f9b9 6
f9cc5e69 7from stgit import exception, utils
352446d4 8from stgit.utils import any, all
cbe4567e 9from stgit.out import *
117ed129 10from stgit.lib import git, log
cbe4567e
KH
11
12class TransactionException(exception.StgException):
652b2e67
KH
13 """Exception raised when something goes wrong with a
14 L{StackTransaction}."""
cbe4567e 15
dcd32afa 16class TransactionHalted(TransactionException):
652b2e67
KH
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."""
dcd32afa
KH
20
21def _print_current_patch(old_applied, new_applied):
cbe4567e
KH
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
dcd32afa 35class _TransPatchMap(dict):
652b2e67 36 """Maps patch names to sha1 strings."""
dcd32afa
KH
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
cbe4567e 46class StackTransaction(object):
652b2e67
KH
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."""
9690617a 77 def __init__(self, stack, msg, discard_changes = False,
f4e6a60e 78 allow_conflicts = False, allow_bad_head = False):
9690617a
KH
79 """Create a new L{StackTransaction}.
80
81 @param discard_changes: Discard any changes in index+worktree
82 @type discard_changes: bool
83 @param allow_conflicts: Whether to allow pre-existing conflicts
84 @type allow_conflicts: bool or function of L{StackTransaction}"""
cbe4567e
KH
85 self.__stack = stack
86 self.__msg = msg
dcd32afa 87 self.__patches = _TransPatchMap(stack)
cbe4567e
KH
88 self.__applied = list(self.__stack.patchorder.applied)
89 self.__unapplied = list(self.__stack.patchorder.unapplied)
dcd2e3da 90 self.__hidden = list(self.__stack.patchorder.hidden)
a3d7efcc 91 self.__conflicting_push = None
dcd32afa
KH
92 self.__error = None
93 self.__current_tree = self.__stack.head.data.tree
3b0552a7 94 self.__base = self.__stack.base
9690617a 95 self.__discard_changes = discard_changes
c70033b4 96 self.__bad_head = None
a79cd5d5 97 self.__conflicts = None
781e549a
KH
98 if isinstance(allow_conflicts, bool):
99 self.__allow_conflicts = lambda trans: allow_conflicts
100 else:
101 self.__allow_conflicts = allow_conflicts
afa3f9b9 102 self.__temp_index = self.temp_index_tree = None
f4e6a60e
KH
103 if not allow_bad_head:
104 self.__assert_head_top_equal()
dcd32afa
KH
105 stack = property(lambda self: self.__stack)
106 patches = property(lambda self: self.__patches)
cbe4567e
KH
107 def __set_applied(self, val):
108 self.__applied = list(val)
109 applied = property(lambda self: self.__applied, __set_applied)
110 def __set_unapplied(self, val):
111 self.__unapplied = list(val)
112 unapplied = property(lambda self: self.__unapplied, __set_unapplied)
dcd2e3da
KH
113 def __set_hidden(self, val):
114 self.__hidden = list(val)
115 hidden = property(lambda self: self.__hidden, __set_hidden)
4ae6b67e
KH
116 all_patches = property(lambda self: (self.__applied + self.__unapplied
117 + self.__hidden))
3b0552a7 118 def __set_base(self, val):
980bde6a
KH
119 assert (not self.__applied
120 or self.patches[self.applied[0]].data.parent == val)
3b0552a7
KH
121 self.__base = val
122 base = property(lambda self: self.__base, __set_base)
afa3f9b9
KH
123 @property
124 def temp_index(self):
125 if not self.__temp_index:
126 self.__temp_index = self.__stack.repository.temp_index()
127 atexit.register(self.__temp_index.delete)
128 return self.__temp_index
c70033b4
KH
129 @property
130 def top(self):
131 if self.__applied:
132 return self.__patches[self.__applied[-1]]
133 else:
134 return self.__base
135 def __get_head(self):
136 if self.__bad_head:
137 return self.__bad_head
138 else:
139 return self.top
140 def __set_head(self, val):
141 self.__bad_head = val
142 head = property(__get_head, __set_head)
f4e6a60e
KH
143 def __assert_head_top_equal(self):
144 if not self.__stack.head_top_equal():
dcd32afa
KH
145 out.error(
146 'HEAD and top are not the same.',
147 'This can happen if you modify a branch with git.',
148 '"stg repair --help" explains more about what to do next.')
149 self.__abort()
f4e6a60e
KH
150 def __checkout(self, tree, iw, allow_bad_head):
151 if not allow_bad_head:
152 self.__assert_head_top_equal()
9690617a 153 if self.__current_tree == tree and not self.__discard_changes:
781e549a
KH
154 # No tree change, but we still want to make sure that
155 # there are no unresolved conflicts. Conflicts
156 # conceptually "belong" to the topmost patch, and just
157 # carrying them along to another patch is confusing.
158 if (self.__allow_conflicts(self) or iw == None
159 or not iw.index.conflicts()):
160 return
161 out.error('Need to resolve conflicts first')
162 self.__abort()
163 assert iw != None
9690617a
KH
164 if self.__discard_changes:
165 iw.checkout_hard(tree)
166 else:
167 iw.checkout(self.__current_tree, tree)
781e549a 168 self.__current_tree = tree
dcd32afa
KH
169 @staticmethod
170 def __abort():
171 raise TransactionException(
172 'Command aborted (all changes rolled back)')
cbe4567e 173 def __check_consistency(self):
4ae6b67e 174 remaining = set(self.all_patches)
cbe4567e
KH
175 for pn, commit in self.__patches.iteritems():
176 if commit == None:
177 assert self.__stack.patches.exists(pn)
178 else:
179 assert pn in remaining
59032ccd
KH
180 def abort(self, iw = None):
181 # The only state we need to restore is index+worktree.
182 if iw:
c70033b4
KH
183 self.__checkout(self.__stack.head.data.tree, iw,
184 allow_bad_head = True)
85aaed81
KH
185 def run(self, iw = None, set_head = True, allow_bad_head = False,
186 print_current_patch = True):
652b2e67
KH
187 """Execute the transaction. Will either succeed, or fail (with an
188 exception) and do nothing."""
dcd32afa 189 self.__check_consistency()
c70033b4
KH
190 log.log_external_mods(self.__stack)
191 new_head = self.head
cbe4567e
KH
192
193 # Set branch head.
bca31c2f
KH
194 if set_head:
195 if iw:
196 try:
c70033b4 197 self.__checkout(new_head.data.tree, iw, allow_bad_head)
bca31c2f
KH
198 except git.CheckoutException:
199 # We have to abort the transaction.
200 self.abort(iw)
201 self.__abort()
202 self.__stack.set_head(new_head, self.__msg)
cbe4567e 203
dcd32afa 204 if self.__error:
a79cd5d5
CM
205 if self.__conflicts:
206 out.error(*([self.__error] + self.__conflicts))
207 else:
208 out.error(self.__error)
dcd32afa 209
cbe4567e 210 # Write patches.
a3d7efcc
KH
211 def write(msg):
212 for pn, commit in self.__patches.iteritems():
213 if self.__stack.patches.exists(pn):
214 p = self.__stack.patches.get(pn)
215 if commit == None:
216 p.delete()
217 else:
218 p.set_commit(commit, msg)
cbe4567e 219 else:
a3d7efcc
KH
220 self.__stack.patches.new(pn, commit, msg)
221 self.__stack.patchorder.applied = self.__applied
222 self.__stack.patchorder.unapplied = self.__unapplied
223 self.__stack.patchorder.hidden = self.__hidden
224 log.log_entry(self.__stack, msg)
225 old_applied = self.__stack.patchorder.applied
226 write(self.__msg)
227 if self.__conflicting_push != None:
228 self.__patches = _TransPatchMap(self.__stack)
229 self.__conflicting_push()
230 write(self.__msg + ' (CONFLICT)')
85aaed81
KH
231 if print_current_patch:
232 _print_current_patch(old_applied, self.__applied)
dcd32afa 233
f9cc5e69
KH
234 if self.__error:
235 return utils.STGIT_CONFLICT
236 else:
237 return utils.STGIT_SUCCESS
238
dcd32afa
KH
239 def __halt(self, msg):
240 self.__error = msg
241 raise TransactionHalted(msg)
242
243 @staticmethod
244 def __print_popped(popped):
245 if len(popped) == 0:
246 pass
247 elif len(popped) == 1:
248 out.info('Popped %s' % popped[0])
249 else:
250 out.info('Popped %s -- %s' % (popped[-1], popped[0]))
251
252 def pop_patches(self, p):
253 """Pop all patches pn for which p(pn) is true. Return the list of
652b2e67
KH
254 other patches that had to be popped to accomplish this. Always
255 succeeds."""
dcd32afa
KH
256 popped = []
257 for i in xrange(len(self.applied)):
258 if p(self.applied[i]):
259 popped = self.applied[i:]
260 del self.applied[i:]
261 break
262 popped1 = [pn for pn in popped if not p(pn)]
263 popped2 = [pn for pn in popped if p(pn)]
264 self.unapplied = popped1 + popped2 + self.unapplied
265 self.__print_popped(popped)
266 return popped1
267
85aaed81 268 def delete_patches(self, p, quiet = False):
dcd32afa 269 """Delete all patches pn for which p(pn) is true. Return the list of
652b2e67
KH
270 other patches that had to be popped to accomplish this. Always
271 succeeds."""
dcd32afa 272 popped = []
dcd2e3da 273 all_patches = self.applied + self.unapplied + self.hidden
dcd32afa
KH
274 for i in xrange(len(self.applied)):
275 if p(self.applied[i]):
276 popped = self.applied[i:]
277 del self.applied[i:]
278 break
279 popped = [pn for pn in popped if not p(pn)]
280 self.unapplied = popped + [pn for pn in self.unapplied if not p(pn)]
dcd2e3da 281 self.hidden = [pn for pn in self.hidden if not p(pn)]
dcd32afa
KH
282 self.__print_popped(popped)
283 for pn in all_patches:
284 if p(pn):
285 s = ['', ' (empty)'][self.patches[pn].data.is_nochange()]
286 self.patches[pn] = None
85aaed81
KH
287 if not quiet:
288 out.info('Deleted %s%s' % (pn, s))
dcd32afa
KH
289 return popped
290
291 def push_patch(self, pn, iw = None):
292 """Attempt to push the named patch. If this results in conflicts,
293 halts the transaction. If index+worktree are given, spill any
294 conflicts to them."""
352446d4
KH
295 orig_cd = self.patches[pn].data
296 cd = orig_cd.set_committer(None)
dcd32afa 297 oldparent = cd.parent
c70033b4 298 cd = cd.set_parent(self.top)
dcd32afa
KH
299 base = oldparent.data.tree
300 ours = cd.parent.data.tree
301 theirs = cd.tree
afa3f9b9
KH
302 tree, self.temp_index_tree = self.temp_index.merge(
303 base, ours, theirs, self.temp_index_tree)
60a82557 304 s = ''
dcd32afa
KH
305 merge_conflict = False
306 if not tree:
307 if iw == None:
308 self.__halt('%s does not apply cleanly' % pn)
309 try:
c70033b4 310 self.__checkout(ours, iw, allow_bad_head = False)
dcd32afa
KH
311 except git.CheckoutException:
312 self.__halt('Index/worktree dirty')
313 try:
314 iw.merge(base, ours, theirs)
315 tree = iw.index.write_tree()
316 self.__current_tree = tree
317 s = ' (modified)'
a79cd5d5 318 except git.MergeConflictException, e:
dcd32afa
KH
319 tree = ours
320 merge_conflict = True
a79cd5d5 321 self.__conflicts = e.conflicts
dcd32afa 322 s = ' (conflict)'
363d432f
KH
323 except git.MergeException, e:
324 self.__halt(str(e))
dcd32afa 325 cd = cd.set_tree(tree)
352446d4
KH
326 if any(getattr(cd, a) != getattr(orig_cd, a) for a in
327 ['parent', 'tree', 'author', 'message']):
a3d7efcc 328 comm = self.__stack.repository.commit(cd)
d2a270a6 329 self.head = comm
352446d4 330 else:
a3d7efcc 331 comm = None
352446d4 332 s = ' (unmodified)'
60a82557
CM
333 if not merge_conflict and cd.is_nochange():
334 s = ' (empty)'
dcd32afa 335 out.info('Pushed %s%s' % (pn, s))
a3d7efcc
KH
336 def update():
337 if comm:
338 self.patches[pn] = comm
339 if pn in self.hidden:
340 x = self.hidden
341 else:
342 x = self.unapplied
343 del x[x.index(pn)]
344 self.applied.append(pn)
dcd32afa 345 if merge_conflict:
781e549a
KH
346 # We've just caused conflicts, so we must allow them in
347 # the final checkout.
348 self.__allow_conflicts = lambda trans: True
349
a3d7efcc
KH
350 # Save this update so that we can run it a little later.
351 self.__conflicting_push = update
a79cd5d5 352 self.__halt("%d merge conflict(s)" % len(self.__conflicts))
a3d7efcc
KH
353 else:
354 # Update immediately.
355 update()
1743e459
KH
356
357 def reorder_patches(self, applied, unapplied, hidden, iw = None):
358 """Push and pop patches to attain the given ordering."""
359 common = len(list(it.takewhile(lambda (a, b): a == b,
360 zip(self.applied, applied))))
361 to_pop = set(self.applied[common:])
362 self.pop_patches(lambda pn: pn in to_pop)
363 for pn in applied[common:]:
364 self.push_patch(pn, iw)
365 assert self.applied == applied
366 assert set(self.unapplied + self.hidden) == set(unapplied + hidden)
367 self.unapplied = unapplied
368 self.hidden = hidden