##// END OF EJS Templates
cmdutil: make checknotesize() work on str-keyed opts...
Martin von Zweigbergk -
r48221:54849b65 default
parent child Browse files
Show More
@@ -1,75 +1,74 b''
1 # amend.py - provide the amend command
1 # amend.py - provide the amend command
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """provide the amend command (EXPERIMENTAL)
7 """provide the amend command (EXPERIMENTAL)
8
8
9 This extension provides an ``amend`` command that is similar to
9 This extension provides an ``amend`` command that is similar to
10 ``commit --amend`` but does not prompt an editor.
10 ``commit --amend`` but does not prompt an editor.
11 """
11 """
12
12
13 from __future__ import absolute_import
13 from __future__ import absolute_import
14
14
15 from mercurial.i18n import _
15 from mercurial.i18n import _
16 from mercurial import (
16 from mercurial import (
17 cmdutil,
17 cmdutil,
18 commands,
18 commands,
19 pycompat,
20 registrar,
19 registrar,
21 )
20 )
22
21
23 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
22 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
24 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
23 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
25 # be specifying the version(s) of Mercurial they are tested with, or
24 # be specifying the version(s) of Mercurial they are tested with, or
26 # leave the attribute unspecified.
25 # leave the attribute unspecified.
27 testedwith = b'ships-with-hg-core'
26 testedwith = b'ships-with-hg-core'
28
27
29 cmdtable = {}
28 cmdtable = {}
30 command = registrar.command(cmdtable)
29 command = registrar.command(cmdtable)
31
30
32
31
33 @command(
32 @command(
34 b'amend',
33 b'amend',
35 [
34 [
36 (
35 (
37 b'A',
36 b'A',
38 b'addremove',
37 b'addremove',
39 None,
38 None,
40 _(b'mark new/missing files as added/removed before committing'),
39 _(b'mark new/missing files as added/removed before committing'),
41 ),
40 ),
42 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
41 (b'e', b'edit', None, _(b'invoke editor on commit messages')),
43 (b'i', b'interactive', None, _(b'use interactive mode')),
42 (b'i', b'interactive', None, _(b'use interactive mode')),
44 (
43 (
45 b'',
44 b'',
46 b'close-branch',
45 b'close-branch',
47 None,
46 None,
48 _(b'mark a branch as closed, hiding it from the branch list'),
47 _(b'mark a branch as closed, hiding it from the branch list'),
49 ),
48 ),
50 (b's', b'secret', None, _(b'use the secret phase for committing')),
49 (b's', b'secret', None, _(b'use the secret phase for committing')),
51 (b'n', b'note', b'', _(b'store a note on the amend')),
50 (b'n', b'note', b'', _(b'store a note on the amend')),
52 ]
51 ]
53 + cmdutil.walkopts
52 + cmdutil.walkopts
54 + cmdutil.commitopts
53 + cmdutil.commitopts
55 + cmdutil.commitopts2
54 + cmdutil.commitopts2
56 + cmdutil.commitopts3,
55 + cmdutil.commitopts3,
57 _(b'[OPTION]... [FILE]...'),
56 _(b'[OPTION]... [FILE]...'),
58 helpcategory=command.CATEGORY_COMMITTING,
57 helpcategory=command.CATEGORY_COMMITTING,
59 inferrepo=True,
58 inferrepo=True,
60 )
59 )
61 def amend(ui, repo, *pats, **opts):
60 def amend(ui, repo, *pats, **opts):
62 """amend the working copy parent with all or specified outstanding changes
61 """amend the working copy parent with all or specified outstanding changes
63
62
64 Similar to :hg:`commit --amend`, but reuse the commit message without
63 Similar to :hg:`commit --amend`, but reuse the commit message without
65 invoking editor, unless ``--edit`` was set.
64 invoking editor, unless ``--edit`` was set.
66
65
67 See :hg:`help commit` for more details.
66 See :hg:`help commit` for more details.
68 """
67 """
69 cmdutil.checknotesize(ui, pycompat.byteskwargs(opts))
68 cmdutil.check_note_size(opts)
70
69
71 with repo.wlock(), repo.lock():
70 with repo.wlock(), repo.lock():
72 if not opts.get('logfile'):
71 if not opts.get('logfile'):
73 opts['message'] = opts.get('message') or repo[b'.'].description()
72 opts['message'] = opts.get('message') or repo[b'.'].description()
74 opts['amend'] = True
73 opts['amend'] = True
75 return commands._docommit(ui, repo, *pats, **opts)
74 return commands._docommit(ui, repo, *pats, **opts)
@@ -1,317 +1,316 b''
1 # uncommit - undo the actions of a commit
1 # uncommit - undo the actions of a commit
2 #
2 #
3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 # Pierre-Yves David <pierre-yves.david@ens-lyon.org>
5 # Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 # Patrick Mezard <patrick@mezard.eu>
6 # Patrick Mezard <patrick@mezard.eu>
7 # Copyright 2016 Facebook, Inc.
7 # Copyright 2016 Facebook, Inc.
8 #
8 #
9 # This software may be used and distributed according to the terms of the
9 # This software may be used and distributed according to the terms of the
10 # GNU General Public License version 2 or any later version.
10 # GNU General Public License version 2 or any later version.
11
11
12 """uncommit part or all of a local changeset (EXPERIMENTAL)
12 """uncommit part or all of a local changeset (EXPERIMENTAL)
13
13
14 This command undoes the effect of a local commit, returning the affected
14 This command undoes the effect of a local commit, returning the affected
15 files to their uncommitted state. This means that files modified, added or
15 files to their uncommitted state. This means that files modified, added or
16 removed in the changeset will be left unchanged, and so will remain modified,
16 removed in the changeset will be left unchanged, and so will remain modified,
17 added and removed in the working directory.
17 added and removed in the working directory.
18 """
18 """
19
19
20 from __future__ import absolute_import
20 from __future__ import absolute_import
21
21
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23
23
24 from mercurial import (
24 from mercurial import (
25 cmdutil,
25 cmdutil,
26 commands,
26 commands,
27 context,
27 context,
28 copies as copiesmod,
28 copies as copiesmod,
29 error,
29 error,
30 obsutil,
30 obsutil,
31 pathutil,
31 pathutil,
32 pycompat,
32 pycompat,
33 registrar,
33 registrar,
34 rewriteutil,
34 rewriteutil,
35 scmutil,
35 scmutil,
36 )
36 )
37
37
38 cmdtable = {}
38 cmdtable = {}
39 command = registrar.command(cmdtable)
39 command = registrar.command(cmdtable)
40
40
41 configtable = {}
41 configtable = {}
42 configitem = registrar.configitem(configtable)
42 configitem = registrar.configitem(configtable)
43
43
44 configitem(
44 configitem(
45 b'experimental',
45 b'experimental',
46 b'uncommitondirtywdir',
46 b'uncommitondirtywdir',
47 default=False,
47 default=False,
48 )
48 )
49 configitem(
49 configitem(
50 b'experimental',
50 b'experimental',
51 b'uncommit.keep',
51 b'uncommit.keep',
52 default=False,
52 default=False,
53 )
53 )
54
54
55 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
55 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
56 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
56 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
57 # be specifying the version(s) of Mercurial they are tested with, or
57 # be specifying the version(s) of Mercurial they are tested with, or
58 # leave the attribute unspecified.
58 # leave the attribute unspecified.
59 testedwith = b'ships-with-hg-core'
59 testedwith = b'ships-with-hg-core'
60
60
61
61
62 def _commitfiltered(
62 def _commitfiltered(
63 repo, ctx, match, keepcommit, message=None, user=None, date=None
63 repo, ctx, match, keepcommit, message=None, user=None, date=None
64 ):
64 ):
65 """Recommit ctx with changed files not in match. Return the new
65 """Recommit ctx with changed files not in match. Return the new
66 node identifier, or None if nothing changed.
66 node identifier, or None if nothing changed.
67 """
67 """
68 base = ctx.p1()
68 base = ctx.p1()
69 # ctx
69 # ctx
70 initialfiles = set(ctx.files())
70 initialfiles = set(ctx.files())
71 exclude = {f for f in initialfiles if match(f)}
71 exclude = {f for f in initialfiles if match(f)}
72
72
73 # No files matched commit, so nothing excluded
73 # No files matched commit, so nothing excluded
74 if not exclude:
74 if not exclude:
75 return None
75 return None
76
76
77 # return the p1 so that we don't create an obsmarker later
77 # return the p1 so that we don't create an obsmarker later
78 if not keepcommit:
78 if not keepcommit:
79 return ctx.p1().node()
79 return ctx.p1().node()
80
80
81 files = initialfiles - exclude
81 files = initialfiles - exclude
82 # Filter copies
82 # Filter copies
83 copied = copiesmod.pathcopies(base, ctx)
83 copied = copiesmod.pathcopies(base, ctx)
84 copied = {
84 copied = {
85 dst: src for dst, src in pycompat.iteritems(copied) if dst in files
85 dst: src for dst, src in pycompat.iteritems(copied) if dst in files
86 }
86 }
87
87
88 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
88 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
89 if path not in contentctx:
89 if path not in contentctx:
90 return None
90 return None
91 fctx = contentctx[path]
91 fctx = contentctx[path]
92 mctx = context.memfilectx(
92 mctx = context.memfilectx(
93 repo,
93 repo,
94 memctx,
94 memctx,
95 fctx.path(),
95 fctx.path(),
96 fctx.data(),
96 fctx.data(),
97 fctx.islink(),
97 fctx.islink(),
98 fctx.isexec(),
98 fctx.isexec(),
99 copysource=copied.get(path),
99 copysource=copied.get(path),
100 )
100 )
101 return mctx
101 return mctx
102
102
103 if not files:
103 if not files:
104 repo.ui.status(_(b"note: keeping empty commit\n"))
104 repo.ui.status(_(b"note: keeping empty commit\n"))
105
105
106 if message is None:
106 if message is None:
107 message = ctx.description()
107 message = ctx.description()
108 if not user:
108 if not user:
109 user = ctx.user()
109 user = ctx.user()
110 if not date:
110 if not date:
111 date = ctx.date()
111 date = ctx.date()
112
112
113 new = context.memctx(
113 new = context.memctx(
114 repo,
114 repo,
115 parents=[base.node(), repo.nullid],
115 parents=[base.node(), repo.nullid],
116 text=message,
116 text=message,
117 files=files,
117 files=files,
118 filectxfn=filectxfn,
118 filectxfn=filectxfn,
119 user=user,
119 user=user,
120 date=date,
120 date=date,
121 extra=ctx.extra(),
121 extra=ctx.extra(),
122 )
122 )
123 return repo.commitctx(new)
123 return repo.commitctx(new)
124
124
125
125
126 @command(
126 @command(
127 b'uncommit',
127 b'uncommit',
128 [
128 [
129 (b'', b'keep', None, _(b'allow an empty commit after uncommitting')),
129 (b'', b'keep', None, _(b'allow an empty commit after uncommitting')),
130 (
130 (
131 b'',
131 b'',
132 b'allow-dirty-working-copy',
132 b'allow-dirty-working-copy',
133 False,
133 False,
134 _(b'allow uncommit with outstanding changes'),
134 _(b'allow uncommit with outstanding changes'),
135 ),
135 ),
136 (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
136 (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
137 ]
137 ]
138 + commands.walkopts
138 + commands.walkopts
139 + commands.commitopts
139 + commands.commitopts
140 + commands.commitopts2
140 + commands.commitopts2
141 + commands.commitopts3,
141 + commands.commitopts3,
142 _(b'[OPTION]... [FILE]...'),
142 _(b'[OPTION]... [FILE]...'),
143 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
143 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
144 )
144 )
145 def uncommit(ui, repo, *pats, **opts):
145 def uncommit(ui, repo, *pats, **opts):
146 """uncommit part or all of a local changeset
146 """uncommit part or all of a local changeset
147
147
148 This command undoes the effect of a local commit, returning the affected
148 This command undoes the effect of a local commit, returning the affected
149 files to their uncommitted state. This means that files modified or
149 files to their uncommitted state. This means that files modified or
150 deleted in the changeset will be left unchanged, and so will remain
150 deleted in the changeset will be left unchanged, and so will remain
151 modified in the working directory.
151 modified in the working directory.
152
152
153 If no files are specified, the commit will be pruned, unless --keep is
153 If no files are specified, the commit will be pruned, unless --keep is
154 given.
154 given.
155 """
155 """
156 cmdutil.check_note_size(opts)
156 opts = pycompat.byteskwargs(opts)
157 opts = pycompat.byteskwargs(opts)
157
158 cmdutil.checknotesize(ui, opts)
159 cmdutil.resolvecommitoptions(ui, opts)
158 cmdutil.resolvecommitoptions(ui, opts)
160
159
161 with repo.wlock(), repo.lock():
160 with repo.wlock(), repo.lock():
162
161
163 st = repo.status()
162 st = repo.status()
164 m, a, r, d = st.modified, st.added, st.removed, st.deleted
163 m, a, r, d = st.modified, st.added, st.removed, st.deleted
165 isdirtypath = any(set(m + a + r + d) & set(pats))
164 isdirtypath = any(set(m + a + r + d) & set(pats))
166 allowdirtywcopy = opts[
165 allowdirtywcopy = opts[
167 b'allow_dirty_working_copy'
166 b'allow_dirty_working_copy'
168 ] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir')
167 ] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir')
169 if not allowdirtywcopy and (not pats or isdirtypath):
168 if not allowdirtywcopy and (not pats or isdirtypath):
170 cmdutil.bailifchanged(
169 cmdutil.bailifchanged(
171 repo,
170 repo,
172 hint=_(b'requires --allow-dirty-working-copy to uncommit'),
171 hint=_(b'requires --allow-dirty-working-copy to uncommit'),
173 )
172 )
174 old = repo[b'.']
173 old = repo[b'.']
175 rewriteutil.precheck(repo, [old.rev()], b'uncommit')
174 rewriteutil.precheck(repo, [old.rev()], b'uncommit')
176 if len(old.parents()) > 1:
175 if len(old.parents()) > 1:
177 raise error.InputError(_(b"cannot uncommit merge changeset"))
176 raise error.InputError(_(b"cannot uncommit merge changeset"))
178
177
179 match = scmutil.match(old, pats, opts)
178 match = scmutil.match(old, pats, opts)
180
179
181 # Check all explicitly given files; abort if there's a problem.
180 # Check all explicitly given files; abort if there's a problem.
182 if match.files():
181 if match.files():
183 s = old.status(old.p1(), match, listclean=True)
182 s = old.status(old.p1(), match, listclean=True)
184 eligible = set(s.added) | set(s.modified) | set(s.removed)
183 eligible = set(s.added) | set(s.modified) | set(s.removed)
185
184
186 badfiles = set(match.files()) - eligible
185 badfiles = set(match.files()) - eligible
187
186
188 # Naming a parent directory of an eligible file is OK, even
187 # Naming a parent directory of an eligible file is OK, even
189 # if not everything tracked in that directory can be
188 # if not everything tracked in that directory can be
190 # uncommitted.
189 # uncommitted.
191 if badfiles:
190 if badfiles:
192 badfiles -= {f for f in pathutil.dirs(eligible)}
191 badfiles -= {f for f in pathutil.dirs(eligible)}
193
192
194 for f in sorted(badfiles):
193 for f in sorted(badfiles):
195 if f in s.clean:
194 if f in s.clean:
196 hint = _(
195 hint = _(
197 b"file was not changed in working directory parent"
196 b"file was not changed in working directory parent"
198 )
197 )
199 elif repo.wvfs.exists(f):
198 elif repo.wvfs.exists(f):
200 hint = _(b"file was untracked in working directory parent")
199 hint = _(b"file was untracked in working directory parent")
201 else:
200 else:
202 hint = _(b"file does not exist")
201 hint = _(b"file does not exist")
203
202
204 raise error.InputError(
203 raise error.InputError(
205 _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f),
204 _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f),
206 hint=hint,
205 hint=hint,
207 )
206 )
208
207
209 with repo.transaction(b'uncommit'):
208 with repo.transaction(b'uncommit'):
210 if not (opts[b'message'] or opts[b'logfile']):
209 if not (opts[b'message'] or opts[b'logfile']):
211 opts[b'message'] = old.description()
210 opts[b'message'] = old.description()
212 message = cmdutil.logmessage(ui, opts)
211 message = cmdutil.logmessage(ui, opts)
213
212
214 keepcommit = pats
213 keepcommit = pats
215 if not keepcommit:
214 if not keepcommit:
216 if opts.get(b'keep') is not None:
215 if opts.get(b'keep') is not None:
217 keepcommit = opts.get(b'keep')
216 keepcommit = opts.get(b'keep')
218 else:
217 else:
219 keepcommit = ui.configbool(
218 keepcommit = ui.configbool(
220 b'experimental', b'uncommit.keep'
219 b'experimental', b'uncommit.keep'
221 )
220 )
222 newid = _commitfiltered(
221 newid = _commitfiltered(
223 repo,
222 repo,
224 old,
223 old,
225 match,
224 match,
226 keepcommit,
225 keepcommit,
227 message=message,
226 message=message,
228 user=opts.get(b'user'),
227 user=opts.get(b'user'),
229 date=opts.get(b'date'),
228 date=opts.get(b'date'),
230 )
229 )
231 if newid is None:
230 if newid is None:
232 ui.status(_(b"nothing to uncommit\n"))
231 ui.status(_(b"nothing to uncommit\n"))
233 return 1
232 return 1
234
233
235 mapping = {}
234 mapping = {}
236 if newid != old.p1().node():
235 if newid != old.p1().node():
237 # Move local changes on filtered changeset
236 # Move local changes on filtered changeset
238 mapping[old.node()] = (newid,)
237 mapping[old.node()] = (newid,)
239 else:
238 else:
240 # Fully removed the old commit
239 # Fully removed the old commit
241 mapping[old.node()] = ()
240 mapping[old.node()] = ()
242
241
243 with repo.dirstate.parentchange():
242 with repo.dirstate.parentchange():
244 scmutil.movedirstate(repo, repo[newid], match)
243 scmutil.movedirstate(repo, repo[newid], match)
245
244
246 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
245 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
247
246
248
247
249 def predecessormarkers(ctx):
248 def predecessormarkers(ctx):
250 """yields the obsolete markers marking the given changeset as a successor"""
249 """yields the obsolete markers marking the given changeset as a successor"""
251 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
250 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
252 yield obsutil.marker(ctx.repo(), data)
251 yield obsutil.marker(ctx.repo(), data)
253
252
254
253
255 @command(
254 @command(
256 b'unamend',
255 b'unamend',
257 [],
256 [],
258 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
257 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
259 helpbasic=True,
258 helpbasic=True,
260 )
259 )
261 def unamend(ui, repo, **opts):
260 def unamend(ui, repo, **opts):
262 """undo the most recent amend operation on a current changeset
261 """undo the most recent amend operation on a current changeset
263
262
264 This command will roll back to the previous version of a changeset,
263 This command will roll back to the previous version of a changeset,
265 leaving working directory in state in which it was before running
264 leaving working directory in state in which it was before running
266 `hg amend` (e.g. files modified as part of an amend will be
265 `hg amend` (e.g. files modified as part of an amend will be
267 marked as modified `hg status`)
266 marked as modified `hg status`)
268 """
267 """
269
268
270 unfi = repo.unfiltered()
269 unfi = repo.unfiltered()
271 with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
270 with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
272
271
273 # identify the commit from which to unamend
272 # identify the commit from which to unamend
274 curctx = repo[b'.']
273 curctx = repo[b'.']
275
274
276 rewriteutil.precheck(repo, [curctx.rev()], b'unamend')
275 rewriteutil.precheck(repo, [curctx.rev()], b'unamend')
277
276
278 # identify the commit to which to unamend
277 # identify the commit to which to unamend
279 markers = list(predecessormarkers(curctx))
278 markers = list(predecessormarkers(curctx))
280 if len(markers) != 1:
279 if len(markers) != 1:
281 e = _(b"changeset must have one predecessor, found %i predecessors")
280 e = _(b"changeset must have one predecessor, found %i predecessors")
282 raise error.InputError(e % len(markers))
281 raise error.InputError(e % len(markers))
283
282
284 prednode = markers[0].prednode()
283 prednode = markers[0].prednode()
285 predctx = unfi[prednode]
284 predctx = unfi[prednode]
286
285
287 # add an extra so that we get a new hash
286 # add an extra so that we get a new hash
288 # note: allowing unamend to undo an unamend is an intentional feature
287 # note: allowing unamend to undo an unamend is an intentional feature
289 extras = predctx.extra()
288 extras = predctx.extra()
290 extras[b'unamend_source'] = curctx.hex()
289 extras[b'unamend_source'] = curctx.hex()
291
290
292 def filectxfn(repo, ctx_, path):
291 def filectxfn(repo, ctx_, path):
293 try:
292 try:
294 return predctx.filectx(path)
293 return predctx.filectx(path)
295 except KeyError:
294 except KeyError:
296 return None
295 return None
297
296
298 # Make a new commit same as predctx
297 # Make a new commit same as predctx
299 newctx = context.memctx(
298 newctx = context.memctx(
300 repo,
299 repo,
301 parents=(predctx.p1(), predctx.p2()),
300 parents=(predctx.p1(), predctx.p2()),
302 text=predctx.description(),
301 text=predctx.description(),
303 files=predctx.files(),
302 files=predctx.files(),
304 filectxfn=filectxfn,
303 filectxfn=filectxfn,
305 user=predctx.user(),
304 user=predctx.user(),
306 date=predctx.date(),
305 date=predctx.date(),
307 extra=extras,
306 extra=extras,
308 )
307 )
309 newprednode = repo.commitctx(newctx)
308 newprednode = repo.commitctx(newctx)
310 newpredctx = repo[newprednode]
309 newpredctx = repo[newprednode]
311 dirstate = repo.dirstate
310 dirstate = repo.dirstate
312
311
313 with dirstate.parentchange():
312 with dirstate.parentchange():
314 scmutil.movedirstate(repo, newpredctx)
313 scmutil.movedirstate(repo, newpredctx)
315
314
316 mapping = {curctx.node(): (newprednode,)}
315 mapping = {curctx.node(): (newprednode,)}
317 scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
316 scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
@@ -1,3931 +1,3931 b''
1 # cmdutil.py - help for command processing in mercurial
1 # cmdutil.py - help for command processing in mercurial
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy as copymod
10 import copy as copymod
11 import errno
11 import errno
12 import os
12 import os
13 import re
13 import re
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 hex,
17 hex,
18 nullrev,
18 nullrev,
19 short,
19 short,
20 )
20 )
21 from .pycompat import (
21 from .pycompat import (
22 getattr,
22 getattr,
23 open,
23 open,
24 setattr,
24 setattr,
25 )
25 )
26 from .thirdparty import attr
26 from .thirdparty import attr
27
27
28 from . import (
28 from . import (
29 bookmarks,
29 bookmarks,
30 changelog,
30 changelog,
31 copies,
31 copies,
32 crecord as crecordmod,
32 crecord as crecordmod,
33 dirstateguard,
33 dirstateguard,
34 encoding,
34 encoding,
35 error,
35 error,
36 formatter,
36 formatter,
37 logcmdutil,
37 logcmdutil,
38 match as matchmod,
38 match as matchmod,
39 merge as mergemod,
39 merge as mergemod,
40 mergestate as mergestatemod,
40 mergestate as mergestatemod,
41 mergeutil,
41 mergeutil,
42 obsolete,
42 obsolete,
43 patch,
43 patch,
44 pathutil,
44 pathutil,
45 phases,
45 phases,
46 pycompat,
46 pycompat,
47 repair,
47 repair,
48 revlog,
48 revlog,
49 rewriteutil,
49 rewriteutil,
50 scmutil,
50 scmutil,
51 state as statemod,
51 state as statemod,
52 subrepoutil,
52 subrepoutil,
53 templatekw,
53 templatekw,
54 templater,
54 templater,
55 util,
55 util,
56 vfs as vfsmod,
56 vfs as vfsmod,
57 )
57 )
58
58
59 from .utils import (
59 from .utils import (
60 dateutil,
60 dateutil,
61 stringutil,
61 stringutil,
62 )
62 )
63
63
64 from .revlogutils import (
64 from .revlogutils import (
65 constants as revlog_constants,
65 constants as revlog_constants,
66 )
66 )
67
67
68 if pycompat.TYPE_CHECKING:
68 if pycompat.TYPE_CHECKING:
69 from typing import (
69 from typing import (
70 Any,
70 Any,
71 Dict,
71 Dict,
72 )
72 )
73
73
74 for t in (Any, Dict):
74 for t in (Any, Dict):
75 assert t
75 assert t
76
76
77 stringio = util.stringio
77 stringio = util.stringio
78
78
79 # templates of common command options
79 # templates of common command options
80
80
81 dryrunopts = [
81 dryrunopts = [
82 (b'n', b'dry-run', None, _(b'do not perform actions, just print output')),
82 (b'n', b'dry-run', None, _(b'do not perform actions, just print output')),
83 ]
83 ]
84
84
85 confirmopts = [
85 confirmopts = [
86 (b'', b'confirm', None, _(b'ask before applying actions')),
86 (b'', b'confirm', None, _(b'ask before applying actions')),
87 ]
87 ]
88
88
89 remoteopts = [
89 remoteopts = [
90 (b'e', b'ssh', b'', _(b'specify ssh command to use'), _(b'CMD')),
90 (b'e', b'ssh', b'', _(b'specify ssh command to use'), _(b'CMD')),
91 (
91 (
92 b'',
92 b'',
93 b'remotecmd',
93 b'remotecmd',
94 b'',
94 b'',
95 _(b'specify hg command to run on the remote side'),
95 _(b'specify hg command to run on the remote side'),
96 _(b'CMD'),
96 _(b'CMD'),
97 ),
97 ),
98 (
98 (
99 b'',
99 b'',
100 b'insecure',
100 b'insecure',
101 None,
101 None,
102 _(b'do not verify server certificate (ignoring web.cacerts config)'),
102 _(b'do not verify server certificate (ignoring web.cacerts config)'),
103 ),
103 ),
104 ]
104 ]
105
105
106 walkopts = [
106 walkopts = [
107 (
107 (
108 b'I',
108 b'I',
109 b'include',
109 b'include',
110 [],
110 [],
111 _(b'include names matching the given patterns'),
111 _(b'include names matching the given patterns'),
112 _(b'PATTERN'),
112 _(b'PATTERN'),
113 ),
113 ),
114 (
114 (
115 b'X',
115 b'X',
116 b'exclude',
116 b'exclude',
117 [],
117 [],
118 _(b'exclude names matching the given patterns'),
118 _(b'exclude names matching the given patterns'),
119 _(b'PATTERN'),
119 _(b'PATTERN'),
120 ),
120 ),
121 ]
121 ]
122
122
123 commitopts = [
123 commitopts = [
124 (b'm', b'message', b'', _(b'use text as commit message'), _(b'TEXT')),
124 (b'm', b'message', b'', _(b'use text as commit message'), _(b'TEXT')),
125 (b'l', b'logfile', b'', _(b'read commit message from file'), _(b'FILE')),
125 (b'l', b'logfile', b'', _(b'read commit message from file'), _(b'FILE')),
126 ]
126 ]
127
127
128 commitopts2 = [
128 commitopts2 = [
129 (
129 (
130 b'd',
130 b'd',
131 b'date',
131 b'date',
132 b'',
132 b'',
133 _(b'record the specified date as commit date'),
133 _(b'record the specified date as commit date'),
134 _(b'DATE'),
134 _(b'DATE'),
135 ),
135 ),
136 (
136 (
137 b'u',
137 b'u',
138 b'user',
138 b'user',
139 b'',
139 b'',
140 _(b'record the specified user as committer'),
140 _(b'record the specified user as committer'),
141 _(b'USER'),
141 _(b'USER'),
142 ),
142 ),
143 ]
143 ]
144
144
145 commitopts3 = [
145 commitopts3 = [
146 (b'D', b'currentdate', None, _(b'record the current date as commit date')),
146 (b'D', b'currentdate', None, _(b'record the current date as commit date')),
147 (b'U', b'currentuser', None, _(b'record the current user as committer')),
147 (b'U', b'currentuser', None, _(b'record the current user as committer')),
148 ]
148 ]
149
149
150 formatteropts = [
150 formatteropts = [
151 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
151 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
152 ]
152 ]
153
153
154 templateopts = [
154 templateopts = [
155 (
155 (
156 b'',
156 b'',
157 b'style',
157 b'style',
158 b'',
158 b'',
159 _(b'display using template map file (DEPRECATED)'),
159 _(b'display using template map file (DEPRECATED)'),
160 _(b'STYLE'),
160 _(b'STYLE'),
161 ),
161 ),
162 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
162 (b'T', b'template', b'', _(b'display with template'), _(b'TEMPLATE')),
163 ]
163 ]
164
164
165 logopts = [
165 logopts = [
166 (b'p', b'patch', None, _(b'show patch')),
166 (b'p', b'patch', None, _(b'show patch')),
167 (b'g', b'git', None, _(b'use git extended diff format')),
167 (b'g', b'git', None, _(b'use git extended diff format')),
168 (b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM')),
168 (b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM')),
169 (b'M', b'no-merges', None, _(b'do not show merges')),
169 (b'M', b'no-merges', None, _(b'do not show merges')),
170 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
170 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
171 (b'G', b'graph', None, _(b"show the revision DAG")),
171 (b'G', b'graph', None, _(b"show the revision DAG")),
172 ] + templateopts
172 ] + templateopts
173
173
174 diffopts = [
174 diffopts = [
175 (b'a', b'text', None, _(b'treat all files as text')),
175 (b'a', b'text', None, _(b'treat all files as text')),
176 (
176 (
177 b'g',
177 b'g',
178 b'git',
178 b'git',
179 None,
179 None,
180 _(b'use git extended diff format (DEFAULT: diff.git)'),
180 _(b'use git extended diff format (DEFAULT: diff.git)'),
181 ),
181 ),
182 (b'', b'binary', None, _(b'generate binary diffs in git mode (default)')),
182 (b'', b'binary', None, _(b'generate binary diffs in git mode (default)')),
183 (b'', b'nodates', None, _(b'omit dates from diff headers')),
183 (b'', b'nodates', None, _(b'omit dates from diff headers')),
184 ]
184 ]
185
185
186 diffwsopts = [
186 diffwsopts = [
187 (
187 (
188 b'w',
188 b'w',
189 b'ignore-all-space',
189 b'ignore-all-space',
190 None,
190 None,
191 _(b'ignore white space when comparing lines'),
191 _(b'ignore white space when comparing lines'),
192 ),
192 ),
193 (
193 (
194 b'b',
194 b'b',
195 b'ignore-space-change',
195 b'ignore-space-change',
196 None,
196 None,
197 _(b'ignore changes in the amount of white space'),
197 _(b'ignore changes in the amount of white space'),
198 ),
198 ),
199 (
199 (
200 b'B',
200 b'B',
201 b'ignore-blank-lines',
201 b'ignore-blank-lines',
202 None,
202 None,
203 _(b'ignore changes whose lines are all blank'),
203 _(b'ignore changes whose lines are all blank'),
204 ),
204 ),
205 (
205 (
206 b'Z',
206 b'Z',
207 b'ignore-space-at-eol',
207 b'ignore-space-at-eol',
208 None,
208 None,
209 _(b'ignore changes in whitespace at EOL'),
209 _(b'ignore changes in whitespace at EOL'),
210 ),
210 ),
211 ]
211 ]
212
212
213 diffopts2 = (
213 diffopts2 = (
214 [
214 [
215 (b'', b'noprefix', None, _(b'omit a/ and b/ prefixes from filenames')),
215 (b'', b'noprefix', None, _(b'omit a/ and b/ prefixes from filenames')),
216 (
216 (
217 b'p',
217 b'p',
218 b'show-function',
218 b'show-function',
219 None,
219 None,
220 _(
220 _(
221 b'show which function each change is in (DEFAULT: diff.showfunc)'
221 b'show which function each change is in (DEFAULT: diff.showfunc)'
222 ),
222 ),
223 ),
223 ),
224 (b'', b'reverse', None, _(b'produce a diff that undoes the changes')),
224 (b'', b'reverse', None, _(b'produce a diff that undoes the changes')),
225 ]
225 ]
226 + diffwsopts
226 + diffwsopts
227 + [
227 + [
228 (
228 (
229 b'U',
229 b'U',
230 b'unified',
230 b'unified',
231 b'',
231 b'',
232 _(b'number of lines of context to show'),
232 _(b'number of lines of context to show'),
233 _(b'NUM'),
233 _(b'NUM'),
234 ),
234 ),
235 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
235 (b'', b'stat', None, _(b'output diffstat-style summary of changes')),
236 (
236 (
237 b'',
237 b'',
238 b'root',
238 b'root',
239 b'',
239 b'',
240 _(b'produce diffs relative to subdirectory'),
240 _(b'produce diffs relative to subdirectory'),
241 _(b'DIR'),
241 _(b'DIR'),
242 ),
242 ),
243 ]
243 ]
244 )
244 )
245
245
246 mergetoolopts = [
246 mergetoolopts = [
247 (b't', b'tool', b'', _(b'specify merge tool'), _(b'TOOL')),
247 (b't', b'tool', b'', _(b'specify merge tool'), _(b'TOOL')),
248 ]
248 ]
249
249
250 similarityopts = [
250 similarityopts = [
251 (
251 (
252 b's',
252 b's',
253 b'similarity',
253 b'similarity',
254 b'',
254 b'',
255 _(b'guess renamed files by similarity (0<=s<=100)'),
255 _(b'guess renamed files by similarity (0<=s<=100)'),
256 _(b'SIMILARITY'),
256 _(b'SIMILARITY'),
257 )
257 )
258 ]
258 ]
259
259
260 subrepoopts = [(b'S', b'subrepos', None, _(b'recurse into subrepositories'))]
260 subrepoopts = [(b'S', b'subrepos', None, _(b'recurse into subrepositories'))]
261
261
262 debugrevlogopts = [
262 debugrevlogopts = [
263 (b'c', b'changelog', False, _(b'open changelog')),
263 (b'c', b'changelog', False, _(b'open changelog')),
264 (b'm', b'manifest', False, _(b'open manifest')),
264 (b'm', b'manifest', False, _(b'open manifest')),
265 (b'', b'dir', b'', _(b'open directory manifest')),
265 (b'', b'dir', b'', _(b'open directory manifest')),
266 ]
266 ]
267
267
268 # special string such that everything below this line will be ingored in the
268 # special string such that everything below this line will be ingored in the
269 # editor text
269 # editor text
270 _linebelow = b"^HG: ------------------------ >8 ------------------------$"
270 _linebelow = b"^HG: ------------------------ >8 ------------------------$"
271
271
272
272
273 def check_at_most_one_arg(opts, *args):
273 def check_at_most_one_arg(opts, *args):
274 """abort if more than one of the arguments are in opts
274 """abort if more than one of the arguments are in opts
275
275
276 Returns the unique argument or None if none of them were specified.
276 Returns the unique argument or None if none of them were specified.
277 """
277 """
278
278
279 def to_display(name):
279 def to_display(name):
280 return pycompat.sysbytes(name).replace(b'_', b'-')
280 return pycompat.sysbytes(name).replace(b'_', b'-')
281
281
282 previous = None
282 previous = None
283 for x in args:
283 for x in args:
284 if opts.get(x):
284 if opts.get(x):
285 if previous:
285 if previous:
286 raise error.InputError(
286 raise error.InputError(
287 _(b'cannot specify both --%s and --%s')
287 _(b'cannot specify both --%s and --%s')
288 % (to_display(previous), to_display(x))
288 % (to_display(previous), to_display(x))
289 )
289 )
290 previous = x
290 previous = x
291 return previous
291 return previous
292
292
293
293
294 def check_incompatible_arguments(opts, first, others):
294 def check_incompatible_arguments(opts, first, others):
295 """abort if the first argument is given along with any of the others
295 """abort if the first argument is given along with any of the others
296
296
297 Unlike check_at_most_one_arg(), `others` are not mutually exclusive
297 Unlike check_at_most_one_arg(), `others` are not mutually exclusive
298 among themselves, and they're passed as a single collection.
298 among themselves, and they're passed as a single collection.
299 """
299 """
300 for other in others:
300 for other in others:
301 check_at_most_one_arg(opts, first, other)
301 check_at_most_one_arg(opts, first, other)
302
302
303
303
304 def resolvecommitoptions(ui, opts):
304 def resolvecommitoptions(ui, opts):
305 """modify commit options dict to handle related options
305 """modify commit options dict to handle related options
306
306
307 The return value indicates that ``rewrite.update-timestamp`` is the reason
307 The return value indicates that ``rewrite.update-timestamp`` is the reason
308 the ``date`` option is set.
308 the ``date`` option is set.
309 """
309 """
310 check_at_most_one_arg(opts, b'date', b'currentdate')
310 check_at_most_one_arg(opts, b'date', b'currentdate')
311 check_at_most_one_arg(opts, b'user', b'currentuser')
311 check_at_most_one_arg(opts, b'user', b'currentuser')
312
312
313 datemaydiffer = False # date-only change should be ignored?
313 datemaydiffer = False # date-only change should be ignored?
314
314
315 if opts.get(b'currentdate'):
315 if opts.get(b'currentdate'):
316 opts[b'date'] = b'%d %d' % dateutil.makedate()
316 opts[b'date'] = b'%d %d' % dateutil.makedate()
317 elif (
317 elif (
318 not opts.get(b'date')
318 not opts.get(b'date')
319 and ui.configbool(b'rewrite', b'update-timestamp')
319 and ui.configbool(b'rewrite', b'update-timestamp')
320 and opts.get(b'currentdate') is None
320 and opts.get(b'currentdate') is None
321 ):
321 ):
322 opts[b'date'] = b'%d %d' % dateutil.makedate()
322 opts[b'date'] = b'%d %d' % dateutil.makedate()
323 datemaydiffer = True
323 datemaydiffer = True
324
324
325 if opts.get(b'currentuser'):
325 if opts.get(b'currentuser'):
326 opts[b'user'] = ui.username()
326 opts[b'user'] = ui.username()
327
327
328 return datemaydiffer
328 return datemaydiffer
329
329
330
330
331 def checknotesize(ui, opts):
331 def check_note_size(opts):
332 """make sure note is of valid format"""
332 """make sure note is of valid format"""
333
333
334 note = opts.get(b'note')
334 note = opts.get('note')
335 if not note:
335 if not note:
336 return
336 return
337
337
338 if len(note) > 255:
338 if len(note) > 255:
339 raise error.InputError(_(b"cannot store a note of more than 255 bytes"))
339 raise error.InputError(_(b"cannot store a note of more than 255 bytes"))
340 if b'\n' in note:
340 if b'\n' in note:
341 raise error.InputError(_(b"note cannot contain a newline"))
341 raise error.InputError(_(b"note cannot contain a newline"))
342
342
343
343
344 def ishunk(x):
344 def ishunk(x):
345 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
345 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
346 return isinstance(x, hunkclasses)
346 return isinstance(x, hunkclasses)
347
347
348
348
349 def newandmodified(chunks, originalchunks):
349 def newandmodified(chunks, originalchunks):
350 newlyaddedandmodifiedfiles = set()
350 newlyaddedandmodifiedfiles = set()
351 alsorestore = set()
351 alsorestore = set()
352 for chunk in chunks:
352 for chunk in chunks:
353 if (
353 if (
354 ishunk(chunk)
354 ishunk(chunk)
355 and chunk.header.isnewfile()
355 and chunk.header.isnewfile()
356 and chunk not in originalchunks
356 and chunk not in originalchunks
357 ):
357 ):
358 newlyaddedandmodifiedfiles.add(chunk.header.filename())
358 newlyaddedandmodifiedfiles.add(chunk.header.filename())
359 alsorestore.update(
359 alsorestore.update(
360 set(chunk.header.files()) - {chunk.header.filename()}
360 set(chunk.header.files()) - {chunk.header.filename()}
361 )
361 )
362 return newlyaddedandmodifiedfiles, alsorestore
362 return newlyaddedandmodifiedfiles, alsorestore
363
363
364
364
365 def parsealiases(cmd):
365 def parsealiases(cmd):
366 base_aliases = cmd.split(b"|")
366 base_aliases = cmd.split(b"|")
367 all_aliases = set(base_aliases)
367 all_aliases = set(base_aliases)
368 extra_aliases = []
368 extra_aliases = []
369 for alias in base_aliases:
369 for alias in base_aliases:
370 if b'-' in alias:
370 if b'-' in alias:
371 folded_alias = alias.replace(b'-', b'')
371 folded_alias = alias.replace(b'-', b'')
372 if folded_alias not in all_aliases:
372 if folded_alias not in all_aliases:
373 all_aliases.add(folded_alias)
373 all_aliases.add(folded_alias)
374 extra_aliases.append(folded_alias)
374 extra_aliases.append(folded_alias)
375 base_aliases.extend(extra_aliases)
375 base_aliases.extend(extra_aliases)
376 return base_aliases
376 return base_aliases
377
377
378
378
379 def setupwrapcolorwrite(ui):
379 def setupwrapcolorwrite(ui):
380 # wrap ui.write so diff output can be labeled/colorized
380 # wrap ui.write so diff output can be labeled/colorized
381 def wrapwrite(orig, *args, **kw):
381 def wrapwrite(orig, *args, **kw):
382 label = kw.pop('label', b'')
382 label = kw.pop('label', b'')
383 for chunk, l in patch.difflabel(lambda: args):
383 for chunk, l in patch.difflabel(lambda: args):
384 orig(chunk, label=label + l)
384 orig(chunk, label=label + l)
385
385
386 oldwrite = ui.write
386 oldwrite = ui.write
387
387
388 def wrap(*args, **kwargs):
388 def wrap(*args, **kwargs):
389 return wrapwrite(oldwrite, *args, **kwargs)
389 return wrapwrite(oldwrite, *args, **kwargs)
390
390
391 setattr(ui, 'write', wrap)
391 setattr(ui, 'write', wrap)
392 return oldwrite
392 return oldwrite
393
393
394
394
395 def filterchunks(ui, originalhunks, usecurses, testfile, match, operation=None):
395 def filterchunks(ui, originalhunks, usecurses, testfile, match, operation=None):
396 try:
396 try:
397 if usecurses:
397 if usecurses:
398 if testfile:
398 if testfile:
399 recordfn = crecordmod.testdecorator(
399 recordfn = crecordmod.testdecorator(
400 testfile, crecordmod.testchunkselector
400 testfile, crecordmod.testchunkselector
401 )
401 )
402 else:
402 else:
403 recordfn = crecordmod.chunkselector
403 recordfn = crecordmod.chunkselector
404
404
405 return crecordmod.filterpatch(
405 return crecordmod.filterpatch(
406 ui, originalhunks, recordfn, operation
406 ui, originalhunks, recordfn, operation
407 )
407 )
408 except crecordmod.fallbackerror as e:
408 except crecordmod.fallbackerror as e:
409 ui.warn(b'%s\n' % e)
409 ui.warn(b'%s\n' % e)
410 ui.warn(_(b'falling back to text mode\n'))
410 ui.warn(_(b'falling back to text mode\n'))
411
411
412 return patch.filterpatch(ui, originalhunks, match, operation)
412 return patch.filterpatch(ui, originalhunks, match, operation)
413
413
414
414
415 def recordfilter(ui, originalhunks, match, operation=None):
415 def recordfilter(ui, originalhunks, match, operation=None):
416 """Prompts the user to filter the originalhunks and return a list of
416 """Prompts the user to filter the originalhunks and return a list of
417 selected hunks.
417 selected hunks.
418 *operation* is used for to build ui messages to indicate the user what
418 *operation* is used for to build ui messages to indicate the user what
419 kind of filtering they are doing: reverting, committing, shelving, etc.
419 kind of filtering they are doing: reverting, committing, shelving, etc.
420 (see patch.filterpatch).
420 (see patch.filterpatch).
421 """
421 """
422 usecurses = crecordmod.checkcurses(ui)
422 usecurses = crecordmod.checkcurses(ui)
423 testfile = ui.config(b'experimental', b'crecordtest')
423 testfile = ui.config(b'experimental', b'crecordtest')
424 oldwrite = setupwrapcolorwrite(ui)
424 oldwrite = setupwrapcolorwrite(ui)
425 try:
425 try:
426 newchunks, newopts = filterchunks(
426 newchunks, newopts = filterchunks(
427 ui, originalhunks, usecurses, testfile, match, operation
427 ui, originalhunks, usecurses, testfile, match, operation
428 )
428 )
429 finally:
429 finally:
430 ui.write = oldwrite
430 ui.write = oldwrite
431 return newchunks, newopts
431 return newchunks, newopts
432
432
433
433
434 def dorecord(
434 def dorecord(
435 ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts
435 ui, repo, commitfunc, cmdsuggest, backupall, filterfn, *pats, **opts
436 ):
436 ):
437 opts = pycompat.byteskwargs(opts)
437 opts = pycompat.byteskwargs(opts)
438 if not ui.interactive():
438 if not ui.interactive():
439 if cmdsuggest:
439 if cmdsuggest:
440 msg = _(b'running non-interactively, use %s instead') % cmdsuggest
440 msg = _(b'running non-interactively, use %s instead') % cmdsuggest
441 else:
441 else:
442 msg = _(b'running non-interactively')
442 msg = _(b'running non-interactively')
443 raise error.InputError(msg)
443 raise error.InputError(msg)
444
444
445 # make sure username is set before going interactive
445 # make sure username is set before going interactive
446 if not opts.get(b'user'):
446 if not opts.get(b'user'):
447 ui.username() # raise exception, username not provided
447 ui.username() # raise exception, username not provided
448
448
449 def recordfunc(ui, repo, message, match, opts):
449 def recordfunc(ui, repo, message, match, opts):
450 """This is generic record driver.
450 """This is generic record driver.
451
451
452 Its job is to interactively filter local changes, and
452 Its job is to interactively filter local changes, and
453 accordingly prepare working directory into a state in which the
453 accordingly prepare working directory into a state in which the
454 job can be delegated to a non-interactive commit command such as
454 job can be delegated to a non-interactive commit command such as
455 'commit' or 'qrefresh'.
455 'commit' or 'qrefresh'.
456
456
457 After the actual job is done by non-interactive command, the
457 After the actual job is done by non-interactive command, the
458 working directory is restored to its original state.
458 working directory is restored to its original state.
459
459
460 In the end we'll record interesting changes, and everything else
460 In the end we'll record interesting changes, and everything else
461 will be left in place, so the user can continue working.
461 will be left in place, so the user can continue working.
462 """
462 """
463 if not opts.get(b'interactive-unshelve'):
463 if not opts.get(b'interactive-unshelve'):
464 checkunfinished(repo, commit=True)
464 checkunfinished(repo, commit=True)
465 wctx = repo[None]
465 wctx = repo[None]
466 merge = len(wctx.parents()) > 1
466 merge = len(wctx.parents()) > 1
467 if merge:
467 if merge:
468 raise error.InputError(
468 raise error.InputError(
469 _(
469 _(
470 b'cannot partially commit a merge '
470 b'cannot partially commit a merge '
471 b'(use "hg commit" instead)'
471 b'(use "hg commit" instead)'
472 )
472 )
473 )
473 )
474
474
475 def fail(f, msg):
475 def fail(f, msg):
476 raise error.InputError(b'%s: %s' % (f, msg))
476 raise error.InputError(b'%s: %s' % (f, msg))
477
477
478 force = opts.get(b'force')
478 force = opts.get(b'force')
479 if not force:
479 if not force:
480 match = matchmod.badmatch(match, fail)
480 match = matchmod.badmatch(match, fail)
481
481
482 status = repo.status(match=match)
482 status = repo.status(match=match)
483
483
484 overrides = {(b'ui', b'commitsubrepos'): True}
484 overrides = {(b'ui', b'commitsubrepos'): True}
485
485
486 with repo.ui.configoverride(overrides, b'record'):
486 with repo.ui.configoverride(overrides, b'record'):
487 # subrepoutil.precommit() modifies the status
487 # subrepoutil.precommit() modifies the status
488 tmpstatus = scmutil.status(
488 tmpstatus = scmutil.status(
489 copymod.copy(status.modified),
489 copymod.copy(status.modified),
490 copymod.copy(status.added),
490 copymod.copy(status.added),
491 copymod.copy(status.removed),
491 copymod.copy(status.removed),
492 copymod.copy(status.deleted),
492 copymod.copy(status.deleted),
493 copymod.copy(status.unknown),
493 copymod.copy(status.unknown),
494 copymod.copy(status.ignored),
494 copymod.copy(status.ignored),
495 copymod.copy(status.clean), # pytype: disable=wrong-arg-count
495 copymod.copy(status.clean), # pytype: disable=wrong-arg-count
496 )
496 )
497
497
498 # Force allows -X subrepo to skip the subrepo.
498 # Force allows -X subrepo to skip the subrepo.
499 subs, commitsubs, newstate = subrepoutil.precommit(
499 subs, commitsubs, newstate = subrepoutil.precommit(
500 repo.ui, wctx, tmpstatus, match, force=True
500 repo.ui, wctx, tmpstatus, match, force=True
501 )
501 )
502 for s in subs:
502 for s in subs:
503 if s in commitsubs:
503 if s in commitsubs:
504 dirtyreason = wctx.sub(s).dirtyreason(True)
504 dirtyreason = wctx.sub(s).dirtyreason(True)
505 raise error.Abort(dirtyreason)
505 raise error.Abort(dirtyreason)
506
506
507 if not force:
507 if not force:
508 repo.checkcommitpatterns(wctx, match, status, fail)
508 repo.checkcommitpatterns(wctx, match, status, fail)
509 diffopts = patch.difffeatureopts(
509 diffopts = patch.difffeatureopts(
510 ui,
510 ui,
511 opts=opts,
511 opts=opts,
512 whitespace=True,
512 whitespace=True,
513 section=b'commands',
513 section=b'commands',
514 configprefix=b'commit.interactive.',
514 configprefix=b'commit.interactive.',
515 )
515 )
516 diffopts.nodates = True
516 diffopts.nodates = True
517 diffopts.git = True
517 diffopts.git = True
518 diffopts.showfunc = True
518 diffopts.showfunc = True
519 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
519 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
520 originalchunks = patch.parsepatch(originaldiff)
520 originalchunks = patch.parsepatch(originaldiff)
521 match = scmutil.match(repo[None], pats)
521 match = scmutil.match(repo[None], pats)
522
522
523 # 1. filter patch, since we are intending to apply subset of it
523 # 1. filter patch, since we are intending to apply subset of it
524 try:
524 try:
525 chunks, newopts = filterfn(ui, originalchunks, match)
525 chunks, newopts = filterfn(ui, originalchunks, match)
526 except error.PatchError as err:
526 except error.PatchError as err:
527 raise error.InputError(_(b'error parsing patch: %s') % err)
527 raise error.InputError(_(b'error parsing patch: %s') % err)
528 opts.update(newopts)
528 opts.update(newopts)
529
529
530 # We need to keep a backup of files that have been newly added and
530 # We need to keep a backup of files that have been newly added and
531 # modified during the recording process because there is a previous
531 # modified during the recording process because there is a previous
532 # version without the edit in the workdir. We also will need to restore
532 # version without the edit in the workdir. We also will need to restore
533 # files that were the sources of renames so that the patch application
533 # files that were the sources of renames so that the patch application
534 # works.
534 # works.
535 newlyaddedandmodifiedfiles, alsorestore = newandmodified(
535 newlyaddedandmodifiedfiles, alsorestore = newandmodified(
536 chunks, originalchunks
536 chunks, originalchunks
537 )
537 )
538 contenders = set()
538 contenders = set()
539 for h in chunks:
539 for h in chunks:
540 try:
540 try:
541 contenders.update(set(h.files()))
541 contenders.update(set(h.files()))
542 except AttributeError:
542 except AttributeError:
543 pass
543 pass
544
544
545 changed = status.modified + status.added + status.removed
545 changed = status.modified + status.added + status.removed
546 newfiles = [f for f in changed if f in contenders]
546 newfiles = [f for f in changed if f in contenders]
547 if not newfiles:
547 if not newfiles:
548 ui.status(_(b'no changes to record\n'))
548 ui.status(_(b'no changes to record\n'))
549 return 0
549 return 0
550
550
551 modified = set(status.modified)
551 modified = set(status.modified)
552
552
553 # 2. backup changed files, so we can restore them in the end
553 # 2. backup changed files, so we can restore them in the end
554
554
555 if backupall:
555 if backupall:
556 tobackup = changed
556 tobackup = changed
557 else:
557 else:
558 tobackup = [
558 tobackup = [
559 f
559 f
560 for f in newfiles
560 for f in newfiles
561 if f in modified or f in newlyaddedandmodifiedfiles
561 if f in modified or f in newlyaddedandmodifiedfiles
562 ]
562 ]
563 backups = {}
563 backups = {}
564 if tobackup:
564 if tobackup:
565 backupdir = repo.vfs.join(b'record-backups')
565 backupdir = repo.vfs.join(b'record-backups')
566 try:
566 try:
567 os.mkdir(backupdir)
567 os.mkdir(backupdir)
568 except OSError as err:
568 except OSError as err:
569 if err.errno != errno.EEXIST:
569 if err.errno != errno.EEXIST:
570 raise
570 raise
571 try:
571 try:
572 # backup continues
572 # backup continues
573 for f in tobackup:
573 for f in tobackup:
574 fd, tmpname = pycompat.mkstemp(
574 fd, tmpname = pycompat.mkstemp(
575 prefix=os.path.basename(f) + b'.', dir=backupdir
575 prefix=os.path.basename(f) + b'.', dir=backupdir
576 )
576 )
577 os.close(fd)
577 os.close(fd)
578 ui.debug(b'backup %r as %r\n' % (f, tmpname))
578 ui.debug(b'backup %r as %r\n' % (f, tmpname))
579 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
579 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
580 backups[f] = tmpname
580 backups[f] = tmpname
581
581
582 fp = stringio()
582 fp = stringio()
583 for c in chunks:
583 for c in chunks:
584 fname = c.filename()
584 fname = c.filename()
585 if fname in backups:
585 if fname in backups:
586 c.write(fp)
586 c.write(fp)
587 dopatch = fp.tell()
587 dopatch = fp.tell()
588 fp.seek(0)
588 fp.seek(0)
589
589
590 # 2.5 optionally review / modify patch in text editor
590 # 2.5 optionally review / modify patch in text editor
591 if opts.get(b'review', False):
591 if opts.get(b'review', False):
592 patchtext = (
592 patchtext = (
593 crecordmod.diffhelptext
593 crecordmod.diffhelptext
594 + crecordmod.patchhelptext
594 + crecordmod.patchhelptext
595 + fp.read()
595 + fp.read()
596 )
596 )
597 reviewedpatch = ui.edit(
597 reviewedpatch = ui.edit(
598 patchtext, b"", action=b"diff", repopath=repo.path
598 patchtext, b"", action=b"diff", repopath=repo.path
599 )
599 )
600 fp.truncate(0)
600 fp.truncate(0)
601 fp.write(reviewedpatch)
601 fp.write(reviewedpatch)
602 fp.seek(0)
602 fp.seek(0)
603
603
604 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
604 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
605 # 3a. apply filtered patch to clean repo (clean)
605 # 3a. apply filtered patch to clean repo (clean)
606 if backups:
606 if backups:
607 m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore)
607 m = scmutil.matchfiles(repo, set(backups.keys()) | alsorestore)
608 mergemod.revert_to(repo[b'.'], matcher=m)
608 mergemod.revert_to(repo[b'.'], matcher=m)
609
609
610 # 3b. (apply)
610 # 3b. (apply)
611 if dopatch:
611 if dopatch:
612 try:
612 try:
613 ui.debug(b'applying patch\n')
613 ui.debug(b'applying patch\n')
614 ui.debug(fp.getvalue())
614 ui.debug(fp.getvalue())
615 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
615 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
616 except error.PatchError as err:
616 except error.PatchError as err:
617 raise error.InputError(pycompat.bytestr(err))
617 raise error.InputError(pycompat.bytestr(err))
618 del fp
618 del fp
619
619
620 # 4. We prepared working directory according to filtered
620 # 4. We prepared working directory according to filtered
621 # patch. Now is the time to delegate the job to
621 # patch. Now is the time to delegate the job to
622 # commit/qrefresh or the like!
622 # commit/qrefresh or the like!
623
623
624 # Make all of the pathnames absolute.
624 # Make all of the pathnames absolute.
625 newfiles = [repo.wjoin(nf) for nf in newfiles]
625 newfiles = [repo.wjoin(nf) for nf in newfiles]
626 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
626 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
627 finally:
627 finally:
628 # 5. finally restore backed-up files
628 # 5. finally restore backed-up files
629 try:
629 try:
630 dirstate = repo.dirstate
630 dirstate = repo.dirstate
631 for realname, tmpname in pycompat.iteritems(backups):
631 for realname, tmpname in pycompat.iteritems(backups):
632 ui.debug(b'restoring %r to %r\n' % (tmpname, realname))
632 ui.debug(b'restoring %r to %r\n' % (tmpname, realname))
633
633
634 if dirstate[realname] == b'n':
634 if dirstate[realname] == b'n':
635 # without normallookup, restoring timestamp
635 # without normallookup, restoring timestamp
636 # may cause partially committed files
636 # may cause partially committed files
637 # to be treated as unmodified
637 # to be treated as unmodified
638 dirstate.normallookup(realname)
638 dirstate.normallookup(realname)
639
639
640 # copystat=True here and above are a hack to trick any
640 # copystat=True here and above are a hack to trick any
641 # editors that have f open that we haven't modified them.
641 # editors that have f open that we haven't modified them.
642 #
642 #
643 # Also note that this racy as an editor could notice the
643 # Also note that this racy as an editor could notice the
644 # file's mtime before we've finished writing it.
644 # file's mtime before we've finished writing it.
645 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
645 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
646 os.unlink(tmpname)
646 os.unlink(tmpname)
647 if tobackup:
647 if tobackup:
648 os.rmdir(backupdir)
648 os.rmdir(backupdir)
649 except OSError:
649 except OSError:
650 pass
650 pass
651
651
652 def recordinwlock(ui, repo, message, match, opts):
652 def recordinwlock(ui, repo, message, match, opts):
653 with repo.wlock():
653 with repo.wlock():
654 return recordfunc(ui, repo, message, match, opts)
654 return recordfunc(ui, repo, message, match, opts)
655
655
656 return commit(ui, repo, recordinwlock, pats, opts)
656 return commit(ui, repo, recordinwlock, pats, opts)
657
657
658
658
659 class dirnode(object):
659 class dirnode(object):
660 """
660 """
661 Represent a directory in user working copy with information required for
661 Represent a directory in user working copy with information required for
662 the purpose of tersing its status.
662 the purpose of tersing its status.
663
663
664 path is the path to the directory, without a trailing '/'
664 path is the path to the directory, without a trailing '/'
665
665
666 statuses is a set of statuses of all files in this directory (this includes
666 statuses is a set of statuses of all files in this directory (this includes
667 all the files in all the subdirectories too)
667 all the files in all the subdirectories too)
668
668
669 files is a list of files which are direct child of this directory
669 files is a list of files which are direct child of this directory
670
670
671 subdirs is a dictionary of sub-directory name as the key and it's own
671 subdirs is a dictionary of sub-directory name as the key and it's own
672 dirnode object as the value
672 dirnode object as the value
673 """
673 """
674
674
675 def __init__(self, dirpath):
675 def __init__(self, dirpath):
676 self.path = dirpath
676 self.path = dirpath
677 self.statuses = set()
677 self.statuses = set()
678 self.files = []
678 self.files = []
679 self.subdirs = {}
679 self.subdirs = {}
680
680
681 def _addfileindir(self, filename, status):
681 def _addfileindir(self, filename, status):
682 """Add a file in this directory as a direct child."""
682 """Add a file in this directory as a direct child."""
683 self.files.append((filename, status))
683 self.files.append((filename, status))
684
684
685 def addfile(self, filename, status):
685 def addfile(self, filename, status):
686 """
686 """
687 Add a file to this directory or to its direct parent directory.
687 Add a file to this directory or to its direct parent directory.
688
688
689 If the file is not direct child of this directory, we traverse to the
689 If the file is not direct child of this directory, we traverse to the
690 directory of which this file is a direct child of and add the file
690 directory of which this file is a direct child of and add the file
691 there.
691 there.
692 """
692 """
693
693
694 # the filename contains a path separator, it means it's not the direct
694 # the filename contains a path separator, it means it's not the direct
695 # child of this directory
695 # child of this directory
696 if b'/' in filename:
696 if b'/' in filename:
697 subdir, filep = filename.split(b'/', 1)
697 subdir, filep = filename.split(b'/', 1)
698
698
699 # does the dirnode object for subdir exists
699 # does the dirnode object for subdir exists
700 if subdir not in self.subdirs:
700 if subdir not in self.subdirs:
701 subdirpath = pathutil.join(self.path, subdir)
701 subdirpath = pathutil.join(self.path, subdir)
702 self.subdirs[subdir] = dirnode(subdirpath)
702 self.subdirs[subdir] = dirnode(subdirpath)
703
703
704 # try adding the file in subdir
704 # try adding the file in subdir
705 self.subdirs[subdir].addfile(filep, status)
705 self.subdirs[subdir].addfile(filep, status)
706
706
707 else:
707 else:
708 self._addfileindir(filename, status)
708 self._addfileindir(filename, status)
709
709
710 if status not in self.statuses:
710 if status not in self.statuses:
711 self.statuses.add(status)
711 self.statuses.add(status)
712
712
713 def iterfilepaths(self):
713 def iterfilepaths(self):
714 """Yield (status, path) for files directly under this directory."""
714 """Yield (status, path) for files directly under this directory."""
715 for f, st in self.files:
715 for f, st in self.files:
716 yield st, pathutil.join(self.path, f)
716 yield st, pathutil.join(self.path, f)
717
717
718 def tersewalk(self, terseargs):
718 def tersewalk(self, terseargs):
719 """
719 """
720 Yield (status, path) obtained by processing the status of this
720 Yield (status, path) obtained by processing the status of this
721 dirnode.
721 dirnode.
722
722
723 terseargs is the string of arguments passed by the user with `--terse`
723 terseargs is the string of arguments passed by the user with `--terse`
724 flag.
724 flag.
725
725
726 Following are the cases which can happen:
726 Following are the cases which can happen:
727
727
728 1) All the files in the directory (including all the files in its
728 1) All the files in the directory (including all the files in its
729 subdirectories) share the same status and the user has asked us to terse
729 subdirectories) share the same status and the user has asked us to terse
730 that status. -> yield (status, dirpath). dirpath will end in '/'.
730 that status. -> yield (status, dirpath). dirpath will end in '/'.
731
731
732 2) Otherwise, we do following:
732 2) Otherwise, we do following:
733
733
734 a) Yield (status, filepath) for all the files which are in this
734 a) Yield (status, filepath) for all the files which are in this
735 directory (only the ones in this directory, not the subdirs)
735 directory (only the ones in this directory, not the subdirs)
736
736
737 b) Recurse the function on all the subdirectories of this
737 b) Recurse the function on all the subdirectories of this
738 directory
738 directory
739 """
739 """
740
740
741 if len(self.statuses) == 1:
741 if len(self.statuses) == 1:
742 onlyst = self.statuses.pop()
742 onlyst = self.statuses.pop()
743
743
744 # Making sure we terse only when the status abbreviation is
744 # Making sure we terse only when the status abbreviation is
745 # passed as terse argument
745 # passed as terse argument
746 if onlyst in terseargs:
746 if onlyst in terseargs:
747 yield onlyst, self.path + b'/'
747 yield onlyst, self.path + b'/'
748 return
748 return
749
749
750 # add the files to status list
750 # add the files to status list
751 for st, fpath in self.iterfilepaths():
751 for st, fpath in self.iterfilepaths():
752 yield st, fpath
752 yield st, fpath
753
753
754 # recurse on the subdirs
754 # recurse on the subdirs
755 for dirobj in self.subdirs.values():
755 for dirobj in self.subdirs.values():
756 for st, fpath in dirobj.tersewalk(terseargs):
756 for st, fpath in dirobj.tersewalk(terseargs):
757 yield st, fpath
757 yield st, fpath
758
758
759
759
760 def tersedir(statuslist, terseargs):
760 def tersedir(statuslist, terseargs):
761 """
761 """
762 Terse the status if all the files in a directory shares the same status.
762 Terse the status if all the files in a directory shares the same status.
763
763
764 statuslist is scmutil.status() object which contains a list of files for
764 statuslist is scmutil.status() object which contains a list of files for
765 each status.
765 each status.
766 terseargs is string which is passed by the user as the argument to `--terse`
766 terseargs is string which is passed by the user as the argument to `--terse`
767 flag.
767 flag.
768
768
769 The function makes a tree of objects of dirnode class, and at each node it
769 The function makes a tree of objects of dirnode class, and at each node it
770 stores the information required to know whether we can terse a certain
770 stores the information required to know whether we can terse a certain
771 directory or not.
771 directory or not.
772 """
772 """
773 # the order matters here as that is used to produce final list
773 # the order matters here as that is used to produce final list
774 allst = (b'm', b'a', b'r', b'd', b'u', b'i', b'c')
774 allst = (b'm', b'a', b'r', b'd', b'u', b'i', b'c')
775
775
776 # checking the argument validity
776 # checking the argument validity
777 for s in pycompat.bytestr(terseargs):
777 for s in pycompat.bytestr(terseargs):
778 if s not in allst:
778 if s not in allst:
779 raise error.InputError(_(b"'%s' not recognized") % s)
779 raise error.InputError(_(b"'%s' not recognized") % s)
780
780
781 # creating a dirnode object for the root of the repo
781 # creating a dirnode object for the root of the repo
782 rootobj = dirnode(b'')
782 rootobj = dirnode(b'')
783 pstatus = (
783 pstatus = (
784 b'modified',
784 b'modified',
785 b'added',
785 b'added',
786 b'deleted',
786 b'deleted',
787 b'clean',
787 b'clean',
788 b'unknown',
788 b'unknown',
789 b'ignored',
789 b'ignored',
790 b'removed',
790 b'removed',
791 )
791 )
792
792
793 tersedict = {}
793 tersedict = {}
794 for attrname in pstatus:
794 for attrname in pstatus:
795 statuschar = attrname[0:1]
795 statuschar = attrname[0:1]
796 for f in getattr(statuslist, attrname):
796 for f in getattr(statuslist, attrname):
797 rootobj.addfile(f, statuschar)
797 rootobj.addfile(f, statuschar)
798 tersedict[statuschar] = []
798 tersedict[statuschar] = []
799
799
800 # we won't be tersing the root dir, so add files in it
800 # we won't be tersing the root dir, so add files in it
801 for st, fpath in rootobj.iterfilepaths():
801 for st, fpath in rootobj.iterfilepaths():
802 tersedict[st].append(fpath)
802 tersedict[st].append(fpath)
803
803
804 # process each sub-directory and build tersedict
804 # process each sub-directory and build tersedict
805 for subdir in rootobj.subdirs.values():
805 for subdir in rootobj.subdirs.values():
806 for st, f in subdir.tersewalk(terseargs):
806 for st, f in subdir.tersewalk(terseargs):
807 tersedict[st].append(f)
807 tersedict[st].append(f)
808
808
809 tersedlist = []
809 tersedlist = []
810 for st in allst:
810 for st in allst:
811 tersedict[st].sort()
811 tersedict[st].sort()
812 tersedlist.append(tersedict[st])
812 tersedlist.append(tersedict[st])
813
813
814 return scmutil.status(*tersedlist)
814 return scmutil.status(*tersedlist)
815
815
816
816
817 def _commentlines(raw):
817 def _commentlines(raw):
818 '''Surround lineswith a comment char and a new line'''
818 '''Surround lineswith a comment char and a new line'''
819 lines = raw.splitlines()
819 lines = raw.splitlines()
820 commentedlines = [b'# %s' % line for line in lines]
820 commentedlines = [b'# %s' % line for line in lines]
821 return b'\n'.join(commentedlines) + b'\n'
821 return b'\n'.join(commentedlines) + b'\n'
822
822
823
823
824 @attr.s(frozen=True)
824 @attr.s(frozen=True)
825 class morestatus(object):
825 class morestatus(object):
826 reporoot = attr.ib()
826 reporoot = attr.ib()
827 unfinishedop = attr.ib()
827 unfinishedop = attr.ib()
828 unfinishedmsg = attr.ib()
828 unfinishedmsg = attr.ib()
829 activemerge = attr.ib()
829 activemerge = attr.ib()
830 unresolvedpaths = attr.ib()
830 unresolvedpaths = attr.ib()
831 _formattedpaths = attr.ib(init=False, default=set())
831 _formattedpaths = attr.ib(init=False, default=set())
832 _label = b'status.morestatus'
832 _label = b'status.morestatus'
833
833
834 def formatfile(self, path, fm):
834 def formatfile(self, path, fm):
835 self._formattedpaths.add(path)
835 self._formattedpaths.add(path)
836 if self.activemerge and path in self.unresolvedpaths:
836 if self.activemerge and path in self.unresolvedpaths:
837 fm.data(unresolved=True)
837 fm.data(unresolved=True)
838
838
839 def formatfooter(self, fm):
839 def formatfooter(self, fm):
840 if self.unfinishedop or self.unfinishedmsg:
840 if self.unfinishedop or self.unfinishedmsg:
841 fm.startitem()
841 fm.startitem()
842 fm.data(itemtype=b'morestatus')
842 fm.data(itemtype=b'morestatus')
843
843
844 if self.unfinishedop:
844 if self.unfinishedop:
845 fm.data(unfinished=self.unfinishedop)
845 fm.data(unfinished=self.unfinishedop)
846 statemsg = (
846 statemsg = (
847 _(b'The repository is in an unfinished *%s* state.')
847 _(b'The repository is in an unfinished *%s* state.')
848 % self.unfinishedop
848 % self.unfinishedop
849 )
849 )
850 fm.plain(b'%s\n' % _commentlines(statemsg), label=self._label)
850 fm.plain(b'%s\n' % _commentlines(statemsg), label=self._label)
851 if self.unfinishedmsg:
851 if self.unfinishedmsg:
852 fm.data(unfinishedmsg=self.unfinishedmsg)
852 fm.data(unfinishedmsg=self.unfinishedmsg)
853
853
854 # May also start new data items.
854 # May also start new data items.
855 self._formatconflicts(fm)
855 self._formatconflicts(fm)
856
856
857 if self.unfinishedmsg:
857 if self.unfinishedmsg:
858 fm.plain(
858 fm.plain(
859 b'%s\n' % _commentlines(self.unfinishedmsg), label=self._label
859 b'%s\n' % _commentlines(self.unfinishedmsg), label=self._label
860 )
860 )
861
861
862 def _formatconflicts(self, fm):
862 def _formatconflicts(self, fm):
863 if not self.activemerge:
863 if not self.activemerge:
864 return
864 return
865
865
866 if self.unresolvedpaths:
866 if self.unresolvedpaths:
867 mergeliststr = b'\n'.join(
867 mergeliststr = b'\n'.join(
868 [
868 [
869 b' %s'
869 b' %s'
870 % util.pathto(self.reporoot, encoding.getcwd(), path)
870 % util.pathto(self.reporoot, encoding.getcwd(), path)
871 for path in self.unresolvedpaths
871 for path in self.unresolvedpaths
872 ]
872 ]
873 )
873 )
874 msg = (
874 msg = (
875 _(
875 _(
876 b'''Unresolved merge conflicts:
876 b'''Unresolved merge conflicts:
877
877
878 %s
878 %s
879
879
880 To mark files as resolved: hg resolve --mark FILE'''
880 To mark files as resolved: hg resolve --mark FILE'''
881 )
881 )
882 % mergeliststr
882 % mergeliststr
883 )
883 )
884
884
885 # If any paths with unresolved conflicts were not previously
885 # If any paths with unresolved conflicts were not previously
886 # formatted, output them now.
886 # formatted, output them now.
887 for f in self.unresolvedpaths:
887 for f in self.unresolvedpaths:
888 if f in self._formattedpaths:
888 if f in self._formattedpaths:
889 # Already output.
889 # Already output.
890 continue
890 continue
891 fm.startitem()
891 fm.startitem()
892 # We can't claim to know the status of the file - it may just
892 # We can't claim to know the status of the file - it may just
893 # have been in one of the states that were not requested for
893 # have been in one of the states that were not requested for
894 # display, so it could be anything.
894 # display, so it could be anything.
895 fm.data(itemtype=b'file', path=f, unresolved=True)
895 fm.data(itemtype=b'file', path=f, unresolved=True)
896
896
897 else:
897 else:
898 msg = _(b'No unresolved merge conflicts.')
898 msg = _(b'No unresolved merge conflicts.')
899
899
900 fm.plain(b'%s\n' % _commentlines(msg), label=self._label)
900 fm.plain(b'%s\n' % _commentlines(msg), label=self._label)
901
901
902
902
903 def readmorestatus(repo):
903 def readmorestatus(repo):
904 """Returns a morestatus object if the repo has unfinished state."""
904 """Returns a morestatus object if the repo has unfinished state."""
905 statetuple = statemod.getrepostate(repo)
905 statetuple = statemod.getrepostate(repo)
906 mergestate = mergestatemod.mergestate.read(repo)
906 mergestate = mergestatemod.mergestate.read(repo)
907 activemerge = mergestate.active()
907 activemerge = mergestate.active()
908 if not statetuple and not activemerge:
908 if not statetuple and not activemerge:
909 return None
909 return None
910
910
911 unfinishedop = unfinishedmsg = unresolved = None
911 unfinishedop = unfinishedmsg = unresolved = None
912 if statetuple:
912 if statetuple:
913 unfinishedop, unfinishedmsg = statetuple
913 unfinishedop, unfinishedmsg = statetuple
914 if activemerge:
914 if activemerge:
915 unresolved = sorted(mergestate.unresolved())
915 unresolved = sorted(mergestate.unresolved())
916 return morestatus(
916 return morestatus(
917 repo.root, unfinishedop, unfinishedmsg, activemerge, unresolved
917 repo.root, unfinishedop, unfinishedmsg, activemerge, unresolved
918 )
918 )
919
919
920
920
921 def findpossible(cmd, table, strict=False):
921 def findpossible(cmd, table, strict=False):
922 """
922 """
923 Return cmd -> (aliases, command table entry)
923 Return cmd -> (aliases, command table entry)
924 for each matching command.
924 for each matching command.
925 Return debug commands (or their aliases) only if no normal command matches.
925 Return debug commands (or their aliases) only if no normal command matches.
926 """
926 """
927 choice = {}
927 choice = {}
928 debugchoice = {}
928 debugchoice = {}
929
929
930 if cmd in table:
930 if cmd in table:
931 # short-circuit exact matches, "log" alias beats "log|history"
931 # short-circuit exact matches, "log" alias beats "log|history"
932 keys = [cmd]
932 keys = [cmd]
933 else:
933 else:
934 keys = table.keys()
934 keys = table.keys()
935
935
936 allcmds = []
936 allcmds = []
937 for e in keys:
937 for e in keys:
938 aliases = parsealiases(e)
938 aliases = parsealiases(e)
939 allcmds.extend(aliases)
939 allcmds.extend(aliases)
940 found = None
940 found = None
941 if cmd in aliases:
941 if cmd in aliases:
942 found = cmd
942 found = cmd
943 elif not strict:
943 elif not strict:
944 for a in aliases:
944 for a in aliases:
945 if a.startswith(cmd):
945 if a.startswith(cmd):
946 found = a
946 found = a
947 break
947 break
948 if found is not None:
948 if found is not None:
949 if aliases[0].startswith(b"debug") or found.startswith(b"debug"):
949 if aliases[0].startswith(b"debug") or found.startswith(b"debug"):
950 debugchoice[found] = (aliases, table[e])
950 debugchoice[found] = (aliases, table[e])
951 else:
951 else:
952 choice[found] = (aliases, table[e])
952 choice[found] = (aliases, table[e])
953
953
954 if not choice and debugchoice:
954 if not choice and debugchoice:
955 choice = debugchoice
955 choice = debugchoice
956
956
957 return choice, allcmds
957 return choice, allcmds
958
958
959
959
960 def findcmd(cmd, table, strict=True):
960 def findcmd(cmd, table, strict=True):
961 """Return (aliases, command table entry) for command string."""
961 """Return (aliases, command table entry) for command string."""
962 choice, allcmds = findpossible(cmd, table, strict)
962 choice, allcmds = findpossible(cmd, table, strict)
963
963
964 if cmd in choice:
964 if cmd in choice:
965 return choice[cmd]
965 return choice[cmd]
966
966
967 if len(choice) > 1:
967 if len(choice) > 1:
968 clist = sorted(choice)
968 clist = sorted(choice)
969 raise error.AmbiguousCommand(cmd, clist)
969 raise error.AmbiguousCommand(cmd, clist)
970
970
971 if choice:
971 if choice:
972 return list(choice.values())[0]
972 return list(choice.values())[0]
973
973
974 raise error.UnknownCommand(cmd, allcmds)
974 raise error.UnknownCommand(cmd, allcmds)
975
975
976
976
977 def changebranch(ui, repo, revs, label, opts):
977 def changebranch(ui, repo, revs, label, opts):
978 """Change the branch name of given revs to label"""
978 """Change the branch name of given revs to label"""
979
979
980 with repo.wlock(), repo.lock(), repo.transaction(b'branches'):
980 with repo.wlock(), repo.lock(), repo.transaction(b'branches'):
981 # abort in case of uncommitted merge or dirty wdir
981 # abort in case of uncommitted merge or dirty wdir
982 bailifchanged(repo)
982 bailifchanged(repo)
983 revs = scmutil.revrange(repo, revs)
983 revs = scmutil.revrange(repo, revs)
984 if not revs:
984 if not revs:
985 raise error.InputError(b"empty revision set")
985 raise error.InputError(b"empty revision set")
986 roots = repo.revs(b'roots(%ld)', revs)
986 roots = repo.revs(b'roots(%ld)', revs)
987 if len(roots) > 1:
987 if len(roots) > 1:
988 raise error.InputError(
988 raise error.InputError(
989 _(b"cannot change branch of non-linear revisions")
989 _(b"cannot change branch of non-linear revisions")
990 )
990 )
991 rewriteutil.precheck(repo, revs, b'change branch of')
991 rewriteutil.precheck(repo, revs, b'change branch of')
992
992
993 root = repo[roots.first()]
993 root = repo[roots.first()]
994 rpb = {parent.branch() for parent in root.parents()}
994 rpb = {parent.branch() for parent in root.parents()}
995 if (
995 if (
996 not opts.get(b'force')
996 not opts.get(b'force')
997 and label not in rpb
997 and label not in rpb
998 and label in repo.branchmap()
998 and label in repo.branchmap()
999 ):
999 ):
1000 raise error.InputError(
1000 raise error.InputError(
1001 _(b"a branch of the same name already exists")
1001 _(b"a branch of the same name already exists")
1002 )
1002 )
1003
1003
1004 # make sure only topological heads
1004 # make sure only topological heads
1005 if repo.revs(b'heads(%ld) - head()', revs):
1005 if repo.revs(b'heads(%ld) - head()', revs):
1006 raise error.InputError(
1006 raise error.InputError(
1007 _(b"cannot change branch in middle of a stack")
1007 _(b"cannot change branch in middle of a stack")
1008 )
1008 )
1009
1009
1010 replacements = {}
1010 replacements = {}
1011 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
1011 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
1012 # mercurial.subrepo -> mercurial.cmdutil
1012 # mercurial.subrepo -> mercurial.cmdutil
1013 from . import context
1013 from . import context
1014
1014
1015 for rev in revs:
1015 for rev in revs:
1016 ctx = repo[rev]
1016 ctx = repo[rev]
1017 oldbranch = ctx.branch()
1017 oldbranch = ctx.branch()
1018 # check if ctx has same branch
1018 # check if ctx has same branch
1019 if oldbranch == label:
1019 if oldbranch == label:
1020 continue
1020 continue
1021
1021
1022 def filectxfn(repo, newctx, path):
1022 def filectxfn(repo, newctx, path):
1023 try:
1023 try:
1024 return ctx[path]
1024 return ctx[path]
1025 except error.ManifestLookupError:
1025 except error.ManifestLookupError:
1026 return None
1026 return None
1027
1027
1028 ui.debug(
1028 ui.debug(
1029 b"changing branch of '%s' from '%s' to '%s'\n"
1029 b"changing branch of '%s' from '%s' to '%s'\n"
1030 % (hex(ctx.node()), oldbranch, label)
1030 % (hex(ctx.node()), oldbranch, label)
1031 )
1031 )
1032 extra = ctx.extra()
1032 extra = ctx.extra()
1033 extra[b'branch_change'] = hex(ctx.node())
1033 extra[b'branch_change'] = hex(ctx.node())
1034 # While changing branch of set of linear commits, make sure that
1034 # While changing branch of set of linear commits, make sure that
1035 # we base our commits on new parent rather than old parent which
1035 # we base our commits on new parent rather than old parent which
1036 # was obsoleted while changing the branch
1036 # was obsoleted while changing the branch
1037 p1 = ctx.p1().node()
1037 p1 = ctx.p1().node()
1038 p2 = ctx.p2().node()
1038 p2 = ctx.p2().node()
1039 if p1 in replacements:
1039 if p1 in replacements:
1040 p1 = replacements[p1][0]
1040 p1 = replacements[p1][0]
1041 if p2 in replacements:
1041 if p2 in replacements:
1042 p2 = replacements[p2][0]
1042 p2 = replacements[p2][0]
1043
1043
1044 mc = context.memctx(
1044 mc = context.memctx(
1045 repo,
1045 repo,
1046 (p1, p2),
1046 (p1, p2),
1047 ctx.description(),
1047 ctx.description(),
1048 ctx.files(),
1048 ctx.files(),
1049 filectxfn,
1049 filectxfn,
1050 user=ctx.user(),
1050 user=ctx.user(),
1051 date=ctx.date(),
1051 date=ctx.date(),
1052 extra=extra,
1052 extra=extra,
1053 branch=label,
1053 branch=label,
1054 )
1054 )
1055
1055
1056 newnode = repo.commitctx(mc)
1056 newnode = repo.commitctx(mc)
1057 replacements[ctx.node()] = (newnode,)
1057 replacements[ctx.node()] = (newnode,)
1058 ui.debug(b'new node id is %s\n' % hex(newnode))
1058 ui.debug(b'new node id is %s\n' % hex(newnode))
1059
1059
1060 # create obsmarkers and move bookmarks
1060 # create obsmarkers and move bookmarks
1061 scmutil.cleanupnodes(
1061 scmutil.cleanupnodes(
1062 repo, replacements, b'branch-change', fixphase=True
1062 repo, replacements, b'branch-change', fixphase=True
1063 )
1063 )
1064
1064
1065 # move the working copy too
1065 # move the working copy too
1066 wctx = repo[None]
1066 wctx = repo[None]
1067 # in-progress merge is a bit too complex for now.
1067 # in-progress merge is a bit too complex for now.
1068 if len(wctx.parents()) == 1:
1068 if len(wctx.parents()) == 1:
1069 newid = replacements.get(wctx.p1().node())
1069 newid = replacements.get(wctx.p1().node())
1070 if newid is not None:
1070 if newid is not None:
1071 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
1071 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
1072 # mercurial.cmdutil
1072 # mercurial.cmdutil
1073 from . import hg
1073 from . import hg
1074
1074
1075 hg.update(repo, newid[0], quietempty=True)
1075 hg.update(repo, newid[0], quietempty=True)
1076
1076
1077 ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
1077 ui.status(_(b"changed branch on %d changesets\n") % len(replacements))
1078
1078
1079
1079
1080 def findrepo(p):
1080 def findrepo(p):
1081 while not os.path.isdir(os.path.join(p, b".hg")):
1081 while not os.path.isdir(os.path.join(p, b".hg")):
1082 oldp, p = p, os.path.dirname(p)
1082 oldp, p = p, os.path.dirname(p)
1083 if p == oldp:
1083 if p == oldp:
1084 return None
1084 return None
1085
1085
1086 return p
1086 return p
1087
1087
1088
1088
1089 def bailifchanged(repo, merge=True, hint=None):
1089 def bailifchanged(repo, merge=True, hint=None):
1090 """enforce the precondition that working directory must be clean.
1090 """enforce the precondition that working directory must be clean.
1091
1091
1092 'merge' can be set to false if a pending uncommitted merge should be
1092 'merge' can be set to false if a pending uncommitted merge should be
1093 ignored (such as when 'update --check' runs).
1093 ignored (such as when 'update --check' runs).
1094
1094
1095 'hint' is the usual hint given to Abort exception.
1095 'hint' is the usual hint given to Abort exception.
1096 """
1096 """
1097
1097
1098 if merge and repo.dirstate.p2() != repo.nullid:
1098 if merge and repo.dirstate.p2() != repo.nullid:
1099 raise error.StateError(_(b'outstanding uncommitted merge'), hint=hint)
1099 raise error.StateError(_(b'outstanding uncommitted merge'), hint=hint)
1100 st = repo.status()
1100 st = repo.status()
1101 if st.modified or st.added or st.removed or st.deleted:
1101 if st.modified or st.added or st.removed or st.deleted:
1102 raise error.StateError(_(b'uncommitted changes'), hint=hint)
1102 raise error.StateError(_(b'uncommitted changes'), hint=hint)
1103 ctx = repo[None]
1103 ctx = repo[None]
1104 for s in sorted(ctx.substate):
1104 for s in sorted(ctx.substate):
1105 ctx.sub(s).bailifchanged(hint=hint)
1105 ctx.sub(s).bailifchanged(hint=hint)
1106
1106
1107
1107
1108 def logmessage(ui, opts):
1108 def logmessage(ui, opts):
1109 """get the log message according to -m and -l option"""
1109 """get the log message according to -m and -l option"""
1110
1110
1111 check_at_most_one_arg(opts, b'message', b'logfile')
1111 check_at_most_one_arg(opts, b'message', b'logfile')
1112
1112
1113 message = opts.get(b'message')
1113 message = opts.get(b'message')
1114 logfile = opts.get(b'logfile')
1114 logfile = opts.get(b'logfile')
1115
1115
1116 if not message and logfile:
1116 if not message and logfile:
1117 try:
1117 try:
1118 if isstdiofilename(logfile):
1118 if isstdiofilename(logfile):
1119 message = ui.fin.read()
1119 message = ui.fin.read()
1120 else:
1120 else:
1121 message = b'\n'.join(util.readfile(logfile).splitlines())
1121 message = b'\n'.join(util.readfile(logfile).splitlines())
1122 except IOError as inst:
1122 except IOError as inst:
1123 raise error.Abort(
1123 raise error.Abort(
1124 _(b"can't read commit message '%s': %s")
1124 _(b"can't read commit message '%s': %s")
1125 % (logfile, encoding.strtolocal(inst.strerror))
1125 % (logfile, encoding.strtolocal(inst.strerror))
1126 )
1126 )
1127 return message
1127 return message
1128
1128
1129
1129
1130 def mergeeditform(ctxorbool, baseformname):
1130 def mergeeditform(ctxorbool, baseformname):
1131 """return appropriate editform name (referencing a committemplate)
1131 """return appropriate editform name (referencing a committemplate)
1132
1132
1133 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
1133 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
1134 merging is committed.
1134 merging is committed.
1135
1135
1136 This returns baseformname with '.merge' appended if it is a merge,
1136 This returns baseformname with '.merge' appended if it is a merge,
1137 otherwise '.normal' is appended.
1137 otherwise '.normal' is appended.
1138 """
1138 """
1139 if isinstance(ctxorbool, bool):
1139 if isinstance(ctxorbool, bool):
1140 if ctxorbool:
1140 if ctxorbool:
1141 return baseformname + b".merge"
1141 return baseformname + b".merge"
1142 elif len(ctxorbool.parents()) > 1:
1142 elif len(ctxorbool.parents()) > 1:
1143 return baseformname + b".merge"
1143 return baseformname + b".merge"
1144
1144
1145 return baseformname + b".normal"
1145 return baseformname + b".normal"
1146
1146
1147
1147
1148 def getcommiteditor(
1148 def getcommiteditor(
1149 edit=False, finishdesc=None, extramsg=None, editform=b'', **opts
1149 edit=False, finishdesc=None, extramsg=None, editform=b'', **opts
1150 ):
1150 ):
1151 """get appropriate commit message editor according to '--edit' option
1151 """get appropriate commit message editor according to '--edit' option
1152
1152
1153 'finishdesc' is a function to be called with edited commit message
1153 'finishdesc' is a function to be called with edited commit message
1154 (= 'description' of the new changeset) just after editing, but
1154 (= 'description' of the new changeset) just after editing, but
1155 before checking empty-ness. It should return actual text to be
1155 before checking empty-ness. It should return actual text to be
1156 stored into history. This allows to change description before
1156 stored into history. This allows to change description before
1157 storing.
1157 storing.
1158
1158
1159 'extramsg' is a extra message to be shown in the editor instead of
1159 'extramsg' is a extra message to be shown in the editor instead of
1160 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
1160 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
1161 is automatically added.
1161 is automatically added.
1162
1162
1163 'editform' is a dot-separated list of names, to distinguish
1163 'editform' is a dot-separated list of names, to distinguish
1164 the purpose of commit text editing.
1164 the purpose of commit text editing.
1165
1165
1166 'getcommiteditor' returns 'commitforceeditor' regardless of
1166 'getcommiteditor' returns 'commitforceeditor' regardless of
1167 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
1167 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
1168 they are specific for usage in MQ.
1168 they are specific for usage in MQ.
1169 """
1169 """
1170 if edit or finishdesc or extramsg:
1170 if edit or finishdesc or extramsg:
1171 return lambda r, c, s: commitforceeditor(
1171 return lambda r, c, s: commitforceeditor(
1172 r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform
1172 r, c, s, finishdesc=finishdesc, extramsg=extramsg, editform=editform
1173 )
1173 )
1174 elif editform:
1174 elif editform:
1175 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
1175 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
1176 else:
1176 else:
1177 return commiteditor
1177 return commiteditor
1178
1178
1179
1179
1180 def _escapecommandtemplate(tmpl):
1180 def _escapecommandtemplate(tmpl):
1181 parts = []
1181 parts = []
1182 for typ, start, end in templater.scantemplate(tmpl, raw=True):
1182 for typ, start, end in templater.scantemplate(tmpl, raw=True):
1183 if typ == b'string':
1183 if typ == b'string':
1184 parts.append(stringutil.escapestr(tmpl[start:end]))
1184 parts.append(stringutil.escapestr(tmpl[start:end]))
1185 else:
1185 else:
1186 parts.append(tmpl[start:end])
1186 parts.append(tmpl[start:end])
1187 return b''.join(parts)
1187 return b''.join(parts)
1188
1188
1189
1189
1190 def rendercommandtemplate(ui, tmpl, props):
1190 def rendercommandtemplate(ui, tmpl, props):
1191 r"""Expand a literal template 'tmpl' in a way suitable for command line
1191 r"""Expand a literal template 'tmpl' in a way suitable for command line
1192
1192
1193 '\' in outermost string is not taken as an escape character because it
1193 '\' in outermost string is not taken as an escape character because it
1194 is a directory separator on Windows.
1194 is a directory separator on Windows.
1195
1195
1196 >>> from . import ui as uimod
1196 >>> from . import ui as uimod
1197 >>> ui = uimod.ui()
1197 >>> ui = uimod.ui()
1198 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
1198 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
1199 'c:\\foo'
1199 'c:\\foo'
1200 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
1200 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
1201 'c:{path}'
1201 'c:{path}'
1202 """
1202 """
1203 if not tmpl:
1203 if not tmpl:
1204 return tmpl
1204 return tmpl
1205 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
1205 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
1206 return t.renderdefault(props)
1206 return t.renderdefault(props)
1207
1207
1208
1208
1209 def rendertemplate(ctx, tmpl, props=None):
1209 def rendertemplate(ctx, tmpl, props=None):
1210 """Expand a literal template 'tmpl' byte-string against one changeset
1210 """Expand a literal template 'tmpl' byte-string against one changeset
1211
1211
1212 Each props item must be a stringify-able value or a callable returning
1212 Each props item must be a stringify-able value or a callable returning
1213 such value, i.e. no bare list nor dict should be passed.
1213 such value, i.e. no bare list nor dict should be passed.
1214 """
1214 """
1215 repo = ctx.repo()
1215 repo = ctx.repo()
1216 tres = formatter.templateresources(repo.ui, repo)
1216 tres = formatter.templateresources(repo.ui, repo)
1217 t = formatter.maketemplater(
1217 t = formatter.maketemplater(
1218 repo.ui, tmpl, defaults=templatekw.keywords, resources=tres
1218 repo.ui, tmpl, defaults=templatekw.keywords, resources=tres
1219 )
1219 )
1220 mapping = {b'ctx': ctx}
1220 mapping = {b'ctx': ctx}
1221 if props:
1221 if props:
1222 mapping.update(props)
1222 mapping.update(props)
1223 return t.renderdefault(mapping)
1223 return t.renderdefault(mapping)
1224
1224
1225
1225
1226 def format_changeset_summary(ui, ctx, command=None, default_spec=None):
1226 def format_changeset_summary(ui, ctx, command=None, default_spec=None):
1227 """Format a changeset summary (one line)."""
1227 """Format a changeset summary (one line)."""
1228 spec = None
1228 spec = None
1229 if command:
1229 if command:
1230 spec = ui.config(
1230 spec = ui.config(
1231 b'command-templates', b'oneline-summary.%s' % command, None
1231 b'command-templates', b'oneline-summary.%s' % command, None
1232 )
1232 )
1233 if not spec:
1233 if not spec:
1234 spec = ui.config(b'command-templates', b'oneline-summary')
1234 spec = ui.config(b'command-templates', b'oneline-summary')
1235 if not spec:
1235 if not spec:
1236 spec = default_spec
1236 spec = default_spec
1237 if not spec:
1237 if not spec:
1238 spec = (
1238 spec = (
1239 b'{separate(" ", '
1239 b'{separate(" ", '
1240 b'label("oneline-summary.changeset", "{rev}:{node|short}")'
1240 b'label("oneline-summary.changeset", "{rev}:{node|short}")'
1241 b', '
1241 b', '
1242 b'join(filter(namespaces % "{ifeq(namespace, "branches", "", join(names % "{label("oneline-summary.{namespace}", name)}", " "))}"), " ")'
1242 b'join(filter(namespaces % "{ifeq(namespace, "branches", "", join(names % "{label("oneline-summary.{namespace}", name)}", " "))}"), " ")'
1243 b')} '
1243 b')} '
1244 b'"{label("oneline-summary.desc", desc|firstline)}"'
1244 b'"{label("oneline-summary.desc", desc|firstline)}"'
1245 )
1245 )
1246 text = rendertemplate(ctx, spec)
1246 text = rendertemplate(ctx, spec)
1247 return text.split(b'\n')[0]
1247 return text.split(b'\n')[0]
1248
1248
1249
1249
1250 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
1250 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
1251 r"""Convert old-style filename format string to template string
1251 r"""Convert old-style filename format string to template string
1252
1252
1253 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
1253 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
1254 'foo-{reporoot|basename}-{seqno}.patch'
1254 'foo-{reporoot|basename}-{seqno}.patch'
1255 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
1255 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
1256 '{rev}{tags % "{tag}"}{node}'
1256 '{rev}{tags % "{tag}"}{node}'
1257
1257
1258 '\' in outermost strings has to be escaped because it is a directory
1258 '\' in outermost strings has to be escaped because it is a directory
1259 separator on Windows:
1259 separator on Windows:
1260
1260
1261 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
1261 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
1262 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
1262 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
1263 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
1263 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
1264 '\\\\\\\\foo\\\\bar.patch'
1264 '\\\\\\\\foo\\\\bar.patch'
1265 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
1265 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
1266 '\\\\{tags % "{tag}"}'
1266 '\\\\{tags % "{tag}"}'
1267
1267
1268 but inner strings follow the template rules (i.e. '\' is taken as an
1268 but inner strings follow the template rules (i.e. '\' is taken as an
1269 escape character):
1269 escape character):
1270
1270
1271 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
1271 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
1272 '{"c:\\tmp"}'
1272 '{"c:\\tmp"}'
1273 """
1273 """
1274 expander = {
1274 expander = {
1275 b'H': b'{node}',
1275 b'H': b'{node}',
1276 b'R': b'{rev}',
1276 b'R': b'{rev}',
1277 b'h': b'{node|short}',
1277 b'h': b'{node|short}',
1278 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
1278 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
1279 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
1279 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
1280 b'%': b'%',
1280 b'%': b'%',
1281 b'b': b'{reporoot|basename}',
1281 b'b': b'{reporoot|basename}',
1282 }
1282 }
1283 if total is not None:
1283 if total is not None:
1284 expander[b'N'] = b'{total}'
1284 expander[b'N'] = b'{total}'
1285 if seqno is not None:
1285 if seqno is not None:
1286 expander[b'n'] = b'{seqno}'
1286 expander[b'n'] = b'{seqno}'
1287 if total is not None and seqno is not None:
1287 if total is not None and seqno is not None:
1288 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1288 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1289 if pathname is not None:
1289 if pathname is not None:
1290 expander[b's'] = b'{pathname|basename}'
1290 expander[b's'] = b'{pathname|basename}'
1291 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1291 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1292 expander[b'p'] = b'{pathname}'
1292 expander[b'p'] = b'{pathname}'
1293
1293
1294 newname = []
1294 newname = []
1295 for typ, start, end in templater.scantemplate(pat, raw=True):
1295 for typ, start, end in templater.scantemplate(pat, raw=True):
1296 if typ != b'string':
1296 if typ != b'string':
1297 newname.append(pat[start:end])
1297 newname.append(pat[start:end])
1298 continue
1298 continue
1299 i = start
1299 i = start
1300 while i < end:
1300 while i < end:
1301 n = pat.find(b'%', i, end)
1301 n = pat.find(b'%', i, end)
1302 if n < 0:
1302 if n < 0:
1303 newname.append(stringutil.escapestr(pat[i:end]))
1303 newname.append(stringutil.escapestr(pat[i:end]))
1304 break
1304 break
1305 newname.append(stringutil.escapestr(pat[i:n]))
1305 newname.append(stringutil.escapestr(pat[i:n]))
1306 if n + 2 > end:
1306 if n + 2 > end:
1307 raise error.Abort(
1307 raise error.Abort(
1308 _(b"incomplete format spec in output filename")
1308 _(b"incomplete format spec in output filename")
1309 )
1309 )
1310 c = pat[n + 1 : n + 2]
1310 c = pat[n + 1 : n + 2]
1311 i = n + 2
1311 i = n + 2
1312 try:
1312 try:
1313 newname.append(expander[c])
1313 newname.append(expander[c])
1314 except KeyError:
1314 except KeyError:
1315 raise error.Abort(
1315 raise error.Abort(
1316 _(b"invalid format spec '%%%s' in output filename") % c
1316 _(b"invalid format spec '%%%s' in output filename") % c
1317 )
1317 )
1318 return b''.join(newname)
1318 return b''.join(newname)
1319
1319
1320
1320
1321 def makefilename(ctx, pat, **props):
1321 def makefilename(ctx, pat, **props):
1322 if not pat:
1322 if not pat:
1323 return pat
1323 return pat
1324 tmpl = _buildfntemplate(pat, **props)
1324 tmpl = _buildfntemplate(pat, **props)
1325 # BUG: alias expansion shouldn't be made against template fragments
1325 # BUG: alias expansion shouldn't be made against template fragments
1326 # rewritten from %-format strings, but we have no easy way to partially
1326 # rewritten from %-format strings, but we have no easy way to partially
1327 # disable the expansion.
1327 # disable the expansion.
1328 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1328 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1329
1329
1330
1330
1331 def isstdiofilename(pat):
1331 def isstdiofilename(pat):
1332 """True if the given pat looks like a filename denoting stdin/stdout"""
1332 """True if the given pat looks like a filename denoting stdin/stdout"""
1333 return not pat or pat == b'-'
1333 return not pat or pat == b'-'
1334
1334
1335
1335
1336 class _unclosablefile(object):
1336 class _unclosablefile(object):
1337 def __init__(self, fp):
1337 def __init__(self, fp):
1338 self._fp = fp
1338 self._fp = fp
1339
1339
1340 def close(self):
1340 def close(self):
1341 pass
1341 pass
1342
1342
1343 def __iter__(self):
1343 def __iter__(self):
1344 return iter(self._fp)
1344 return iter(self._fp)
1345
1345
1346 def __getattr__(self, attr):
1346 def __getattr__(self, attr):
1347 return getattr(self._fp, attr)
1347 return getattr(self._fp, attr)
1348
1348
1349 def __enter__(self):
1349 def __enter__(self):
1350 return self
1350 return self
1351
1351
1352 def __exit__(self, exc_type, exc_value, exc_tb):
1352 def __exit__(self, exc_type, exc_value, exc_tb):
1353 pass
1353 pass
1354
1354
1355
1355
1356 def makefileobj(ctx, pat, mode=b'wb', **props):
1356 def makefileobj(ctx, pat, mode=b'wb', **props):
1357 writable = mode not in (b'r', b'rb')
1357 writable = mode not in (b'r', b'rb')
1358
1358
1359 if isstdiofilename(pat):
1359 if isstdiofilename(pat):
1360 repo = ctx.repo()
1360 repo = ctx.repo()
1361 if writable:
1361 if writable:
1362 fp = repo.ui.fout
1362 fp = repo.ui.fout
1363 else:
1363 else:
1364 fp = repo.ui.fin
1364 fp = repo.ui.fin
1365 return _unclosablefile(fp)
1365 return _unclosablefile(fp)
1366 fn = makefilename(ctx, pat, **props)
1366 fn = makefilename(ctx, pat, **props)
1367 return open(fn, mode)
1367 return open(fn, mode)
1368
1368
1369
1369
1370 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1370 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1371 """opens the changelog, manifest, a filelog or a given revlog"""
1371 """opens the changelog, manifest, a filelog or a given revlog"""
1372 cl = opts[b'changelog']
1372 cl = opts[b'changelog']
1373 mf = opts[b'manifest']
1373 mf = opts[b'manifest']
1374 dir = opts[b'dir']
1374 dir = opts[b'dir']
1375 msg = None
1375 msg = None
1376 if cl and mf:
1376 if cl and mf:
1377 msg = _(b'cannot specify --changelog and --manifest at the same time')
1377 msg = _(b'cannot specify --changelog and --manifest at the same time')
1378 elif cl and dir:
1378 elif cl and dir:
1379 msg = _(b'cannot specify --changelog and --dir at the same time')
1379 msg = _(b'cannot specify --changelog and --dir at the same time')
1380 elif cl or mf or dir:
1380 elif cl or mf or dir:
1381 if file_:
1381 if file_:
1382 msg = _(b'cannot specify filename with --changelog or --manifest')
1382 msg = _(b'cannot specify filename with --changelog or --manifest')
1383 elif not repo:
1383 elif not repo:
1384 msg = _(
1384 msg = _(
1385 b'cannot specify --changelog or --manifest or --dir '
1385 b'cannot specify --changelog or --manifest or --dir '
1386 b'without a repository'
1386 b'without a repository'
1387 )
1387 )
1388 if msg:
1388 if msg:
1389 raise error.InputError(msg)
1389 raise error.InputError(msg)
1390
1390
1391 r = None
1391 r = None
1392 if repo:
1392 if repo:
1393 if cl:
1393 if cl:
1394 r = repo.unfiltered().changelog
1394 r = repo.unfiltered().changelog
1395 elif dir:
1395 elif dir:
1396 if not scmutil.istreemanifest(repo):
1396 if not scmutil.istreemanifest(repo):
1397 raise error.InputError(
1397 raise error.InputError(
1398 _(
1398 _(
1399 b"--dir can only be used on repos with "
1399 b"--dir can only be used on repos with "
1400 b"treemanifest enabled"
1400 b"treemanifest enabled"
1401 )
1401 )
1402 )
1402 )
1403 if not dir.endswith(b'/'):
1403 if not dir.endswith(b'/'):
1404 dir = dir + b'/'
1404 dir = dir + b'/'
1405 dirlog = repo.manifestlog.getstorage(dir)
1405 dirlog = repo.manifestlog.getstorage(dir)
1406 if len(dirlog):
1406 if len(dirlog):
1407 r = dirlog
1407 r = dirlog
1408 elif mf:
1408 elif mf:
1409 r = repo.manifestlog.getstorage(b'')
1409 r = repo.manifestlog.getstorage(b'')
1410 elif file_:
1410 elif file_:
1411 filelog = repo.file(file_)
1411 filelog = repo.file(file_)
1412 if len(filelog):
1412 if len(filelog):
1413 r = filelog
1413 r = filelog
1414
1414
1415 # Not all storage may be revlogs. If requested, try to return an actual
1415 # Not all storage may be revlogs. If requested, try to return an actual
1416 # revlog instance.
1416 # revlog instance.
1417 if returnrevlog:
1417 if returnrevlog:
1418 if isinstance(r, revlog.revlog):
1418 if isinstance(r, revlog.revlog):
1419 pass
1419 pass
1420 elif util.safehasattr(r, b'_revlog'):
1420 elif util.safehasattr(r, b'_revlog'):
1421 r = r._revlog # pytype: disable=attribute-error
1421 r = r._revlog # pytype: disable=attribute-error
1422 elif r is not None:
1422 elif r is not None:
1423 raise error.InputError(
1423 raise error.InputError(
1424 _(b'%r does not appear to be a revlog') % r
1424 _(b'%r does not appear to be a revlog') % r
1425 )
1425 )
1426
1426
1427 if not r:
1427 if not r:
1428 if not returnrevlog:
1428 if not returnrevlog:
1429 raise error.InputError(_(b'cannot give path to non-revlog'))
1429 raise error.InputError(_(b'cannot give path to non-revlog'))
1430
1430
1431 if not file_:
1431 if not file_:
1432 raise error.CommandError(cmd, _(b'invalid arguments'))
1432 raise error.CommandError(cmd, _(b'invalid arguments'))
1433 if not os.path.isfile(file_):
1433 if not os.path.isfile(file_):
1434 raise error.InputError(_(b"revlog '%s' not found") % file_)
1434 raise error.InputError(_(b"revlog '%s' not found") % file_)
1435
1435
1436 target = (revlog_constants.KIND_OTHER, b'free-form:%s' % file_)
1436 target = (revlog_constants.KIND_OTHER, b'free-form:%s' % file_)
1437 r = revlog.revlog(
1437 r = revlog.revlog(
1438 vfsmod.vfs(encoding.getcwd(), audit=False),
1438 vfsmod.vfs(encoding.getcwd(), audit=False),
1439 target=target,
1439 target=target,
1440 radix=file_[:-2],
1440 radix=file_[:-2],
1441 )
1441 )
1442 return r
1442 return r
1443
1443
1444
1444
1445 def openrevlog(repo, cmd, file_, opts):
1445 def openrevlog(repo, cmd, file_, opts):
1446 """Obtain a revlog backing storage of an item.
1446 """Obtain a revlog backing storage of an item.
1447
1447
1448 This is similar to ``openstorage()`` except it always returns a revlog.
1448 This is similar to ``openstorage()`` except it always returns a revlog.
1449
1449
1450 In most cases, a caller cares about the main storage object - not the
1450 In most cases, a caller cares about the main storage object - not the
1451 revlog backing it. Therefore, this function should only be used by code
1451 revlog backing it. Therefore, this function should only be used by code
1452 that needs to examine low-level revlog implementation details. e.g. debug
1452 that needs to examine low-level revlog implementation details. e.g. debug
1453 commands.
1453 commands.
1454 """
1454 """
1455 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1455 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1456
1456
1457
1457
1458 def copy(ui, repo, pats, opts, rename=False):
1458 def copy(ui, repo, pats, opts, rename=False):
1459 check_incompatible_arguments(opts, b'forget', [b'dry_run'])
1459 check_incompatible_arguments(opts, b'forget', [b'dry_run'])
1460
1460
1461 # called with the repo lock held
1461 # called with the repo lock held
1462 #
1462 #
1463 # hgsep => pathname that uses "/" to separate directories
1463 # hgsep => pathname that uses "/" to separate directories
1464 # ossep => pathname that uses os.sep to separate directories
1464 # ossep => pathname that uses os.sep to separate directories
1465 cwd = repo.getcwd()
1465 cwd = repo.getcwd()
1466 targets = {}
1466 targets = {}
1467 forget = opts.get(b"forget")
1467 forget = opts.get(b"forget")
1468 after = opts.get(b"after")
1468 after = opts.get(b"after")
1469 dryrun = opts.get(b"dry_run")
1469 dryrun = opts.get(b"dry_run")
1470 rev = opts.get(b'at_rev')
1470 rev = opts.get(b'at_rev')
1471 if rev:
1471 if rev:
1472 if not forget and not after:
1472 if not forget and not after:
1473 # TODO: Remove this restriction and make it also create the copy
1473 # TODO: Remove this restriction and make it also create the copy
1474 # targets (and remove the rename source if rename==True).
1474 # targets (and remove the rename source if rename==True).
1475 raise error.InputError(_(b'--at-rev requires --after'))
1475 raise error.InputError(_(b'--at-rev requires --after'))
1476 ctx = scmutil.revsingle(repo, rev)
1476 ctx = scmutil.revsingle(repo, rev)
1477 if len(ctx.parents()) > 1:
1477 if len(ctx.parents()) > 1:
1478 raise error.InputError(
1478 raise error.InputError(
1479 _(b'cannot mark/unmark copy in merge commit')
1479 _(b'cannot mark/unmark copy in merge commit')
1480 )
1480 )
1481 else:
1481 else:
1482 ctx = repo[None]
1482 ctx = repo[None]
1483
1483
1484 pctx = ctx.p1()
1484 pctx = ctx.p1()
1485
1485
1486 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1486 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1487
1487
1488 if forget:
1488 if forget:
1489 if ctx.rev() is None:
1489 if ctx.rev() is None:
1490 new_ctx = ctx
1490 new_ctx = ctx
1491 else:
1491 else:
1492 if len(ctx.parents()) > 1:
1492 if len(ctx.parents()) > 1:
1493 raise error.InputError(_(b'cannot unmark copy in merge commit'))
1493 raise error.InputError(_(b'cannot unmark copy in merge commit'))
1494 # avoid cycle context -> subrepo -> cmdutil
1494 # avoid cycle context -> subrepo -> cmdutil
1495 from . import context
1495 from . import context
1496
1496
1497 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1497 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1498 new_ctx = context.overlayworkingctx(repo)
1498 new_ctx = context.overlayworkingctx(repo)
1499 new_ctx.setbase(ctx.p1())
1499 new_ctx.setbase(ctx.p1())
1500 mergemod.graft(repo, ctx, wctx=new_ctx)
1500 mergemod.graft(repo, ctx, wctx=new_ctx)
1501
1501
1502 match = scmutil.match(ctx, pats, opts)
1502 match = scmutil.match(ctx, pats, opts)
1503
1503
1504 current_copies = ctx.p1copies()
1504 current_copies = ctx.p1copies()
1505 current_copies.update(ctx.p2copies())
1505 current_copies.update(ctx.p2copies())
1506
1506
1507 uipathfn = scmutil.getuipathfn(repo)
1507 uipathfn = scmutil.getuipathfn(repo)
1508 for f in ctx.walk(match):
1508 for f in ctx.walk(match):
1509 if f in current_copies:
1509 if f in current_copies:
1510 new_ctx[f].markcopied(None)
1510 new_ctx[f].markcopied(None)
1511 elif match.exact(f):
1511 elif match.exact(f):
1512 ui.warn(
1512 ui.warn(
1513 _(
1513 _(
1514 b'%s: not unmarking as copy - file is not marked as copied\n'
1514 b'%s: not unmarking as copy - file is not marked as copied\n'
1515 )
1515 )
1516 % uipathfn(f)
1516 % uipathfn(f)
1517 )
1517 )
1518
1518
1519 if ctx.rev() is not None:
1519 if ctx.rev() is not None:
1520 with repo.lock():
1520 with repo.lock():
1521 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1521 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1522 new_node = mem_ctx.commit()
1522 new_node = mem_ctx.commit()
1523
1523
1524 if repo.dirstate.p1() == ctx.node():
1524 if repo.dirstate.p1() == ctx.node():
1525 with repo.dirstate.parentchange():
1525 with repo.dirstate.parentchange():
1526 scmutil.movedirstate(repo, repo[new_node])
1526 scmutil.movedirstate(repo, repo[new_node])
1527 replacements = {ctx.node(): [new_node]}
1527 replacements = {ctx.node(): [new_node]}
1528 scmutil.cleanupnodes(
1528 scmutil.cleanupnodes(
1529 repo, replacements, b'uncopy', fixphase=True
1529 repo, replacements, b'uncopy', fixphase=True
1530 )
1530 )
1531
1531
1532 return
1532 return
1533
1533
1534 pats = scmutil.expandpats(pats)
1534 pats = scmutil.expandpats(pats)
1535 if not pats:
1535 if not pats:
1536 raise error.InputError(_(b'no source or destination specified'))
1536 raise error.InputError(_(b'no source or destination specified'))
1537 if len(pats) == 1:
1537 if len(pats) == 1:
1538 raise error.InputError(_(b'no destination specified'))
1538 raise error.InputError(_(b'no destination specified'))
1539 dest = pats.pop()
1539 dest = pats.pop()
1540
1540
1541 def walkpat(pat):
1541 def walkpat(pat):
1542 srcs = []
1542 srcs = []
1543 # TODO: Inline and simplify the non-working-copy version of this code
1543 # TODO: Inline and simplify the non-working-copy version of this code
1544 # since it shares very little with the working-copy version of it.
1544 # since it shares very little with the working-copy version of it.
1545 ctx_to_walk = ctx if ctx.rev() is None else pctx
1545 ctx_to_walk = ctx if ctx.rev() is None else pctx
1546 m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True)
1546 m = scmutil.match(ctx_to_walk, [pat], opts, globbed=True)
1547 for abs in ctx_to_walk.walk(m):
1547 for abs in ctx_to_walk.walk(m):
1548 rel = uipathfn(abs)
1548 rel = uipathfn(abs)
1549 exact = m.exact(abs)
1549 exact = m.exact(abs)
1550 if abs not in ctx:
1550 if abs not in ctx:
1551 if abs in pctx:
1551 if abs in pctx:
1552 if not after:
1552 if not after:
1553 if exact:
1553 if exact:
1554 ui.warn(
1554 ui.warn(
1555 _(
1555 _(
1556 b'%s: not copying - file has been marked '
1556 b'%s: not copying - file has been marked '
1557 b'for remove\n'
1557 b'for remove\n'
1558 )
1558 )
1559 % rel
1559 % rel
1560 )
1560 )
1561 continue
1561 continue
1562 else:
1562 else:
1563 if exact:
1563 if exact:
1564 ui.warn(
1564 ui.warn(
1565 _(b'%s: not copying - file is not managed\n') % rel
1565 _(b'%s: not copying - file is not managed\n') % rel
1566 )
1566 )
1567 continue
1567 continue
1568
1568
1569 # abs: hgsep
1569 # abs: hgsep
1570 # rel: ossep
1570 # rel: ossep
1571 srcs.append((abs, rel, exact))
1571 srcs.append((abs, rel, exact))
1572 return srcs
1572 return srcs
1573
1573
1574 if ctx.rev() is not None:
1574 if ctx.rev() is not None:
1575 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1575 rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
1576 absdest = pathutil.canonpath(repo.root, cwd, dest)
1576 absdest = pathutil.canonpath(repo.root, cwd, dest)
1577 if ctx.hasdir(absdest):
1577 if ctx.hasdir(absdest):
1578 raise error.InputError(
1578 raise error.InputError(
1579 _(b'%s: --at-rev does not support a directory as destination')
1579 _(b'%s: --at-rev does not support a directory as destination')
1580 % uipathfn(absdest)
1580 % uipathfn(absdest)
1581 )
1581 )
1582 if absdest not in ctx:
1582 if absdest not in ctx:
1583 raise error.InputError(
1583 raise error.InputError(
1584 _(b'%s: copy destination does not exist in %s')
1584 _(b'%s: copy destination does not exist in %s')
1585 % (uipathfn(absdest), ctx)
1585 % (uipathfn(absdest), ctx)
1586 )
1586 )
1587
1587
1588 # avoid cycle context -> subrepo -> cmdutil
1588 # avoid cycle context -> subrepo -> cmdutil
1589 from . import context
1589 from . import context
1590
1590
1591 copylist = []
1591 copylist = []
1592 for pat in pats:
1592 for pat in pats:
1593 srcs = walkpat(pat)
1593 srcs = walkpat(pat)
1594 if not srcs:
1594 if not srcs:
1595 continue
1595 continue
1596 for abs, rel, exact in srcs:
1596 for abs, rel, exact in srcs:
1597 copylist.append(abs)
1597 copylist.append(abs)
1598
1598
1599 if not copylist:
1599 if not copylist:
1600 raise error.InputError(_(b'no files to copy'))
1600 raise error.InputError(_(b'no files to copy'))
1601 # TODO: Add support for `hg cp --at-rev . foo bar dir` and
1601 # TODO: Add support for `hg cp --at-rev . foo bar dir` and
1602 # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
1602 # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
1603 # existing functions below.
1603 # existing functions below.
1604 if len(copylist) != 1:
1604 if len(copylist) != 1:
1605 raise error.InputError(_(b'--at-rev requires a single source'))
1605 raise error.InputError(_(b'--at-rev requires a single source'))
1606
1606
1607 new_ctx = context.overlayworkingctx(repo)
1607 new_ctx = context.overlayworkingctx(repo)
1608 new_ctx.setbase(ctx.p1())
1608 new_ctx.setbase(ctx.p1())
1609 mergemod.graft(repo, ctx, wctx=new_ctx)
1609 mergemod.graft(repo, ctx, wctx=new_ctx)
1610
1610
1611 new_ctx.markcopied(absdest, copylist[0])
1611 new_ctx.markcopied(absdest, copylist[0])
1612
1612
1613 with repo.lock():
1613 with repo.lock():
1614 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1614 mem_ctx = new_ctx.tomemctx_for_amend(ctx)
1615 new_node = mem_ctx.commit()
1615 new_node = mem_ctx.commit()
1616
1616
1617 if repo.dirstate.p1() == ctx.node():
1617 if repo.dirstate.p1() == ctx.node():
1618 with repo.dirstate.parentchange():
1618 with repo.dirstate.parentchange():
1619 scmutil.movedirstate(repo, repo[new_node])
1619 scmutil.movedirstate(repo, repo[new_node])
1620 replacements = {ctx.node(): [new_node]}
1620 replacements = {ctx.node(): [new_node]}
1621 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1621 scmutil.cleanupnodes(repo, replacements, b'copy', fixphase=True)
1622
1622
1623 return
1623 return
1624
1624
1625 # abssrc: hgsep
1625 # abssrc: hgsep
1626 # relsrc: ossep
1626 # relsrc: ossep
1627 # otarget: ossep
1627 # otarget: ossep
1628 def copyfile(abssrc, relsrc, otarget, exact):
1628 def copyfile(abssrc, relsrc, otarget, exact):
1629 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1629 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1630 if b'/' in abstarget:
1630 if b'/' in abstarget:
1631 # We cannot normalize abstarget itself, this would prevent
1631 # We cannot normalize abstarget itself, this would prevent
1632 # case only renames, like a => A.
1632 # case only renames, like a => A.
1633 abspath, absname = abstarget.rsplit(b'/', 1)
1633 abspath, absname = abstarget.rsplit(b'/', 1)
1634 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1634 abstarget = repo.dirstate.normalize(abspath) + b'/' + absname
1635 reltarget = repo.pathto(abstarget, cwd)
1635 reltarget = repo.pathto(abstarget, cwd)
1636 target = repo.wjoin(abstarget)
1636 target = repo.wjoin(abstarget)
1637 src = repo.wjoin(abssrc)
1637 src = repo.wjoin(abssrc)
1638 state = repo.dirstate[abstarget]
1638 state = repo.dirstate[abstarget]
1639
1639
1640 scmutil.checkportable(ui, abstarget)
1640 scmutil.checkportable(ui, abstarget)
1641
1641
1642 # check for collisions
1642 # check for collisions
1643 prevsrc = targets.get(abstarget)
1643 prevsrc = targets.get(abstarget)
1644 if prevsrc is not None:
1644 if prevsrc is not None:
1645 ui.warn(
1645 ui.warn(
1646 _(b'%s: not overwriting - %s collides with %s\n')
1646 _(b'%s: not overwriting - %s collides with %s\n')
1647 % (
1647 % (
1648 reltarget,
1648 reltarget,
1649 repo.pathto(abssrc, cwd),
1649 repo.pathto(abssrc, cwd),
1650 repo.pathto(prevsrc, cwd),
1650 repo.pathto(prevsrc, cwd),
1651 )
1651 )
1652 )
1652 )
1653 return True # report a failure
1653 return True # report a failure
1654
1654
1655 # check for overwrites
1655 # check for overwrites
1656 exists = os.path.lexists(target)
1656 exists = os.path.lexists(target)
1657 samefile = False
1657 samefile = False
1658 if exists and abssrc != abstarget:
1658 if exists and abssrc != abstarget:
1659 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1659 if repo.dirstate.normalize(abssrc) == repo.dirstate.normalize(
1660 abstarget
1660 abstarget
1661 ):
1661 ):
1662 if not rename:
1662 if not rename:
1663 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1663 ui.warn(_(b"%s: can't copy - same file\n") % reltarget)
1664 return True # report a failure
1664 return True # report a failure
1665 exists = False
1665 exists = False
1666 samefile = True
1666 samefile = True
1667
1667
1668 if not after and exists or after and state in b'mn':
1668 if not after and exists or after and state in b'mn':
1669 if not opts[b'force']:
1669 if not opts[b'force']:
1670 if state in b'mn':
1670 if state in b'mn':
1671 msg = _(b'%s: not overwriting - file already committed\n')
1671 msg = _(b'%s: not overwriting - file already committed\n')
1672 if after:
1672 if after:
1673 flags = b'--after --force'
1673 flags = b'--after --force'
1674 else:
1674 else:
1675 flags = b'--force'
1675 flags = b'--force'
1676 if rename:
1676 if rename:
1677 hint = (
1677 hint = (
1678 _(
1678 _(
1679 b"('hg rename %s' to replace the file by "
1679 b"('hg rename %s' to replace the file by "
1680 b'recording a rename)\n'
1680 b'recording a rename)\n'
1681 )
1681 )
1682 % flags
1682 % flags
1683 )
1683 )
1684 else:
1684 else:
1685 hint = (
1685 hint = (
1686 _(
1686 _(
1687 b"('hg copy %s' to replace the file by "
1687 b"('hg copy %s' to replace the file by "
1688 b'recording a copy)\n'
1688 b'recording a copy)\n'
1689 )
1689 )
1690 % flags
1690 % flags
1691 )
1691 )
1692 else:
1692 else:
1693 msg = _(b'%s: not overwriting - file exists\n')
1693 msg = _(b'%s: not overwriting - file exists\n')
1694 if rename:
1694 if rename:
1695 hint = _(
1695 hint = _(
1696 b"('hg rename --after' to record the rename)\n"
1696 b"('hg rename --after' to record the rename)\n"
1697 )
1697 )
1698 else:
1698 else:
1699 hint = _(b"('hg copy --after' to record the copy)\n")
1699 hint = _(b"('hg copy --after' to record the copy)\n")
1700 ui.warn(msg % reltarget)
1700 ui.warn(msg % reltarget)
1701 ui.warn(hint)
1701 ui.warn(hint)
1702 return True # report a failure
1702 return True # report a failure
1703
1703
1704 if after:
1704 if after:
1705 if not exists:
1705 if not exists:
1706 if rename:
1706 if rename:
1707 ui.warn(
1707 ui.warn(
1708 _(b'%s: not recording move - %s does not exist\n')
1708 _(b'%s: not recording move - %s does not exist\n')
1709 % (relsrc, reltarget)
1709 % (relsrc, reltarget)
1710 )
1710 )
1711 else:
1711 else:
1712 ui.warn(
1712 ui.warn(
1713 _(b'%s: not recording copy - %s does not exist\n')
1713 _(b'%s: not recording copy - %s does not exist\n')
1714 % (relsrc, reltarget)
1714 % (relsrc, reltarget)
1715 )
1715 )
1716 return True # report a failure
1716 return True # report a failure
1717 elif not dryrun:
1717 elif not dryrun:
1718 try:
1718 try:
1719 if exists:
1719 if exists:
1720 os.unlink(target)
1720 os.unlink(target)
1721 targetdir = os.path.dirname(target) or b'.'
1721 targetdir = os.path.dirname(target) or b'.'
1722 if not os.path.isdir(targetdir):
1722 if not os.path.isdir(targetdir):
1723 os.makedirs(targetdir)
1723 os.makedirs(targetdir)
1724 if samefile:
1724 if samefile:
1725 tmp = target + b"~hgrename"
1725 tmp = target + b"~hgrename"
1726 os.rename(src, tmp)
1726 os.rename(src, tmp)
1727 os.rename(tmp, target)
1727 os.rename(tmp, target)
1728 else:
1728 else:
1729 # Preserve stat info on renames, not on copies; this matches
1729 # Preserve stat info on renames, not on copies; this matches
1730 # Linux CLI behavior.
1730 # Linux CLI behavior.
1731 util.copyfile(src, target, copystat=rename)
1731 util.copyfile(src, target, copystat=rename)
1732 srcexists = True
1732 srcexists = True
1733 except IOError as inst:
1733 except IOError as inst:
1734 if inst.errno == errno.ENOENT:
1734 if inst.errno == errno.ENOENT:
1735 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1735 ui.warn(_(b'%s: deleted in working directory\n') % relsrc)
1736 srcexists = False
1736 srcexists = False
1737 else:
1737 else:
1738 ui.warn(
1738 ui.warn(
1739 _(b'%s: cannot copy - %s\n')
1739 _(b'%s: cannot copy - %s\n')
1740 % (relsrc, encoding.strtolocal(inst.strerror))
1740 % (relsrc, encoding.strtolocal(inst.strerror))
1741 )
1741 )
1742 return True # report a failure
1742 return True # report a failure
1743
1743
1744 if ui.verbose or not exact:
1744 if ui.verbose or not exact:
1745 if rename:
1745 if rename:
1746 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1746 ui.status(_(b'moving %s to %s\n') % (relsrc, reltarget))
1747 else:
1747 else:
1748 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1748 ui.status(_(b'copying %s to %s\n') % (relsrc, reltarget))
1749
1749
1750 targets[abstarget] = abssrc
1750 targets[abstarget] = abssrc
1751
1751
1752 # fix up dirstate
1752 # fix up dirstate
1753 scmutil.dirstatecopy(
1753 scmutil.dirstatecopy(
1754 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1754 ui, repo, ctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd
1755 )
1755 )
1756 if rename and not dryrun:
1756 if rename and not dryrun:
1757 if not after and srcexists and not samefile:
1757 if not after and srcexists and not samefile:
1758 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1758 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
1759 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1759 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1760 ctx.forget([abssrc])
1760 ctx.forget([abssrc])
1761
1761
1762 # pat: ossep
1762 # pat: ossep
1763 # dest ossep
1763 # dest ossep
1764 # srcs: list of (hgsep, hgsep, ossep, bool)
1764 # srcs: list of (hgsep, hgsep, ossep, bool)
1765 # return: function that takes hgsep and returns ossep
1765 # return: function that takes hgsep and returns ossep
1766 def targetpathfn(pat, dest, srcs):
1766 def targetpathfn(pat, dest, srcs):
1767 if os.path.isdir(pat):
1767 if os.path.isdir(pat):
1768 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1768 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1769 abspfx = util.localpath(abspfx)
1769 abspfx = util.localpath(abspfx)
1770 if destdirexists:
1770 if destdirexists:
1771 striplen = len(os.path.split(abspfx)[0])
1771 striplen = len(os.path.split(abspfx)[0])
1772 else:
1772 else:
1773 striplen = len(abspfx)
1773 striplen = len(abspfx)
1774 if striplen:
1774 if striplen:
1775 striplen += len(pycompat.ossep)
1775 striplen += len(pycompat.ossep)
1776 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1776 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1777 elif destdirexists:
1777 elif destdirexists:
1778 res = lambda p: os.path.join(
1778 res = lambda p: os.path.join(
1779 dest, os.path.basename(util.localpath(p))
1779 dest, os.path.basename(util.localpath(p))
1780 )
1780 )
1781 else:
1781 else:
1782 res = lambda p: dest
1782 res = lambda p: dest
1783 return res
1783 return res
1784
1784
1785 # pat: ossep
1785 # pat: ossep
1786 # dest ossep
1786 # dest ossep
1787 # srcs: list of (hgsep, hgsep, ossep, bool)
1787 # srcs: list of (hgsep, hgsep, ossep, bool)
1788 # return: function that takes hgsep and returns ossep
1788 # return: function that takes hgsep and returns ossep
1789 def targetpathafterfn(pat, dest, srcs):
1789 def targetpathafterfn(pat, dest, srcs):
1790 if matchmod.patkind(pat):
1790 if matchmod.patkind(pat):
1791 # a mercurial pattern
1791 # a mercurial pattern
1792 res = lambda p: os.path.join(
1792 res = lambda p: os.path.join(
1793 dest, os.path.basename(util.localpath(p))
1793 dest, os.path.basename(util.localpath(p))
1794 )
1794 )
1795 else:
1795 else:
1796 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1796 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1797 if len(abspfx) < len(srcs[0][0]):
1797 if len(abspfx) < len(srcs[0][0]):
1798 # A directory. Either the target path contains the last
1798 # A directory. Either the target path contains the last
1799 # component of the source path or it does not.
1799 # component of the source path or it does not.
1800 def evalpath(striplen):
1800 def evalpath(striplen):
1801 score = 0
1801 score = 0
1802 for s in srcs:
1802 for s in srcs:
1803 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1803 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1804 if os.path.lexists(t):
1804 if os.path.lexists(t):
1805 score += 1
1805 score += 1
1806 return score
1806 return score
1807
1807
1808 abspfx = util.localpath(abspfx)
1808 abspfx = util.localpath(abspfx)
1809 striplen = len(abspfx)
1809 striplen = len(abspfx)
1810 if striplen:
1810 if striplen:
1811 striplen += len(pycompat.ossep)
1811 striplen += len(pycompat.ossep)
1812 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1812 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1813 score = evalpath(striplen)
1813 score = evalpath(striplen)
1814 striplen1 = len(os.path.split(abspfx)[0])
1814 striplen1 = len(os.path.split(abspfx)[0])
1815 if striplen1:
1815 if striplen1:
1816 striplen1 += len(pycompat.ossep)
1816 striplen1 += len(pycompat.ossep)
1817 if evalpath(striplen1) > score:
1817 if evalpath(striplen1) > score:
1818 striplen = striplen1
1818 striplen = striplen1
1819 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1819 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1820 else:
1820 else:
1821 # a file
1821 # a file
1822 if destdirexists:
1822 if destdirexists:
1823 res = lambda p: os.path.join(
1823 res = lambda p: os.path.join(
1824 dest, os.path.basename(util.localpath(p))
1824 dest, os.path.basename(util.localpath(p))
1825 )
1825 )
1826 else:
1826 else:
1827 res = lambda p: dest
1827 res = lambda p: dest
1828 return res
1828 return res
1829
1829
1830 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1830 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1831 if not destdirexists:
1831 if not destdirexists:
1832 if len(pats) > 1 or matchmod.patkind(pats[0]):
1832 if len(pats) > 1 or matchmod.patkind(pats[0]):
1833 raise error.InputError(
1833 raise error.InputError(
1834 _(
1834 _(
1835 b'with multiple sources, destination must be an '
1835 b'with multiple sources, destination must be an '
1836 b'existing directory'
1836 b'existing directory'
1837 )
1837 )
1838 )
1838 )
1839 if util.endswithsep(dest):
1839 if util.endswithsep(dest):
1840 raise error.InputError(
1840 raise error.InputError(
1841 _(b'destination %s is not a directory') % dest
1841 _(b'destination %s is not a directory') % dest
1842 )
1842 )
1843
1843
1844 tfn = targetpathfn
1844 tfn = targetpathfn
1845 if after:
1845 if after:
1846 tfn = targetpathafterfn
1846 tfn = targetpathafterfn
1847 copylist = []
1847 copylist = []
1848 for pat in pats:
1848 for pat in pats:
1849 srcs = walkpat(pat)
1849 srcs = walkpat(pat)
1850 if not srcs:
1850 if not srcs:
1851 continue
1851 continue
1852 copylist.append((tfn(pat, dest, srcs), srcs))
1852 copylist.append((tfn(pat, dest, srcs), srcs))
1853 if not copylist:
1853 if not copylist:
1854 hint = None
1854 hint = None
1855 if rename:
1855 if rename:
1856 hint = _(b'maybe you meant to use --after --at-rev=.')
1856 hint = _(b'maybe you meant to use --after --at-rev=.')
1857 raise error.InputError(_(b'no files to copy'), hint=hint)
1857 raise error.InputError(_(b'no files to copy'), hint=hint)
1858
1858
1859 errors = 0
1859 errors = 0
1860 for targetpath, srcs in copylist:
1860 for targetpath, srcs in copylist:
1861 for abssrc, relsrc, exact in srcs:
1861 for abssrc, relsrc, exact in srcs:
1862 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1862 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1863 errors += 1
1863 errors += 1
1864
1864
1865 return errors != 0
1865 return errors != 0
1866
1866
1867
1867
1868 ## facility to let extension process additional data into an import patch
1868 ## facility to let extension process additional data into an import patch
1869 # list of identifier to be executed in order
1869 # list of identifier to be executed in order
1870 extrapreimport = [] # run before commit
1870 extrapreimport = [] # run before commit
1871 extrapostimport = [] # run after commit
1871 extrapostimport = [] # run after commit
1872 # mapping from identifier to actual import function
1872 # mapping from identifier to actual import function
1873 #
1873 #
1874 # 'preimport' are run before the commit is made and are provided the following
1874 # 'preimport' are run before the commit is made and are provided the following
1875 # arguments:
1875 # arguments:
1876 # - repo: the localrepository instance,
1876 # - repo: the localrepository instance,
1877 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1877 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1878 # - extra: the future extra dictionary of the changeset, please mutate it,
1878 # - extra: the future extra dictionary of the changeset, please mutate it,
1879 # - opts: the import options.
1879 # - opts: the import options.
1880 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1880 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1881 # mutation of in memory commit and more. Feel free to rework the code to get
1881 # mutation of in memory commit and more. Feel free to rework the code to get
1882 # there.
1882 # there.
1883 extrapreimportmap = {}
1883 extrapreimportmap = {}
1884 # 'postimport' are run after the commit is made and are provided the following
1884 # 'postimport' are run after the commit is made and are provided the following
1885 # argument:
1885 # argument:
1886 # - ctx: the changectx created by import.
1886 # - ctx: the changectx created by import.
1887 extrapostimportmap = {}
1887 extrapostimportmap = {}
1888
1888
1889
1889
1890 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1890 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1891 """Utility function used by commands.import to import a single patch
1891 """Utility function used by commands.import to import a single patch
1892
1892
1893 This function is explicitly defined here to help the evolve extension to
1893 This function is explicitly defined here to help the evolve extension to
1894 wrap this part of the import logic.
1894 wrap this part of the import logic.
1895
1895
1896 The API is currently a bit ugly because it a simple code translation from
1896 The API is currently a bit ugly because it a simple code translation from
1897 the import command. Feel free to make it better.
1897 the import command. Feel free to make it better.
1898
1898
1899 :patchdata: a dictionary containing parsed patch data (such as from
1899 :patchdata: a dictionary containing parsed patch data (such as from
1900 ``patch.extract()``)
1900 ``patch.extract()``)
1901 :parents: nodes that will be parent of the created commit
1901 :parents: nodes that will be parent of the created commit
1902 :opts: the full dict of option passed to the import command
1902 :opts: the full dict of option passed to the import command
1903 :msgs: list to save commit message to.
1903 :msgs: list to save commit message to.
1904 (used in case we need to save it when failing)
1904 (used in case we need to save it when failing)
1905 :updatefunc: a function that update a repo to a given node
1905 :updatefunc: a function that update a repo to a given node
1906 updatefunc(<repo>, <node>)
1906 updatefunc(<repo>, <node>)
1907 """
1907 """
1908 # avoid cycle context -> subrepo -> cmdutil
1908 # avoid cycle context -> subrepo -> cmdutil
1909 from . import context
1909 from . import context
1910
1910
1911 tmpname = patchdata.get(b'filename')
1911 tmpname = patchdata.get(b'filename')
1912 message = patchdata.get(b'message')
1912 message = patchdata.get(b'message')
1913 user = opts.get(b'user') or patchdata.get(b'user')
1913 user = opts.get(b'user') or patchdata.get(b'user')
1914 date = opts.get(b'date') or patchdata.get(b'date')
1914 date = opts.get(b'date') or patchdata.get(b'date')
1915 branch = patchdata.get(b'branch')
1915 branch = patchdata.get(b'branch')
1916 nodeid = patchdata.get(b'nodeid')
1916 nodeid = patchdata.get(b'nodeid')
1917 p1 = patchdata.get(b'p1')
1917 p1 = patchdata.get(b'p1')
1918 p2 = patchdata.get(b'p2')
1918 p2 = patchdata.get(b'p2')
1919
1919
1920 nocommit = opts.get(b'no_commit')
1920 nocommit = opts.get(b'no_commit')
1921 importbranch = opts.get(b'import_branch')
1921 importbranch = opts.get(b'import_branch')
1922 update = not opts.get(b'bypass')
1922 update = not opts.get(b'bypass')
1923 strip = opts[b"strip"]
1923 strip = opts[b"strip"]
1924 prefix = opts[b"prefix"]
1924 prefix = opts[b"prefix"]
1925 sim = float(opts.get(b'similarity') or 0)
1925 sim = float(opts.get(b'similarity') or 0)
1926
1926
1927 if not tmpname:
1927 if not tmpname:
1928 return None, None, False
1928 return None, None, False
1929
1929
1930 rejects = False
1930 rejects = False
1931
1931
1932 cmdline_message = logmessage(ui, opts)
1932 cmdline_message = logmessage(ui, opts)
1933 if cmdline_message:
1933 if cmdline_message:
1934 # pickup the cmdline msg
1934 # pickup the cmdline msg
1935 message = cmdline_message
1935 message = cmdline_message
1936 elif message:
1936 elif message:
1937 # pickup the patch msg
1937 # pickup the patch msg
1938 message = message.strip()
1938 message = message.strip()
1939 else:
1939 else:
1940 # launch the editor
1940 # launch the editor
1941 message = None
1941 message = None
1942 ui.debug(b'message:\n%s\n' % (message or b''))
1942 ui.debug(b'message:\n%s\n' % (message or b''))
1943
1943
1944 if len(parents) == 1:
1944 if len(parents) == 1:
1945 parents.append(repo[nullrev])
1945 parents.append(repo[nullrev])
1946 if opts.get(b'exact'):
1946 if opts.get(b'exact'):
1947 if not nodeid or not p1:
1947 if not nodeid or not p1:
1948 raise error.InputError(_(b'not a Mercurial patch'))
1948 raise error.InputError(_(b'not a Mercurial patch'))
1949 p1 = repo[p1]
1949 p1 = repo[p1]
1950 p2 = repo[p2 or nullrev]
1950 p2 = repo[p2 or nullrev]
1951 elif p2:
1951 elif p2:
1952 try:
1952 try:
1953 p1 = repo[p1]
1953 p1 = repo[p1]
1954 p2 = repo[p2]
1954 p2 = repo[p2]
1955 # Without any options, consider p2 only if the
1955 # Without any options, consider p2 only if the
1956 # patch is being applied on top of the recorded
1956 # patch is being applied on top of the recorded
1957 # first parent.
1957 # first parent.
1958 if p1 != parents[0]:
1958 if p1 != parents[0]:
1959 p1 = parents[0]
1959 p1 = parents[0]
1960 p2 = repo[nullrev]
1960 p2 = repo[nullrev]
1961 except error.RepoError:
1961 except error.RepoError:
1962 p1, p2 = parents
1962 p1, p2 = parents
1963 if p2.rev() == nullrev:
1963 if p2.rev() == nullrev:
1964 ui.warn(
1964 ui.warn(
1965 _(
1965 _(
1966 b"warning: import the patch as a normal revision\n"
1966 b"warning: import the patch as a normal revision\n"
1967 b"(use --exact to import the patch as a merge)\n"
1967 b"(use --exact to import the patch as a merge)\n"
1968 )
1968 )
1969 )
1969 )
1970 else:
1970 else:
1971 p1, p2 = parents
1971 p1, p2 = parents
1972
1972
1973 n = None
1973 n = None
1974 if update:
1974 if update:
1975 if p1 != parents[0]:
1975 if p1 != parents[0]:
1976 updatefunc(repo, p1.node())
1976 updatefunc(repo, p1.node())
1977 if p2 != parents[1]:
1977 if p2 != parents[1]:
1978 repo.setparents(p1.node(), p2.node())
1978 repo.setparents(p1.node(), p2.node())
1979
1979
1980 if opts.get(b'exact') or importbranch:
1980 if opts.get(b'exact') or importbranch:
1981 repo.dirstate.setbranch(branch or b'default')
1981 repo.dirstate.setbranch(branch or b'default')
1982
1982
1983 partial = opts.get(b'partial', False)
1983 partial = opts.get(b'partial', False)
1984 files = set()
1984 files = set()
1985 try:
1985 try:
1986 patch.patch(
1986 patch.patch(
1987 ui,
1987 ui,
1988 repo,
1988 repo,
1989 tmpname,
1989 tmpname,
1990 strip=strip,
1990 strip=strip,
1991 prefix=prefix,
1991 prefix=prefix,
1992 files=files,
1992 files=files,
1993 eolmode=None,
1993 eolmode=None,
1994 similarity=sim / 100.0,
1994 similarity=sim / 100.0,
1995 )
1995 )
1996 except error.PatchError as e:
1996 except error.PatchError as e:
1997 if not partial:
1997 if not partial:
1998 raise error.Abort(pycompat.bytestr(e))
1998 raise error.Abort(pycompat.bytestr(e))
1999 if partial:
1999 if partial:
2000 rejects = True
2000 rejects = True
2001
2001
2002 files = list(files)
2002 files = list(files)
2003 if nocommit:
2003 if nocommit:
2004 if message:
2004 if message:
2005 msgs.append(message)
2005 msgs.append(message)
2006 else:
2006 else:
2007 if opts.get(b'exact') or p2:
2007 if opts.get(b'exact') or p2:
2008 # If you got here, you either use --force and know what
2008 # If you got here, you either use --force and know what
2009 # you are doing or used --exact or a merge patch while
2009 # you are doing or used --exact or a merge patch while
2010 # being updated to its first parent.
2010 # being updated to its first parent.
2011 m = None
2011 m = None
2012 else:
2012 else:
2013 m = scmutil.matchfiles(repo, files or [])
2013 m = scmutil.matchfiles(repo, files or [])
2014 editform = mergeeditform(repo[None], b'import.normal')
2014 editform = mergeeditform(repo[None], b'import.normal')
2015 if opts.get(b'exact'):
2015 if opts.get(b'exact'):
2016 editor = None
2016 editor = None
2017 else:
2017 else:
2018 editor = getcommiteditor(
2018 editor = getcommiteditor(
2019 editform=editform, **pycompat.strkwargs(opts)
2019 editform=editform, **pycompat.strkwargs(opts)
2020 )
2020 )
2021 extra = {}
2021 extra = {}
2022 for idfunc in extrapreimport:
2022 for idfunc in extrapreimport:
2023 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2023 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
2024 overrides = {}
2024 overrides = {}
2025 if partial:
2025 if partial:
2026 overrides[(b'ui', b'allowemptycommit')] = True
2026 overrides[(b'ui', b'allowemptycommit')] = True
2027 if opts.get(b'secret'):
2027 if opts.get(b'secret'):
2028 overrides[(b'phases', b'new-commit')] = b'secret'
2028 overrides[(b'phases', b'new-commit')] = b'secret'
2029 with repo.ui.configoverride(overrides, b'import'):
2029 with repo.ui.configoverride(overrides, b'import'):
2030 n = repo.commit(
2030 n = repo.commit(
2031 message, user, date, match=m, editor=editor, extra=extra
2031 message, user, date, match=m, editor=editor, extra=extra
2032 )
2032 )
2033 for idfunc in extrapostimport:
2033 for idfunc in extrapostimport:
2034 extrapostimportmap[idfunc](repo[n])
2034 extrapostimportmap[idfunc](repo[n])
2035 else:
2035 else:
2036 if opts.get(b'exact') or importbranch:
2036 if opts.get(b'exact') or importbranch:
2037 branch = branch or b'default'
2037 branch = branch or b'default'
2038 else:
2038 else:
2039 branch = p1.branch()
2039 branch = p1.branch()
2040 store = patch.filestore()
2040 store = patch.filestore()
2041 try:
2041 try:
2042 files = set()
2042 files = set()
2043 try:
2043 try:
2044 patch.patchrepo(
2044 patch.patchrepo(
2045 ui,
2045 ui,
2046 repo,
2046 repo,
2047 p1,
2047 p1,
2048 store,
2048 store,
2049 tmpname,
2049 tmpname,
2050 strip,
2050 strip,
2051 prefix,
2051 prefix,
2052 files,
2052 files,
2053 eolmode=None,
2053 eolmode=None,
2054 )
2054 )
2055 except error.PatchError as e:
2055 except error.PatchError as e:
2056 raise error.Abort(stringutil.forcebytestr(e))
2056 raise error.Abort(stringutil.forcebytestr(e))
2057 if opts.get(b'exact'):
2057 if opts.get(b'exact'):
2058 editor = None
2058 editor = None
2059 else:
2059 else:
2060 editor = getcommiteditor(editform=b'import.bypass')
2060 editor = getcommiteditor(editform=b'import.bypass')
2061 memctx = context.memctx(
2061 memctx = context.memctx(
2062 repo,
2062 repo,
2063 (p1.node(), p2.node()),
2063 (p1.node(), p2.node()),
2064 message,
2064 message,
2065 files=files,
2065 files=files,
2066 filectxfn=store,
2066 filectxfn=store,
2067 user=user,
2067 user=user,
2068 date=date,
2068 date=date,
2069 branch=branch,
2069 branch=branch,
2070 editor=editor,
2070 editor=editor,
2071 )
2071 )
2072
2072
2073 overrides = {}
2073 overrides = {}
2074 if opts.get(b'secret'):
2074 if opts.get(b'secret'):
2075 overrides[(b'phases', b'new-commit')] = b'secret'
2075 overrides[(b'phases', b'new-commit')] = b'secret'
2076 with repo.ui.configoverride(overrides, b'import'):
2076 with repo.ui.configoverride(overrides, b'import'):
2077 n = memctx.commit()
2077 n = memctx.commit()
2078 finally:
2078 finally:
2079 store.close()
2079 store.close()
2080 if opts.get(b'exact') and nocommit:
2080 if opts.get(b'exact') and nocommit:
2081 # --exact with --no-commit is still useful in that it does merge
2081 # --exact with --no-commit is still useful in that it does merge
2082 # and branch bits
2082 # and branch bits
2083 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2083 ui.warn(_(b"warning: can't check exact import with --no-commit\n"))
2084 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2084 elif opts.get(b'exact') and (not n or hex(n) != nodeid):
2085 raise error.Abort(_(b'patch is damaged or loses information'))
2085 raise error.Abort(_(b'patch is damaged or loses information'))
2086 msg = _(b'applied to working directory')
2086 msg = _(b'applied to working directory')
2087 if n:
2087 if n:
2088 # i18n: refers to a short changeset id
2088 # i18n: refers to a short changeset id
2089 msg = _(b'created %s') % short(n)
2089 msg = _(b'created %s') % short(n)
2090 return msg, n, rejects
2090 return msg, n, rejects
2091
2091
2092
2092
2093 # facility to let extensions include additional data in an exported patch
2093 # facility to let extensions include additional data in an exported patch
2094 # list of identifiers to be executed in order
2094 # list of identifiers to be executed in order
2095 extraexport = []
2095 extraexport = []
2096 # mapping from identifier to actual export function
2096 # mapping from identifier to actual export function
2097 # function as to return a string to be added to the header or None
2097 # function as to return a string to be added to the header or None
2098 # it is given two arguments (sequencenumber, changectx)
2098 # it is given two arguments (sequencenumber, changectx)
2099 extraexportmap = {}
2099 extraexportmap = {}
2100
2100
2101
2101
2102 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2102 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
2103 node = scmutil.binnode(ctx)
2103 node = scmutil.binnode(ctx)
2104 parents = [p.node() for p in ctx.parents() if p]
2104 parents = [p.node() for p in ctx.parents() if p]
2105 branch = ctx.branch()
2105 branch = ctx.branch()
2106 if switch_parent:
2106 if switch_parent:
2107 parents.reverse()
2107 parents.reverse()
2108
2108
2109 if parents:
2109 if parents:
2110 prev = parents[0]
2110 prev = parents[0]
2111 else:
2111 else:
2112 prev = repo.nullid
2112 prev = repo.nullid
2113
2113
2114 fm.context(ctx=ctx)
2114 fm.context(ctx=ctx)
2115 fm.plain(b'# HG changeset patch\n')
2115 fm.plain(b'# HG changeset patch\n')
2116 fm.write(b'user', b'# User %s\n', ctx.user())
2116 fm.write(b'user', b'# User %s\n', ctx.user())
2117 fm.plain(b'# Date %d %d\n' % ctx.date())
2117 fm.plain(b'# Date %d %d\n' % ctx.date())
2118 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2118 fm.write(b'date', b'# %s\n', fm.formatdate(ctx.date()))
2119 fm.condwrite(
2119 fm.condwrite(
2120 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2120 branch and branch != b'default', b'branch', b'# Branch %s\n', branch
2121 )
2121 )
2122 fm.write(b'node', b'# Node ID %s\n', hex(node))
2122 fm.write(b'node', b'# Node ID %s\n', hex(node))
2123 fm.plain(b'# Parent %s\n' % hex(prev))
2123 fm.plain(b'# Parent %s\n' % hex(prev))
2124 if len(parents) > 1:
2124 if len(parents) > 1:
2125 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2125 fm.plain(b'# Parent %s\n' % hex(parents[1]))
2126 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2126 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name=b'node'))
2127
2127
2128 # TODO: redesign extraexportmap function to support formatter
2128 # TODO: redesign extraexportmap function to support formatter
2129 for headerid in extraexport:
2129 for headerid in extraexport:
2130 header = extraexportmap[headerid](seqno, ctx)
2130 header = extraexportmap[headerid](seqno, ctx)
2131 if header is not None:
2131 if header is not None:
2132 fm.plain(b'# %s\n' % header)
2132 fm.plain(b'# %s\n' % header)
2133
2133
2134 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2134 fm.write(b'desc', b'%s\n', ctx.description().rstrip())
2135 fm.plain(b'\n')
2135 fm.plain(b'\n')
2136
2136
2137 if fm.isplain():
2137 if fm.isplain():
2138 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2138 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
2139 for chunk, label in chunkiter:
2139 for chunk, label in chunkiter:
2140 fm.plain(chunk, label=label)
2140 fm.plain(chunk, label=label)
2141 else:
2141 else:
2142 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2142 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
2143 # TODO: make it structured?
2143 # TODO: make it structured?
2144 fm.data(diff=b''.join(chunkiter))
2144 fm.data(diff=b''.join(chunkiter))
2145
2145
2146
2146
2147 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2147 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
2148 """Export changesets to stdout or a single file"""
2148 """Export changesets to stdout or a single file"""
2149 for seqno, rev in enumerate(revs, 1):
2149 for seqno, rev in enumerate(revs, 1):
2150 ctx = repo[rev]
2150 ctx = repo[rev]
2151 if not dest.startswith(b'<'):
2151 if not dest.startswith(b'<'):
2152 repo.ui.note(b"%s\n" % dest)
2152 repo.ui.note(b"%s\n" % dest)
2153 fm.startitem()
2153 fm.startitem()
2154 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2154 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
2155
2155
2156
2156
2157 def _exportfntemplate(
2157 def _exportfntemplate(
2158 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2158 repo, revs, basefm, fntemplate, switch_parent, diffopts, match
2159 ):
2159 ):
2160 """Export changesets to possibly multiple files"""
2160 """Export changesets to possibly multiple files"""
2161 total = len(revs)
2161 total = len(revs)
2162 revwidth = max(len(str(rev)) for rev in revs)
2162 revwidth = max(len(str(rev)) for rev in revs)
2163 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2163 filemap = util.sortdict() # filename: [(seqno, rev), ...]
2164
2164
2165 for seqno, rev in enumerate(revs, 1):
2165 for seqno, rev in enumerate(revs, 1):
2166 ctx = repo[rev]
2166 ctx = repo[rev]
2167 dest = makefilename(
2167 dest = makefilename(
2168 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2168 ctx, fntemplate, total=total, seqno=seqno, revwidth=revwidth
2169 )
2169 )
2170 filemap.setdefault(dest, []).append((seqno, rev))
2170 filemap.setdefault(dest, []).append((seqno, rev))
2171
2171
2172 for dest in filemap:
2172 for dest in filemap:
2173 with formatter.maybereopen(basefm, dest) as fm:
2173 with formatter.maybereopen(basefm, dest) as fm:
2174 repo.ui.note(b"%s\n" % dest)
2174 repo.ui.note(b"%s\n" % dest)
2175 for seqno, rev in filemap[dest]:
2175 for seqno, rev in filemap[dest]:
2176 fm.startitem()
2176 fm.startitem()
2177 ctx = repo[rev]
2177 ctx = repo[rev]
2178 _exportsingle(
2178 _exportsingle(
2179 repo, ctx, fm, match, switch_parent, seqno, diffopts
2179 repo, ctx, fm, match, switch_parent, seqno, diffopts
2180 )
2180 )
2181
2181
2182
2182
2183 def _prefetchchangedfiles(repo, revs, match):
2183 def _prefetchchangedfiles(repo, revs, match):
2184 allfiles = set()
2184 allfiles = set()
2185 for rev in revs:
2185 for rev in revs:
2186 for file in repo[rev].files():
2186 for file in repo[rev].files():
2187 if not match or match(file):
2187 if not match or match(file):
2188 allfiles.add(file)
2188 allfiles.add(file)
2189 match = scmutil.matchfiles(repo, allfiles)
2189 match = scmutil.matchfiles(repo, allfiles)
2190 revmatches = [(rev, match) for rev in revs]
2190 revmatches = [(rev, match) for rev in revs]
2191 scmutil.prefetchfiles(repo, revmatches)
2191 scmutil.prefetchfiles(repo, revmatches)
2192
2192
2193
2193
2194 def export(
2194 def export(
2195 repo,
2195 repo,
2196 revs,
2196 revs,
2197 basefm,
2197 basefm,
2198 fntemplate=b'hg-%h.patch',
2198 fntemplate=b'hg-%h.patch',
2199 switch_parent=False,
2199 switch_parent=False,
2200 opts=None,
2200 opts=None,
2201 match=None,
2201 match=None,
2202 ):
2202 ):
2203 """export changesets as hg patches
2203 """export changesets as hg patches
2204
2204
2205 Args:
2205 Args:
2206 repo: The repository from which we're exporting revisions.
2206 repo: The repository from which we're exporting revisions.
2207 revs: A list of revisions to export as revision numbers.
2207 revs: A list of revisions to export as revision numbers.
2208 basefm: A formatter to which patches should be written.
2208 basefm: A formatter to which patches should be written.
2209 fntemplate: An optional string to use for generating patch file names.
2209 fntemplate: An optional string to use for generating patch file names.
2210 switch_parent: If True, show diffs against second parent when not nullid.
2210 switch_parent: If True, show diffs against second parent when not nullid.
2211 Default is false, which always shows diff against p1.
2211 Default is false, which always shows diff against p1.
2212 opts: diff options to use for generating the patch.
2212 opts: diff options to use for generating the patch.
2213 match: If specified, only export changes to files matching this matcher.
2213 match: If specified, only export changes to files matching this matcher.
2214
2214
2215 Returns:
2215 Returns:
2216 Nothing.
2216 Nothing.
2217
2217
2218 Side Effect:
2218 Side Effect:
2219 "HG Changeset Patch" data is emitted to one of the following
2219 "HG Changeset Patch" data is emitted to one of the following
2220 destinations:
2220 destinations:
2221 fntemplate specified: Each rev is written to a unique file named using
2221 fntemplate specified: Each rev is written to a unique file named using
2222 the given template.
2222 the given template.
2223 Otherwise: All revs will be written to basefm.
2223 Otherwise: All revs will be written to basefm.
2224 """
2224 """
2225 _prefetchchangedfiles(repo, revs, match)
2225 _prefetchchangedfiles(repo, revs, match)
2226
2226
2227 if not fntemplate:
2227 if not fntemplate:
2228 _exportfile(
2228 _exportfile(
2229 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2229 repo, revs, basefm, b'<unnamed>', switch_parent, opts, match
2230 )
2230 )
2231 else:
2231 else:
2232 _exportfntemplate(
2232 _exportfntemplate(
2233 repo, revs, basefm, fntemplate, switch_parent, opts, match
2233 repo, revs, basefm, fntemplate, switch_parent, opts, match
2234 )
2234 )
2235
2235
2236
2236
2237 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2237 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
2238 """Export changesets to the given file stream"""
2238 """Export changesets to the given file stream"""
2239 _prefetchchangedfiles(repo, revs, match)
2239 _prefetchchangedfiles(repo, revs, match)
2240
2240
2241 dest = getattr(fp, 'name', b'<unnamed>')
2241 dest = getattr(fp, 'name', b'<unnamed>')
2242 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2242 with formatter.formatter(repo.ui, fp, b'export', {}) as fm:
2243 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2243 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
2244
2244
2245
2245
2246 def showmarker(fm, marker, index=None):
2246 def showmarker(fm, marker, index=None):
2247 """utility function to display obsolescence marker in a readable way
2247 """utility function to display obsolescence marker in a readable way
2248
2248
2249 To be used by debug function."""
2249 To be used by debug function."""
2250 if index is not None:
2250 if index is not None:
2251 fm.write(b'index', b'%i ', index)
2251 fm.write(b'index', b'%i ', index)
2252 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2252 fm.write(b'prednode', b'%s ', hex(marker.prednode()))
2253 succs = marker.succnodes()
2253 succs = marker.succnodes()
2254 fm.condwrite(
2254 fm.condwrite(
2255 succs,
2255 succs,
2256 b'succnodes',
2256 b'succnodes',
2257 b'%s ',
2257 b'%s ',
2258 fm.formatlist(map(hex, succs), name=b'node'),
2258 fm.formatlist(map(hex, succs), name=b'node'),
2259 )
2259 )
2260 fm.write(b'flag', b'%X ', marker.flags())
2260 fm.write(b'flag', b'%X ', marker.flags())
2261 parents = marker.parentnodes()
2261 parents = marker.parentnodes()
2262 if parents is not None:
2262 if parents is not None:
2263 fm.write(
2263 fm.write(
2264 b'parentnodes',
2264 b'parentnodes',
2265 b'{%s} ',
2265 b'{%s} ',
2266 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2266 fm.formatlist(map(hex, parents), name=b'node', sep=b', '),
2267 )
2267 )
2268 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2268 fm.write(b'date', b'(%s) ', fm.formatdate(marker.date()))
2269 meta = marker.metadata().copy()
2269 meta = marker.metadata().copy()
2270 meta.pop(b'date', None)
2270 meta.pop(b'date', None)
2271 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2271 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
2272 fm.write(
2272 fm.write(
2273 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2273 b'metadata', b'{%s}', fm.formatdict(smeta, fmt=b'%r: %r', sep=b', ')
2274 )
2274 )
2275 fm.plain(b'\n')
2275 fm.plain(b'\n')
2276
2276
2277
2277
2278 def finddate(ui, repo, date):
2278 def finddate(ui, repo, date):
2279 """Find the tipmost changeset that matches the given date spec"""
2279 """Find the tipmost changeset that matches the given date spec"""
2280 mrevs = repo.revs(b'date(%s)', date)
2280 mrevs = repo.revs(b'date(%s)', date)
2281 try:
2281 try:
2282 rev = mrevs.max()
2282 rev = mrevs.max()
2283 except ValueError:
2283 except ValueError:
2284 raise error.InputError(_(b"revision matching date not found"))
2284 raise error.InputError(_(b"revision matching date not found"))
2285
2285
2286 ui.status(
2286 ui.status(
2287 _(b"found revision %d from %s\n")
2287 _(b"found revision %d from %s\n")
2288 % (rev, dateutil.datestr(repo[rev].date()))
2288 % (rev, dateutil.datestr(repo[rev].date()))
2289 )
2289 )
2290 return b'%d' % rev
2290 return b'%d' % rev
2291
2291
2292
2292
2293 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2293 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2294 bad = []
2294 bad = []
2295
2295
2296 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2296 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2297 names = []
2297 names = []
2298 wctx = repo[None]
2298 wctx = repo[None]
2299 cca = None
2299 cca = None
2300 abort, warn = scmutil.checkportabilityalert(ui)
2300 abort, warn = scmutil.checkportabilityalert(ui)
2301 if abort or warn:
2301 if abort or warn:
2302 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2302 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2303
2303
2304 match = repo.narrowmatch(match, includeexact=True)
2304 match = repo.narrowmatch(match, includeexact=True)
2305 badmatch = matchmod.badmatch(match, badfn)
2305 badmatch = matchmod.badmatch(match, badfn)
2306 dirstate = repo.dirstate
2306 dirstate = repo.dirstate
2307 # We don't want to just call wctx.walk here, since it would return a lot of
2307 # We don't want to just call wctx.walk here, since it would return a lot of
2308 # clean files, which we aren't interested in and takes time.
2308 # clean files, which we aren't interested in and takes time.
2309 for f in sorted(
2309 for f in sorted(
2310 dirstate.walk(
2310 dirstate.walk(
2311 badmatch,
2311 badmatch,
2312 subrepos=sorted(wctx.substate),
2312 subrepos=sorted(wctx.substate),
2313 unknown=True,
2313 unknown=True,
2314 ignored=False,
2314 ignored=False,
2315 full=False,
2315 full=False,
2316 )
2316 )
2317 ):
2317 ):
2318 exact = match.exact(f)
2318 exact = match.exact(f)
2319 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2319 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2320 if cca:
2320 if cca:
2321 cca(f)
2321 cca(f)
2322 names.append(f)
2322 names.append(f)
2323 if ui.verbose or not exact:
2323 if ui.verbose or not exact:
2324 ui.status(
2324 ui.status(
2325 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2325 _(b'adding %s\n') % uipathfn(f), label=b'ui.addremove.added'
2326 )
2326 )
2327
2327
2328 for subpath in sorted(wctx.substate):
2328 for subpath in sorted(wctx.substate):
2329 sub = wctx.sub(subpath)
2329 sub = wctx.sub(subpath)
2330 try:
2330 try:
2331 submatch = matchmod.subdirmatcher(subpath, match)
2331 submatch = matchmod.subdirmatcher(subpath, match)
2332 subprefix = repo.wvfs.reljoin(prefix, subpath)
2332 subprefix = repo.wvfs.reljoin(prefix, subpath)
2333 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2333 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2334 if opts.get('subrepos'):
2334 if opts.get('subrepos'):
2335 bad.extend(
2335 bad.extend(
2336 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2336 sub.add(ui, submatch, subprefix, subuipathfn, False, **opts)
2337 )
2337 )
2338 else:
2338 else:
2339 bad.extend(
2339 bad.extend(
2340 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2340 sub.add(ui, submatch, subprefix, subuipathfn, True, **opts)
2341 )
2341 )
2342 except error.LookupError:
2342 except error.LookupError:
2343 ui.status(
2343 ui.status(
2344 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2344 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2345 )
2345 )
2346
2346
2347 if not opts.get('dry_run'):
2347 if not opts.get('dry_run'):
2348 rejected = wctx.add(names, prefix)
2348 rejected = wctx.add(names, prefix)
2349 bad.extend(f for f in rejected if f in match.files())
2349 bad.extend(f for f in rejected if f in match.files())
2350 return bad
2350 return bad
2351
2351
2352
2352
2353 def addwebdirpath(repo, serverpath, webconf):
2353 def addwebdirpath(repo, serverpath, webconf):
2354 webconf[serverpath] = repo.root
2354 webconf[serverpath] = repo.root
2355 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2355 repo.ui.debug(b'adding %s = %s\n' % (serverpath, repo.root))
2356
2356
2357 for r in repo.revs(b'filelog("path:.hgsub")'):
2357 for r in repo.revs(b'filelog("path:.hgsub")'):
2358 ctx = repo[r]
2358 ctx = repo[r]
2359 for subpath in ctx.substate:
2359 for subpath in ctx.substate:
2360 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2360 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2361
2361
2362
2362
2363 def forget(
2363 def forget(
2364 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2364 ui, repo, match, prefix, uipathfn, explicitonly, dryrun, interactive
2365 ):
2365 ):
2366 if dryrun and interactive:
2366 if dryrun and interactive:
2367 raise error.InputError(
2367 raise error.InputError(
2368 _(b"cannot specify both --dry-run and --interactive")
2368 _(b"cannot specify both --dry-run and --interactive")
2369 )
2369 )
2370 bad = []
2370 bad = []
2371 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2371 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2372 wctx = repo[None]
2372 wctx = repo[None]
2373 forgot = []
2373 forgot = []
2374
2374
2375 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2375 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2376 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2376 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2377 if explicitonly:
2377 if explicitonly:
2378 forget = [f for f in forget if match.exact(f)]
2378 forget = [f for f in forget if match.exact(f)]
2379
2379
2380 for subpath in sorted(wctx.substate):
2380 for subpath in sorted(wctx.substate):
2381 sub = wctx.sub(subpath)
2381 sub = wctx.sub(subpath)
2382 submatch = matchmod.subdirmatcher(subpath, match)
2382 submatch = matchmod.subdirmatcher(subpath, match)
2383 subprefix = repo.wvfs.reljoin(prefix, subpath)
2383 subprefix = repo.wvfs.reljoin(prefix, subpath)
2384 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2384 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2385 try:
2385 try:
2386 subbad, subforgot = sub.forget(
2386 subbad, subforgot = sub.forget(
2387 submatch,
2387 submatch,
2388 subprefix,
2388 subprefix,
2389 subuipathfn,
2389 subuipathfn,
2390 dryrun=dryrun,
2390 dryrun=dryrun,
2391 interactive=interactive,
2391 interactive=interactive,
2392 )
2392 )
2393 bad.extend([subpath + b'/' + f for f in subbad])
2393 bad.extend([subpath + b'/' + f for f in subbad])
2394 forgot.extend([subpath + b'/' + f for f in subforgot])
2394 forgot.extend([subpath + b'/' + f for f in subforgot])
2395 except error.LookupError:
2395 except error.LookupError:
2396 ui.status(
2396 ui.status(
2397 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2397 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2398 )
2398 )
2399
2399
2400 if not explicitonly:
2400 if not explicitonly:
2401 for f in match.files():
2401 for f in match.files():
2402 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2402 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2403 if f not in forgot:
2403 if f not in forgot:
2404 if repo.wvfs.exists(f):
2404 if repo.wvfs.exists(f):
2405 # Don't complain if the exact case match wasn't given.
2405 # Don't complain if the exact case match wasn't given.
2406 # But don't do this until after checking 'forgot', so
2406 # But don't do this until after checking 'forgot', so
2407 # that subrepo files aren't normalized, and this op is
2407 # that subrepo files aren't normalized, and this op is
2408 # purely from data cached by the status walk above.
2408 # purely from data cached by the status walk above.
2409 if repo.dirstate.normalize(f) in repo.dirstate:
2409 if repo.dirstate.normalize(f) in repo.dirstate:
2410 continue
2410 continue
2411 ui.warn(
2411 ui.warn(
2412 _(
2412 _(
2413 b'not removing %s: '
2413 b'not removing %s: '
2414 b'file is already untracked\n'
2414 b'file is already untracked\n'
2415 )
2415 )
2416 % uipathfn(f)
2416 % uipathfn(f)
2417 )
2417 )
2418 bad.append(f)
2418 bad.append(f)
2419
2419
2420 if interactive:
2420 if interactive:
2421 responses = _(
2421 responses = _(
2422 b'[Ynsa?]'
2422 b'[Ynsa?]'
2423 b'$$ &Yes, forget this file'
2423 b'$$ &Yes, forget this file'
2424 b'$$ &No, skip this file'
2424 b'$$ &No, skip this file'
2425 b'$$ &Skip remaining files'
2425 b'$$ &Skip remaining files'
2426 b'$$ Include &all remaining files'
2426 b'$$ Include &all remaining files'
2427 b'$$ &? (display help)'
2427 b'$$ &? (display help)'
2428 )
2428 )
2429 for filename in forget[:]:
2429 for filename in forget[:]:
2430 r = ui.promptchoice(
2430 r = ui.promptchoice(
2431 _(b'forget %s %s') % (uipathfn(filename), responses)
2431 _(b'forget %s %s') % (uipathfn(filename), responses)
2432 )
2432 )
2433 if r == 4: # ?
2433 if r == 4: # ?
2434 while r == 4:
2434 while r == 4:
2435 for c, t in ui.extractchoices(responses)[1]:
2435 for c, t in ui.extractchoices(responses)[1]:
2436 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2436 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
2437 r = ui.promptchoice(
2437 r = ui.promptchoice(
2438 _(b'forget %s %s') % (uipathfn(filename), responses)
2438 _(b'forget %s %s') % (uipathfn(filename), responses)
2439 )
2439 )
2440 if r == 0: # yes
2440 if r == 0: # yes
2441 continue
2441 continue
2442 elif r == 1: # no
2442 elif r == 1: # no
2443 forget.remove(filename)
2443 forget.remove(filename)
2444 elif r == 2: # Skip
2444 elif r == 2: # Skip
2445 fnindex = forget.index(filename)
2445 fnindex = forget.index(filename)
2446 del forget[fnindex:]
2446 del forget[fnindex:]
2447 break
2447 break
2448 elif r == 3: # All
2448 elif r == 3: # All
2449 break
2449 break
2450
2450
2451 for f in forget:
2451 for f in forget:
2452 if ui.verbose or not match.exact(f) or interactive:
2452 if ui.verbose or not match.exact(f) or interactive:
2453 ui.status(
2453 ui.status(
2454 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2454 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2455 )
2455 )
2456
2456
2457 if not dryrun:
2457 if not dryrun:
2458 rejected = wctx.forget(forget, prefix)
2458 rejected = wctx.forget(forget, prefix)
2459 bad.extend(f for f in rejected if f in match.files())
2459 bad.extend(f for f in rejected if f in match.files())
2460 forgot.extend(f for f in forget if f not in rejected)
2460 forgot.extend(f for f in forget if f not in rejected)
2461 return bad, forgot
2461 return bad, forgot
2462
2462
2463
2463
2464 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2464 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2465 ret = 1
2465 ret = 1
2466
2466
2467 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2467 needsfctx = ui.verbose or {b'size', b'flags'} & fm.datahint()
2468 if fm.isplain() and not needsfctx:
2468 if fm.isplain() and not needsfctx:
2469 # Fast path. The speed-up comes from skipping the formatter, and batching
2469 # Fast path. The speed-up comes from skipping the formatter, and batching
2470 # calls to ui.write.
2470 # calls to ui.write.
2471 buf = []
2471 buf = []
2472 for f in ctx.matches(m):
2472 for f in ctx.matches(m):
2473 buf.append(fmt % uipathfn(f))
2473 buf.append(fmt % uipathfn(f))
2474 if len(buf) > 100:
2474 if len(buf) > 100:
2475 ui.write(b''.join(buf))
2475 ui.write(b''.join(buf))
2476 del buf[:]
2476 del buf[:]
2477 ret = 0
2477 ret = 0
2478 if buf:
2478 if buf:
2479 ui.write(b''.join(buf))
2479 ui.write(b''.join(buf))
2480 else:
2480 else:
2481 for f in ctx.matches(m):
2481 for f in ctx.matches(m):
2482 fm.startitem()
2482 fm.startitem()
2483 fm.context(ctx=ctx)
2483 fm.context(ctx=ctx)
2484 if needsfctx:
2484 if needsfctx:
2485 fc = ctx[f]
2485 fc = ctx[f]
2486 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2486 fm.write(b'size flags', b'% 10d % 1s ', fc.size(), fc.flags())
2487 fm.data(path=f)
2487 fm.data(path=f)
2488 fm.plain(fmt % uipathfn(f))
2488 fm.plain(fmt % uipathfn(f))
2489 ret = 0
2489 ret = 0
2490
2490
2491 for subpath in sorted(ctx.substate):
2491 for subpath in sorted(ctx.substate):
2492 submatch = matchmod.subdirmatcher(subpath, m)
2492 submatch = matchmod.subdirmatcher(subpath, m)
2493 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2493 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2494 if subrepos or m.exact(subpath) or any(submatch.files()):
2494 if subrepos or m.exact(subpath) or any(submatch.files()):
2495 sub = ctx.sub(subpath)
2495 sub = ctx.sub(subpath)
2496 try:
2496 try:
2497 recurse = m.exact(subpath) or subrepos
2497 recurse = m.exact(subpath) or subrepos
2498 if (
2498 if (
2499 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2499 sub.printfiles(ui, submatch, subuipathfn, fm, fmt, recurse)
2500 == 0
2500 == 0
2501 ):
2501 ):
2502 ret = 0
2502 ret = 0
2503 except error.LookupError:
2503 except error.LookupError:
2504 ui.status(
2504 ui.status(
2505 _(b"skipping missing subrepository: %s\n")
2505 _(b"skipping missing subrepository: %s\n")
2506 % uipathfn(subpath)
2506 % uipathfn(subpath)
2507 )
2507 )
2508
2508
2509 return ret
2509 return ret
2510
2510
2511
2511
2512 def remove(
2512 def remove(
2513 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2513 ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun, warnings=None
2514 ):
2514 ):
2515 ret = 0
2515 ret = 0
2516 s = repo.status(match=m, clean=True)
2516 s = repo.status(match=m, clean=True)
2517 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2517 modified, added, deleted, clean = s.modified, s.added, s.deleted, s.clean
2518
2518
2519 wctx = repo[None]
2519 wctx = repo[None]
2520
2520
2521 if warnings is None:
2521 if warnings is None:
2522 warnings = []
2522 warnings = []
2523 warn = True
2523 warn = True
2524 else:
2524 else:
2525 warn = False
2525 warn = False
2526
2526
2527 subs = sorted(wctx.substate)
2527 subs = sorted(wctx.substate)
2528 progress = ui.makeprogress(
2528 progress = ui.makeprogress(
2529 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2529 _(b'searching'), total=len(subs), unit=_(b'subrepos')
2530 )
2530 )
2531 for subpath in subs:
2531 for subpath in subs:
2532 submatch = matchmod.subdirmatcher(subpath, m)
2532 submatch = matchmod.subdirmatcher(subpath, m)
2533 subprefix = repo.wvfs.reljoin(prefix, subpath)
2533 subprefix = repo.wvfs.reljoin(prefix, subpath)
2534 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2534 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2535 if subrepos or m.exact(subpath) or any(submatch.files()):
2535 if subrepos or m.exact(subpath) or any(submatch.files()):
2536 progress.increment()
2536 progress.increment()
2537 sub = wctx.sub(subpath)
2537 sub = wctx.sub(subpath)
2538 try:
2538 try:
2539 if sub.removefiles(
2539 if sub.removefiles(
2540 submatch,
2540 submatch,
2541 subprefix,
2541 subprefix,
2542 subuipathfn,
2542 subuipathfn,
2543 after,
2543 after,
2544 force,
2544 force,
2545 subrepos,
2545 subrepos,
2546 dryrun,
2546 dryrun,
2547 warnings,
2547 warnings,
2548 ):
2548 ):
2549 ret = 1
2549 ret = 1
2550 except error.LookupError:
2550 except error.LookupError:
2551 warnings.append(
2551 warnings.append(
2552 _(b"skipping missing subrepository: %s\n")
2552 _(b"skipping missing subrepository: %s\n")
2553 % uipathfn(subpath)
2553 % uipathfn(subpath)
2554 )
2554 )
2555 progress.complete()
2555 progress.complete()
2556
2556
2557 # warn about failure to delete explicit files/dirs
2557 # warn about failure to delete explicit files/dirs
2558 deleteddirs = pathutil.dirs(deleted)
2558 deleteddirs = pathutil.dirs(deleted)
2559 files = m.files()
2559 files = m.files()
2560 progress = ui.makeprogress(
2560 progress = ui.makeprogress(
2561 _(b'deleting'), total=len(files), unit=_(b'files')
2561 _(b'deleting'), total=len(files), unit=_(b'files')
2562 )
2562 )
2563 for f in files:
2563 for f in files:
2564
2564
2565 def insubrepo():
2565 def insubrepo():
2566 for subpath in wctx.substate:
2566 for subpath in wctx.substate:
2567 if f.startswith(subpath + b'/'):
2567 if f.startswith(subpath + b'/'):
2568 return True
2568 return True
2569 return False
2569 return False
2570
2570
2571 progress.increment()
2571 progress.increment()
2572 isdir = f in deleteddirs or wctx.hasdir(f)
2572 isdir = f in deleteddirs or wctx.hasdir(f)
2573 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2573 if f in repo.dirstate or isdir or f == b'.' or insubrepo() or f in subs:
2574 continue
2574 continue
2575
2575
2576 if repo.wvfs.exists(f):
2576 if repo.wvfs.exists(f):
2577 if repo.wvfs.isdir(f):
2577 if repo.wvfs.isdir(f):
2578 warnings.append(
2578 warnings.append(
2579 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2579 _(b'not removing %s: no tracked files\n') % uipathfn(f)
2580 )
2580 )
2581 else:
2581 else:
2582 warnings.append(
2582 warnings.append(
2583 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2583 _(b'not removing %s: file is untracked\n') % uipathfn(f)
2584 )
2584 )
2585 # missing files will generate a warning elsewhere
2585 # missing files will generate a warning elsewhere
2586 ret = 1
2586 ret = 1
2587 progress.complete()
2587 progress.complete()
2588
2588
2589 if force:
2589 if force:
2590 list = modified + deleted + clean + added
2590 list = modified + deleted + clean + added
2591 elif after:
2591 elif after:
2592 list = deleted
2592 list = deleted
2593 remaining = modified + added + clean
2593 remaining = modified + added + clean
2594 progress = ui.makeprogress(
2594 progress = ui.makeprogress(
2595 _(b'skipping'), total=len(remaining), unit=_(b'files')
2595 _(b'skipping'), total=len(remaining), unit=_(b'files')
2596 )
2596 )
2597 for f in remaining:
2597 for f in remaining:
2598 progress.increment()
2598 progress.increment()
2599 if ui.verbose or (f in files):
2599 if ui.verbose or (f in files):
2600 warnings.append(
2600 warnings.append(
2601 _(b'not removing %s: file still exists\n') % uipathfn(f)
2601 _(b'not removing %s: file still exists\n') % uipathfn(f)
2602 )
2602 )
2603 ret = 1
2603 ret = 1
2604 progress.complete()
2604 progress.complete()
2605 else:
2605 else:
2606 list = deleted + clean
2606 list = deleted + clean
2607 progress = ui.makeprogress(
2607 progress = ui.makeprogress(
2608 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2608 _(b'skipping'), total=(len(modified) + len(added)), unit=_(b'files')
2609 )
2609 )
2610 for f in modified:
2610 for f in modified:
2611 progress.increment()
2611 progress.increment()
2612 warnings.append(
2612 warnings.append(
2613 _(
2613 _(
2614 b'not removing %s: file is modified (use -f'
2614 b'not removing %s: file is modified (use -f'
2615 b' to force removal)\n'
2615 b' to force removal)\n'
2616 )
2616 )
2617 % uipathfn(f)
2617 % uipathfn(f)
2618 )
2618 )
2619 ret = 1
2619 ret = 1
2620 for f in added:
2620 for f in added:
2621 progress.increment()
2621 progress.increment()
2622 warnings.append(
2622 warnings.append(
2623 _(
2623 _(
2624 b"not removing %s: file has been marked for add"
2624 b"not removing %s: file has been marked for add"
2625 b" (use 'hg forget' to undo add)\n"
2625 b" (use 'hg forget' to undo add)\n"
2626 )
2626 )
2627 % uipathfn(f)
2627 % uipathfn(f)
2628 )
2628 )
2629 ret = 1
2629 ret = 1
2630 progress.complete()
2630 progress.complete()
2631
2631
2632 list = sorted(list)
2632 list = sorted(list)
2633 progress = ui.makeprogress(
2633 progress = ui.makeprogress(
2634 _(b'deleting'), total=len(list), unit=_(b'files')
2634 _(b'deleting'), total=len(list), unit=_(b'files')
2635 )
2635 )
2636 for f in list:
2636 for f in list:
2637 if ui.verbose or not m.exact(f):
2637 if ui.verbose or not m.exact(f):
2638 progress.increment()
2638 progress.increment()
2639 ui.status(
2639 ui.status(
2640 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2640 _(b'removing %s\n') % uipathfn(f), label=b'ui.addremove.removed'
2641 )
2641 )
2642 progress.complete()
2642 progress.complete()
2643
2643
2644 if not dryrun:
2644 if not dryrun:
2645 with repo.wlock():
2645 with repo.wlock():
2646 if not after:
2646 if not after:
2647 for f in list:
2647 for f in list:
2648 if f in added:
2648 if f in added:
2649 continue # we never unlink added files on remove
2649 continue # we never unlink added files on remove
2650 rmdir = repo.ui.configbool(
2650 rmdir = repo.ui.configbool(
2651 b'experimental', b'removeemptydirs'
2651 b'experimental', b'removeemptydirs'
2652 )
2652 )
2653 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2653 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2654 repo[None].forget(list)
2654 repo[None].forget(list)
2655
2655
2656 if warn:
2656 if warn:
2657 for warning in warnings:
2657 for warning in warnings:
2658 ui.warn(warning)
2658 ui.warn(warning)
2659
2659
2660 return ret
2660 return ret
2661
2661
2662
2662
2663 def _catfmtneedsdata(fm):
2663 def _catfmtneedsdata(fm):
2664 return not fm.datahint() or b'data' in fm.datahint()
2664 return not fm.datahint() or b'data' in fm.datahint()
2665
2665
2666
2666
2667 def _updatecatformatter(fm, ctx, matcher, path, decode):
2667 def _updatecatformatter(fm, ctx, matcher, path, decode):
2668 """Hook for adding data to the formatter used by ``hg cat``.
2668 """Hook for adding data to the formatter used by ``hg cat``.
2669
2669
2670 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2670 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2671 this method first."""
2671 this method first."""
2672
2672
2673 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2673 # data() can be expensive to fetch (e.g. lfs), so don't fetch it if it
2674 # wasn't requested.
2674 # wasn't requested.
2675 data = b''
2675 data = b''
2676 if _catfmtneedsdata(fm):
2676 if _catfmtneedsdata(fm):
2677 data = ctx[path].data()
2677 data = ctx[path].data()
2678 if decode:
2678 if decode:
2679 data = ctx.repo().wwritedata(path, data)
2679 data = ctx.repo().wwritedata(path, data)
2680 fm.startitem()
2680 fm.startitem()
2681 fm.context(ctx=ctx)
2681 fm.context(ctx=ctx)
2682 fm.write(b'data', b'%s', data)
2682 fm.write(b'data', b'%s', data)
2683 fm.data(path=path)
2683 fm.data(path=path)
2684
2684
2685
2685
2686 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2686 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2687 err = 1
2687 err = 1
2688 opts = pycompat.byteskwargs(opts)
2688 opts = pycompat.byteskwargs(opts)
2689
2689
2690 def write(path):
2690 def write(path):
2691 filename = None
2691 filename = None
2692 if fntemplate:
2692 if fntemplate:
2693 filename = makefilename(
2693 filename = makefilename(
2694 ctx, fntemplate, pathname=os.path.join(prefix, path)
2694 ctx, fntemplate, pathname=os.path.join(prefix, path)
2695 )
2695 )
2696 # attempt to create the directory if it does not already exist
2696 # attempt to create the directory if it does not already exist
2697 try:
2697 try:
2698 os.makedirs(os.path.dirname(filename))
2698 os.makedirs(os.path.dirname(filename))
2699 except OSError:
2699 except OSError:
2700 pass
2700 pass
2701 with formatter.maybereopen(basefm, filename) as fm:
2701 with formatter.maybereopen(basefm, filename) as fm:
2702 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2702 _updatecatformatter(fm, ctx, matcher, path, opts.get(b'decode'))
2703
2703
2704 # Automation often uses hg cat on single files, so special case it
2704 # Automation often uses hg cat on single files, so special case it
2705 # for performance to avoid the cost of parsing the manifest.
2705 # for performance to avoid the cost of parsing the manifest.
2706 if len(matcher.files()) == 1 and not matcher.anypats():
2706 if len(matcher.files()) == 1 and not matcher.anypats():
2707 file = matcher.files()[0]
2707 file = matcher.files()[0]
2708 mfl = repo.manifestlog
2708 mfl = repo.manifestlog
2709 mfnode = ctx.manifestnode()
2709 mfnode = ctx.manifestnode()
2710 try:
2710 try:
2711 if mfnode and mfl[mfnode].find(file)[0]:
2711 if mfnode and mfl[mfnode].find(file)[0]:
2712 if _catfmtneedsdata(basefm):
2712 if _catfmtneedsdata(basefm):
2713 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2713 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2714 write(file)
2714 write(file)
2715 return 0
2715 return 0
2716 except KeyError:
2716 except KeyError:
2717 pass
2717 pass
2718
2718
2719 if _catfmtneedsdata(basefm):
2719 if _catfmtneedsdata(basefm):
2720 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2720 scmutil.prefetchfiles(repo, [(ctx.rev(), matcher)])
2721
2721
2722 for abs in ctx.walk(matcher):
2722 for abs in ctx.walk(matcher):
2723 write(abs)
2723 write(abs)
2724 err = 0
2724 err = 0
2725
2725
2726 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2726 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2727 for subpath in sorted(ctx.substate):
2727 for subpath in sorted(ctx.substate):
2728 sub = ctx.sub(subpath)
2728 sub = ctx.sub(subpath)
2729 try:
2729 try:
2730 submatch = matchmod.subdirmatcher(subpath, matcher)
2730 submatch = matchmod.subdirmatcher(subpath, matcher)
2731 subprefix = os.path.join(prefix, subpath)
2731 subprefix = os.path.join(prefix, subpath)
2732 if not sub.cat(
2732 if not sub.cat(
2733 submatch,
2733 submatch,
2734 basefm,
2734 basefm,
2735 fntemplate,
2735 fntemplate,
2736 subprefix,
2736 subprefix,
2737 **pycompat.strkwargs(opts)
2737 **pycompat.strkwargs(opts)
2738 ):
2738 ):
2739 err = 0
2739 err = 0
2740 except error.RepoLookupError:
2740 except error.RepoLookupError:
2741 ui.status(
2741 ui.status(
2742 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2742 _(b"skipping missing subrepository: %s\n") % uipathfn(subpath)
2743 )
2743 )
2744
2744
2745 return err
2745 return err
2746
2746
2747
2747
2748 def commit(ui, repo, commitfunc, pats, opts):
2748 def commit(ui, repo, commitfunc, pats, opts):
2749 '''commit the specified files or all outstanding changes'''
2749 '''commit the specified files or all outstanding changes'''
2750 date = opts.get(b'date')
2750 date = opts.get(b'date')
2751 if date:
2751 if date:
2752 opts[b'date'] = dateutil.parsedate(date)
2752 opts[b'date'] = dateutil.parsedate(date)
2753 message = logmessage(ui, opts)
2753 message = logmessage(ui, opts)
2754 matcher = scmutil.match(repo[None], pats, opts)
2754 matcher = scmutil.match(repo[None], pats, opts)
2755
2755
2756 dsguard = None
2756 dsguard = None
2757 # extract addremove carefully -- this function can be called from a command
2757 # extract addremove carefully -- this function can be called from a command
2758 # that doesn't support addremove
2758 # that doesn't support addremove
2759 if opts.get(b'addremove'):
2759 if opts.get(b'addremove'):
2760 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2760 dsguard = dirstateguard.dirstateguard(repo, b'commit')
2761 with dsguard or util.nullcontextmanager():
2761 with dsguard or util.nullcontextmanager():
2762 if dsguard:
2762 if dsguard:
2763 relative = scmutil.anypats(pats, opts)
2763 relative = scmutil.anypats(pats, opts)
2764 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2764 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2765 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2765 if scmutil.addremove(repo, matcher, b"", uipathfn, opts) != 0:
2766 raise error.Abort(
2766 raise error.Abort(
2767 _(b"failed to mark all new/missing files as added/removed")
2767 _(b"failed to mark all new/missing files as added/removed")
2768 )
2768 )
2769
2769
2770 return commitfunc(ui, repo, message, matcher, opts)
2770 return commitfunc(ui, repo, message, matcher, opts)
2771
2771
2772
2772
2773 def samefile(f, ctx1, ctx2):
2773 def samefile(f, ctx1, ctx2):
2774 if f in ctx1.manifest():
2774 if f in ctx1.manifest():
2775 a = ctx1.filectx(f)
2775 a = ctx1.filectx(f)
2776 if f in ctx2.manifest():
2776 if f in ctx2.manifest():
2777 b = ctx2.filectx(f)
2777 b = ctx2.filectx(f)
2778 return not a.cmp(b) and a.flags() == b.flags()
2778 return not a.cmp(b) and a.flags() == b.flags()
2779 else:
2779 else:
2780 return False
2780 return False
2781 else:
2781 else:
2782 return f not in ctx2.manifest()
2782 return f not in ctx2.manifest()
2783
2783
2784
2784
2785 def amend(ui, repo, old, extra, pats, opts):
2785 def amend(ui, repo, old, extra, pats, opts):
2786 # avoid cycle context -> subrepo -> cmdutil
2786 # avoid cycle context -> subrepo -> cmdutil
2787 from . import context
2787 from . import context
2788
2788
2789 # amend will reuse the existing user if not specified, but the obsolete
2789 # amend will reuse the existing user if not specified, but the obsolete
2790 # marker creation requires that the current user's name is specified.
2790 # marker creation requires that the current user's name is specified.
2791 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2791 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2792 ui.username() # raise exception if username not set
2792 ui.username() # raise exception if username not set
2793
2793
2794 ui.note(_(b'amending changeset %s\n') % old)
2794 ui.note(_(b'amending changeset %s\n') % old)
2795 base = old.p1()
2795 base = old.p1()
2796
2796
2797 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2797 with repo.wlock(), repo.lock(), repo.transaction(b'amend'):
2798 # Participating changesets:
2798 # Participating changesets:
2799 #
2799 #
2800 # wctx o - workingctx that contains changes from working copy
2800 # wctx o - workingctx that contains changes from working copy
2801 # | to go into amending commit
2801 # | to go into amending commit
2802 # |
2802 # |
2803 # old o - changeset to amend
2803 # old o - changeset to amend
2804 # |
2804 # |
2805 # base o - first parent of the changeset to amend
2805 # base o - first parent of the changeset to amend
2806 wctx = repo[None]
2806 wctx = repo[None]
2807
2807
2808 # Copy to avoid mutating input
2808 # Copy to avoid mutating input
2809 extra = extra.copy()
2809 extra = extra.copy()
2810 # Update extra dict from amended commit (e.g. to preserve graft
2810 # Update extra dict from amended commit (e.g. to preserve graft
2811 # source)
2811 # source)
2812 extra.update(old.extra())
2812 extra.update(old.extra())
2813
2813
2814 # Also update it from the from the wctx
2814 # Also update it from the from the wctx
2815 extra.update(wctx.extra())
2815 extra.update(wctx.extra())
2816
2816
2817 # date-only change should be ignored?
2817 # date-only change should be ignored?
2818 datemaydiffer = resolvecommitoptions(ui, opts)
2818 datemaydiffer = resolvecommitoptions(ui, opts)
2819
2819
2820 date = old.date()
2820 date = old.date()
2821 if opts.get(b'date'):
2821 if opts.get(b'date'):
2822 date = dateutil.parsedate(opts.get(b'date'))
2822 date = dateutil.parsedate(opts.get(b'date'))
2823 user = opts.get(b'user') or old.user()
2823 user = opts.get(b'user') or old.user()
2824
2824
2825 if len(old.parents()) > 1:
2825 if len(old.parents()) > 1:
2826 # ctx.files() isn't reliable for merges, so fall back to the
2826 # ctx.files() isn't reliable for merges, so fall back to the
2827 # slower repo.status() method
2827 # slower repo.status() method
2828 st = base.status(old)
2828 st = base.status(old)
2829 files = set(st.modified) | set(st.added) | set(st.removed)
2829 files = set(st.modified) | set(st.added) | set(st.removed)
2830 else:
2830 else:
2831 files = set(old.files())
2831 files = set(old.files())
2832
2832
2833 # add/remove the files to the working copy if the "addremove" option
2833 # add/remove the files to the working copy if the "addremove" option
2834 # was specified.
2834 # was specified.
2835 matcher = scmutil.match(wctx, pats, opts)
2835 matcher = scmutil.match(wctx, pats, opts)
2836 relative = scmutil.anypats(pats, opts)
2836 relative = scmutil.anypats(pats, opts)
2837 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2837 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2838 if opts.get(b'addremove') and scmutil.addremove(
2838 if opts.get(b'addremove') and scmutil.addremove(
2839 repo, matcher, b"", uipathfn, opts
2839 repo, matcher, b"", uipathfn, opts
2840 ):
2840 ):
2841 raise error.Abort(
2841 raise error.Abort(
2842 _(b"failed to mark all new/missing files as added/removed")
2842 _(b"failed to mark all new/missing files as added/removed")
2843 )
2843 )
2844
2844
2845 # Check subrepos. This depends on in-place wctx._status update in
2845 # Check subrepos. This depends on in-place wctx._status update in
2846 # subrepo.precommit(). To minimize the risk of this hack, we do
2846 # subrepo.precommit(). To minimize the risk of this hack, we do
2847 # nothing if .hgsub does not exist.
2847 # nothing if .hgsub does not exist.
2848 if b'.hgsub' in wctx or b'.hgsub' in old:
2848 if b'.hgsub' in wctx or b'.hgsub' in old:
2849 subs, commitsubs, newsubstate = subrepoutil.precommit(
2849 subs, commitsubs, newsubstate = subrepoutil.precommit(
2850 ui, wctx, wctx._status, matcher
2850 ui, wctx, wctx._status, matcher
2851 )
2851 )
2852 # amend should abort if commitsubrepos is enabled
2852 # amend should abort if commitsubrepos is enabled
2853 assert not commitsubs
2853 assert not commitsubs
2854 if subs:
2854 if subs:
2855 subrepoutil.writestate(repo, newsubstate)
2855 subrepoutil.writestate(repo, newsubstate)
2856
2856
2857 ms = mergestatemod.mergestate.read(repo)
2857 ms = mergestatemod.mergestate.read(repo)
2858 mergeutil.checkunresolved(ms)
2858 mergeutil.checkunresolved(ms)
2859
2859
2860 filestoamend = {f for f in wctx.files() if matcher(f)}
2860 filestoamend = {f for f in wctx.files() if matcher(f)}
2861
2861
2862 changes = len(filestoamend) > 0
2862 changes = len(filestoamend) > 0
2863 if changes:
2863 if changes:
2864 # Recompute copies (avoid recording a -> b -> a)
2864 # Recompute copies (avoid recording a -> b -> a)
2865 copied = copies.pathcopies(base, wctx, matcher)
2865 copied = copies.pathcopies(base, wctx, matcher)
2866 if old.p2:
2866 if old.p2:
2867 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2867 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2868
2868
2869 # Prune files which were reverted by the updates: if old
2869 # Prune files which were reverted by the updates: if old
2870 # introduced file X and the file was renamed in the working
2870 # introduced file X and the file was renamed in the working
2871 # copy, then those two files are the same and
2871 # copy, then those two files are the same and
2872 # we can discard X from our list of files. Likewise if X
2872 # we can discard X from our list of files. Likewise if X
2873 # was removed, it's no longer relevant. If X is missing (aka
2873 # was removed, it's no longer relevant. If X is missing (aka
2874 # deleted), old X must be preserved.
2874 # deleted), old X must be preserved.
2875 files.update(filestoamend)
2875 files.update(filestoamend)
2876 files = [
2876 files = [
2877 f
2877 f
2878 for f in files
2878 for f in files
2879 if (f not in filestoamend or not samefile(f, wctx, base))
2879 if (f not in filestoamend or not samefile(f, wctx, base))
2880 ]
2880 ]
2881
2881
2882 def filectxfn(repo, ctx_, path):
2882 def filectxfn(repo, ctx_, path):
2883 try:
2883 try:
2884 # If the file being considered is not amongst the files
2884 # If the file being considered is not amongst the files
2885 # to be amended, we should return the file context from the
2885 # to be amended, we should return the file context from the
2886 # old changeset. This avoids issues when only some files in
2886 # old changeset. This avoids issues when only some files in
2887 # the working copy are being amended but there are also
2887 # the working copy are being amended but there are also
2888 # changes to other files from the old changeset.
2888 # changes to other files from the old changeset.
2889 if path not in filestoamend:
2889 if path not in filestoamend:
2890 return old.filectx(path)
2890 return old.filectx(path)
2891
2891
2892 # Return None for removed files.
2892 # Return None for removed files.
2893 if path in wctx.removed():
2893 if path in wctx.removed():
2894 return None
2894 return None
2895
2895
2896 fctx = wctx[path]
2896 fctx = wctx[path]
2897 flags = fctx.flags()
2897 flags = fctx.flags()
2898 mctx = context.memfilectx(
2898 mctx = context.memfilectx(
2899 repo,
2899 repo,
2900 ctx_,
2900 ctx_,
2901 fctx.path(),
2901 fctx.path(),
2902 fctx.data(),
2902 fctx.data(),
2903 islink=b'l' in flags,
2903 islink=b'l' in flags,
2904 isexec=b'x' in flags,
2904 isexec=b'x' in flags,
2905 copysource=copied.get(path),
2905 copysource=copied.get(path),
2906 )
2906 )
2907 return mctx
2907 return mctx
2908 except KeyError:
2908 except KeyError:
2909 return None
2909 return None
2910
2910
2911 else:
2911 else:
2912 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2912 ui.note(_(b'copying changeset %s to %s\n') % (old, base))
2913
2913
2914 # Use version of files as in the old cset
2914 # Use version of files as in the old cset
2915 def filectxfn(repo, ctx_, path):
2915 def filectxfn(repo, ctx_, path):
2916 try:
2916 try:
2917 return old.filectx(path)
2917 return old.filectx(path)
2918 except KeyError:
2918 except KeyError:
2919 return None
2919 return None
2920
2920
2921 # See if we got a message from -m or -l, if not, open the editor with
2921 # See if we got a message from -m or -l, if not, open the editor with
2922 # the message of the changeset to amend.
2922 # the message of the changeset to amend.
2923 message = logmessage(ui, opts)
2923 message = logmessage(ui, opts)
2924
2924
2925 editform = mergeeditform(old, b'commit.amend')
2925 editform = mergeeditform(old, b'commit.amend')
2926
2926
2927 if not message:
2927 if not message:
2928 message = old.description()
2928 message = old.description()
2929 # Default if message isn't provided and --edit is not passed is to
2929 # Default if message isn't provided and --edit is not passed is to
2930 # invoke editor, but allow --no-edit. If somehow we don't have any
2930 # invoke editor, but allow --no-edit. If somehow we don't have any
2931 # description, let's always start the editor.
2931 # description, let's always start the editor.
2932 doedit = not message or opts.get(b'edit') in [True, None]
2932 doedit = not message or opts.get(b'edit') in [True, None]
2933 else:
2933 else:
2934 # Default if message is provided is to not invoke editor, but allow
2934 # Default if message is provided is to not invoke editor, but allow
2935 # --edit.
2935 # --edit.
2936 doedit = opts.get(b'edit') is True
2936 doedit = opts.get(b'edit') is True
2937 editor = getcommiteditor(edit=doedit, editform=editform)
2937 editor = getcommiteditor(edit=doedit, editform=editform)
2938
2938
2939 pureextra = extra.copy()
2939 pureextra = extra.copy()
2940 extra[b'amend_source'] = old.hex()
2940 extra[b'amend_source'] = old.hex()
2941
2941
2942 new = context.memctx(
2942 new = context.memctx(
2943 repo,
2943 repo,
2944 parents=[base.node(), old.p2().node()],
2944 parents=[base.node(), old.p2().node()],
2945 text=message,
2945 text=message,
2946 files=files,
2946 files=files,
2947 filectxfn=filectxfn,
2947 filectxfn=filectxfn,
2948 user=user,
2948 user=user,
2949 date=date,
2949 date=date,
2950 extra=extra,
2950 extra=extra,
2951 editor=editor,
2951 editor=editor,
2952 )
2952 )
2953
2953
2954 newdesc = changelog.stripdesc(new.description())
2954 newdesc = changelog.stripdesc(new.description())
2955 if (
2955 if (
2956 (not changes)
2956 (not changes)
2957 and newdesc == old.description()
2957 and newdesc == old.description()
2958 and user == old.user()
2958 and user == old.user()
2959 and (date == old.date() or datemaydiffer)
2959 and (date == old.date() or datemaydiffer)
2960 and pureextra == old.extra()
2960 and pureextra == old.extra()
2961 ):
2961 ):
2962 # nothing changed. continuing here would create a new node
2962 # nothing changed. continuing here would create a new node
2963 # anyway because of the amend_source noise.
2963 # anyway because of the amend_source noise.
2964 #
2964 #
2965 # This not what we expect from amend.
2965 # This not what we expect from amend.
2966 return old.node()
2966 return old.node()
2967
2967
2968 commitphase = None
2968 commitphase = None
2969 if opts.get(b'secret'):
2969 if opts.get(b'secret'):
2970 commitphase = phases.secret
2970 commitphase = phases.secret
2971 newid = repo.commitctx(new)
2971 newid = repo.commitctx(new)
2972 ms.reset()
2972 ms.reset()
2973
2973
2974 # Reroute the working copy parent to the new changeset
2974 # Reroute the working copy parent to the new changeset
2975 repo.setparents(newid, repo.nullid)
2975 repo.setparents(newid, repo.nullid)
2976
2976
2977 # Fixing the dirstate because localrepo.commitctx does not update
2977 # Fixing the dirstate because localrepo.commitctx does not update
2978 # it. This is rather convenient because we did not need to update
2978 # it. This is rather convenient because we did not need to update
2979 # the dirstate for all the files in the new commit which commitctx
2979 # the dirstate for all the files in the new commit which commitctx
2980 # could have done if it updated the dirstate. Now, we can
2980 # could have done if it updated the dirstate. Now, we can
2981 # selectively update the dirstate only for the amended files.
2981 # selectively update the dirstate only for the amended files.
2982 dirstate = repo.dirstate
2982 dirstate = repo.dirstate
2983
2983
2984 # Update the state of the files which were added and modified in the
2984 # Update the state of the files which were added and modified in the
2985 # amend to "normal" in the dirstate. We need to use "normallookup" since
2985 # amend to "normal" in the dirstate. We need to use "normallookup" since
2986 # the files may have changed since the command started; using "normal"
2986 # the files may have changed since the command started; using "normal"
2987 # would mark them as clean but with uncommitted contents.
2987 # would mark them as clean but with uncommitted contents.
2988 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2988 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2989 for f in normalfiles:
2989 for f in normalfiles:
2990 dirstate.normallookup(f)
2990 dirstate.normallookup(f)
2991
2991
2992 # Update the state of files which were removed in the amend
2992 # Update the state of files which were removed in the amend
2993 # to "removed" in the dirstate.
2993 # to "removed" in the dirstate.
2994 removedfiles = set(wctx.removed()) & filestoamend
2994 removedfiles = set(wctx.removed()) & filestoamend
2995 for f in removedfiles:
2995 for f in removedfiles:
2996 dirstate.drop(f)
2996 dirstate.drop(f)
2997
2997
2998 mapping = {old.node(): (newid,)}
2998 mapping = {old.node(): (newid,)}
2999 obsmetadata = None
2999 obsmetadata = None
3000 if opts.get(b'note'):
3000 if opts.get(b'note'):
3001 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3001 obsmetadata = {b'note': encoding.fromlocal(opts[b'note'])}
3002 backup = ui.configbool(b'rewrite', b'backup-bundle')
3002 backup = ui.configbool(b'rewrite', b'backup-bundle')
3003 scmutil.cleanupnodes(
3003 scmutil.cleanupnodes(
3004 repo,
3004 repo,
3005 mapping,
3005 mapping,
3006 b'amend',
3006 b'amend',
3007 metadata=obsmetadata,
3007 metadata=obsmetadata,
3008 fixphase=True,
3008 fixphase=True,
3009 targetphase=commitphase,
3009 targetphase=commitphase,
3010 backup=backup,
3010 backup=backup,
3011 )
3011 )
3012
3012
3013 return newid
3013 return newid
3014
3014
3015
3015
3016 def commiteditor(repo, ctx, subs, editform=b''):
3016 def commiteditor(repo, ctx, subs, editform=b''):
3017 if ctx.description():
3017 if ctx.description():
3018 return ctx.description()
3018 return ctx.description()
3019 return commitforceeditor(
3019 return commitforceeditor(
3020 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3020 repo, ctx, subs, editform=editform, unchangedmessagedetection=True
3021 )
3021 )
3022
3022
3023
3023
3024 def commitforceeditor(
3024 def commitforceeditor(
3025 repo,
3025 repo,
3026 ctx,
3026 ctx,
3027 subs,
3027 subs,
3028 finishdesc=None,
3028 finishdesc=None,
3029 extramsg=None,
3029 extramsg=None,
3030 editform=b'',
3030 editform=b'',
3031 unchangedmessagedetection=False,
3031 unchangedmessagedetection=False,
3032 ):
3032 ):
3033 if not extramsg:
3033 if not extramsg:
3034 extramsg = _(b"Leave message empty to abort commit.")
3034 extramsg = _(b"Leave message empty to abort commit.")
3035
3035
3036 forms = [e for e in editform.split(b'.') if e]
3036 forms = [e for e in editform.split(b'.') if e]
3037 forms.insert(0, b'changeset')
3037 forms.insert(0, b'changeset')
3038 templatetext = None
3038 templatetext = None
3039 while forms:
3039 while forms:
3040 ref = b'.'.join(forms)
3040 ref = b'.'.join(forms)
3041 if repo.ui.config(b'committemplate', ref):
3041 if repo.ui.config(b'committemplate', ref):
3042 templatetext = committext = buildcommittemplate(
3042 templatetext = committext = buildcommittemplate(
3043 repo, ctx, subs, extramsg, ref
3043 repo, ctx, subs, extramsg, ref
3044 )
3044 )
3045 break
3045 break
3046 forms.pop()
3046 forms.pop()
3047 else:
3047 else:
3048 committext = buildcommittext(repo, ctx, subs, extramsg)
3048 committext = buildcommittext(repo, ctx, subs, extramsg)
3049
3049
3050 # run editor in the repository root
3050 # run editor in the repository root
3051 olddir = encoding.getcwd()
3051 olddir = encoding.getcwd()
3052 os.chdir(repo.root)
3052 os.chdir(repo.root)
3053
3053
3054 # make in-memory changes visible to external process
3054 # make in-memory changes visible to external process
3055 tr = repo.currenttransaction()
3055 tr = repo.currenttransaction()
3056 repo.dirstate.write(tr)
3056 repo.dirstate.write(tr)
3057 pending = tr and tr.writepending() and repo.root
3057 pending = tr and tr.writepending() and repo.root
3058
3058
3059 editortext = repo.ui.edit(
3059 editortext = repo.ui.edit(
3060 committext,
3060 committext,
3061 ctx.user(),
3061 ctx.user(),
3062 ctx.extra(),
3062 ctx.extra(),
3063 editform=editform,
3063 editform=editform,
3064 pending=pending,
3064 pending=pending,
3065 repopath=repo.path,
3065 repopath=repo.path,
3066 action=b'commit',
3066 action=b'commit',
3067 )
3067 )
3068 text = editortext
3068 text = editortext
3069
3069
3070 # strip away anything below this special string (used for editors that want
3070 # strip away anything below this special string (used for editors that want
3071 # to display the diff)
3071 # to display the diff)
3072 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3072 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3073 if stripbelow:
3073 if stripbelow:
3074 text = text[: stripbelow.start()]
3074 text = text[: stripbelow.start()]
3075
3075
3076 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3076 text = re.sub(b"(?m)^HG:.*(\n|$)", b"", text)
3077 os.chdir(olddir)
3077 os.chdir(olddir)
3078
3078
3079 if finishdesc:
3079 if finishdesc:
3080 text = finishdesc(text)
3080 text = finishdesc(text)
3081 if not text.strip():
3081 if not text.strip():
3082 raise error.InputError(_(b"empty commit message"))
3082 raise error.InputError(_(b"empty commit message"))
3083 if unchangedmessagedetection and editortext == templatetext:
3083 if unchangedmessagedetection and editortext == templatetext:
3084 raise error.InputError(_(b"commit message unchanged"))
3084 raise error.InputError(_(b"commit message unchanged"))
3085
3085
3086 return text
3086 return text
3087
3087
3088
3088
3089 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3089 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3090 ui = repo.ui
3090 ui = repo.ui
3091 spec = formatter.reference_templatespec(ref)
3091 spec = formatter.reference_templatespec(ref)
3092 t = logcmdutil.changesettemplater(ui, repo, spec)
3092 t = logcmdutil.changesettemplater(ui, repo, spec)
3093 t.t.cache.update(
3093 t.t.cache.update(
3094 (k, templater.unquotestring(v))
3094 (k, templater.unquotestring(v))
3095 for k, v in repo.ui.configitems(b'committemplate')
3095 for k, v in repo.ui.configitems(b'committemplate')
3096 )
3096 )
3097
3097
3098 if not extramsg:
3098 if not extramsg:
3099 extramsg = b'' # ensure that extramsg is string
3099 extramsg = b'' # ensure that extramsg is string
3100
3100
3101 ui.pushbuffer()
3101 ui.pushbuffer()
3102 t.show(ctx, extramsg=extramsg)
3102 t.show(ctx, extramsg=extramsg)
3103 return ui.popbuffer()
3103 return ui.popbuffer()
3104
3104
3105
3105
3106 def hgprefix(msg):
3106 def hgprefix(msg):
3107 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3107 return b"\n".join([b"HG: %s" % a for a in msg.split(b"\n") if a])
3108
3108
3109
3109
3110 def buildcommittext(repo, ctx, subs, extramsg):
3110 def buildcommittext(repo, ctx, subs, extramsg):
3111 edittext = []
3111 edittext = []
3112 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3112 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3113 if ctx.description():
3113 if ctx.description():
3114 edittext.append(ctx.description())
3114 edittext.append(ctx.description())
3115 edittext.append(b"")
3115 edittext.append(b"")
3116 edittext.append(b"") # Empty line between message and comments.
3116 edittext.append(b"") # Empty line between message and comments.
3117 edittext.append(
3117 edittext.append(
3118 hgprefix(
3118 hgprefix(
3119 _(
3119 _(
3120 b"Enter commit message."
3120 b"Enter commit message."
3121 b" Lines beginning with 'HG:' are removed."
3121 b" Lines beginning with 'HG:' are removed."
3122 )
3122 )
3123 )
3123 )
3124 )
3124 )
3125 edittext.append(hgprefix(extramsg))
3125 edittext.append(hgprefix(extramsg))
3126 edittext.append(b"HG: --")
3126 edittext.append(b"HG: --")
3127 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3127 edittext.append(hgprefix(_(b"user: %s") % ctx.user()))
3128 if ctx.p2():
3128 if ctx.p2():
3129 edittext.append(hgprefix(_(b"branch merge")))
3129 edittext.append(hgprefix(_(b"branch merge")))
3130 if ctx.branch():
3130 if ctx.branch():
3131 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3131 edittext.append(hgprefix(_(b"branch '%s'") % ctx.branch()))
3132 if bookmarks.isactivewdirparent(repo):
3132 if bookmarks.isactivewdirparent(repo):
3133 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3133 edittext.append(hgprefix(_(b"bookmark '%s'") % repo._activebookmark))
3134 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3134 edittext.extend([hgprefix(_(b"subrepo %s") % s) for s in subs])
3135 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3135 edittext.extend([hgprefix(_(b"added %s") % f) for f in added])
3136 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3136 edittext.extend([hgprefix(_(b"changed %s") % f) for f in modified])
3137 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3137 edittext.extend([hgprefix(_(b"removed %s") % f) for f in removed])
3138 if not added and not modified and not removed:
3138 if not added and not modified and not removed:
3139 edittext.append(hgprefix(_(b"no files changed")))
3139 edittext.append(hgprefix(_(b"no files changed")))
3140 edittext.append(b"")
3140 edittext.append(b"")
3141
3141
3142 return b"\n".join(edittext)
3142 return b"\n".join(edittext)
3143
3143
3144
3144
3145 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3145 def commitstatus(repo, node, branch, bheads=None, tip=None, opts=None):
3146 if opts is None:
3146 if opts is None:
3147 opts = {}
3147 opts = {}
3148 ctx = repo[node]
3148 ctx = repo[node]
3149 parents = ctx.parents()
3149 parents = ctx.parents()
3150
3150
3151 if tip is not None and repo.changelog.tip() == tip:
3151 if tip is not None and repo.changelog.tip() == tip:
3152 # avoid reporting something like "committed new head" when
3152 # avoid reporting something like "committed new head" when
3153 # recommitting old changesets, and issue a helpful warning
3153 # recommitting old changesets, and issue a helpful warning
3154 # for most instances
3154 # for most instances
3155 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3155 repo.ui.warn(_(b"warning: commit already existed in the repository!\n"))
3156 elif (
3156 elif (
3157 not opts.get(b'amend')
3157 not opts.get(b'amend')
3158 and bheads
3158 and bheads
3159 and node not in bheads
3159 and node not in bheads
3160 and not any(
3160 and not any(
3161 p.node() in bheads and p.branch() == branch for p in parents
3161 p.node() in bheads and p.branch() == branch for p in parents
3162 )
3162 )
3163 ):
3163 ):
3164 repo.ui.status(_(b'created new head\n'))
3164 repo.ui.status(_(b'created new head\n'))
3165 # The message is not printed for initial roots. For the other
3165 # The message is not printed for initial roots. For the other
3166 # changesets, it is printed in the following situations:
3166 # changesets, it is printed in the following situations:
3167 #
3167 #
3168 # Par column: for the 2 parents with ...
3168 # Par column: for the 2 parents with ...
3169 # N: null or no parent
3169 # N: null or no parent
3170 # B: parent is on another named branch
3170 # B: parent is on another named branch
3171 # C: parent is a regular non head changeset
3171 # C: parent is a regular non head changeset
3172 # H: parent was a branch head of the current branch
3172 # H: parent was a branch head of the current branch
3173 # Msg column: whether we print "created new head" message
3173 # Msg column: whether we print "created new head" message
3174 # In the following, it is assumed that there already exists some
3174 # In the following, it is assumed that there already exists some
3175 # initial branch heads of the current branch, otherwise nothing is
3175 # initial branch heads of the current branch, otherwise nothing is
3176 # printed anyway.
3176 # printed anyway.
3177 #
3177 #
3178 # Par Msg Comment
3178 # Par Msg Comment
3179 # N N y additional topo root
3179 # N N y additional topo root
3180 #
3180 #
3181 # B N y additional branch root
3181 # B N y additional branch root
3182 # C N y additional topo head
3182 # C N y additional topo head
3183 # H N n usual case
3183 # H N n usual case
3184 #
3184 #
3185 # B B y weird additional branch root
3185 # B B y weird additional branch root
3186 # C B y branch merge
3186 # C B y branch merge
3187 # H B n merge with named branch
3187 # H B n merge with named branch
3188 #
3188 #
3189 # C C y additional head from merge
3189 # C C y additional head from merge
3190 # C H n merge with a head
3190 # C H n merge with a head
3191 #
3191 #
3192 # H H n head merge: head count decreases
3192 # H H n head merge: head count decreases
3193
3193
3194 if not opts.get(b'close_branch'):
3194 if not opts.get(b'close_branch'):
3195 for r in parents:
3195 for r in parents:
3196 if r.closesbranch() and r.branch() == branch:
3196 if r.closesbranch() and r.branch() == branch:
3197 repo.ui.status(
3197 repo.ui.status(
3198 _(b'reopening closed branch head %d\n') % r.rev()
3198 _(b'reopening closed branch head %d\n') % r.rev()
3199 )
3199 )
3200
3200
3201 if repo.ui.debugflag:
3201 if repo.ui.debugflag:
3202 repo.ui.write(
3202 repo.ui.write(
3203 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3203 _(b'committed changeset %d:%s\n') % (ctx.rev(), ctx.hex())
3204 )
3204 )
3205 elif repo.ui.verbose:
3205 elif repo.ui.verbose:
3206 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3206 repo.ui.write(_(b'committed changeset %d:%s\n') % (ctx.rev(), ctx))
3207
3207
3208
3208
3209 def postcommitstatus(repo, pats, opts):
3209 def postcommitstatus(repo, pats, opts):
3210 return repo.status(match=scmutil.match(repo[None], pats, opts))
3210 return repo.status(match=scmutil.match(repo[None], pats, opts))
3211
3211
3212
3212
3213 def revert(ui, repo, ctx, *pats, **opts):
3213 def revert(ui, repo, ctx, *pats, **opts):
3214 opts = pycompat.byteskwargs(opts)
3214 opts = pycompat.byteskwargs(opts)
3215 parent, p2 = repo.dirstate.parents()
3215 parent, p2 = repo.dirstate.parents()
3216 node = ctx.node()
3216 node = ctx.node()
3217
3217
3218 mf = ctx.manifest()
3218 mf = ctx.manifest()
3219 if node == p2:
3219 if node == p2:
3220 parent = p2
3220 parent = p2
3221
3221
3222 # need all matching names in dirstate and manifest of target rev,
3222 # need all matching names in dirstate and manifest of target rev,
3223 # so have to walk both. do not print errors if files exist in one
3223 # so have to walk both. do not print errors if files exist in one
3224 # but not other. in both cases, filesets should be evaluated against
3224 # but not other. in both cases, filesets should be evaluated against
3225 # workingctx to get consistent result (issue4497). this means 'set:**'
3225 # workingctx to get consistent result (issue4497). this means 'set:**'
3226 # cannot be used to select missing files from target rev.
3226 # cannot be used to select missing files from target rev.
3227
3227
3228 # `names` is a mapping for all elements in working copy and target revision
3228 # `names` is a mapping for all elements in working copy and target revision
3229 # The mapping is in the form:
3229 # The mapping is in the form:
3230 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3230 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3231 names = {}
3231 names = {}
3232 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3232 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
3233
3233
3234 with repo.wlock():
3234 with repo.wlock():
3235 ## filling of the `names` mapping
3235 ## filling of the `names` mapping
3236 # walk dirstate to fill `names`
3236 # walk dirstate to fill `names`
3237
3237
3238 interactive = opts.get(b'interactive', False)
3238 interactive = opts.get(b'interactive', False)
3239 wctx = repo[None]
3239 wctx = repo[None]
3240 m = scmutil.match(wctx, pats, opts)
3240 m = scmutil.match(wctx, pats, opts)
3241
3241
3242 # we'll need this later
3242 # we'll need this later
3243 targetsubs = sorted(s for s in wctx.substate if m(s))
3243 targetsubs = sorted(s for s in wctx.substate if m(s))
3244
3244
3245 if not m.always():
3245 if not m.always():
3246 matcher = matchmod.badmatch(m, lambda x, y: False)
3246 matcher = matchmod.badmatch(m, lambda x, y: False)
3247 for abs in wctx.walk(matcher):
3247 for abs in wctx.walk(matcher):
3248 names[abs] = m.exact(abs)
3248 names[abs] = m.exact(abs)
3249
3249
3250 # walk target manifest to fill `names`
3250 # walk target manifest to fill `names`
3251
3251
3252 def badfn(path, msg):
3252 def badfn(path, msg):
3253 if path in names:
3253 if path in names:
3254 return
3254 return
3255 if path in ctx.substate:
3255 if path in ctx.substate:
3256 return
3256 return
3257 path_ = path + b'/'
3257 path_ = path + b'/'
3258 for f in names:
3258 for f in names:
3259 if f.startswith(path_):
3259 if f.startswith(path_):
3260 return
3260 return
3261 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3261 ui.warn(b"%s: %s\n" % (uipathfn(path), msg))
3262
3262
3263 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3263 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3264 if abs not in names:
3264 if abs not in names:
3265 names[abs] = m.exact(abs)
3265 names[abs] = m.exact(abs)
3266
3266
3267 # Find status of all file in `names`.
3267 # Find status of all file in `names`.
3268 m = scmutil.matchfiles(repo, names)
3268 m = scmutil.matchfiles(repo, names)
3269
3269
3270 changes = repo.status(
3270 changes = repo.status(
3271 node1=node, match=m, unknown=True, ignored=True, clean=True
3271 node1=node, match=m, unknown=True, ignored=True, clean=True
3272 )
3272 )
3273 else:
3273 else:
3274 changes = repo.status(node1=node, match=m)
3274 changes = repo.status(node1=node, match=m)
3275 for kind in changes:
3275 for kind in changes:
3276 for abs in kind:
3276 for abs in kind:
3277 names[abs] = m.exact(abs)
3277 names[abs] = m.exact(abs)
3278
3278
3279 m = scmutil.matchfiles(repo, names)
3279 m = scmutil.matchfiles(repo, names)
3280
3280
3281 modified = set(changes.modified)
3281 modified = set(changes.modified)
3282 added = set(changes.added)
3282 added = set(changes.added)
3283 removed = set(changes.removed)
3283 removed = set(changes.removed)
3284 _deleted = set(changes.deleted)
3284 _deleted = set(changes.deleted)
3285 unknown = set(changes.unknown)
3285 unknown = set(changes.unknown)
3286 unknown.update(changes.ignored)
3286 unknown.update(changes.ignored)
3287 clean = set(changes.clean)
3287 clean = set(changes.clean)
3288 modadded = set()
3288 modadded = set()
3289
3289
3290 # We need to account for the state of the file in the dirstate,
3290 # We need to account for the state of the file in the dirstate,
3291 # even when we revert against something else than parent. This will
3291 # even when we revert against something else than parent. This will
3292 # slightly alter the behavior of revert (doing back up or not, delete
3292 # slightly alter the behavior of revert (doing back up or not, delete
3293 # or just forget etc).
3293 # or just forget etc).
3294 if parent == node:
3294 if parent == node:
3295 dsmodified = modified
3295 dsmodified = modified
3296 dsadded = added
3296 dsadded = added
3297 dsremoved = removed
3297 dsremoved = removed
3298 # store all local modifications, useful later for rename detection
3298 # store all local modifications, useful later for rename detection
3299 localchanges = dsmodified | dsadded
3299 localchanges = dsmodified | dsadded
3300 modified, added, removed = set(), set(), set()
3300 modified, added, removed = set(), set(), set()
3301 else:
3301 else:
3302 changes = repo.status(node1=parent, match=m)
3302 changes = repo.status(node1=parent, match=m)
3303 dsmodified = set(changes.modified)
3303 dsmodified = set(changes.modified)
3304 dsadded = set(changes.added)
3304 dsadded = set(changes.added)
3305 dsremoved = set(changes.removed)
3305 dsremoved = set(changes.removed)
3306 # store all local modifications, useful later for rename detection
3306 # store all local modifications, useful later for rename detection
3307 localchanges = dsmodified | dsadded
3307 localchanges = dsmodified | dsadded
3308
3308
3309 # only take into account for removes between wc and target
3309 # only take into account for removes between wc and target
3310 clean |= dsremoved - removed
3310 clean |= dsremoved - removed
3311 dsremoved &= removed
3311 dsremoved &= removed
3312 # distinct between dirstate remove and other
3312 # distinct between dirstate remove and other
3313 removed -= dsremoved
3313 removed -= dsremoved
3314
3314
3315 modadded = added & dsmodified
3315 modadded = added & dsmodified
3316 added -= modadded
3316 added -= modadded
3317
3317
3318 # tell newly modified apart.
3318 # tell newly modified apart.
3319 dsmodified &= modified
3319 dsmodified &= modified
3320 dsmodified |= modified & dsadded # dirstate added may need backup
3320 dsmodified |= modified & dsadded # dirstate added may need backup
3321 modified -= dsmodified
3321 modified -= dsmodified
3322
3322
3323 # We need to wait for some post-processing to update this set
3323 # We need to wait for some post-processing to update this set
3324 # before making the distinction. The dirstate will be used for
3324 # before making the distinction. The dirstate will be used for
3325 # that purpose.
3325 # that purpose.
3326 dsadded = added
3326 dsadded = added
3327
3327
3328 # in case of merge, files that are actually added can be reported as
3328 # in case of merge, files that are actually added can be reported as
3329 # modified, we need to post process the result
3329 # modified, we need to post process the result
3330 if p2 != repo.nullid:
3330 if p2 != repo.nullid:
3331 mergeadd = set(dsmodified)
3331 mergeadd = set(dsmodified)
3332 for path in dsmodified:
3332 for path in dsmodified:
3333 if path in mf:
3333 if path in mf:
3334 mergeadd.remove(path)
3334 mergeadd.remove(path)
3335 dsadded |= mergeadd
3335 dsadded |= mergeadd
3336 dsmodified -= mergeadd
3336 dsmodified -= mergeadd
3337
3337
3338 # if f is a rename, update `names` to also revert the source
3338 # if f is a rename, update `names` to also revert the source
3339 for f in localchanges:
3339 for f in localchanges:
3340 src = repo.dirstate.copied(f)
3340 src = repo.dirstate.copied(f)
3341 # XXX should we check for rename down to target node?
3341 # XXX should we check for rename down to target node?
3342 if src and src not in names and repo.dirstate[src] == b'r':
3342 if src and src not in names and repo.dirstate[src] == b'r':
3343 dsremoved.add(src)
3343 dsremoved.add(src)
3344 names[src] = True
3344 names[src] = True
3345
3345
3346 # determine the exact nature of the deleted changesets
3346 # determine the exact nature of the deleted changesets
3347 deladded = set(_deleted)
3347 deladded = set(_deleted)
3348 for path in _deleted:
3348 for path in _deleted:
3349 if path in mf:
3349 if path in mf:
3350 deladded.remove(path)
3350 deladded.remove(path)
3351 deleted = _deleted - deladded
3351 deleted = _deleted - deladded
3352
3352
3353 # distinguish between file to forget and the other
3353 # distinguish between file to forget and the other
3354 added = set()
3354 added = set()
3355 for abs in dsadded:
3355 for abs in dsadded:
3356 if repo.dirstate[abs] != b'a':
3356 if repo.dirstate[abs] != b'a':
3357 added.add(abs)
3357 added.add(abs)
3358 dsadded -= added
3358 dsadded -= added
3359
3359
3360 for abs in deladded:
3360 for abs in deladded:
3361 if repo.dirstate[abs] == b'a':
3361 if repo.dirstate[abs] == b'a':
3362 dsadded.add(abs)
3362 dsadded.add(abs)
3363 deladded -= dsadded
3363 deladded -= dsadded
3364
3364
3365 # For files marked as removed, we check if an unknown file is present at
3365 # For files marked as removed, we check if an unknown file is present at
3366 # the same path. If a such file exists it may need to be backed up.
3366 # the same path. If a such file exists it may need to be backed up.
3367 # Making the distinction at this stage helps have simpler backup
3367 # Making the distinction at this stage helps have simpler backup
3368 # logic.
3368 # logic.
3369 removunk = set()
3369 removunk = set()
3370 for abs in removed:
3370 for abs in removed:
3371 target = repo.wjoin(abs)
3371 target = repo.wjoin(abs)
3372 if os.path.lexists(target):
3372 if os.path.lexists(target):
3373 removunk.add(abs)
3373 removunk.add(abs)
3374 removed -= removunk
3374 removed -= removunk
3375
3375
3376 dsremovunk = set()
3376 dsremovunk = set()
3377 for abs in dsremoved:
3377 for abs in dsremoved:
3378 target = repo.wjoin(abs)
3378 target = repo.wjoin(abs)
3379 if os.path.lexists(target):
3379 if os.path.lexists(target):
3380 dsremovunk.add(abs)
3380 dsremovunk.add(abs)
3381 dsremoved -= dsremovunk
3381 dsremoved -= dsremovunk
3382
3382
3383 # action to be actually performed by revert
3383 # action to be actually performed by revert
3384 # (<list of file>, message>) tuple
3384 # (<list of file>, message>) tuple
3385 actions = {
3385 actions = {
3386 b'revert': ([], _(b'reverting %s\n')),
3386 b'revert': ([], _(b'reverting %s\n')),
3387 b'add': ([], _(b'adding %s\n')),
3387 b'add': ([], _(b'adding %s\n')),
3388 b'remove': ([], _(b'removing %s\n')),
3388 b'remove': ([], _(b'removing %s\n')),
3389 b'drop': ([], _(b'removing %s\n')),
3389 b'drop': ([], _(b'removing %s\n')),
3390 b'forget': ([], _(b'forgetting %s\n')),
3390 b'forget': ([], _(b'forgetting %s\n')),
3391 b'undelete': ([], _(b'undeleting %s\n')),
3391 b'undelete': ([], _(b'undeleting %s\n')),
3392 b'noop': (None, _(b'no changes needed to %s\n')),
3392 b'noop': (None, _(b'no changes needed to %s\n')),
3393 b'unknown': (None, _(b'file not managed: %s\n')),
3393 b'unknown': (None, _(b'file not managed: %s\n')),
3394 }
3394 }
3395
3395
3396 # "constant" that convey the backup strategy.
3396 # "constant" that convey the backup strategy.
3397 # All set to `discard` if `no-backup` is set do avoid checking
3397 # All set to `discard` if `no-backup` is set do avoid checking
3398 # no_backup lower in the code.
3398 # no_backup lower in the code.
3399 # These values are ordered for comparison purposes
3399 # These values are ordered for comparison purposes
3400 backupinteractive = 3 # do backup if interactively modified
3400 backupinteractive = 3 # do backup if interactively modified
3401 backup = 2 # unconditionally do backup
3401 backup = 2 # unconditionally do backup
3402 check = 1 # check if the existing file differs from target
3402 check = 1 # check if the existing file differs from target
3403 discard = 0 # never do backup
3403 discard = 0 # never do backup
3404 if opts.get(b'no_backup'):
3404 if opts.get(b'no_backup'):
3405 backupinteractive = backup = check = discard
3405 backupinteractive = backup = check = discard
3406 if interactive:
3406 if interactive:
3407 dsmodifiedbackup = backupinteractive
3407 dsmodifiedbackup = backupinteractive
3408 else:
3408 else:
3409 dsmodifiedbackup = backup
3409 dsmodifiedbackup = backup
3410 tobackup = set()
3410 tobackup = set()
3411
3411
3412 backupanddel = actions[b'remove']
3412 backupanddel = actions[b'remove']
3413 if not opts.get(b'no_backup'):
3413 if not opts.get(b'no_backup'):
3414 backupanddel = actions[b'drop']
3414 backupanddel = actions[b'drop']
3415
3415
3416 disptable = (
3416 disptable = (
3417 # dispatch table:
3417 # dispatch table:
3418 # file state
3418 # file state
3419 # action
3419 # action
3420 # make backup
3420 # make backup
3421 ## Sets that results that will change file on disk
3421 ## Sets that results that will change file on disk
3422 # Modified compared to target, no local change
3422 # Modified compared to target, no local change
3423 (modified, actions[b'revert'], discard),
3423 (modified, actions[b'revert'], discard),
3424 # Modified compared to target, but local file is deleted
3424 # Modified compared to target, but local file is deleted
3425 (deleted, actions[b'revert'], discard),
3425 (deleted, actions[b'revert'], discard),
3426 # Modified compared to target, local change
3426 # Modified compared to target, local change
3427 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3427 (dsmodified, actions[b'revert'], dsmodifiedbackup),
3428 # Added since target
3428 # Added since target
3429 (added, actions[b'remove'], discard),
3429 (added, actions[b'remove'], discard),
3430 # Added in working directory
3430 # Added in working directory
3431 (dsadded, actions[b'forget'], discard),
3431 (dsadded, actions[b'forget'], discard),
3432 # Added since target, have local modification
3432 # Added since target, have local modification
3433 (modadded, backupanddel, backup),
3433 (modadded, backupanddel, backup),
3434 # Added since target but file is missing in working directory
3434 # Added since target but file is missing in working directory
3435 (deladded, actions[b'drop'], discard),
3435 (deladded, actions[b'drop'], discard),
3436 # Removed since target, before working copy parent
3436 # Removed since target, before working copy parent
3437 (removed, actions[b'add'], discard),
3437 (removed, actions[b'add'], discard),
3438 # Same as `removed` but an unknown file exists at the same path
3438 # Same as `removed` but an unknown file exists at the same path
3439 (removunk, actions[b'add'], check),
3439 (removunk, actions[b'add'], check),
3440 # Removed since targe, marked as such in working copy parent
3440 # Removed since targe, marked as such in working copy parent
3441 (dsremoved, actions[b'undelete'], discard),
3441 (dsremoved, actions[b'undelete'], discard),
3442 # Same as `dsremoved` but an unknown file exists at the same path
3442 # Same as `dsremoved` but an unknown file exists at the same path
3443 (dsremovunk, actions[b'undelete'], check),
3443 (dsremovunk, actions[b'undelete'], check),
3444 ## the following sets does not result in any file changes
3444 ## the following sets does not result in any file changes
3445 # File with no modification
3445 # File with no modification
3446 (clean, actions[b'noop'], discard),
3446 (clean, actions[b'noop'], discard),
3447 # Existing file, not tracked anywhere
3447 # Existing file, not tracked anywhere
3448 (unknown, actions[b'unknown'], discard),
3448 (unknown, actions[b'unknown'], discard),
3449 )
3449 )
3450
3450
3451 for abs, exact in sorted(names.items()):
3451 for abs, exact in sorted(names.items()):
3452 # target file to be touch on disk (relative to cwd)
3452 # target file to be touch on disk (relative to cwd)
3453 target = repo.wjoin(abs)
3453 target = repo.wjoin(abs)
3454 # search the entry in the dispatch table.
3454 # search the entry in the dispatch table.
3455 # if the file is in any of these sets, it was touched in the working
3455 # if the file is in any of these sets, it was touched in the working
3456 # directory parent and we are sure it needs to be reverted.
3456 # directory parent and we are sure it needs to be reverted.
3457 for table, (xlist, msg), dobackup in disptable:
3457 for table, (xlist, msg), dobackup in disptable:
3458 if abs not in table:
3458 if abs not in table:
3459 continue
3459 continue
3460 if xlist is not None:
3460 if xlist is not None:
3461 xlist.append(abs)
3461 xlist.append(abs)
3462 if dobackup:
3462 if dobackup:
3463 # If in interactive mode, don't automatically create
3463 # If in interactive mode, don't automatically create
3464 # .orig files (issue4793)
3464 # .orig files (issue4793)
3465 if dobackup == backupinteractive:
3465 if dobackup == backupinteractive:
3466 tobackup.add(abs)
3466 tobackup.add(abs)
3467 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3467 elif backup <= dobackup or wctx[abs].cmp(ctx[abs]):
3468 absbakname = scmutil.backuppath(ui, repo, abs)
3468 absbakname = scmutil.backuppath(ui, repo, abs)
3469 bakname = os.path.relpath(
3469 bakname = os.path.relpath(
3470 absbakname, start=repo.root
3470 absbakname, start=repo.root
3471 )
3471 )
3472 ui.note(
3472 ui.note(
3473 _(b'saving current version of %s as %s\n')
3473 _(b'saving current version of %s as %s\n')
3474 % (uipathfn(abs), uipathfn(bakname))
3474 % (uipathfn(abs), uipathfn(bakname))
3475 )
3475 )
3476 if not opts.get(b'dry_run'):
3476 if not opts.get(b'dry_run'):
3477 if interactive:
3477 if interactive:
3478 util.copyfile(target, absbakname)
3478 util.copyfile(target, absbakname)
3479 else:
3479 else:
3480 util.rename(target, absbakname)
3480 util.rename(target, absbakname)
3481 if opts.get(b'dry_run'):
3481 if opts.get(b'dry_run'):
3482 if ui.verbose or not exact:
3482 if ui.verbose or not exact:
3483 ui.status(msg % uipathfn(abs))
3483 ui.status(msg % uipathfn(abs))
3484 elif exact:
3484 elif exact:
3485 ui.warn(msg % uipathfn(abs))
3485 ui.warn(msg % uipathfn(abs))
3486 break
3486 break
3487
3487
3488 if not opts.get(b'dry_run'):
3488 if not opts.get(b'dry_run'):
3489 needdata = (b'revert', b'add', b'undelete')
3489 needdata = (b'revert', b'add', b'undelete')
3490 oplist = [actions[name][0] for name in needdata]
3490 oplist = [actions[name][0] for name in needdata]
3491 prefetch = scmutil.prefetchfiles
3491 prefetch = scmutil.prefetchfiles
3492 matchfiles = scmutil.matchfiles(
3492 matchfiles = scmutil.matchfiles(
3493 repo, [f for sublist in oplist for f in sublist]
3493 repo, [f for sublist in oplist for f in sublist]
3494 )
3494 )
3495 prefetch(
3495 prefetch(
3496 repo,
3496 repo,
3497 [(ctx.rev(), matchfiles)],
3497 [(ctx.rev(), matchfiles)],
3498 )
3498 )
3499 match = scmutil.match(repo[None], pats)
3499 match = scmutil.match(repo[None], pats)
3500 _performrevert(
3500 _performrevert(
3501 repo,
3501 repo,
3502 ctx,
3502 ctx,
3503 names,
3503 names,
3504 uipathfn,
3504 uipathfn,
3505 actions,
3505 actions,
3506 match,
3506 match,
3507 interactive,
3507 interactive,
3508 tobackup,
3508 tobackup,
3509 )
3509 )
3510
3510
3511 if targetsubs:
3511 if targetsubs:
3512 # Revert the subrepos on the revert list
3512 # Revert the subrepos on the revert list
3513 for sub in targetsubs:
3513 for sub in targetsubs:
3514 try:
3514 try:
3515 wctx.sub(sub).revert(
3515 wctx.sub(sub).revert(
3516 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3516 ctx.substate[sub], *pats, **pycompat.strkwargs(opts)
3517 )
3517 )
3518 except KeyError:
3518 except KeyError:
3519 raise error.Abort(
3519 raise error.Abort(
3520 b"subrepository '%s' does not exist in %s!"
3520 b"subrepository '%s' does not exist in %s!"
3521 % (sub, short(ctx.node()))
3521 % (sub, short(ctx.node()))
3522 )
3522 )
3523
3523
3524
3524
3525 def _performrevert(
3525 def _performrevert(
3526 repo,
3526 repo,
3527 ctx,
3527 ctx,
3528 names,
3528 names,
3529 uipathfn,
3529 uipathfn,
3530 actions,
3530 actions,
3531 match,
3531 match,
3532 interactive=False,
3532 interactive=False,
3533 tobackup=None,
3533 tobackup=None,
3534 ):
3534 ):
3535 """function that actually perform all the actions computed for revert
3535 """function that actually perform all the actions computed for revert
3536
3536
3537 This is an independent function to let extension to plug in and react to
3537 This is an independent function to let extension to plug in and react to
3538 the imminent revert.
3538 the imminent revert.
3539
3539
3540 Make sure you have the working directory locked when calling this function.
3540 Make sure you have the working directory locked when calling this function.
3541 """
3541 """
3542 parent, p2 = repo.dirstate.parents()
3542 parent, p2 = repo.dirstate.parents()
3543 node = ctx.node()
3543 node = ctx.node()
3544 excluded_files = []
3544 excluded_files = []
3545
3545
3546 def checkout(f):
3546 def checkout(f):
3547 fc = ctx[f]
3547 fc = ctx[f]
3548 repo.wwrite(f, fc.data(), fc.flags())
3548 repo.wwrite(f, fc.data(), fc.flags())
3549
3549
3550 def doremove(f):
3550 def doremove(f):
3551 try:
3551 try:
3552 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3552 rmdir = repo.ui.configbool(b'experimental', b'removeemptydirs')
3553 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3553 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3554 except OSError:
3554 except OSError:
3555 pass
3555 pass
3556 repo.dirstate.remove(f)
3556 repo.dirstate.remove(f)
3557
3557
3558 def prntstatusmsg(action, f):
3558 def prntstatusmsg(action, f):
3559 exact = names[f]
3559 exact = names[f]
3560 if repo.ui.verbose or not exact:
3560 if repo.ui.verbose or not exact:
3561 repo.ui.status(actions[action][1] % uipathfn(f))
3561 repo.ui.status(actions[action][1] % uipathfn(f))
3562
3562
3563 audit_path = pathutil.pathauditor(repo.root, cached=True)
3563 audit_path = pathutil.pathauditor(repo.root, cached=True)
3564 for f in actions[b'forget'][0]:
3564 for f in actions[b'forget'][0]:
3565 if interactive:
3565 if interactive:
3566 choice = repo.ui.promptchoice(
3566 choice = repo.ui.promptchoice(
3567 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3567 _(b"forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3568 )
3568 )
3569 if choice == 0:
3569 if choice == 0:
3570 prntstatusmsg(b'forget', f)
3570 prntstatusmsg(b'forget', f)
3571 repo.dirstate.drop(f)
3571 repo.dirstate.drop(f)
3572 else:
3572 else:
3573 excluded_files.append(f)
3573 excluded_files.append(f)
3574 else:
3574 else:
3575 prntstatusmsg(b'forget', f)
3575 prntstatusmsg(b'forget', f)
3576 repo.dirstate.drop(f)
3576 repo.dirstate.drop(f)
3577 for f in actions[b'remove'][0]:
3577 for f in actions[b'remove'][0]:
3578 audit_path(f)
3578 audit_path(f)
3579 if interactive:
3579 if interactive:
3580 choice = repo.ui.promptchoice(
3580 choice = repo.ui.promptchoice(
3581 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3581 _(b"remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f)
3582 )
3582 )
3583 if choice == 0:
3583 if choice == 0:
3584 prntstatusmsg(b'remove', f)
3584 prntstatusmsg(b'remove', f)
3585 doremove(f)
3585 doremove(f)
3586 else:
3586 else:
3587 excluded_files.append(f)
3587 excluded_files.append(f)
3588 else:
3588 else:
3589 prntstatusmsg(b'remove', f)
3589 prntstatusmsg(b'remove', f)
3590 doremove(f)
3590 doremove(f)
3591 for f in actions[b'drop'][0]:
3591 for f in actions[b'drop'][0]:
3592 audit_path(f)
3592 audit_path(f)
3593 prntstatusmsg(b'drop', f)
3593 prntstatusmsg(b'drop', f)
3594 repo.dirstate.remove(f)
3594 repo.dirstate.remove(f)
3595
3595
3596 normal = None
3596 normal = None
3597 if node == parent:
3597 if node == parent:
3598 # We're reverting to our parent. If possible, we'd like status
3598 # We're reverting to our parent. If possible, we'd like status
3599 # to report the file as clean. We have to use normallookup for
3599 # to report the file as clean. We have to use normallookup for
3600 # merges to avoid losing information about merged/dirty files.
3600 # merges to avoid losing information about merged/dirty files.
3601 if p2 != repo.nullid:
3601 if p2 != repo.nullid:
3602 normal = repo.dirstate.normallookup
3602 normal = repo.dirstate.normallookup
3603 else:
3603 else:
3604 normal = repo.dirstate.normal
3604 normal = repo.dirstate.normal
3605
3605
3606 newlyaddedandmodifiedfiles = set()
3606 newlyaddedandmodifiedfiles = set()
3607 if interactive:
3607 if interactive:
3608 # Prompt the user for changes to revert
3608 # Prompt the user for changes to revert
3609 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3609 torevert = [f for f in actions[b'revert'][0] if f not in excluded_files]
3610 m = scmutil.matchfiles(repo, torevert)
3610 m = scmutil.matchfiles(repo, torevert)
3611 diffopts = patch.difffeatureopts(
3611 diffopts = patch.difffeatureopts(
3612 repo.ui,
3612 repo.ui,
3613 whitespace=True,
3613 whitespace=True,
3614 section=b'commands',
3614 section=b'commands',
3615 configprefix=b'revert.interactive.',
3615 configprefix=b'revert.interactive.',
3616 )
3616 )
3617 diffopts.nodates = True
3617 diffopts.nodates = True
3618 diffopts.git = True
3618 diffopts.git = True
3619 operation = b'apply'
3619 operation = b'apply'
3620 if node == parent:
3620 if node == parent:
3621 if repo.ui.configbool(
3621 if repo.ui.configbool(
3622 b'experimental', b'revert.interactive.select-to-keep'
3622 b'experimental', b'revert.interactive.select-to-keep'
3623 ):
3623 ):
3624 operation = b'keep'
3624 operation = b'keep'
3625 else:
3625 else:
3626 operation = b'discard'
3626 operation = b'discard'
3627
3627
3628 if operation == b'apply':
3628 if operation == b'apply':
3629 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3629 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3630 else:
3630 else:
3631 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3631 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3632 originalchunks = patch.parsepatch(diff)
3632 originalchunks = patch.parsepatch(diff)
3633
3633
3634 try:
3634 try:
3635
3635
3636 chunks, opts = recordfilter(
3636 chunks, opts = recordfilter(
3637 repo.ui, originalchunks, match, operation=operation
3637 repo.ui, originalchunks, match, operation=operation
3638 )
3638 )
3639 if operation == b'discard':
3639 if operation == b'discard':
3640 chunks = patch.reversehunks(chunks)
3640 chunks = patch.reversehunks(chunks)
3641
3641
3642 except error.PatchError as err:
3642 except error.PatchError as err:
3643 raise error.Abort(_(b'error parsing patch: %s') % err)
3643 raise error.Abort(_(b'error parsing patch: %s') % err)
3644
3644
3645 # FIXME: when doing an interactive revert of a copy, there's no way of
3645 # FIXME: when doing an interactive revert of a copy, there's no way of
3646 # performing a partial revert of the added file, the only option is
3646 # performing a partial revert of the added file, the only option is
3647 # "remove added file <name> (Yn)?", so we don't need to worry about the
3647 # "remove added file <name> (Yn)?", so we don't need to worry about the
3648 # alsorestore value. Ideally we'd be able to partially revert
3648 # alsorestore value. Ideally we'd be able to partially revert
3649 # copied/renamed files.
3649 # copied/renamed files.
3650 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3650 newlyaddedandmodifiedfiles, unusedalsorestore = newandmodified(
3651 chunks, originalchunks
3651 chunks, originalchunks
3652 )
3652 )
3653 if tobackup is None:
3653 if tobackup is None:
3654 tobackup = set()
3654 tobackup = set()
3655 # Apply changes
3655 # Apply changes
3656 fp = stringio()
3656 fp = stringio()
3657 # chunks are serialized per file, but files aren't sorted
3657 # chunks are serialized per file, but files aren't sorted
3658 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3658 for f in sorted({c.header.filename() for c in chunks if ishunk(c)}):
3659 prntstatusmsg(b'revert', f)
3659 prntstatusmsg(b'revert', f)
3660 files = set()
3660 files = set()
3661 for c in chunks:
3661 for c in chunks:
3662 if ishunk(c):
3662 if ishunk(c):
3663 abs = c.header.filename()
3663 abs = c.header.filename()
3664 # Create a backup file only if this hunk should be backed up
3664 # Create a backup file only if this hunk should be backed up
3665 if c.header.filename() in tobackup:
3665 if c.header.filename() in tobackup:
3666 target = repo.wjoin(abs)
3666 target = repo.wjoin(abs)
3667 bakname = scmutil.backuppath(repo.ui, repo, abs)
3667 bakname = scmutil.backuppath(repo.ui, repo, abs)
3668 util.copyfile(target, bakname)
3668 util.copyfile(target, bakname)
3669 tobackup.remove(abs)
3669 tobackup.remove(abs)
3670 if abs not in files:
3670 if abs not in files:
3671 files.add(abs)
3671 files.add(abs)
3672 if operation == b'keep':
3672 if operation == b'keep':
3673 checkout(abs)
3673 checkout(abs)
3674 c.write(fp)
3674 c.write(fp)
3675 dopatch = fp.tell()
3675 dopatch = fp.tell()
3676 fp.seek(0)
3676 fp.seek(0)
3677 if dopatch:
3677 if dopatch:
3678 try:
3678 try:
3679 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3679 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3680 except error.PatchError as err:
3680 except error.PatchError as err:
3681 raise error.Abort(pycompat.bytestr(err))
3681 raise error.Abort(pycompat.bytestr(err))
3682 del fp
3682 del fp
3683 else:
3683 else:
3684 for f in actions[b'revert'][0]:
3684 for f in actions[b'revert'][0]:
3685 prntstatusmsg(b'revert', f)
3685 prntstatusmsg(b'revert', f)
3686 checkout(f)
3686 checkout(f)
3687 if normal:
3687 if normal:
3688 normal(f)
3688 normal(f)
3689
3689
3690 for f in actions[b'add'][0]:
3690 for f in actions[b'add'][0]:
3691 # Don't checkout modified files, they are already created by the diff
3691 # Don't checkout modified files, they are already created by the diff
3692 if f not in newlyaddedandmodifiedfiles:
3692 if f not in newlyaddedandmodifiedfiles:
3693 prntstatusmsg(b'add', f)
3693 prntstatusmsg(b'add', f)
3694 checkout(f)
3694 checkout(f)
3695 repo.dirstate.add(f)
3695 repo.dirstate.add(f)
3696
3696
3697 normal = repo.dirstate.normallookup
3697 normal = repo.dirstate.normallookup
3698 if node == parent and p2 == repo.nullid:
3698 if node == parent and p2 == repo.nullid:
3699 normal = repo.dirstate.normal
3699 normal = repo.dirstate.normal
3700 for f in actions[b'undelete'][0]:
3700 for f in actions[b'undelete'][0]:
3701 if interactive:
3701 if interactive:
3702 choice = repo.ui.promptchoice(
3702 choice = repo.ui.promptchoice(
3703 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3703 _(b"add back removed file %s (Yn)?$$ &Yes $$ &No") % f
3704 )
3704 )
3705 if choice == 0:
3705 if choice == 0:
3706 prntstatusmsg(b'undelete', f)
3706 prntstatusmsg(b'undelete', f)
3707 checkout(f)
3707 checkout(f)
3708 normal(f)
3708 normal(f)
3709 else:
3709 else:
3710 excluded_files.append(f)
3710 excluded_files.append(f)
3711 else:
3711 else:
3712 prntstatusmsg(b'undelete', f)
3712 prntstatusmsg(b'undelete', f)
3713 checkout(f)
3713 checkout(f)
3714 normal(f)
3714 normal(f)
3715
3715
3716 copied = copies.pathcopies(repo[parent], ctx)
3716 copied = copies.pathcopies(repo[parent], ctx)
3717
3717
3718 for f in (
3718 for f in (
3719 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3719 actions[b'add'][0] + actions[b'undelete'][0] + actions[b'revert'][0]
3720 ):
3720 ):
3721 if f in copied:
3721 if f in copied:
3722 repo.dirstate.copy(copied[f], f)
3722 repo.dirstate.copy(copied[f], f)
3723
3723
3724
3724
3725 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3725 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3726 # commands.outgoing. "missing" is "missing" of the result of
3726 # commands.outgoing. "missing" is "missing" of the result of
3727 # "findcommonoutgoing()"
3727 # "findcommonoutgoing()"
3728 outgoinghooks = util.hooks()
3728 outgoinghooks = util.hooks()
3729
3729
3730 # a list of (ui, repo) functions called by commands.summary
3730 # a list of (ui, repo) functions called by commands.summary
3731 summaryhooks = util.hooks()
3731 summaryhooks = util.hooks()
3732
3732
3733 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3733 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3734 #
3734 #
3735 # functions should return tuple of booleans below, if 'changes' is None:
3735 # functions should return tuple of booleans below, if 'changes' is None:
3736 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3736 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3737 #
3737 #
3738 # otherwise, 'changes' is a tuple of tuples below:
3738 # otherwise, 'changes' is a tuple of tuples below:
3739 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3739 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3740 # - (desturl, destbranch, destpeer, outgoing)
3740 # - (desturl, destbranch, destpeer, outgoing)
3741 summaryremotehooks = util.hooks()
3741 summaryremotehooks = util.hooks()
3742
3742
3743
3743
3744 def checkunfinished(repo, commit=False, skipmerge=False):
3744 def checkunfinished(repo, commit=False, skipmerge=False):
3745 """Look for an unfinished multistep operation, like graft, and abort
3745 """Look for an unfinished multistep operation, like graft, and abort
3746 if found. It's probably good to check this right before
3746 if found. It's probably good to check this right before
3747 bailifchanged().
3747 bailifchanged().
3748 """
3748 """
3749 # Check for non-clearable states first, so things like rebase will take
3749 # Check for non-clearable states first, so things like rebase will take
3750 # precedence over update.
3750 # precedence over update.
3751 for state in statemod._unfinishedstates:
3751 for state in statemod._unfinishedstates:
3752 if (
3752 if (
3753 state._clearable
3753 state._clearable
3754 or (commit and state._allowcommit)
3754 or (commit and state._allowcommit)
3755 or state._reportonly
3755 or state._reportonly
3756 ):
3756 ):
3757 continue
3757 continue
3758 if state.isunfinished(repo):
3758 if state.isunfinished(repo):
3759 raise error.StateError(state.msg(), hint=state.hint())
3759 raise error.StateError(state.msg(), hint=state.hint())
3760
3760
3761 for s in statemod._unfinishedstates:
3761 for s in statemod._unfinishedstates:
3762 if (
3762 if (
3763 not s._clearable
3763 not s._clearable
3764 or (commit and s._allowcommit)
3764 or (commit and s._allowcommit)
3765 or (s._opname == b'merge' and skipmerge)
3765 or (s._opname == b'merge' and skipmerge)
3766 or s._reportonly
3766 or s._reportonly
3767 ):
3767 ):
3768 continue
3768 continue
3769 if s.isunfinished(repo):
3769 if s.isunfinished(repo):
3770 raise error.StateError(s.msg(), hint=s.hint())
3770 raise error.StateError(s.msg(), hint=s.hint())
3771
3771
3772
3772
3773 def clearunfinished(repo):
3773 def clearunfinished(repo):
3774 """Check for unfinished operations (as above), and clear the ones
3774 """Check for unfinished operations (as above), and clear the ones
3775 that are clearable.
3775 that are clearable.
3776 """
3776 """
3777 for state in statemod._unfinishedstates:
3777 for state in statemod._unfinishedstates:
3778 if state._reportonly:
3778 if state._reportonly:
3779 continue
3779 continue
3780 if not state._clearable and state.isunfinished(repo):
3780 if not state._clearable and state.isunfinished(repo):
3781 raise error.StateError(state.msg(), hint=state.hint())
3781 raise error.StateError(state.msg(), hint=state.hint())
3782
3782
3783 for s in statemod._unfinishedstates:
3783 for s in statemod._unfinishedstates:
3784 if s._opname == b'merge' or s._reportonly:
3784 if s._opname == b'merge' or s._reportonly:
3785 continue
3785 continue
3786 if s._clearable and s.isunfinished(repo):
3786 if s._clearable and s.isunfinished(repo):
3787 util.unlink(repo.vfs.join(s._fname))
3787 util.unlink(repo.vfs.join(s._fname))
3788
3788
3789
3789
3790 def getunfinishedstate(repo):
3790 def getunfinishedstate(repo):
3791 """Checks for unfinished operations and returns statecheck object
3791 """Checks for unfinished operations and returns statecheck object
3792 for it"""
3792 for it"""
3793 for state in statemod._unfinishedstates:
3793 for state in statemod._unfinishedstates:
3794 if state.isunfinished(repo):
3794 if state.isunfinished(repo):
3795 return state
3795 return state
3796 return None
3796 return None
3797
3797
3798
3798
3799 def howtocontinue(repo):
3799 def howtocontinue(repo):
3800 """Check for an unfinished operation and return the command to finish
3800 """Check for an unfinished operation and return the command to finish
3801 it.
3801 it.
3802
3802
3803 statemod._unfinishedstates list is checked for an unfinished operation
3803 statemod._unfinishedstates list is checked for an unfinished operation
3804 and the corresponding message to finish it is generated if a method to
3804 and the corresponding message to finish it is generated if a method to
3805 continue is supported by the operation.
3805 continue is supported by the operation.
3806
3806
3807 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3807 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3808 a boolean.
3808 a boolean.
3809 """
3809 """
3810 contmsg = _(b"continue: %s")
3810 contmsg = _(b"continue: %s")
3811 for state in statemod._unfinishedstates:
3811 for state in statemod._unfinishedstates:
3812 if not state._continueflag:
3812 if not state._continueflag:
3813 continue
3813 continue
3814 if state.isunfinished(repo):
3814 if state.isunfinished(repo):
3815 return contmsg % state.continuemsg(), True
3815 return contmsg % state.continuemsg(), True
3816 if repo[None].dirty(missing=True, merge=False, branch=False):
3816 if repo[None].dirty(missing=True, merge=False, branch=False):
3817 return contmsg % _(b"hg commit"), False
3817 return contmsg % _(b"hg commit"), False
3818 return None, None
3818 return None, None
3819
3819
3820
3820
3821 def checkafterresolved(repo):
3821 def checkafterresolved(repo):
3822 """Inform the user about the next action after completing hg resolve
3822 """Inform the user about the next action after completing hg resolve
3823
3823
3824 If there's a an unfinished operation that supports continue flag,
3824 If there's a an unfinished operation that supports continue flag,
3825 howtocontinue will yield repo.ui.warn as the reporter.
3825 howtocontinue will yield repo.ui.warn as the reporter.
3826
3826
3827 Otherwise, it will yield repo.ui.note.
3827 Otherwise, it will yield repo.ui.note.
3828 """
3828 """
3829 msg, warning = howtocontinue(repo)
3829 msg, warning = howtocontinue(repo)
3830 if msg is not None:
3830 if msg is not None:
3831 if warning:
3831 if warning:
3832 repo.ui.warn(b"%s\n" % msg)
3832 repo.ui.warn(b"%s\n" % msg)
3833 else:
3833 else:
3834 repo.ui.note(b"%s\n" % msg)
3834 repo.ui.note(b"%s\n" % msg)
3835
3835
3836
3836
3837 def wrongtooltocontinue(repo, task):
3837 def wrongtooltocontinue(repo, task):
3838 """Raise an abort suggesting how to properly continue if there is an
3838 """Raise an abort suggesting how to properly continue if there is an
3839 active task.
3839 active task.
3840
3840
3841 Uses howtocontinue() to find the active task.
3841 Uses howtocontinue() to find the active task.
3842
3842
3843 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3843 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3844 a hint.
3844 a hint.
3845 """
3845 """
3846 after = howtocontinue(repo)
3846 after = howtocontinue(repo)
3847 hint = None
3847 hint = None
3848 if after[1]:
3848 if after[1]:
3849 hint = after[0]
3849 hint = after[0]
3850 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3850 raise error.StateError(_(b'no %s in progress') % task, hint=hint)
3851
3851
3852
3852
3853 def abortgraft(ui, repo, graftstate):
3853 def abortgraft(ui, repo, graftstate):
3854 """abort the interrupted graft and rollbacks to the state before interrupted
3854 """abort the interrupted graft and rollbacks to the state before interrupted
3855 graft"""
3855 graft"""
3856 if not graftstate.exists():
3856 if not graftstate.exists():
3857 raise error.StateError(_(b"no interrupted graft to abort"))
3857 raise error.StateError(_(b"no interrupted graft to abort"))
3858 statedata = readgraftstate(repo, graftstate)
3858 statedata = readgraftstate(repo, graftstate)
3859 newnodes = statedata.get(b'newnodes')
3859 newnodes = statedata.get(b'newnodes')
3860 if newnodes is None:
3860 if newnodes is None:
3861 # and old graft state which does not have all the data required to abort
3861 # and old graft state which does not have all the data required to abort
3862 # the graft
3862 # the graft
3863 raise error.Abort(_(b"cannot abort using an old graftstate"))
3863 raise error.Abort(_(b"cannot abort using an old graftstate"))
3864
3864
3865 # changeset from which graft operation was started
3865 # changeset from which graft operation was started
3866 if len(newnodes) > 0:
3866 if len(newnodes) > 0:
3867 startctx = repo[newnodes[0]].p1()
3867 startctx = repo[newnodes[0]].p1()
3868 else:
3868 else:
3869 startctx = repo[b'.']
3869 startctx = repo[b'.']
3870 # whether to strip or not
3870 # whether to strip or not
3871 cleanup = False
3871 cleanup = False
3872
3872
3873 if newnodes:
3873 if newnodes:
3874 newnodes = [repo[r].rev() for r in newnodes]
3874 newnodes = [repo[r].rev() for r in newnodes]
3875 cleanup = True
3875 cleanup = True
3876 # checking that none of the newnodes turned public or is public
3876 # checking that none of the newnodes turned public or is public
3877 immutable = [c for c in newnodes if not repo[c].mutable()]
3877 immutable = [c for c in newnodes if not repo[c].mutable()]
3878 if immutable:
3878 if immutable:
3879 repo.ui.warn(
3879 repo.ui.warn(
3880 _(b"cannot clean up public changesets %s\n")
3880 _(b"cannot clean up public changesets %s\n")
3881 % b', '.join(bytes(repo[r]) for r in immutable),
3881 % b', '.join(bytes(repo[r]) for r in immutable),
3882 hint=_(b"see 'hg help phases' for details"),
3882 hint=_(b"see 'hg help phases' for details"),
3883 )
3883 )
3884 cleanup = False
3884 cleanup = False
3885
3885
3886 # checking that no new nodes are created on top of grafted revs
3886 # checking that no new nodes are created on top of grafted revs
3887 desc = set(repo.changelog.descendants(newnodes))
3887 desc = set(repo.changelog.descendants(newnodes))
3888 if desc - set(newnodes):
3888 if desc - set(newnodes):
3889 repo.ui.warn(
3889 repo.ui.warn(
3890 _(
3890 _(
3891 b"new changesets detected on destination "
3891 b"new changesets detected on destination "
3892 b"branch, can't strip\n"
3892 b"branch, can't strip\n"
3893 )
3893 )
3894 )
3894 )
3895 cleanup = False
3895 cleanup = False
3896
3896
3897 if cleanup:
3897 if cleanup:
3898 with repo.wlock(), repo.lock():
3898 with repo.wlock(), repo.lock():
3899 mergemod.clean_update(startctx)
3899 mergemod.clean_update(startctx)
3900 # stripping the new nodes created
3900 # stripping the new nodes created
3901 strippoints = [
3901 strippoints = [
3902 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3902 c.node() for c in repo.set(b"roots(%ld)", newnodes)
3903 ]
3903 ]
3904 repair.strip(repo.ui, repo, strippoints, backup=False)
3904 repair.strip(repo.ui, repo, strippoints, backup=False)
3905
3905
3906 if not cleanup:
3906 if not cleanup:
3907 # we don't update to the startnode if we can't strip
3907 # we don't update to the startnode if we can't strip
3908 startctx = repo[b'.']
3908 startctx = repo[b'.']
3909 mergemod.clean_update(startctx)
3909 mergemod.clean_update(startctx)
3910
3910
3911 ui.status(_(b"graft aborted\n"))
3911 ui.status(_(b"graft aborted\n"))
3912 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3912 ui.status(_(b"working directory is now at %s\n") % startctx.hex()[:12])
3913 graftstate.delete()
3913 graftstate.delete()
3914 return 0
3914 return 0
3915
3915
3916
3916
3917 def readgraftstate(repo, graftstate):
3917 def readgraftstate(repo, graftstate):
3918 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3918 # type: (Any, statemod.cmdstate) -> Dict[bytes, Any]
3919 """read the graft state file and return a dict of the data stored in it"""
3919 """read the graft state file and return a dict of the data stored in it"""
3920 try:
3920 try:
3921 return graftstate.read()
3921 return graftstate.read()
3922 except error.CorruptedState:
3922 except error.CorruptedState:
3923 nodes = repo.vfs.read(b'graftstate').splitlines()
3923 nodes = repo.vfs.read(b'graftstate').splitlines()
3924 return {b'nodes': nodes}
3924 return {b'nodes': nodes}
3925
3925
3926
3926
3927 def hgabortgraft(ui, repo):
3927 def hgabortgraft(ui, repo):
3928 """abort logic for aborting graft using 'hg abort'"""
3928 """abort logic for aborting graft using 'hg abort'"""
3929 with repo.wlock():
3929 with repo.wlock():
3930 graftstate = statemod.cmdstate(repo, b'graftstate')
3930 graftstate = statemod.cmdstate(repo, b'graftstate')
3931 return abortgraft(ui, repo, graftstate)
3931 return abortgraft(ui, repo, graftstate)
General Comments 0
You need to be logged in to leave comments. Login now