Show More
@@ -0,0 +1,136 b'' | |||||
|
1 | Test extension of unfinished states support. | |||
|
2 | $ mkdir chainify | |||
|
3 | $ cd chainify | |||
|
4 | $ cat >> chainify.py <<EOF | |||
|
5 | > from mercurial import cmdutil, error, extensions, exthelper, node, scmutil, state | |||
|
6 | > from hgext import rebase | |||
|
7 | > | |||
|
8 | > eh = exthelper.exthelper() | |||
|
9 | > | |||
|
10 | > extsetup = eh.finalextsetup | |||
|
11 | > cmdtable = eh.cmdtable | |||
|
12 | > | |||
|
13 | > # Rebase calls addunfinished in uisetup, so we have to call it in extsetup. | |||
|
14 | > # Ideally there'd by an 'extensions.afteruisetup()' just like | |||
|
15 | > # 'extensions.afterloaded()' to allow nesting multiple commands. | |||
|
16 | > @eh.extsetup | |||
|
17 | > def _extsetup(ui): | |||
|
18 | > state.addunfinished( | |||
|
19 | > b'chainify', | |||
|
20 | > b'chainify.state', | |||
|
21 | > continueflag=True, | |||
|
22 | > childopnames=[b'rebase']) | |||
|
23 | > | |||
|
24 | > def _node(repo, arg): | |||
|
25 | > return node.hex(scmutil.revsingle(repo, arg).node()) | |||
|
26 | > | |||
|
27 | > @eh.command( | |||
|
28 | > b'chainify', | |||
|
29 | > [(b'r', b'revs', [], b'revs to chain', b'REV'), | |||
|
30 | > (b'', b'continue', False, b'continue op')], | |||
|
31 | > b'chainify [-r REV] +', | |||
|
32 | > inferrepo=True) | |||
|
33 | > def chainify(ui, repo, **opts): | |||
|
34 | > """Rebases r1, r2, r3, etc. into a chain.""" | |||
|
35 | > with repo.wlock(), repo.lock(): | |||
|
36 | > cmdstate = state.cmdstate(repo, b'chainify.state') | |||
|
37 | > if opts['continue']: | |||
|
38 | > if not cmdstate.exists(): | |||
|
39 | > raise error.Abort(b'no chainify in progress') | |||
|
40 | > else: | |||
|
41 | > cmdutil.checkunfinished(repo) | |||
|
42 | > data = { | |||
|
43 | > b'tip': _node(repo, opts['revs'][0]), | |||
|
44 | > b'revs': b','.join(_node(repo, r) for r in opts['revs'][1:]), | |||
|
45 | > } | |||
|
46 | > cmdstate.save(1, data) | |||
|
47 | > | |||
|
48 | > data = cmdstate.read() | |||
|
49 | > while data[b'revs']: | |||
|
50 | > tip = data[b'tip'] | |||
|
51 | > revs = data[b'revs'].split(b',') | |||
|
52 | > with state.delegating(repo, b'chainify', b'rebase'): | |||
|
53 | > ui.status(b'rebasing %s onto %s\n' % (revs[0][:12], tip[:12])) | |||
|
54 | > if state.ischildunfinished(repo, b'chainify', b'rebase'): | |||
|
55 | > rc = state.continuechild(ui, repo, b'chainify', b'rebase') | |||
|
56 | > else: | |||
|
57 | > rc = rebase.rebase(ui, repo, rev=[revs[0]], dest=tip) | |||
|
58 | > if rc and rc != 0: | |||
|
59 | > raise error.Abort(b'rebase failed (rc: %d)' % rc) | |||
|
60 | > data[b'tip'] = _node(repo, b'tip') | |||
|
61 | > data[b'revs'] = b','.join(revs[1:]) | |||
|
62 | > cmdstate.save(1, data) | |||
|
63 | > cmdstate.delete() | |||
|
64 | > ui.status(b'done chainifying\n') | |||
|
65 | > EOF | |||
|
66 | ||||
|
67 | $ chainifypath=`pwd`/chainify.py | |||
|
68 | $ echo '[extensions]' >> $HGRCPATH | |||
|
69 | $ echo "chainify = $chainifypath" >> $HGRCPATH | |||
|
70 | $ echo "rebase =" >> $HGRCPATH | |||
|
71 | ||||
|
72 | $ cd $TESTTMP | |||
|
73 | $ hg init a | |||
|
74 | $ cd a | |||
|
75 | $ echo base > base.txt | |||
|
76 | $ hg commit -Aqm 'base commit' | |||
|
77 | $ echo foo > file1 | |||
|
78 | $ hg commit -Aqm 'add file' | |||
|
79 | $ hg co -q ".^" | |||
|
80 | $ echo bar > file2 | |||
|
81 | $ hg commit -Aqm 'add other file' | |||
|
82 | $ hg co -q ".^" | |||
|
83 | $ echo foo2 > file1 | |||
|
84 | $ hg commit -Aqm 'add conflicting file' | |||
|
85 | $ hg co -q ".^" | |||
|
86 | $ hg log --graph --template '{rev} {files}' | |||
|
87 | o 3 file1 | |||
|
88 | | | |||
|
89 | | o 2 file2 | |||
|
90 | |/ | |||
|
91 | | o 1 file1 | |||
|
92 | |/ | |||
|
93 | @ 0 base.txt | |||
|
94 | ||||
|
95 | $ hg chainify -r 8430cfdf77c2 -r f8596309dff8 -r a858b338b3e9 | |||
|
96 | rebasing f8596309dff8 onto 8430cfdf77c2 | |||
|
97 | rebasing 2:f8596309dff8 "add other file" | |||
|
98 | saved backup bundle to $TESTTMP/* (glob) | |||
|
99 | rebasing a858b338b3e9 onto 83c722183a8e | |||
|
100 | rebasing 2:a858b338b3e9 "add conflicting file" | |||
|
101 | merging file1 | |||
|
102 | warning: conflicts while merging file1! (edit, then use 'hg resolve --mark') | |||
|
103 | unresolved conflicts (see 'hg resolve', then 'hg chainify --continue') | |||
|
104 | [1] | |||
|
105 | $ hg status --config commands.status.verbose=True | |||
|
106 | M file1 | |||
|
107 | ? file1.orig | |||
|
108 | # The repository is in an unfinished *chainify* state. | |||
|
109 | ||||
|
110 | # Unresolved merge conflicts: | |||
|
111 | # | |||
|
112 | # file1 | |||
|
113 | # | |||
|
114 | # To mark files as resolved: hg resolve --mark FILE | |||
|
115 | ||||
|
116 | # To continue: hg chainify --continue | |||
|
117 | # To abort: hg chainify --abort | |||
|
118 | ||||
|
119 | $ echo foo3 > file1 | |||
|
120 | $ hg resolve --mark file1 | |||
|
121 | (no more unresolved files) | |||
|
122 | continue: hg chainify --continue | |||
|
123 | $ hg chainify --continue | |||
|
124 | rebasing a858b338b3e9 onto 83c722183a8e | |||
|
125 | rebasing 2:a858b338b3e9 "add conflicting file" | |||
|
126 | saved backup bundle to $TESTTMP/* (glob) | |||
|
127 | done chainifying | |||
|
128 | $ hg log --graph --template '{rev} {files}' | |||
|
129 | o 3 file1 | |||
|
130 | | | |||
|
131 | o 2 file2 | |||
|
132 | | | |||
|
133 | o 1 file1 | |||
|
134 | | | |||
|
135 | @ 0 base.txt | |||
|
136 |
@@ -19,6 +19,8 b' the data.' | |||||
19 |
|
19 | |||
20 | from __future__ import absolute_import |
|
20 | from __future__ import absolute_import | |
21 |
|
21 | |||
|
22 | import contextlib | |||
|
23 | ||||
22 | from .i18n import _ |
|
24 | from .i18n import _ | |
23 |
|
25 | |||
24 | from . import ( |
|
26 | from . import ( | |
@@ -119,6 +121,7 b' class _statecheck(object):' | |||||
119 | reportonly, |
|
121 | reportonly, | |
120 | continueflag, |
|
122 | continueflag, | |
121 | stopflag, |
|
123 | stopflag, | |
|
124 | childopnames, | |||
122 | cmdmsg, |
|
125 | cmdmsg, | |
123 | cmdhint, |
|
126 | cmdhint, | |
124 | statushint, |
|
127 | statushint, | |
@@ -132,6 +135,8 b' class _statecheck(object):' | |||||
132 | self._reportonly = reportonly |
|
135 | self._reportonly = reportonly | |
133 | self._continueflag = continueflag |
|
136 | self._continueflag = continueflag | |
134 | self._stopflag = stopflag |
|
137 | self._stopflag = stopflag | |
|
138 | self._childopnames = childopnames | |||
|
139 | self._delegating = False | |||
135 | self._cmdmsg = cmdmsg |
|
140 | self._cmdmsg = cmdmsg | |
136 | self._cmdhint = cmdhint |
|
141 | self._cmdhint = cmdhint | |
137 | self._statushint = statushint |
|
142 | self._statushint = statushint | |
@@ -181,12 +186,15 b' class _statecheck(object):' | |||||
181 | """ |
|
186 | """ | |
182 | if self._opname == b'merge': |
|
187 | if self._opname == b'merge': | |
183 | return len(repo[None].parents()) > 1 |
|
188 | return len(repo[None].parents()) > 1 | |
|
189 | elif self._delegating: | |||
|
190 | return False | |||
184 | else: |
|
191 | else: | |
185 | return repo.vfs.exists(self._fname) |
|
192 | return repo.vfs.exists(self._fname) | |
186 |
|
193 | |||
187 |
|
194 | |||
188 | # A list of statecheck objects for multistep operations like graft. |
|
195 | # A list of statecheck objects for multistep operations like graft. | |
189 | _unfinishedstates = [] |
|
196 | _unfinishedstates = [] | |
|
197 | _unfinishedstatesbyname = {} | |||
190 |
|
198 | |||
191 |
|
199 | |||
192 | def addunfinished( |
|
200 | def addunfinished( | |
@@ -197,6 +205,7 b' def addunfinished(' | |||||
197 | reportonly=False, |
|
205 | reportonly=False, | |
198 | continueflag=False, |
|
206 | continueflag=False, | |
199 | stopflag=False, |
|
207 | stopflag=False, | |
|
208 | childopnames=None, | |||
200 | cmdmsg=b"", |
|
209 | cmdmsg=b"", | |
201 | cmdhint=b"", |
|
210 | cmdhint=b"", | |
202 | statushint=b"", |
|
211 | statushint=b"", | |
@@ -218,6 +227,8 b' def addunfinished(' | |||||
218 | `--continue` option or not. |
|
227 | `--continue` option or not. | |
219 | stopflag is a boolean that determines whether or not a command supports |
|
228 | stopflag is a boolean that determines whether or not a command supports | |
220 | --stop flag |
|
229 | --stop flag | |
|
230 | childopnames is a list of other opnames this op uses as sub-steps of its | |||
|
231 | own execution. They must already be added. | |||
221 | cmdmsg is used to pass a different status message in case standard |
|
232 | cmdmsg is used to pass a different status message in case standard | |
222 | message of the format "abort: cmdname in progress" is not desired. |
|
233 | message of the format "abort: cmdname in progress" is not desired. | |
223 | cmdhint is used to pass a different hint message in case standard |
|
234 | cmdhint is used to pass a different hint message in case standard | |
@@ -230,6 +241,7 b' def addunfinished(' | |||||
230 | continuefunc stores the function required to finish an interrupted |
|
241 | continuefunc stores the function required to finish an interrupted | |
231 | operation. |
|
242 | operation. | |
232 | """ |
|
243 | """ | |
|
244 | childopnames = childopnames or [] | |||
233 | statecheckobj = _statecheck( |
|
245 | statecheckobj = _statecheck( | |
234 | opname, |
|
246 | opname, | |
235 | fname, |
|
247 | fname, | |
@@ -238,17 +250,98 b' def addunfinished(' | |||||
238 | reportonly, |
|
250 | reportonly, | |
239 | continueflag, |
|
251 | continueflag, | |
240 | stopflag, |
|
252 | stopflag, | |
|
253 | childopnames, | |||
241 | cmdmsg, |
|
254 | cmdmsg, | |
242 | cmdhint, |
|
255 | cmdhint, | |
243 | statushint, |
|
256 | statushint, | |
244 | abortfunc, |
|
257 | abortfunc, | |
245 | continuefunc, |
|
258 | continuefunc, | |
246 | ) |
|
259 | ) | |
|
260 | ||||
247 | if opname == b'merge': |
|
261 | if opname == b'merge': | |
248 | _unfinishedstates.append(statecheckobj) |
|
262 | _unfinishedstates.append(statecheckobj) | |
249 | else: |
|
263 | else: | |
|
264 | # This check enforces that for any op 'foo' which depends on op 'bar', | |||
|
265 | # 'foo' comes before 'bar' in _unfinishedstates. This ensures that | |||
|
266 | # getrepostate() always returns the most specific applicable answer. | |||
|
267 | for childopname in childopnames: | |||
|
268 | if childopname not in _unfinishedstatesbyname: | |||
|
269 | raise error.ProgrammingError( | |||
|
270 | _(b'op %s depends on unknown op %s') % (opname, childopname) | |||
|
271 | ) | |||
|
272 | ||||
250 | _unfinishedstates.insert(0, statecheckobj) |
|
273 | _unfinishedstates.insert(0, statecheckobj) | |
251 |
|
274 | |||
|
275 | if opname in _unfinishedstatesbyname: | |||
|
276 | raise error.ProgrammingError(_(b'op %s registered twice') % opname) | |||
|
277 | _unfinishedstatesbyname[opname] = statecheckobj | |||
|
278 | ||||
|
279 | ||||
|
280 | def _getparentandchild(opname, childopname): | |||
|
281 | p = _unfinishedstatesbyname.get(opname, None) | |||
|
282 | if not p: | |||
|
283 | raise error.ProgrammingError(_(b'unknown op %s') % opname) | |||
|
284 | if childopname not in p._childopnames: | |||
|
285 | raise error.ProgrammingError( | |||
|
286 | _(b'op %s does not delegate to %s') % (opname, childopname) | |||
|
287 | ) | |||
|
288 | c = _unfinishedstatesbyname[childopname] | |||
|
289 | return p, c | |||
|
290 | ||||
|
291 | ||||
|
292 | @contextlib.contextmanager | |||
|
293 | def delegating(repo, opname, childopname): | |||
|
294 | """context wrapper for delegations from opname to childopname. | |||
|
295 | ||||
|
296 | requires that childopname was specified when opname was registered. | |||
|
297 | ||||
|
298 | Usage: | |||
|
299 | def my_command_foo_that_uses_rebase(...): | |||
|
300 | ... | |||
|
301 | with state.delegating(repo, 'foo', 'rebase'): | |||
|
302 | _run_rebase(...) | |||
|
303 | ... | |||
|
304 | """ | |||
|
305 | ||||
|
306 | p, c = _getparentandchild(opname, childopname) | |||
|
307 | if p._delegating: | |||
|
308 | raise error.ProgrammingError( | |||
|
309 | _(b'cannot delegate from op %s recursively') % opname | |||
|
310 | ) | |||
|
311 | p._delegating = True | |||
|
312 | try: | |||
|
313 | yield | |||
|
314 | except error.ConflictResolutionRequired as e: | |||
|
315 | # Rewrite conflict resolution advice for the parent opname. | |||
|
316 | if e.opname == childopname: | |||
|
317 | raise error.ConflictResolutionRequired(opname) | |||
|
318 | raise e | |||
|
319 | finally: | |||
|
320 | p._delegating = False | |||
|
321 | ||||
|
322 | ||||
|
323 | def ischildunfinished(repo, opname, childopname): | |||
|
324 | """Returns true if both opname and childopname are unfinished.""" | |||
|
325 | ||||
|
326 | p, c = _getparentandchild(opname, childopname) | |||
|
327 | return (p._delegating or p.isunfinished(repo)) and c.isunfinished(repo) | |||
|
328 | ||||
|
329 | ||||
|
330 | def continuechild(ui, repo, opname, childopname): | |||
|
331 | """Checks that childopname is in progress, and continues it.""" | |||
|
332 | ||||
|
333 | p, c = _getparentandchild(opname, childopname) | |||
|
334 | if not ischildunfinished(repo, opname, childopname): | |||
|
335 | raise error.ProgrammingError( | |||
|
336 | _(b'child op %s of parent %s is not unfinished') | |||
|
337 | % (childopname, opname) | |||
|
338 | ) | |||
|
339 | if not c.continuefunc: | |||
|
340 | raise error.ProgrammingError( | |||
|
341 | _(b'op %s has no continue function') % childopname | |||
|
342 | ) | |||
|
343 | return c.continuefunc(ui, repo) | |||
|
344 | ||||
252 |
|
345 | |||
253 | addunfinished( |
|
346 | addunfinished( | |
254 | b'update', |
|
347 | b'update', |
General Comments 0
You need to be logged in to leave comments.
Login now