##// END OF EJS Templates
state: support validated declaration of nested unfinished ops...
Daniel Ploch -
r45731:5322e738 default
parent child Browse files
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