Mercurial > public > mercurial-scm > hg
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 |