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