##// END OF EJS Templates
unamend: fix command summary line...
Martin von Zweigbergk -
r35827:f9a82b9b stable
parent child Browse files
Show More
@@ -1,257 +1,256 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 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, 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, 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 # 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 _fixdirstate(repo, oldctx, newctx, status):
101 def _fixdirstate(repo, oldctx, newctx, status):
102 """ fix the dirstate after switching the working directory from oldctx to
102 """ fix the dirstate after switching the working directory from oldctx to
103 newctx which can be result of either unamend or uncommit.
103 newctx which can be result of either unamend or uncommit.
104 """
104 """
105 ds = repo.dirstate
105 ds = repo.dirstate
106 copies = dict(ds.copies())
106 copies = dict(ds.copies())
107 s = status
107 s = status
108 for f in s.modified:
108 for f in s.modified:
109 if ds[f] == 'r':
109 if ds[f] == 'r':
110 # modified + removed -> removed
110 # modified + removed -> removed
111 continue
111 continue
112 ds.normallookup(f)
112 ds.normallookup(f)
113
113
114 for f in s.added:
114 for f in s.added:
115 if ds[f] == 'r':
115 if ds[f] == 'r':
116 # added + removed -> unknown
116 # added + removed -> unknown
117 ds.drop(f)
117 ds.drop(f)
118 elif ds[f] != 'a':
118 elif ds[f] != 'a':
119 ds.add(f)
119 ds.add(f)
120
120
121 for f in s.removed:
121 for f in s.removed:
122 if ds[f] == 'a':
122 if ds[f] == 'a':
123 # removed + added -> normal
123 # removed + added -> normal
124 ds.normallookup(f)
124 ds.normallookup(f)
125 elif ds[f] != 'r':
125 elif ds[f] != 'r':
126 ds.remove(f)
126 ds.remove(f)
127
127
128 # Merge old parent and old working dir copies
128 # Merge old parent and old working dir copies
129 oldcopies = {}
129 oldcopies = {}
130 for f in (s.modified + s.added):
130 for f in (s.modified + s.added):
131 src = oldctx[f].renamed()
131 src = oldctx[f].renamed()
132 if src:
132 if src:
133 oldcopies[f] = src[0]
133 oldcopies[f] = src[0]
134 oldcopies.update(copies)
134 oldcopies.update(copies)
135 copies = dict((dst, oldcopies.get(src, src))
135 copies = dict((dst, oldcopies.get(src, src))
136 for dst, src in oldcopies.iteritems())
136 for dst, src in oldcopies.iteritems())
137 # Adjust the dirstate copies
137 # Adjust the dirstate copies
138 for dst, src in copies.iteritems():
138 for dst, src in copies.iteritems():
139 if (src not in newctx or dst in newctx or ds[dst] != 'a'):
139 if (src not in newctx or dst in newctx or ds[dst] != 'a'):
140 src = None
140 src = None
141 ds.copy(src, dst)
141 ds.copy(src, dst)
142
142
143 @command('uncommit',
143 @command('uncommit',
144 [('', 'keep', False, _('allow an empty commit after uncommiting')),
144 [('', 'keep', False, _('allow an empty commit after uncommiting')),
145 ] + commands.walkopts,
145 ] + commands.walkopts,
146 _('[OPTION]... [FILE]...'))
146 _('[OPTION]... [FILE]...'))
147 def uncommit(ui, repo, *pats, **opts):
147 def uncommit(ui, repo, *pats, **opts):
148 """uncommit part or all of a local changeset
148 """uncommit part or all of a local changeset
149
149
150 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
151 files to their uncommitted state. This means that files modified or
151 files to their uncommitted state. This means that files modified or
152 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
153 modified in the working directory.
153 modified in the working directory.
154 """
154 """
155 opts = pycompat.byteskwargs(opts)
155 opts = pycompat.byteskwargs(opts)
156
156
157 with repo.wlock(), repo.lock():
157 with repo.wlock(), repo.lock():
158
158
159 if not pats and not repo.ui.configbool('experimental',
159 if not pats and not repo.ui.configbool('experimental',
160 'uncommitondirtywdir'):
160 'uncommitondirtywdir'):
161 cmdutil.bailifchanged(repo)
161 cmdutil.bailifchanged(repo)
162 old = repo['.']
162 old = repo['.']
163 rewriteutil.precheck(repo, [old.rev()], 'uncommit')
163 rewriteutil.precheck(repo, [old.rev()], 'uncommit')
164 if len(old.parents()) > 1:
164 if len(old.parents()) > 1:
165 raise error.Abort(_("cannot uncommit merge changeset"))
165 raise error.Abort(_("cannot uncommit merge changeset"))
166
166
167 with repo.transaction('uncommit'):
167 with repo.transaction('uncommit'):
168 match = scmutil.match(old, pats, opts)
168 match = scmutil.match(old, pats, opts)
169 newid = _commitfiltered(repo, old, match, opts.get('keep'))
169 newid = _commitfiltered(repo, old, match, opts.get('keep'))
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')
182 scmutil.cleanupnodes(repo, mapping, 'uncommit')
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 = repo.status(old.p1(), old, match=match)
186 s = repo.status(old.p1(), 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', [])
194 @command('^unamend', [])
195 def unamend(ui, repo, **opts):
195 def unamend(ui, repo, **opts):
196 """
196 """undo the most recent amend operation on a current changeset
197 undo the most recent amend operation on a current changeset
198
197
199 This command will roll back to the previous version of a changeset,
198 This command will roll back to the previous version of a changeset,
200 leaving working directory in state in which it was before running
199 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
200 `hg amend` (e.g. files modified as part of an amend will be
202 marked as modified `hg status`)
201 marked as modified `hg status`)
203 """
202 """
204
203
205 unfi = repo.unfiltered()
204 unfi = repo.unfiltered()
206 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
205 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
207
206
208 # identify the commit from which to unamend
207 # identify the commit from which to unamend
209 curctx = repo['.']
208 curctx = repo['.']
210
209
211 rewriteutil.precheck(repo, [curctx.rev()], 'unamend')
210 rewriteutil.precheck(repo, [curctx.rev()], 'unamend')
212
211
213 # identify the commit to which to unamend
212 # identify the commit to which to unamend
214 markers = list(predecessormarkers(curctx))
213 markers = list(predecessormarkers(curctx))
215 if len(markers) != 1:
214 if len(markers) != 1:
216 e = _("changeset must have one predecessor, found %i predecessors")
215 e = _("changeset must have one predecessor, found %i predecessors")
217 raise error.Abort(e % len(markers))
216 raise error.Abort(e % len(markers))
218
217
219 prednode = markers[0].prednode()
218 prednode = markers[0].prednode()
220 predctx = unfi[prednode]
219 predctx = unfi[prednode]
221
220
222 # add an extra so that we get a new hash
221 # add an extra so that we get a new hash
223 # note: allowing unamend to undo an unamend is an intentional feature
222 # note: allowing unamend to undo an unamend is an intentional feature
224 extras = predctx.extra()
223 extras = predctx.extra()
225 extras['unamend_source'] = curctx.hex()
224 extras['unamend_source'] = curctx.hex()
226
225
227 def filectxfn(repo, ctx_, path):
226 def filectxfn(repo, ctx_, path):
228 try:
227 try:
229 return predctx.filectx(path)
228 return predctx.filectx(path)
230 except KeyError:
229 except KeyError:
231 return None
230 return None
232
231
233 # Make a new commit same as predctx
232 # Make a new commit same as predctx
234 newctx = context.memctx(repo,
233 newctx = context.memctx(repo,
235 parents=(predctx.p1(), predctx.p2()),
234 parents=(predctx.p1(), predctx.p2()),
236 text=predctx.description(),
235 text=predctx.description(),
237 files=predctx.files(),
236 files=predctx.files(),
238 filectxfn=filectxfn,
237 filectxfn=filectxfn,
239 user=predctx.user(),
238 user=predctx.user(),
240 date=predctx.date(),
239 date=predctx.date(),
241 extra=extras)
240 extra=extras)
242 # phase handling
241 # phase handling
243 commitphase = curctx.phase()
242 commitphase = curctx.phase()
244 overrides = {('phases', 'new-commit'): commitphase}
243 overrides = {('phases', 'new-commit'): commitphase}
245 with repo.ui.configoverride(overrides, 'uncommit'):
244 with repo.ui.configoverride(overrides, 'uncommit'):
246 newprednode = repo.commitctx(newctx)
245 newprednode = repo.commitctx(newctx)
247
246
248 newpredctx = repo[newprednode]
247 newpredctx = repo[newprednode]
249 dirstate = repo.dirstate
248 dirstate = repo.dirstate
250
249
251 with dirstate.parentchange():
250 with dirstate.parentchange():
252 dirstate.setparents(newprednode, node.nullid)
251 dirstate.setparents(newprednode, node.nullid)
253 s = repo.status(predctx, curctx)
252 s = repo.status(predctx, curctx)
254 _fixdirstate(repo, curctx, newpredctx, s)
253 _fixdirstate(repo, curctx, newpredctx, s)
255
254
256 mapping = {curctx.node(): (newprednode,)}
255 mapping = {curctx.node(): (newprednode,)}
257 scmutil.cleanupnodes(repo, mapping, 'unamend')
256 scmutil.cleanupnodes(repo, mapping, 'unamend')
General Comments 0
You need to be logged in to leave comments. Login now