comparison mercurial/cmd_impls/graft.py @ 52328:f2fc0a91faca

commands: create a "mercurial.cmd_impls" module to host graft The "mercurial.commands" have been overweight for a while. We create a namespace dedicated to host smaller modules containing code revelant to a specific command. This should result in more isolated snd manageable module. We start with moving the code for "hg graft" in "mercurial.cmd_impls.graft" before doing more work on it. Since that code was about 5% of "commands.py" this seems like a success.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Tue, 19 Nov 2024 15:46:12 +0100
parents
children 5ab77b93567c
comparison
equal deleted inserted replaced
52326:d24731850b2e 52328:f2fc0a91faca
1 # graft.py - implementation of the graft command
2
3 from ..i18n import _
4
5 from .. import cmdutil, error, logcmdutil, merge as mergemod, state as statemod
6
7
8 def cmd_graft(ui, repo, *revs, **opts):
9 """implement the graft command as defined in mercuria/commands.py"""
10 if revs and opts.get('rev'):
11 ui.warn(
12 _(
13 b'warning: inconsistent use of --rev might give unexpected '
14 b'revision ordering!\n'
15 )
16 )
17
18 revs = list(revs)
19 revs.extend(opts.get('rev'))
20 # a dict of data to be stored in state file
21 statedata = {}
22 # list of new nodes created by ongoing graft
23 statedata[b'newnodes'] = []
24
25 cmdutil.resolve_commit_options(ui, opts)
26
27 editor = cmdutil.getcommiteditor(editform=b'graft', **opts)
28
29 cmdutil.check_at_most_one_arg(opts, 'abort', 'stop', 'continue')
30
31 cont = False
32 if opts.get('no_commit'):
33 cmdutil.check_incompatible_arguments(
34 opts,
35 'no_commit',
36 ['edit', 'currentuser', 'currentdate', 'log'],
37 )
38
39 graftstate = statemod.cmdstate(repo, b'graftstate')
40
41 if opts.get('stop'):
42 cmdutil.check_incompatible_arguments(
43 opts,
44 'stop',
45 [
46 'edit',
47 'log',
48 'user',
49 'date',
50 'currentdate',
51 'currentuser',
52 'rev',
53 ],
54 )
55 return _stopgraft(ui, repo, graftstate)
56 elif opts.get('abort'):
57 cmdutil.check_incompatible_arguments(
58 opts,
59 'abort',
60 [
61 'edit',
62 'log',
63 'user',
64 'date',
65 'currentdate',
66 'currentuser',
67 'rev',
68 ],
69 )
70 return cmdutil.abortgraft(ui, repo, graftstate)
71 elif opts.get('continue'):
72 cont = True
73 if revs:
74 raise error.InputError(_(b"can't specify --continue and revisions"))
75 # read in unfinished revisions
76 if graftstate.exists():
77 statedata = cmdutil.readgraftstate(repo, graftstate)
78 if statedata.get(b'date'):
79 opts['date'] = statedata[b'date']
80 if statedata.get(b'user'):
81 opts['user'] = statedata[b'user']
82 if statedata.get(b'log'):
83 opts['log'] = True
84 if statedata.get(b'no_commit'):
85 opts['no_commit'] = statedata.get(b'no_commit')
86 if statedata.get(b'base'):
87 opts['base'] = statedata.get(b'base')
88 nodes = statedata[b'nodes']
89 revs = [repo[node].rev() for node in nodes]
90 else:
91 cmdutil.wrongtooltocontinue(repo, _(b'graft'))
92 else:
93 if not revs:
94 raise error.InputError(_(b'no revisions specified'))
95 cmdutil.checkunfinished(repo)
96 cmdutil.bailifchanged(repo)
97 revs = logcmdutil.revrange(repo, revs)
98
99 skipped = set()
100 basectx = None
101 if opts.get('base'):
102 basectx = logcmdutil.revsingle(repo, opts['base'], None)
103 if basectx is None:
104 # check for merges
105 for rev in repo.revs(b'%ld and merge()', revs):
106 ui.warn(_(b'skipping ungraftable merge revision %d\n') % rev)
107 skipped.add(rev)
108 revs = [r for r in revs if r not in skipped]
109 if not revs:
110 return -1
111 if basectx is not None and len(revs) != 1:
112 raise error.InputError(_(b'only one revision allowed with --base '))
113
114 # Don't check in the --continue case, in effect retaining --force across
115 # --continues. That's because without --force, any revisions we decided to
116 # skip would have been filtered out here, so they wouldn't have made their
117 # way to the graftstate. With --force, any revisions we would have otherwise
118 # skipped would not have been filtered out, and if they hadn't been applied
119 # already, they'd have been in the graftstate.
120 if not (cont or opts.get('force')) and basectx is None:
121 # check for ancestors of dest branch
122 ancestors = repo.revs(b'%ld & (::.)', revs)
123 for rev in ancestors:
124 ui.warn(_(b'skipping ancestor revision %d:%s\n') % (rev, repo[rev]))
125
126 revs = [r for r in revs if r not in ancestors]
127
128 if not revs:
129 return -1
130
131 # analyze revs for earlier grafts
132 ids = {}
133 for ctx in repo.set(b"%ld", revs):
134 ids[ctx.hex()] = ctx.rev()
135 n = ctx.extra().get(b'source')
136 if n:
137 ids[n] = ctx.rev()
138
139 # check ancestors for earlier grafts
140 ui.debug(b'scanning for duplicate grafts\n')
141
142 # The only changesets we can be sure doesn't contain grafts of any
143 # revs, are the ones that are common ancestors of *all* revs:
144 for rev in repo.revs(b'only(%d,ancestor(%ld))', repo[b'.'].rev(), revs):
145 ctx = repo[rev]
146 n = ctx.extra().get(b'source')
147 if n in ids:
148 try:
149 r = repo[n].rev()
150 except error.RepoLookupError:
151 r = None
152 if r in revs:
153 ui.warn(
154 _(
155 b'skipping revision %d:%s '
156 b'(already grafted to %d:%s)\n'
157 )
158 % (r, repo[r], rev, ctx)
159 )
160 revs.remove(r)
161 elif ids[n] in revs:
162 if r is None:
163 ui.warn(
164 _(
165 b'skipping already grafted revision %d:%s '
166 b'(%d:%s also has unknown origin %s)\n'
167 )
168 % (ids[n], repo[ids[n]], rev, ctx, n[:12])
169 )
170 else:
171 ui.warn(
172 _(
173 b'skipping already grafted revision %d:%s '
174 b'(%d:%s also has origin %d:%s)\n'
175 )
176 % (ids[n], repo[ids[n]], rev, ctx, r, n[:12])
177 )
178 revs.remove(ids[n])
179 elif ctx.hex() in ids:
180 r = ids[ctx.hex()]
181 if r in revs:
182 ui.warn(
183 _(
184 b'skipping already grafted revision %d:%s '
185 b'(was grafted from %d:%s)\n'
186 )
187 % (r, repo[r], rev, ctx)
188 )
189 revs.remove(r)
190 if not revs:
191 return -1
192
193 if opts.get('no_commit'):
194 statedata[b'no_commit'] = True
195 if opts.get('base'):
196 statedata[b'base'] = opts['base']
197 for pos, ctx in enumerate(repo.set(b"%ld", revs)):
198 desc = b'%d:%s "%s"' % (
199 ctx.rev(),
200 ctx,
201 ctx.description().split(b'\n', 1)[0],
202 )
203 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
204 if names:
205 desc += b' (%s)' % b' '.join(names)
206 ui.status(_(b'grafting %s\n') % desc)
207 if opts.get('dry_run'):
208 continue
209
210 source = ctx.extra().get(b'source')
211 extra = {}
212 if source:
213 extra[b'source'] = source
214 extra[b'intermediate-source'] = ctx.hex()
215 else:
216 extra[b'source'] = ctx.hex()
217 user = ctx.user()
218 if opts.get('user'):
219 user = opts['user']
220 statedata[b'user'] = user
221 date = ctx.date()
222 if opts.get('date'):
223 date = opts['date']
224 statedata[b'date'] = date
225 message = ctx.description()
226 if opts.get('log'):
227 message += b'\n(grafted from %s)' % ctx.hex()
228 statedata[b'log'] = True
229
230 # we don't merge the first commit when continuing
231 if not cont:
232 # perform the graft merge with p1(rev) as 'ancestor'
233 overrides = {(b'ui', b'forcemerge'): opts.get('tool', b'')}
234 base = ctx.p1() if basectx is None else basectx
235 with ui.configoverride(overrides, b'graft'):
236 stats = mergemod.graft(
237 repo, ctx, base, [b'local', b'graft', b'parent of graft']
238 )
239 # report any conflicts
240 if stats.unresolvedcount > 0:
241 # write out state for --continue
242 nodes = [repo[rev].hex() for rev in revs[pos:]]
243 statedata[b'nodes'] = nodes
244 stateversion = 1
245 graftstate.save(stateversion, statedata)
246 ui.error(_(b"abort: unresolved conflicts, can't continue\n"))
247 ui.error(_(b"(use 'hg resolve' and 'hg graft --continue')\n"))
248 return 1
249 else:
250 cont = False
251
252 # commit if --no-commit is false
253 if not opts.get('no_commit'):
254 node = repo.commit(
255 text=message, user=user, date=date, extra=extra, editor=editor
256 )
257 if node is None:
258 ui.warn(
259 _(b'note: graft of %d:%s created no changes to commit\n')
260 % (ctx.rev(), ctx)
261 )
262 # checking that newnodes exist because old state files won't have it
263 elif statedata.get(b'newnodes') is not None:
264 nn = statedata[b'newnodes']
265 assert isinstance(nn, list) # list of bytes
266 nn.append(node)
267
268 # remove state when we complete successfully
269 if not opts.get('dry_run'):
270 graftstate.delete()
271
272 return 0
273
274
275 def _stopgraft(ui, repo, graftstate):
276 """stop the interrupted graft"""
277 if not graftstate.exists():
278 raise error.StateError(_(b"no interrupted graft found"))
279 pctx = repo[b'.']
280 mergemod.clean_update(pctx)
281 graftstate.delete()
282 ui.status(_(b"stopped the interrupted graft\n"))
283 ui.status(_(b"working directory is now at %s\n") % pctx.hex()[:12])
284 return 0