Merge branch 'stable'
[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 *
dcd32afa 10from stgit.lib import git
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."""
781e549a 77 def __init__(self, stack, msg, allow_conflicts = False):
cbe4567e
KH
78 self.__stack = stack
79 self.__msg = msg
dcd32afa 80 self.__patches = _TransPatchMap(stack)
cbe4567e
KH
81 self.__applied = list(self.__stack.patchorder.applied)
82 self.__unapplied = list(self.__stack.patchorder.unapplied)
dcd2e3da 83 self.__hidden = list(self.__stack.patchorder.hidden)
dcd32afa
KH
84 self.__error = None
85 self.__current_tree = self.__stack.head.data.tree
3b0552a7 86 self.__base = self.__stack.base
781e549a
KH
87 if isinstance(allow_conflicts, bool):
88 self.__allow_conflicts = lambda trans: allow_conflicts
89 else:
90 self.__allow_conflicts = allow_conflicts
afa3f9b9 91 self.__temp_index = self.temp_index_tree = None
dcd32afa
KH
92 stack = property(lambda self: self.__stack)
93 patches = property(lambda self: self.__patches)
cbe4567e
KH
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)
dcd2e3da
KH
100 def __set_hidden(self, val):
101 self.__hidden = list(val)
102 hidden = property(lambda self: self.__hidden, __set_hidden)
3b0552a7 103 def __set_base(self, val):
980bde6a
KH
104 assert (not self.__applied
105 or self.patches[self.applied[0]].data.parent == val)
3b0552a7
KH
106 self.__base = val
107 base = property(lambda self: self.__base, __set_base)
afa3f9b9
KH
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
dcd32afa
KH
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()
781e549a
KH
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
dcd32afa
KH
134 @staticmethod
135 def __abort():
136 raise TransactionException(
137 'Command aborted (all changes rolled back)')
cbe4567e 138 def __check_consistency(self):
dcd2e3da 139 remaining = set(self.__applied + self.__unapplied + self.__hidden)
cbe4567e
KH
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
dcd32afa
KH
145 @property
146 def __head(self):
cbe4567e 147 if self.__applied:
dcd32afa 148 return self.__patches[self.__applied[-1]]
cbe4567e 149 else:
3b0552a7 150 return self.__base
59032ccd
KH
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)
bca31c2f 155 def run(self, iw = None, set_head = True):
652b2e67
KH
156 """Execute the transaction. Will either succeed, or fail (with an
157 exception) and do nothing."""
dcd32afa
KH
158 self.__check_consistency()
159 new_head = self.__head
cbe4567e
KH
160
161 # Set branch head.
bca31c2f
KH
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)
cbe4567e 171
dcd32afa
KH
172 if self.__error:
173 out.error(self.__error)
174
cbe4567e
KH
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)
dcd32afa 185 _print_current_patch(self.__stack.patchorder.applied, self.__applied)
cbe4567e
KH
186 self.__stack.patchorder.applied = self.__applied
187 self.__stack.patchorder.unapplied = self.__unapplied
dcd2e3da 188 self.__stack.patchorder.hidden = self.__hidden
dcd32afa 189
f9cc5e69
KH
190 if self.__error:
191 return utils.STGIT_CONFLICT
192 else:
193 return utils.STGIT_SUCCESS
194
dcd32afa
KH
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
652b2e67
KH
210 other patches that had to be popped to accomplish this. Always
211 succeeds."""
dcd32afa
KH
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
652b2e67
KH
226 other patches that had to be popped to accomplish this. Always
227 succeeds."""
dcd32afa 228 popped = []
dcd2e3da 229 all_patches = self.applied + self.unapplied + self.hidden
dcd32afa
KH
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)]
dcd2e3da 237 self.hidden = [pn for pn in self.hidden if not p(pn)]
dcd32afa
KH
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."""
352446d4
KH
250 orig_cd = self.patches[pn].data
251 cd = orig_cd.set_committer(None)
dcd32afa
KH
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
afa3f9b9
KH
258 tree, self.temp_index_tree = self.temp_index.merge(
259 base, ours, theirs, self.temp_index_tree)
dcd32afa
KH
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)'
363d432f 273 except git.MergeConflictException:
dcd32afa
KH
274 tree = ours
275 merge_conflict = True
276 s = ' (conflict)'
363d432f
KH
277 except git.MergeException, e:
278 self.__halt(str(e))
dcd32afa 279 cd = cd.set_tree(tree)
352446d4
KH
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)'
dcd2e3da
KH
285 if pn in self.hidden:
286 x = self.hidden
287 else:
288 x = self.unapplied
289 del x[x.index(pn)]
dcd32afa
KH
290 self.applied.append(pn)
291 out.info('Pushed %s%s' % (pn, s))
292 if merge_conflict:
781e549a
KH
293 # We've just caused conflicts, so we must allow them in
294 # the final checkout.
295 self.__allow_conflicts = lambda trans: True
296
dcd32afa 297 self.__halt('Merge conflict')
1743e459
KH
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