##// END OF EJS Templates
uncommit: unify functions _uncommitdirstate and _unamenddirstate to one...
Pulkit Goyal -
r35178:9dadcb99 default
parent child Browse files
Show More
@@ -1,318 +1,277 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,
28 copies,
29 error,
29 error,
30 node,
30 node,
31 obsolete,
31 obsolete,
32 obsutil,
32 obsutil,
33 pycompat,
33 pycompat,
34 registrar,
34 registrar,
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, allowempty):
54 def _commitfiltered(repo, ctx, match, allowempty):
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 files and not allowempty:
69 if not files and not allowempty:
70 return ctx.parents()[0].node()
70 return ctx.parents()[0].node()
71
71
72 # Filter copies
72 # Filter copies
73 copied = copies.pathcopies(base, ctx)
73 copied = copies.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, fctx.path(), fctx.data(),
80 mctx = context.memfilectx(repo, 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 # phase handling
94 # phase handling
95 commitphase = ctx.phase()
95 commitphase = ctx.phase()
96 overrides = {('phases', 'new-commit'): commitphase}
96 overrides = {('phases', 'new-commit'): commitphase}
97 with repo.ui.configoverride(overrides, 'uncommit'):
97 with repo.ui.configoverride(overrides, 'uncommit'):
98 newid = repo.commitctx(new)
98 newid = repo.commitctx(new)
99 return newid
99 return newid
100
100
101 def _uncommitdirstate(repo, oldctx, match):
101 def _fixdirstate(repo, oldctx, newctx, status):
102 """Fix the dirstate after switching the working directory from
102 """ fix the dirstate after switching the working directory from oldctx to
103 oldctx to a copy of oldctx not containing changed files matched by
103 newctx which can be result of either unamend or uncommit.
104 match.
105 """
104 """
106 ctx = repo['.']
107 ds = repo.dirstate
105 ds = repo.dirstate
108 copies = dict(ds.copies())
106 copies = dict(ds.copies())
109 s = repo.status(oldctx.p1(), oldctx, match=match)
107 s = status
110 for f in s.modified:
108 for f in s.modified:
111 if ds[f] == 'r':
109 if ds[f] == 'r':
112 # modified + removed -> removed
110 # modified + removed -> removed
113 continue
111 continue
114 ds.normallookup(f)
112 ds.normallookup(f)
115
113
116 for f in s.added:
114 for f in s.added:
117 if ds[f] == 'r':
115 if ds[f] == 'r':
118 # added + removed -> unknown
116 # added + removed -> unknown
119 ds.drop(f)
117 ds.drop(f)
120 elif ds[f] != 'a':
118 elif ds[f] != 'a':
121 ds.add(f)
119 ds.add(f)
122
120
123 for f in s.removed:
121 for f in s.removed:
124 if ds[f] == 'a':
122 if ds[f] == 'a':
125 # removed + added -> normal
123 # removed + added -> normal
126 ds.normallookup(f)
124 ds.normallookup(f)
127 elif ds[f] != 'r':
125 elif ds[f] != 'r':
128 ds.remove(f)
126 ds.remove(f)
129
127
130 # Merge old parent and old working dir copies
128 # Merge old parent and old working dir copies
131 oldcopies = {}
129 oldcopies = {}
132 for f in (s.modified + s.added):
130 for f in (s.modified + s.added):
133 src = oldctx[f].renamed()
131 src = oldctx[f].renamed()
134 if src:
132 if src:
135 oldcopies[f] = src[0]
133 oldcopies[f] = src[0]
136 oldcopies.update(copies)
134 oldcopies.update(copies)
137 copies = dict((dst, oldcopies.get(src, src))
135 copies = dict((dst, oldcopies.get(src, src))
138 for dst, src in oldcopies.iteritems())
136 for dst, src in oldcopies.iteritems())
139 # Adjust the dirstate copies
137 # Adjust the dirstate copies
140 for dst, src in copies.iteritems():
138 for dst, src in copies.iteritems():
141 if (src not in ctx or dst in ctx or ds[dst] != 'a'):
139 if (src not in newctx or dst in newctx or ds[dst] != 'a'):
142 src = None
140 src = None
143 ds.copy(src, dst)
141 ds.copy(src, dst)
144
142
145 @command('uncommit',
143 @command('uncommit',
146 [('', 'keep', False, _('allow an empty commit after uncommiting')),
144 [('', 'keep', False, _('allow an empty commit after uncommiting')),
147 ] + commands.walkopts,
145 ] + commands.walkopts,
148 _('[OPTION]... [FILE]...'))
146 _('[OPTION]... [FILE]...'))
149 def uncommit(ui, repo, *pats, **opts):
147 def uncommit(ui, repo, *pats, **opts):
150 """uncommit part or all of a local changeset
148 """uncommit part or all of a local changeset
151
149
152 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
153 files to their uncommitted state. This means that files modified or
151 files to their uncommitted state. This means that files modified or
154 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
155 modified in the working directory.
153 modified in the working directory.
156 """
154 """
157 opts = pycompat.byteskwargs(opts)
155 opts = pycompat.byteskwargs(opts)
158
156
159 with repo.wlock(), repo.lock():
157 with repo.wlock(), repo.lock():
160 wctx = repo[None]
158 wctx = repo[None]
161
159
162 if not pats and not repo.ui.configbool('experimental',
160 if not pats and not repo.ui.configbool('experimental',
163 'uncommitondirtywdir'):
161 'uncommitondirtywdir'):
164 cmdutil.bailifchanged(repo)
162 cmdutil.bailifchanged(repo)
165 if wctx.parents()[0].node() == node.nullid:
163 if wctx.parents()[0].node() == node.nullid:
166 raise error.Abort(_("cannot uncommit null changeset"))
164 raise error.Abort(_("cannot uncommit null changeset"))
167 if len(wctx.parents()) > 1:
165 if len(wctx.parents()) > 1:
168 raise error.Abort(_("cannot uncommit while merging"))
166 raise error.Abort(_("cannot uncommit while merging"))
169 old = repo['.']
167 old = repo['.']
170 if not old.mutable():
168 if not old.mutable():
171 raise error.Abort(_('cannot uncommit public changesets'))
169 raise error.Abort(_('cannot uncommit public changesets'))
172 if len(old.parents()) > 1:
170 if len(old.parents()) > 1:
173 raise error.Abort(_("cannot uncommit merge changeset"))
171 raise error.Abort(_("cannot uncommit merge changeset"))
174 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
172 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
175 if not allowunstable and old.children():
173 if not allowunstable and old.children():
176 raise error.Abort(_('cannot uncommit changeset with children'))
174 raise error.Abort(_('cannot uncommit changeset with children'))
177
175
178 with repo.transaction('uncommit'):
176 with repo.transaction('uncommit'):
179 match = scmutil.match(old, pats, opts)
177 match = scmutil.match(old, pats, opts)
180 newid = _commitfiltered(repo, old, match, opts.get('keep'))
178 newid = _commitfiltered(repo, old, match, opts.get('keep'))
181 if newid is None:
179 if newid is None:
182 ui.status(_("nothing to uncommit\n"))
180 ui.status(_("nothing to uncommit\n"))
183 return 1
181 return 1
184
182
185 mapping = {}
183 mapping = {}
186 if newid != old.p1().node():
184 if newid != old.p1().node():
187 # Move local changes on filtered changeset
185 # Move local changes on filtered changeset
188 mapping[old.node()] = (newid,)
186 mapping[old.node()] = (newid,)
189 else:
187 else:
190 # Fully removed the old commit
188 # Fully removed the old commit
191 mapping[old.node()] = ()
189 mapping[old.node()] = ()
192
190
193 scmutil.cleanupnodes(repo, mapping, 'uncommit')
191 scmutil.cleanupnodes(repo, mapping, 'uncommit')
194
192
195 with repo.dirstate.parentchange():
193 with repo.dirstate.parentchange():
196 repo.dirstate.setparents(newid, node.nullid)
194 repo.dirstate.setparents(newid, node.nullid)
197 _uncommitdirstate(repo, old, match)
195 s = repo.status(old.p1(), old, match=match)
196 _fixdirstate(repo, old, repo[newid], s)
198
197
199 def predecessormarkers(ctx):
198 def predecessormarkers(ctx):
200 """yields the obsolete markers marking the given changeset as a successor"""
199 """yields the obsolete markers marking the given changeset as a successor"""
201 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
200 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
202 yield obsutil.marker(ctx.repo(), data)
201 yield obsutil.marker(ctx.repo(), data)
203
202
204 def _unamenddirstate(repo, predctx, curctx):
205 """"""
206
207 s = repo.status(predctx, curctx)
208 ds = repo.dirstate
209 copies = dict(ds.copies())
210 for f in s.modified:
211 if ds[f] == 'r':
212 # modified + removed -> removed
213 continue
214 ds.normallookup(f)
215
216 for f in s.added:
217 if ds[f] == 'r':
218 # added + removed -> unknown
219 ds.drop(f)
220 elif ds[f] != 'a':
221 ds.add(f)
222
223 for f in s.removed:
224 if ds[f] == 'a':
225 # removed + added -> normal
226 ds.normallookup(f)
227 elif ds[f] != 'r':
228 ds.remove(f)
229
230 # Merge old parent and old working dir copies
231 oldcopies = {}
232 for f in (s.modified + s.added):
233 src = curctx[f].renamed()
234 if src:
235 oldcopies[f] = src[0]
236 oldcopies.update(copies)
237 copies = dict((dst, oldcopies.get(src, src))
238 for dst, src in oldcopies.iteritems())
239 # Adjust the dirstate copies
240 for dst, src in copies.iteritems():
241 if (src not in predctx or dst in predctx or ds[dst] != 'a'):
242 src = None
243 ds.copy(src, dst)
244
245 @command('^unamend', [])
203 @command('^unamend', [])
246 def unamend(ui, repo, **opts):
204 def unamend(ui, repo, **opts):
247 """
205 """
248 undo the most recent amend operation on a current changeset
206 undo the most recent amend operation on a current changeset
249
207
250 This command will roll back to the previous version of a changeset,
208 This command will roll back to the previous version of a changeset,
251 leaving working directory in state in which it was before running
209 leaving working directory in state in which it was before running
252 `hg amend` (e.g. files modified as part of an amend will be
210 `hg amend` (e.g. files modified as part of an amend will be
253 marked as modified `hg status`)
211 marked as modified `hg status`)
254 """
212 """
255
213
256 unfi = repo.unfiltered()
214 unfi = repo.unfiltered()
257
215
258 # identify the commit from which to unamend
216 # identify the commit from which to unamend
259 curctx = repo['.']
217 curctx = repo['.']
260
218
261 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
219 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
262 if not curctx.mutable():
220 if not curctx.mutable():
263 raise error.Abort(_('cannot unamend public changesets'))
221 raise error.Abort(_('cannot unamend public changesets'))
264
222
265 # identify the commit to which to unamend
223 # identify the commit to which to unamend
266 markers = list(predecessormarkers(curctx))
224 markers = list(predecessormarkers(curctx))
267 if len(markers) != 1:
225 if len(markers) != 1:
268 e = _("changeset must have one predecessor, found %i predecessors")
226 e = _("changeset must have one predecessor, found %i predecessors")
269 raise error.Abort(e % len(markers))
227 raise error.Abort(e % len(markers))
270
228
271 prednode = markers[0].prednode()
229 prednode = markers[0].prednode()
272 predctx = unfi[prednode]
230 predctx = unfi[prednode]
273
231
274 if curctx.children():
232 if curctx.children():
275 raise error.Abort(_("cannot unamend a changeset with children"))
233 raise error.Abort(_("cannot unamend a changeset with children"))
276
234
277 # add an extra so that we get a new hash
235 # add an extra so that we get a new hash
278 # note: allowing unamend to undo an unamend is an intentional feature
236 # note: allowing unamend to undo an unamend is an intentional feature
279 extras = predctx.extra()
237 extras = predctx.extra()
280 extras['unamend_source'] = curctx.node()
238 extras['unamend_source'] = curctx.node()
281
239
282 def filectxfn(repo, ctx_, path):
240 def filectxfn(repo, ctx_, path):
283 try:
241 try:
284 return predctx.filectx(path)
242 return predctx.filectx(path)
285 except KeyError:
243 except KeyError:
286 return None
244 return None
287
245
288 # Make a new commit same as predctx
246 # Make a new commit same as predctx
289 newctx = context.memctx(repo,
247 newctx = context.memctx(repo,
290 parents=(predctx.p1(), predctx.p2()),
248 parents=(predctx.p1(), predctx.p2()),
291 text=predctx.description(),
249 text=predctx.description(),
292 files=predctx.files(),
250 files=predctx.files(),
293 filectxfn=filectxfn,
251 filectxfn=filectxfn,
294 user=predctx.user(),
252 user=predctx.user(),
295 date=predctx.date(),
253 date=predctx.date(),
296 extra=extras)
254 extra=extras)
297 # phase handling
255 # phase handling
298 commitphase = curctx.phase()
256 commitphase = curctx.phase()
299 overrides = {('phases', 'new-commit'): commitphase}
257 overrides = {('phases', 'new-commit'): commitphase}
300 with repo.ui.configoverride(overrides, 'uncommit'):
258 with repo.ui.configoverride(overrides, 'uncommit'):
301 newprednode = repo.commitctx(newctx)
259 newprednode = repo.commitctx(newctx)
302
260
303 newpredctx = repo[newprednode]
261 newpredctx = repo[newprednode]
304
262
305 changedfiles = []
263 changedfiles = []
306 wctx = repo[None]
264 wctx = repo[None]
307 wm = wctx.manifest()
265 wm = wctx.manifest()
308 cm = newpredctx.manifest()
266 cm = newpredctx.manifest()
309 dirstate = repo.dirstate
267 dirstate = repo.dirstate
310 diff = cm.diff(wm)
268 diff = cm.diff(wm)
311 changedfiles.extend(diff.iterkeys())
269 changedfiles.extend(diff.iterkeys())
312
270
313 with dirstate.parentchange():
271 with dirstate.parentchange():
314 dirstate.setparents(newprednode, node.nullid)
272 dirstate.setparents(newprednode, node.nullid)
315 _unamenddirstate(repo, newpredctx, curctx)
273 s = repo.status(predctx, curctx)
274 _fixdirstate(repo, curctx, newpredctx, s)
316
275
317 mapping = {curctx.node(): (newprednode,)}
276 mapping = {curctx.node(): (newprednode,)}
318 scmutil.cleanupnodes(repo, mapping, 'unamend')
277 scmutil.cleanupnodes(repo, mapping, 'unamend')
General Comments 0
You need to be logged in to leave comments. Login now