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