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