##// END OF EJS Templates
uncommit: use field names instead of field numbers on scmutil.status...
Augie Fackler -
r44042:d0310f21 default
parent child Browse files
Show More
@@ -1,313 +1,314 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 node,
30 node,
31 obsutil,
31 obsutil,
32 pathutil,
32 pathutil,
33 pycompat,
33 pycompat,
34 registrar,
34 registrar,
35 rewriteutil,
35 rewriteutil,
36 scmutil,
36 scmutil,
37 )
37 )
38
38
39 cmdtable = {}
39 cmdtable = {}
40 command = registrar.command(cmdtable)
40 command = registrar.command(cmdtable)
41
41
42 configtable = {}
42 configtable = {}
43 configitem = registrar.configitem(configtable)
43 configitem = registrar.configitem(configtable)
44
44
45 configitem(
45 configitem(
46 b'experimental', b'uncommitondirtywdir', default=False,
46 b'experimental', b'uncommitondirtywdir', default=False,
47 )
47 )
48 configitem(
48 configitem(
49 b'experimental', b'uncommit.keep', default=False,
49 b'experimental', b'uncommit.keep', default=False,
50 )
50 )
51
51
52 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
52 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
53 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
53 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
54 # be specifying the version(s) of Mercurial they are tested with, or
54 # be specifying the version(s) of Mercurial they are tested with, or
55 # leave the attribute unspecified.
55 # leave the attribute unspecified.
56 testedwith = b'ships-with-hg-core'
56 testedwith = b'ships-with-hg-core'
57
57
58
58
59 def _commitfiltered(
59 def _commitfiltered(
60 repo, ctx, match, keepcommit, message=None, user=None, date=None
60 repo, ctx, match, keepcommit, message=None, user=None, date=None
61 ):
61 ):
62 """Recommit ctx with changed files not in match. Return the new
62 """Recommit ctx with changed files not in match. Return the new
63 node identifier, or None if nothing changed.
63 node identifier, or None if nothing changed.
64 """
64 """
65 base = ctx.p1()
65 base = ctx.p1()
66 # ctx
66 # ctx
67 initialfiles = set(ctx.files())
67 initialfiles = set(ctx.files())
68 exclude = set(f for f in initialfiles if match(f))
68 exclude = set(f for f in initialfiles if match(f))
69
69
70 # No files matched commit, so nothing excluded
70 # No files matched commit, so nothing excluded
71 if not exclude:
71 if not exclude:
72 return None
72 return None
73
73
74 # return the p1 so that we don't create an obsmarker later
74 # return the p1 so that we don't create an obsmarker later
75 if not keepcommit:
75 if not keepcommit:
76 return ctx.p1().node()
76 return ctx.p1().node()
77
77
78 files = initialfiles - exclude
78 files = initialfiles - exclude
79 # Filter copies
79 # Filter copies
80 copied = copiesmod.pathcopies(base, ctx)
80 copied = copiesmod.pathcopies(base, ctx)
81 copied = dict(
81 copied = dict(
82 (dst, src) for dst, src in pycompat.iteritems(copied) if dst in files
82 (dst, src) for dst, src in pycompat.iteritems(copied) if dst in files
83 )
83 )
84
84
85 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
85 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
86 if path not in contentctx:
86 if path not in contentctx:
87 return None
87 return None
88 fctx = contentctx[path]
88 fctx = contentctx[path]
89 mctx = context.memfilectx(
89 mctx = context.memfilectx(
90 repo,
90 repo,
91 memctx,
91 memctx,
92 fctx.path(),
92 fctx.path(),
93 fctx.data(),
93 fctx.data(),
94 fctx.islink(),
94 fctx.islink(),
95 fctx.isexec(),
95 fctx.isexec(),
96 copysource=copied.get(path),
96 copysource=copied.get(path),
97 )
97 )
98 return mctx
98 return mctx
99
99
100 if not files:
100 if not files:
101 repo.ui.status(_(b"note: keeping empty commit\n"))
101 repo.ui.status(_(b"note: keeping empty commit\n"))
102
102
103 if message is None:
103 if message is None:
104 message = ctx.description()
104 message = ctx.description()
105 if not user:
105 if not user:
106 user = ctx.user()
106 user = ctx.user()
107 if not date:
107 if not date:
108 date = ctx.date()
108 date = ctx.date()
109
109
110 new = context.memctx(
110 new = context.memctx(
111 repo,
111 repo,
112 parents=[base.node(), node.nullid],
112 parents=[base.node(), node.nullid],
113 text=message,
113 text=message,
114 files=files,
114 files=files,
115 filectxfn=filectxfn,
115 filectxfn=filectxfn,
116 user=user,
116 user=user,
117 date=date,
117 date=date,
118 extra=ctx.extra(),
118 extra=ctx.extra(),
119 )
119 )
120 return repo.commitctx(new)
120 return repo.commitctx(new)
121
121
122
122
123 @command(
123 @command(
124 b'uncommit',
124 b'uncommit',
125 [
125 [
126 (b'', b'keep', None, _(b'allow an empty commit after uncommitting')),
126 (b'', b'keep', None, _(b'allow an empty commit after uncommitting')),
127 (
127 (
128 b'',
128 b'',
129 b'allow-dirty-working-copy',
129 b'allow-dirty-working-copy',
130 False,
130 False,
131 _(b'allow uncommit with outstanding changes'),
131 _(b'allow uncommit with outstanding changes'),
132 ),
132 ),
133 (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
133 (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
134 ]
134 ]
135 + commands.walkopts
135 + commands.walkopts
136 + commands.commitopts
136 + commands.commitopts
137 + commands.commitopts2
137 + commands.commitopts2
138 + commands.commitopts3,
138 + commands.commitopts3,
139 _(b'[OPTION]... [FILE]...'),
139 _(b'[OPTION]... [FILE]...'),
140 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
140 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
141 )
141 )
142 def uncommit(ui, repo, *pats, **opts):
142 def uncommit(ui, repo, *pats, **opts):
143 """uncommit part or all of a local changeset
143 """uncommit part or all of a local changeset
144
144
145 This command undoes the effect of a local commit, returning the affected
145 This command undoes the effect of a local commit, returning the affected
146 files to their uncommitted state. This means that files modified or
146 files to their uncommitted state. This means that files modified or
147 deleted in the changeset will be left unchanged, and so will remain
147 deleted in the changeset will be left unchanged, and so will remain
148 modified in the working directory.
148 modified in the working directory.
149
149
150 If no files are specified, the commit will be pruned, unless --keep is
150 If no files are specified, the commit will be pruned, unless --keep is
151 given.
151 given.
152 """
152 """
153 opts = pycompat.byteskwargs(opts)
153 opts = pycompat.byteskwargs(opts)
154
154
155 cmdutil.checknotesize(ui, opts)
155 cmdutil.checknotesize(ui, opts)
156 cmdutil.resolvecommitoptions(ui, opts)
156 cmdutil.resolvecommitoptions(ui, opts)
157
157
158 with repo.wlock(), repo.lock():
158 with repo.wlock(), repo.lock():
159
159
160 m, a, r, d = repo.status()[:4]
160 st = repo.status()
161 m, a, r, d = st.modified, st.added, st.removed, st.deleted
161 isdirtypath = any(set(m + a + r + d) & set(pats))
162 isdirtypath = any(set(m + a + r + d) & set(pats))
162 allowdirtywcopy = opts[
163 allowdirtywcopy = opts[
163 b'allow_dirty_working_copy'
164 b'allow_dirty_working_copy'
164 ] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir')
165 ] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir')
165 if not allowdirtywcopy and (not pats or isdirtypath):
166 if not allowdirtywcopy and (not pats or isdirtypath):
166 cmdutil.bailifchanged(
167 cmdutil.bailifchanged(
167 repo,
168 repo,
168 hint=_(b'requires --allow-dirty-working-copy to uncommit'),
169 hint=_(b'requires --allow-dirty-working-copy to uncommit'),
169 )
170 )
170 old = repo[b'.']
171 old = repo[b'.']
171 rewriteutil.precheck(repo, [old.rev()], b'uncommit')
172 rewriteutil.precheck(repo, [old.rev()], b'uncommit')
172 if len(old.parents()) > 1:
173 if len(old.parents()) > 1:
173 raise error.Abort(_(b"cannot uncommit merge changeset"))
174 raise error.Abort(_(b"cannot uncommit merge changeset"))
174
175
175 match = scmutil.match(old, pats, opts)
176 match = scmutil.match(old, pats, opts)
176
177
177 # Check all explicitly given files; abort if there's a problem.
178 # Check all explicitly given files; abort if there's a problem.
178 if match.files():
179 if match.files():
179 s = old.status(old.p1(), match, listclean=True)
180 s = old.status(old.p1(), match, listclean=True)
180 eligible = set(s.added) | set(s.modified) | set(s.removed)
181 eligible = set(s.added) | set(s.modified) | set(s.removed)
181
182
182 badfiles = set(match.files()) - eligible
183 badfiles = set(match.files()) - eligible
183
184
184 # Naming a parent directory of an eligible file is OK, even
185 # Naming a parent directory of an eligible file is OK, even
185 # if not everything tracked in that directory can be
186 # if not everything tracked in that directory can be
186 # uncommitted.
187 # uncommitted.
187 if badfiles:
188 if badfiles:
188 badfiles -= {f for f in pathutil.dirs(eligible)}
189 badfiles -= {f for f in pathutil.dirs(eligible)}
189
190
190 for f in sorted(badfiles):
191 for f in sorted(badfiles):
191 if f in s.clean:
192 if f in s.clean:
192 hint = _(
193 hint = _(
193 b"file was not changed in working directory parent"
194 b"file was not changed in working directory parent"
194 )
195 )
195 elif repo.wvfs.exists(f):
196 elif repo.wvfs.exists(f):
196 hint = _(b"file was untracked in working directory parent")
197 hint = _(b"file was untracked in working directory parent")
197 else:
198 else:
198 hint = _(b"file does not exist")
199 hint = _(b"file does not exist")
199
200
200 raise error.Abort(
201 raise error.Abort(
201 _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f),
202 _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f),
202 hint=hint,
203 hint=hint,
203 )
204 )
204
205
205 with repo.transaction(b'uncommit'):
206 with repo.transaction(b'uncommit'):
206 if not (opts[b'message'] or opts[b'logfile']):
207 if not (opts[b'message'] or opts[b'logfile']):
207 opts[b'message'] = old.description()
208 opts[b'message'] = old.description()
208 message = cmdutil.logmessage(ui, opts)
209 message = cmdutil.logmessage(ui, opts)
209
210
210 keepcommit = pats
211 keepcommit = pats
211 if not keepcommit:
212 if not keepcommit:
212 if opts.get(b'keep') is not None:
213 if opts.get(b'keep') is not None:
213 keepcommit = opts.get(b'keep')
214 keepcommit = opts.get(b'keep')
214 else:
215 else:
215 keepcommit = ui.configbool(
216 keepcommit = ui.configbool(
216 b'experimental', b'uncommit.keep'
217 b'experimental', b'uncommit.keep'
217 )
218 )
218 newid = _commitfiltered(
219 newid = _commitfiltered(
219 repo,
220 repo,
220 old,
221 old,
221 match,
222 match,
222 keepcommit,
223 keepcommit,
223 message=message,
224 message=message,
224 user=opts.get(b'user'),
225 user=opts.get(b'user'),
225 date=opts.get(b'date'),
226 date=opts.get(b'date'),
226 )
227 )
227 if newid is None:
228 if newid is None:
228 ui.status(_(b"nothing to uncommit\n"))
229 ui.status(_(b"nothing to uncommit\n"))
229 return 1
230 return 1
230
231
231 mapping = {}
232 mapping = {}
232 if newid != old.p1().node():
233 if newid != old.p1().node():
233 # Move local changes on filtered changeset
234 # Move local changes on filtered changeset
234 mapping[old.node()] = (newid,)
235 mapping[old.node()] = (newid,)
235 else:
236 else:
236 # Fully removed the old commit
237 # Fully removed the old commit
237 mapping[old.node()] = ()
238 mapping[old.node()] = ()
238
239
239 with repo.dirstate.parentchange():
240 with repo.dirstate.parentchange():
240 scmutil.movedirstate(repo, repo[newid], match)
241 scmutil.movedirstate(repo, repo[newid], match)
241
242
242 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
243 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
243
244
244
245
245 def predecessormarkers(ctx):
246 def predecessormarkers(ctx):
246 """yields the obsolete markers marking the given changeset as a successor"""
247 """yields the obsolete markers marking the given changeset as a successor"""
247 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
248 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
248 yield obsutil.marker(ctx.repo(), data)
249 yield obsutil.marker(ctx.repo(), data)
249
250
250
251
251 @command(
252 @command(
252 b'unamend',
253 b'unamend',
253 [],
254 [],
254 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
255 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
255 helpbasic=True,
256 helpbasic=True,
256 )
257 )
257 def unamend(ui, repo, **opts):
258 def unamend(ui, repo, **opts):
258 """undo the most recent amend operation on a current changeset
259 """undo the most recent amend operation on a current changeset
259
260
260 This command will roll back to the previous version of a changeset,
261 This command will roll back to the previous version of a changeset,
261 leaving working directory in state in which it was before running
262 leaving working directory in state in which it was before running
262 `hg amend` (e.g. files modified as part of an amend will be
263 `hg amend` (e.g. files modified as part of an amend will be
263 marked as modified `hg status`)
264 marked as modified `hg status`)
264 """
265 """
265
266
266 unfi = repo.unfiltered()
267 unfi = repo.unfiltered()
267 with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
268 with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
268
269
269 # identify the commit from which to unamend
270 # identify the commit from which to unamend
270 curctx = repo[b'.']
271 curctx = repo[b'.']
271
272
272 rewriteutil.precheck(repo, [curctx.rev()], b'unamend')
273 rewriteutil.precheck(repo, [curctx.rev()], b'unamend')
273
274
274 # identify the commit to which to unamend
275 # identify the commit to which to unamend
275 markers = list(predecessormarkers(curctx))
276 markers = list(predecessormarkers(curctx))
276 if len(markers) != 1:
277 if len(markers) != 1:
277 e = _(b"changeset must have one predecessor, found %i predecessors")
278 e = _(b"changeset must have one predecessor, found %i predecessors")
278 raise error.Abort(e % len(markers))
279 raise error.Abort(e % len(markers))
279
280
280 prednode = markers[0].prednode()
281 prednode = markers[0].prednode()
281 predctx = unfi[prednode]
282 predctx = unfi[prednode]
282
283
283 # add an extra so that we get a new hash
284 # add an extra so that we get a new hash
284 # note: allowing unamend to undo an unamend is an intentional feature
285 # note: allowing unamend to undo an unamend is an intentional feature
285 extras = predctx.extra()
286 extras = predctx.extra()
286 extras[b'unamend_source'] = curctx.hex()
287 extras[b'unamend_source'] = curctx.hex()
287
288
288 def filectxfn(repo, ctx_, path):
289 def filectxfn(repo, ctx_, path):
289 try:
290 try:
290 return predctx.filectx(path)
291 return predctx.filectx(path)
291 except KeyError:
292 except KeyError:
292 return None
293 return None
293
294
294 # Make a new commit same as predctx
295 # Make a new commit same as predctx
295 newctx = context.memctx(
296 newctx = context.memctx(
296 repo,
297 repo,
297 parents=(predctx.p1(), predctx.p2()),
298 parents=(predctx.p1(), predctx.p2()),
298 text=predctx.description(),
299 text=predctx.description(),
299 files=predctx.files(),
300 files=predctx.files(),
300 filectxfn=filectxfn,
301 filectxfn=filectxfn,
301 user=predctx.user(),
302 user=predctx.user(),
302 date=predctx.date(),
303 date=predctx.date(),
303 extra=extras,
304 extra=extras,
304 )
305 )
305 newprednode = repo.commitctx(newctx)
306 newprednode = repo.commitctx(newctx)
306 newpredctx = repo[newprednode]
307 newpredctx = repo[newprednode]
307 dirstate = repo.dirstate
308 dirstate = repo.dirstate
308
309
309 with dirstate.parentchange():
310 with dirstate.parentchange():
310 scmutil.movedirstate(repo, newpredctx)
311 scmutil.movedirstate(repo, newpredctx)
311
312
312 mapping = {curctx.node(): (newprednode,)}
313 mapping = {curctx.node(): (newprednode,)}
313 scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
314 scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
General Comments 0
You need to be logged in to leave comments. Login now