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