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