##// END OF EJS Templates
graft: extract meta information computation in a function...
marmoute -
r53401:de168009 default
parent child Browse files
Show More
@@ -1,321 +1,326
1 # graft.py - implementation of the graft command
1 # graft.py - implementation of the graft command
2
2
3 from __future__ import annotations
3 from __future__ import annotations
4
4
5 import typing
5 import typing
6
6
7 from typing import (
7 from typing import (
8 Any,
8 Any,
9 Tuple,
9 Tuple,
10 )
10 )
11
11
12 from ..i18n import _
12 from ..i18n import _
13
13
14 from .. import cmdutil, error, logcmdutil, merge as mergemod, state as statemod
14 from .. import cmdutil, error, logcmdutil, merge as mergemod, state as statemod
15
15
16
16
17 if typing.TYPE_CHECKING:
17 if typing.TYPE_CHECKING:
18 _ActionT = str
18 _ActionT = str
19 _CmdArgsT = Any # TODO: (statedata, revs, editor, cont, dry_run, tool)
19 _CmdArgsT = Any # TODO: (statedata, revs, editor, cont, dry_run, tool)
20
20
21
21
22 def cmd_graft(ui, repo, *revs, **opts) -> int:
22 def cmd_graft(ui, repo, *revs, **opts) -> int:
23 """implement the graft command as defined in mercurial/commands.py"""
23 """implement the graft command as defined in mercurial/commands.py"""
24 ret = _process_args(ui, repo, *revs, **opts)
24 ret = _process_args(ui, repo, *revs, **opts)
25 action, graftstate, args = ret
25 action, graftstate, args = ret
26 if action == "ERROR":
26 if action == "ERROR":
27 return -1
27 return -1
28 elif action == "ABORT":
28 elif action == "ABORT":
29 assert args is None
29 assert args is None
30 return cmdutil.abortgraft(ui, repo, graftstate)
30 return cmdutil.abortgraft(ui, repo, graftstate)
31 elif action == "STOP":
31 elif action == "STOP":
32 assert args is None
32 assert args is None
33 return _stopgraft(ui, repo, graftstate)
33 return _stopgraft(ui, repo, graftstate)
34 elif action == "GRAFT":
34 elif action == "GRAFT":
35 return _graft_revisions(ui, repo, graftstate, *args)
35 return _graft_revisions(ui, repo, graftstate, *args)
36 else:
36 else:
37 raise error.ProgrammingError('unknown action: %s' % action)
37 raise error.ProgrammingError('unknown action: %s' % action)
38
38
39
39
40 def _process_args(
40 def _process_args(
41 ui, repo, *revs, **opts
41 ui, repo, *revs, **opts
42 ) -> Tuple[_ActionT, statemod.cmdstate | None, _CmdArgsT | None]:
42 ) -> Tuple[_ActionT, statemod.cmdstate | None, _CmdArgsT | None]:
43 """process the graft command argument to figure out what to do
43 """process the graft command argument to figure out what to do
44
44
45 This also filter the selected revision to skip the one that cannot be graft
45 This also filter the selected revision to skip the one that cannot be graft
46 or were already grafted.
46 or were already grafted.
47 """
47 """
48 if revs and opts.get('rev'):
48 if revs and opts.get('rev'):
49 ui.warn(
49 ui.warn(
50 _(
50 _(
51 b'warning: inconsistent use of --rev might give unexpected '
51 b'warning: inconsistent use of --rev might give unexpected '
52 b'revision ordering!\n'
52 b'revision ordering!\n'
53 )
53 )
54 )
54 )
55
55
56 revs = list(revs)
56 revs = list(revs)
57 revs.extend(opts.get('rev'))
57 revs.extend(opts.get('rev'))
58 # a dict of data to be stored in state file
58 # a dict of data to be stored in state file
59 statedata = {}
59 statedata = {}
60 # list of new nodes created by ongoing graft
60 # list of new nodes created by ongoing graft
61 statedata[b'newnodes'] = []
61 statedata[b'newnodes'] = []
62
62
63 # argument incompatible with followup from an interrupted operation
63 # argument incompatible with followup from an interrupted operation
64 commit_args = ['edit', 'log', 'user', 'date', 'currentdate', 'currentuser']
64 commit_args = ['edit', 'log', 'user', 'date', 'currentdate', 'currentuser']
65 nofollow_args = commit_args + ['base', 'rev']
65 nofollow_args = commit_args + ['base', 'rev']
66
66
67 arg_compatibilities = [
67 arg_compatibilities = [
68 ('no_commit', commit_args),
68 ('no_commit', commit_args),
69 ('stop', nofollow_args),
69 ('stop', nofollow_args),
70 ('abort', nofollow_args),
70 ('abort', nofollow_args),
71 ]
71 ]
72 cmdutil.check_at_most_one_arg(opts, 'abort', 'stop', 'continue')
72 cmdutil.check_at_most_one_arg(opts, 'abort', 'stop', 'continue')
73 for arg, incompatible in arg_compatibilities:
73 for arg, incompatible in arg_compatibilities:
74 cmdutil.check_incompatible_arguments(opts, arg, incompatible)
74 cmdutil.check_incompatible_arguments(opts, arg, incompatible)
75
75
76 cmdutil.resolve_commit_options(ui, opts)
76 cmdutil.resolve_commit_options(ui, opts)
77
77
78 cont = False
78 cont = False
79
79
80 graftstate = statemod.cmdstate(repo, b'graftstate')
80 graftstate = statemod.cmdstate(repo, b'graftstate')
81
81
82 if opts.get('stop'):
82 if opts.get('stop'):
83 return "STOP", graftstate, None
83 return "STOP", graftstate, None
84 elif opts.get('abort'):
84 elif opts.get('abort'):
85 return "ABORT", graftstate, None
85 return "ABORT", graftstate, None
86 elif opts.get('continue'):
86 elif opts.get('continue'):
87 cont = True
87 cont = True
88 if revs:
88 if revs:
89 raise error.InputError(_(b"can't specify --continue and revisions"))
89 raise error.InputError(_(b"can't specify --continue and revisions"))
90 # read in unfinished revisions
90 # read in unfinished revisions
91 if graftstate.exists():
91 if graftstate.exists():
92 statedata = cmdutil.readgraftstate(repo, graftstate)
92 statedata = cmdutil.readgraftstate(repo, graftstate)
93 if statedata.get(b'no_commit'):
93 if statedata.get(b'no_commit'):
94 opts['no_commit'] = statedata.get(b'no_commit')
94 opts['no_commit'] = statedata.get(b'no_commit')
95 if statedata.get(b'base'):
95 if statedata.get(b'base'):
96 opts['base'] = statedata.get(b'base')
96 opts['base'] = statedata.get(b'base')
97 nodes = statedata[b'nodes']
97 nodes = statedata[b'nodes']
98 revs = [repo[node].rev() for node in nodes]
98 revs = [repo[node].rev() for node in nodes]
99 else:
99 else:
100 cmdutil.wrongtooltocontinue(repo, _(b'graft'))
100 cmdutil.wrongtooltocontinue(repo, _(b'graft'))
101 elif not revs:
101 elif not revs:
102 raise error.InputError(_(b'no revisions specified'))
102 raise error.InputError(_(b'no revisions specified'))
103 else:
103 else:
104 cmdutil.checkunfinished(repo)
104 cmdutil.checkunfinished(repo)
105 cmdutil.bailifchanged(repo)
105 cmdutil.bailifchanged(repo)
106 revs = logcmdutil.revrange(repo, revs)
106 revs = logcmdutil.revrange(repo, revs)
107
107
108 for o in (
108 for o in (
109 b'date',
109 b'date',
110 b'user',
110 b'user',
111 b'log',
111 b'log',
112 b'no_commit',
112 b'no_commit',
113 b'dry_run',
113 b'dry_run',
114 ):
114 ):
115 v = opts.get(o.decode('ascii'))
115 v = opts.get(o.decode('ascii'))
116 # if statedata is already set, it comes from --continue and test says
116 # if statedata is already set, it comes from --continue and test says
117 # we should honor them above the options (which seems weird).
117 # we should honor them above the options (which seems weird).
118 if v and o not in statedata:
118 if v and o not in statedata:
119 statedata[o] = v
119 statedata[o] = v
120
120
121 skipped = set()
121 skipped = set()
122 basectx = None
122 basectx = None
123 if opts.get('base'):
123 if opts.get('base'):
124 basectx = logcmdutil.revsingle(repo, opts['base'], None)
124 basectx = logcmdutil.revsingle(repo, opts['base'], None)
125 statedata[b'base'] = basectx.hex()
125 statedata[b'base'] = basectx.hex()
126 if basectx is None:
126 if basectx is None:
127 # check for merges
127 # check for merges
128 for rev in repo.revs(b'%ld and merge()', revs):
128 for rev in repo.revs(b'%ld and merge()', revs):
129 ui.warn(_(b'skipping ungraftable merge revision %d\n') % rev)
129 ui.warn(_(b'skipping ungraftable merge revision %d\n') % rev)
130 skipped.add(rev)
130 skipped.add(rev)
131 revs = [r for r in revs if r not in skipped]
131 revs = [r for r in revs if r not in skipped]
132 if not revs:
132 if not revs:
133 return "ERROR", None, None
133 return "ERROR", None, None
134 if basectx is not None and len(revs) != 1:
134 if basectx is not None and len(revs) != 1:
135 raise error.InputError(_(b'only one revision allowed with --base'))
135 raise error.InputError(_(b'only one revision allowed with --base'))
136
136
137 # Don't check in the --continue case, in effect retaining --force across
137 # Don't check in the --continue case, in effect retaining --force across
138 # --continues. That's because without --force, any revisions we decided to
138 # --continues. That's because without --force, any revisions we decided to
139 # skip would have been filtered out here, so they wouldn't have made their
139 # skip would have been filtered out here, so they wouldn't have made their
140 # way to the graftstate. With --force, any revisions we would have otherwise
140 # way to the graftstate. With --force, any revisions we would have otherwise
141 # skipped would not have been filtered out, and if they hadn't been applied
141 # skipped would not have been filtered out, and if they hadn't been applied
142 # already, they'd have been in the graftstate.
142 # already, they'd have been in the graftstate.
143 if not (cont or opts.get('force')) and basectx is None:
143 if not (cont or opts.get('force')) and basectx is None:
144 # check for ancestors of dest branch
144 # check for ancestors of dest branch
145 ancestors = repo.revs(b'%ld & (::.)', revs)
145 ancestors = repo.revs(b'%ld & (::.)', revs)
146 for rev in ancestors:
146 for rev in ancestors:
147 ui.warn(_(b'skipping ancestor revision %d:%s\n') % (rev, repo[rev]))
147 ui.warn(_(b'skipping ancestor revision %d:%s\n') % (rev, repo[rev]))
148
148
149 revs = [r for r in revs if r not in ancestors]
149 revs = [r for r in revs if r not in ancestors]
150
150
151 if not revs:
151 if not revs:
152 return "ERROR", None, None
152 return "ERROR", None, None
153
153
154 # analyze revs for earlier grafts
154 # analyze revs for earlier grafts
155 ids = {}
155 ids = {}
156 for ctx in repo.set(b"%ld", revs):
156 for ctx in repo.set(b"%ld", revs):
157 ids[ctx.hex()] = ctx.rev()
157 ids[ctx.hex()] = ctx.rev()
158 n = ctx.extra().get(b'source')
158 n = ctx.extra().get(b'source')
159 if n:
159 if n:
160 ids[n] = ctx.rev()
160 ids[n] = ctx.rev()
161
161
162 # check ancestors for earlier grafts
162 # check ancestors for earlier grafts
163 ui.debug(b'scanning for duplicate grafts\n')
163 ui.debug(b'scanning for duplicate grafts\n')
164
164
165 # The only changesets we can be sure doesn't contain grafts of any
165 # The only changesets we can be sure doesn't contain grafts of any
166 # revs, are the ones that are common ancestors of *all* revs:
166 # revs, are the ones that are common ancestors of *all* revs:
167 for rev in repo.revs(b'only(%d,ancestor(%ld))', repo[b'.'].rev(), revs):
167 for rev in repo.revs(b'only(%d,ancestor(%ld))', repo[b'.'].rev(), revs):
168 ctx = repo[rev]
168 ctx = repo[rev]
169 n = ctx.extra().get(b'source')
169 n = ctx.extra().get(b'source')
170 if n in ids:
170 if n in ids:
171 try:
171 try:
172 r = repo[n].rev()
172 r = repo[n].rev()
173 except error.RepoLookupError:
173 except error.RepoLookupError:
174 r = None
174 r = None
175 if r in revs:
175 if r in revs:
176 ui.warn(
176 ui.warn(
177 _(
177 _(
178 b'skipping revision %d:%s '
178 b'skipping revision %d:%s '
179 b'(already grafted to %d:%s)\n'
179 b'(already grafted to %d:%s)\n'
180 )
180 )
181 % (r, repo[r], rev, ctx)
181 % (r, repo[r], rev, ctx)
182 )
182 )
183 revs.remove(r)
183 revs.remove(r)
184 elif ids[n] in revs:
184 elif ids[n] in revs:
185 if r is None:
185 if r is None:
186 ui.warn(
186 ui.warn(
187 _(
187 _(
188 b'skipping already grafted revision %d:%s '
188 b'skipping already grafted revision %d:%s '
189 b'(%d:%s also has unknown origin %s)\n'
189 b'(%d:%s also has unknown origin %s)\n'
190 )
190 )
191 % (ids[n], repo[ids[n]], rev, ctx, n[:12])
191 % (ids[n], repo[ids[n]], rev, ctx, n[:12])
192 )
192 )
193 else:
193 else:
194 ui.warn(
194 ui.warn(
195 _(
195 _(
196 b'skipping already grafted revision %d:%s '
196 b'skipping already grafted revision %d:%s '
197 b'(%d:%s also has origin %d:%s)\n'
197 b'(%d:%s also has origin %d:%s)\n'
198 )
198 )
199 % (ids[n], repo[ids[n]], rev, ctx, r, n[:12])
199 % (ids[n], repo[ids[n]], rev, ctx, r, n[:12])
200 )
200 )
201 revs.remove(ids[n])
201 revs.remove(ids[n])
202 elif ctx.hex() in ids:
202 elif ctx.hex() in ids:
203 r = ids[ctx.hex()]
203 r = ids[ctx.hex()]
204 if r in revs:
204 if r in revs:
205 ui.warn(
205 ui.warn(
206 _(
206 _(
207 b'skipping already grafted revision %d:%s '
207 b'skipping already grafted revision %d:%s '
208 b'(was grafted from %d:%s)\n'
208 b'(was grafted from %d:%s)\n'
209 )
209 )
210 % (r, repo[r], rev, ctx)
210 % (r, repo[r], rev, ctx)
211 )
211 )
212 revs.remove(r)
212 revs.remove(r)
213 if not revs:
213 if not revs:
214 return "ERROR", None, None
214 return "ERROR", None, None
215
215
216 editor = cmdutil.getcommiteditor(editform=b'graft', **opts)
216 editor = cmdutil.getcommiteditor(editform=b'graft', **opts)
217 dry_run = bool(opts.get("dry_run"))
217 dry_run = bool(opts.get("dry_run"))
218 tool = opts.get('tool', b'')
218 tool = opts.get('tool', b'')
219 return "GRAFT", graftstate, (statedata, revs, editor, cont, dry_run, tool)
219 return "GRAFT", graftstate, (statedata, revs, editor, cont, dry_run, tool)
220
220
221
221
222 def _build_progress(ui, repo, ctx):
222 def _build_progress(ui, repo, ctx):
223 rev_sum = b'%d:%s "%s"' % (
223 rev_sum = b'%d:%s "%s"' % (
224 ctx.rev(),
224 ctx.rev(),
225 ctx,
225 ctx,
226 ctx.description().split(b'\n', 1)[0],
226 ctx.description().split(b'\n', 1)[0],
227 )
227 )
228 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
228 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
229 if names:
229 if names:
230 rev_sum += b' (%s)' % b' '.join(names)
230 rev_sum += b' (%s)' % b' '.join(names)
231 return _(b'grafting %s\n') % rev_sum
231 return _(b'grafting %s\n') % rev_sum
232
232
233
233
234 def _build_meta(ui, repo, ctx, statedata):
235 source = ctx.extra().get(b'source')
236 extra = {}
237 if source:
238 extra[b'source'] = source
239 extra[b'intermediate-source'] = ctx.hex()
240 else:
241 extra[b'source'] = ctx.hex()
242 user = statedata.get(b'user', ctx.user())
243 date = statedata.get(b'date', ctx.date())
244 message = ctx.description()
245 if statedata.get(b'log'):
246 message += b'\n(grafted from %s)' % ctx.hex()
247 return (user, date, message, extra)
248
249
234 def _graft_revisions(
250 def _graft_revisions(
235 ui,
251 ui,
236 repo,
252 repo,
237 graftstate,
253 graftstate,
238 statedata,
254 statedata,
239 revs,
255 revs,
240 editor,
256 editor,
241 cont=False,
257 cont=False,
242 dry_run=False,
258 dry_run=False,
243 tool=b'',
259 tool=b'',
244 ):
260 ):
245 """actually graft some revisions"""
261 """actually graft some revisions"""
246 for pos, ctx in enumerate(repo.set(b"%ld", revs)):
262 for pos, ctx in enumerate(repo.set(b"%ld", revs)):
247 ui.status(_build_progress(ui, repo, ctx))
263 ui.status(_build_progress(ui, repo, ctx))
248 if dry_run:
264 if dry_run:
249 continue
265 continue
250
266
251 source = ctx.extra().get(b'source')
267 (user, date, message, extra) = _build_meta(ui, repo, ctx, statedata)
252 extra = {}
253 if source:
254 extra[b'source'] = source
255 extra[b'intermediate-source'] = ctx.hex()
256 else:
257 extra[b'source'] = ctx.hex()
258 user = statedata.get(b'user', ctx.user())
259 date = statedata.get(b'date', ctx.date())
260 message = ctx.description()
261 if statedata.get(b'log'):
262 message += b'\n(grafted from %s)' % ctx.hex()
263
268
264 # we don't merge the first commit when continuing
269 # we don't merge the first commit when continuing
265 if not cont:
270 if not cont:
266 # perform the graft merge with p1(rev) as 'ancestor'
271 # perform the graft merge with p1(rev) as 'ancestor'
267 overrides = {(b'ui', b'forcemerge'): tool}
272 overrides = {(b'ui', b'forcemerge'): tool}
268 if b'base' in statedata:
273 if b'base' in statedata:
269 base = repo[statedata[b'base']]
274 base = repo[statedata[b'base']]
270 else:
275 else:
271 base = ctx.p1()
276 base = ctx.p1()
272 with ui.configoverride(overrides, b'graft'):
277 with ui.configoverride(overrides, b'graft'):
273 stats = mergemod.graft(
278 stats = mergemod.graft(
274 repo, ctx, base, [b'local', b'graft', b'parent of graft']
279 repo, ctx, base, [b'local', b'graft', b'parent of graft']
275 )
280 )
276 # report any conflicts
281 # report any conflicts
277 if stats.unresolvedcount > 0:
282 if stats.unresolvedcount > 0:
278 # write out state for --continue
283 # write out state for --continue
279 nodes = [repo[rev].hex() for rev in revs[pos:]]
284 nodes = [repo[rev].hex() for rev in revs[pos:]]
280 statedata[b'nodes'] = nodes
285 statedata[b'nodes'] = nodes
281 stateversion = 1
286 stateversion = 1
282 graftstate.save(stateversion, statedata)
287 graftstate.save(stateversion, statedata)
283 ui.error(_(b"abort: unresolved conflicts, can't continue\n"))
288 ui.error(_(b"abort: unresolved conflicts, can't continue\n"))
284 ui.error(_(b"(use 'hg resolve' and 'hg graft --continue')\n"))
289 ui.error(_(b"(use 'hg resolve' and 'hg graft --continue')\n"))
285 return 1
290 return 1
286 else:
291 else:
287 cont = False
292 cont = False
288
293
289 # commit if --no-commit is false
294 # commit if --no-commit is false
290 if not statedata.get(b'no_commit'):
295 if not statedata.get(b'no_commit'):
291 node = repo.commit(
296 node = repo.commit(
292 text=message, user=user, date=date, extra=extra, editor=editor
297 text=message, user=user, date=date, extra=extra, editor=editor
293 )
298 )
294 if node is None:
299 if node is None:
295 ui.warn(
300 ui.warn(
296 _(b'note: graft of %d:%s created no changes to commit\n')
301 _(b'note: graft of %d:%s created no changes to commit\n')
297 % (ctx.rev(), ctx)
302 % (ctx.rev(), ctx)
298 )
303 )
299 # checking that newnodes exist because old state files won't have it
304 # checking that newnodes exist because old state files won't have it
300 elif statedata.get(b'newnodes') is not None:
305 elif statedata.get(b'newnodes') is not None:
301 nn = statedata[b'newnodes']
306 nn = statedata[b'newnodes']
302 assert isinstance(nn, list) # list of bytes
307 assert isinstance(nn, list) # list of bytes
303 nn.append(node)
308 nn.append(node)
304
309
305 # remove state when we complete successfully
310 # remove state when we complete successfully
306 if not dry_run:
311 if not dry_run:
307 graftstate.delete()
312 graftstate.delete()
308
313
309 return 0
314 return 0
310
315
311
316
312 def _stopgraft(ui, repo, graftstate):
317 def _stopgraft(ui, repo, graftstate):
313 """stop the interrupted graft"""
318 """stop the interrupted graft"""
314 if not graftstate.exists():
319 if not graftstate.exists():
315 raise error.StateError(_(b"no interrupted graft found"))
320 raise error.StateError(_(b"no interrupted graft found"))
316 pctx = repo[b'.']
321 pctx = repo[b'.']
317 mergemod.clean_update(pctx)
322 mergemod.clean_update(pctx)
318 graftstate.delete()
323 graftstate.delete()
319 ui.status(_(b"stopped the interrupted graft\n"))
324 ui.status(_(b"stopped the interrupted graft\n"))
320 ui.status(_(b"working directory is now at %s\n") % pctx.hex()[:12])
325 ui.status(_(b"working directory is now at %s\n") % pctx.hex()[:12])
321 return 0
326 return 0
General Comments 0
You need to be logged in to leave comments. Login now