##// END OF EJS Templates
unamend: import "copies" module as "copiesmod" to avoid shadowing...
Martin von Zweigbergk -
r41370:7be231f5 default
parent child Browse files
Show More
@@ -1,252 +1,252 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 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 = copies.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, status):
96 def _fixdirstate(repo, oldctx, newctx, status):
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 copies = dict(ds.copies())
101 copies = dict(ds.copies())
102 s = status
102 s = status
103 for f in s.modified:
103 for f in s.modified:
104 if ds[f] == 'r':
104 if ds[f] == 'r':
105 # modified + removed -> removed
105 # modified + removed -> removed
106 continue
106 continue
107 ds.normallookup(f)
107 ds.normallookup(f)
108
108
109 for f in s.added:
109 for f in s.added:
110 if ds[f] == 'r':
110 if ds[f] == 'r':
111 # added + removed -> unknown
111 # added + removed -> unknown
112 ds.drop(f)
112 ds.drop(f)
113 elif ds[f] != 'a':
113 elif ds[f] != 'a':
114 ds.add(f)
114 ds.add(f)
115
115
116 for f in s.removed:
116 for f in s.removed:
117 if ds[f] == 'a':
117 if ds[f] == 'a':
118 # removed + added -> normal
118 # removed + added -> normal
119 ds.normallookup(f)
119 ds.normallookup(f)
120 elif ds[f] != 'r':
120 elif ds[f] != 'r':
121 ds.remove(f)
121 ds.remove(f)
122
122
123 # Merge old parent and old working dir copies
123 # Merge old parent and old working dir copies
124 oldcopies = {}
124 oldcopies = {}
125 for f in (s.modified + s.added):
125 for f in (s.modified + s.added):
126 src = oldctx[f].renamed()
126 src = oldctx[f].renamed()
127 if src:
127 if src:
128 oldcopies[f] = src[0]
128 oldcopies[f] = src[0]
129 oldcopies.update(copies)
129 oldcopies.update(copies)
130 copies = dict((dst, oldcopies.get(src, src))
130 copies = dict((dst, oldcopies.get(src, src))
131 for dst, src in oldcopies.iteritems())
131 for dst, src in oldcopies.iteritems())
132 # Adjust the dirstate copies
132 # Adjust the dirstate copies
133 for dst, src in copies.iteritems():
133 for dst, src in copies.iteritems():
134 if (src not in newctx or dst in newctx or ds[dst] != 'a'):
134 if (src not in newctx or dst in newctx or ds[dst] != 'a'):
135 src = None
135 src = None
136 ds.copy(src, dst)
136 ds.copy(src, dst)
137
137
138 @command('uncommit',
138 @command('uncommit',
139 [('', 'keep', False, _('allow an empty commit after uncommiting')),
139 [('', 'keep', False, _('allow an empty commit after uncommiting')),
140 ] + commands.walkopts,
140 ] + commands.walkopts,
141 _('[OPTION]... [FILE]...'),
141 _('[OPTION]... [FILE]...'),
142 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
142 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
143 def uncommit(ui, repo, *pats, **opts):
143 def uncommit(ui, repo, *pats, **opts):
144 """uncommit part or all of a local changeset
144 """uncommit part or all of a local changeset
145
145
146 This command undoes the effect of a local commit, returning the affected
146 This command undoes the effect of a local commit, returning the affected
147 files to their uncommitted state. This means that files modified or
147 files to their uncommitted state. This means that files modified or
148 deleted in the changeset will be left unchanged, and so will remain
148 deleted in the changeset will be left unchanged, and so will remain
149 modified in the working directory.
149 modified in the working directory.
150
150
151 If no files are specified, the commit will be pruned, unless --keep is
151 If no files are specified, the commit will be pruned, unless --keep is
152 given.
152 given.
153 """
153 """
154 opts = pycompat.byteskwargs(opts)
154 opts = pycompat.byteskwargs(opts)
155
155
156 with repo.wlock(), repo.lock():
156 with repo.wlock(), repo.lock():
157
157
158 if not pats and not repo.ui.configbool('experimental',
158 if not pats and not repo.ui.configbool('experimental',
159 'uncommitondirtywdir'):
159 'uncommitondirtywdir'):
160 cmdutil.bailifchanged(repo)
160 cmdutil.bailifchanged(repo)
161 old = repo['.']
161 old = repo['.']
162 rewriteutil.precheck(repo, [old.rev()], 'uncommit')
162 rewriteutil.precheck(repo, [old.rev()], 'uncommit')
163 if len(old.parents()) > 1:
163 if len(old.parents()) > 1:
164 raise error.Abort(_("cannot uncommit merge changeset"))
164 raise error.Abort(_("cannot uncommit merge changeset"))
165
165
166 with repo.transaction('uncommit'):
166 with repo.transaction('uncommit'):
167 match = scmutil.match(old, pats, opts)
167 match = scmutil.match(old, pats, opts)
168 keepcommit = opts.get('keep') or pats
168 keepcommit = opts.get('keep') or pats
169 newid = _commitfiltered(repo, old, match, keepcommit)
169 newid = _commitfiltered(repo, old, match, keepcommit)
170 if newid is None:
170 if newid is None:
171 ui.status(_("nothing to uncommit\n"))
171 ui.status(_("nothing to uncommit\n"))
172 return 1
172 return 1
173
173
174 mapping = {}
174 mapping = {}
175 if newid != old.p1().node():
175 if newid != old.p1().node():
176 # Move local changes on filtered changeset
176 # Move local changes on filtered changeset
177 mapping[old.node()] = (newid,)
177 mapping[old.node()] = (newid,)
178 else:
178 else:
179 # Fully removed the old commit
179 # Fully removed the old commit
180 mapping[old.node()] = ()
180 mapping[old.node()] = ()
181
181
182 scmutil.cleanupnodes(repo, mapping, 'uncommit', fixphase=True)
182 scmutil.cleanupnodes(repo, mapping, 'uncommit', fixphase=True)
183
183
184 with repo.dirstate.parentchange():
184 with repo.dirstate.parentchange():
185 repo.dirstate.setparents(newid, node.nullid)
185 repo.dirstate.setparents(newid, node.nullid)
186 s = old.p1().status(old, match=match)
186 s = old.p1().status(old, match=match)
187 _fixdirstate(repo, old, repo[newid], s)
187 _fixdirstate(repo, old, repo[newid], s)
188
188
189 def predecessormarkers(ctx):
189 def predecessormarkers(ctx):
190 """yields the obsolete markers marking the given changeset as a successor"""
190 """yields the obsolete markers marking the given changeset as a successor"""
191 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
191 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
192 yield obsutil.marker(ctx.repo(), data)
192 yield obsutil.marker(ctx.repo(), data)
193
193
194 @command('unamend', [], helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
194 @command('unamend', [], helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
195 helpbasic=True)
195 helpbasic=True)
196 def unamend(ui, repo, **opts):
196 def unamend(ui, repo, **opts):
197 """undo the most recent amend operation on a current changeset
197 """undo the most recent amend operation on a current changeset
198
198
199 This command will roll back to the previous version of a changeset,
199 This command will roll back to the previous version of a changeset,
200 leaving working directory in state in which it was before running
200 leaving working directory in state in which it was before running
201 `hg amend` (e.g. files modified as part of an amend will be
201 `hg amend` (e.g. files modified as part of an amend will be
202 marked as modified `hg status`)
202 marked as modified `hg status`)
203 """
203 """
204
204
205 unfi = repo.unfiltered()
205 unfi = repo.unfiltered()
206 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
206 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
207
207
208 # identify the commit from which to unamend
208 # identify the commit from which to unamend
209 curctx = repo['.']
209 curctx = repo['.']
210
210
211 rewriteutil.precheck(repo, [curctx.rev()], 'unamend')
211 rewriteutil.precheck(repo, [curctx.rev()], 'unamend')
212
212
213 # identify the commit to which to unamend
213 # identify the commit to which to unamend
214 markers = list(predecessormarkers(curctx))
214 markers = list(predecessormarkers(curctx))
215 if len(markers) != 1:
215 if len(markers) != 1:
216 e = _("changeset must have one predecessor, found %i predecessors")
216 e = _("changeset must have one predecessor, found %i predecessors")
217 raise error.Abort(e % len(markers))
217 raise error.Abort(e % len(markers))
218
218
219 prednode = markers[0].prednode()
219 prednode = markers[0].prednode()
220 predctx = unfi[prednode]
220 predctx = unfi[prednode]
221
221
222 # add an extra so that we get a new hash
222 # add an extra so that we get a new hash
223 # note: allowing unamend to undo an unamend is an intentional feature
223 # note: allowing unamend to undo an unamend is an intentional feature
224 extras = predctx.extra()
224 extras = predctx.extra()
225 extras['unamend_source'] = curctx.hex()
225 extras['unamend_source'] = curctx.hex()
226
226
227 def filectxfn(repo, ctx_, path):
227 def filectxfn(repo, ctx_, path):
228 try:
228 try:
229 return predctx.filectx(path)
229 return predctx.filectx(path)
230 except KeyError:
230 except KeyError:
231 return None
231 return None
232
232
233 # Make a new commit same as predctx
233 # Make a new commit same as predctx
234 newctx = context.memctx(repo,
234 newctx = context.memctx(repo,
235 parents=(predctx.p1(), predctx.p2()),
235 parents=(predctx.p1(), predctx.p2()),
236 text=predctx.description(),
236 text=predctx.description(),
237 files=predctx.files(),
237 files=predctx.files(),
238 filectxfn=filectxfn,
238 filectxfn=filectxfn,
239 user=predctx.user(),
239 user=predctx.user(),
240 date=predctx.date(),
240 date=predctx.date(),
241 extra=extras)
241 extra=extras)
242 newprednode = repo.commitctx(newctx)
242 newprednode = repo.commitctx(newctx)
243 newpredctx = repo[newprednode]
243 newpredctx = repo[newprednode]
244 dirstate = repo.dirstate
244 dirstate = repo.dirstate
245
245
246 with dirstate.parentchange():
246 with dirstate.parentchange():
247 dirstate.setparents(newprednode, node.nullid)
247 dirstate.setparents(newprednode, node.nullid)
248 s = repo.status(predctx, curctx)
248 s = repo.status(predctx, curctx)
249 _fixdirstate(repo, curctx, newpredctx, s)
249 _fixdirstate(repo, curctx, newpredctx, s)
250
250
251 mapping = {curctx.node(): (newprednode,)}
251 mapping = {curctx.node(): (newprednode,)}
252 scmutil.cleanupnodes(repo, mapping, 'unamend', fixphase=True)
252 scmutil.cleanupnodes(repo, mapping, 'unamend', fixphase=True)
General Comments 0
You need to be logged in to leave comments. Login now