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