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