##// END OF EJS Templates
unamend: move fb extension unamend to core...
Pulkit Goyal -
r35177:86799023 default
parent child Browse files
Show More
@@ -0,0 +1,366 b''
1 Test for command `hg unamend` which lives in uncommit extension
2 ===============================================================
3
4 $ cat >> $HGRCPATH << EOF
5 > [alias]
6 > glog = log -G -T '{rev}:{node|short} {desc}'
7 > [experimental]
8 > evolution = createmarkers, allowunstable
9 > [extensions]
10 > rebase =
11 > amend =
12 > uncommit =
13 > EOF
14
15 Repo Setup
16
17 $ hg init repo
18 $ cd repo
19 $ for ch in {a..h}; do touch $ch; echo "foo" >> $ch; hg ci -Aqm "Added "$ch; done
20
21 $ hg glog
22 @ 7:ec2426147f0e Added h
23 |
24 o 6:87d6d6676308 Added g
25 |
26 o 5:825660c69f0c Added f
27 |
28 o 4:aa98ab95a928 Added e
29 |
30 o 3:62615734edd5 Added d
31 |
32 o 2:28ad74487de9 Added c
33 |
34 o 1:29becc82797a Added b
35 |
36 o 0:18d04c59bb5d Added a
37
38 Trying to unamend when there was no amend done
39
40 $ hg unamend
41 abort: changeset must have one predecessor, found 0 predecessors
42 [255]
43
44 Unamend on clean wdir and tip
45
46 $ echo "bar" >> h
47 $ hg amend
48
49 $ hg exp
50 # HG changeset patch
51 # User test
52 # Date 0 0
53 # Thu Jan 01 00:00:00 1970 +0000
54 # Node ID c9fa1a715c1b7661c0fafb362a9f30bd75878d7d
55 # Parent 87d6d66763085b629e6d7ed56778c79827273022
56 Added h
57
58 diff -r 87d6d6676308 -r c9fa1a715c1b h
59 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
60 +++ b/h Thu Jan 01 00:00:00 1970 +0000
61 @@ -0,0 +1,2 @@
62 +foo
63 +bar
64
65 $ hg glog --hidden
66 @ 8:c9fa1a715c1b Added h
67 |
68 | x 7:ec2426147f0e Added h
69 |/
70 o 6:87d6d6676308 Added g
71 |
72 o 5:825660c69f0c Added f
73 |
74 o 4:aa98ab95a928 Added e
75 |
76 o 3:62615734edd5 Added d
77 |
78 o 2:28ad74487de9 Added c
79 |
80 o 1:29becc82797a Added b
81 |
82 o 0:18d04c59bb5d Added a
83
84 $ hg unamend
85 $ hg glog --hidden
86 @ 9:8da14a1fd653 Added h
87 |
88 | x 8:c9fa1a715c1b Added h
89 |/
90 | x 7:ec2426147f0e Added h
91 |/
92 o 6:87d6d6676308 Added g
93 |
94 o 5:825660c69f0c Added f
95 |
96 o 4:aa98ab95a928 Added e
97 |
98 o 3:62615734edd5 Added d
99 |
100 o 2:28ad74487de9 Added c
101 |
102 o 1:29becc82797a Added b
103 |
104 o 0:18d04c59bb5d Added a
105
106 $ hg diff
107 diff -r 8da14a1fd653 h
108 --- a/h Thu Jan 01 00:00:00 1970 +0000
109 +++ b/h Thu Jan 01 00:00:00 1970 +0000
110 @@ -1,1 +1,2 @@
111 foo
112 +bar
113
114 $ hg exp
115 # HG changeset patch
116 # User test
117 # Date 0 0
118 # Thu Jan 01 00:00:00 1970 +0000
119 # Node ID 8da14a1fd653c3f07fdad5760511c9e12652a306
120 # Parent 87d6d66763085b629e6d7ed56778c79827273022
121 Added h
122
123 diff -r 87d6d6676308 -r 8da14a1fd653 h
124 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
125 +++ b/h Thu Jan 01 00:00:00 1970 +0000
126 @@ -0,0 +1,1 @@
127 +foo
128
129 $ hg status
130 M h
131
132 $ hg log -r . -T '{extras % "{extra}\n"}' --config alias.log=log
133 branch=default
134 unamend_source=\xc9\xfa\x1aq\\\x1bva\xc0\xfa\xfb6*\x9f0\xbdu\x87\x8d}
135
136 Using unamend to undo an unamed (intentional)
137
138 $ hg unamend
139 $ hg exp
140 # HG changeset patch
141 # User test
142 # Date 0 0
143 # Thu Jan 01 00:00:00 1970 +0000
144 # Node ID 1c09887216571a9755fc9d2e7f0e41c2b49e341b
145 # Parent 87d6d66763085b629e6d7ed56778c79827273022
146 Added h
147
148 diff -r 87d6d6676308 -r 1c0988721657 h
149 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
150 +++ b/h Thu Jan 01 00:00:00 1970 +0000
151 @@ -0,0 +1,2 @@
152 +foo
153 +bar
154 $ hg diff
155
156 Unamend on a dirty working directory
157
158 $ echo "bar" >> a
159 $ hg amend
160 $ echo "foobar" >> a
161 $ echo "bar" >> b
162 $ hg status
163 M a
164 M b
165
166 $ hg unamend
167
168 $ hg status
169 M a
170 M b
171
172 $ hg diff
173 diff -r 956548224719 a
174 --- a/a Thu Jan 01 00:00:00 1970 +0000
175 +++ b/a Thu Jan 01 00:00:00 1970 +0000
176 @@ -1,1 +1,3 @@
177 foo
178 +bar
179 +foobar
180 diff -r 956548224719 b
181 --- a/b Thu Jan 01 00:00:00 1970 +0000
182 +++ b/b Thu Jan 01 00:00:00 1970 +0000
183 @@ -1,1 +1,2 @@
184 foo
185 +bar
186
187 Unamending an added file
188
189 $ hg ci -m "Added things to a and b"
190 $ echo foo > bar
191 $ hg add bar
192 $ hg amend
193
194 $ hg unamend
195 $ hg status
196 A bar
197
198 $ hg revert --all
199 forgetting bar
200
201 Unamending a removed file
202
203 $ hg remove a
204 $ hg amend
205
206 $ hg unamend
207 $ hg status
208 R a
209 ? bar
210
211 $ hg revert --all
212 undeleting a
213
214 Unamending an added file with dirty wdir status
215
216 $ hg add bar
217 $ hg amend
218 $ echo bar >> bar
219 $ hg status
220 M bar
221
222 $ hg unamend
223 $ hg status
224 A bar
225 $ hg diff
226 diff -r ff917aa01c86 bar
227 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
228 +++ b/bar Thu Jan 01 00:00:00 1970 +0000
229 @@ -0,0 +1,2 @@
230 +foo
231 +bar
232
233 $ hg revert --all
234 forgetting bar
235
236 Unamending in middle of a stack
237
238 $ hg glog
239 @ 19:ff917aa01c86 Added things to a and b
240 |
241 o 12:956548224719 Added h
242 |
243 o 6:87d6d6676308 Added g
244 |
245 o 5:825660c69f0c Added f
246 |
247 o 4:aa98ab95a928 Added e
248 |
249 o 3:62615734edd5 Added d
250 |
251 o 2:28ad74487de9 Added c
252 |
253 o 1:29becc82797a Added b
254 |
255 o 0:18d04c59bb5d Added a
256
257 $ hg up 5
258 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
259 $ echo bar >> f
260 $ hg amend
261 $ hg rebase -s 6 -d . -q
262
263 $ hg glog
264 o 23:2b41b42e192a Added things to a and b
265 |
266 o 22:1860df151f01 Added h
267 |
268 o 21:49635b68477e Added g
269 |
270 @ 20:93f0e8ffab32 Added f
271 |
272 o 4:aa98ab95a928 Added e
273 |
274 o 3:62615734edd5 Added d
275 |
276 o 2:28ad74487de9 Added c
277 |
278 o 1:29becc82797a Added b
279 |
280 o 0:18d04c59bb5d Added a
281
282
283 $ hg unamend
284 abort: cannot unamend a changeset with children
285 [255]
286
287 Trying to unamend a public changeset
288
289 $ hg up
290 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 $ hg phase -r . -p
292 $ hg unamend
293 abort: cannot unamend public changesets
294 [255]
295
296 Testing whether unamend retains copies or not
297
298 $ hg status
299 ? bar
300
301 $ hg mv a foo
302
303 $ hg ci -m "Moved a to foo"
304 $ hg exp --git
305 # HG changeset patch
306 # User test
307 # Date 0 0
308 # Thu Jan 01 00:00:00 1970 +0000
309 # Node ID b087c66cada118d6c5487d3d7cb29cac239bd98a
310 # Parent 2b41b42e192adc425b106643c3fc89170d6b8add
311 Moved a to foo
312
313 diff --git a/a b/foo
314 rename from a
315 rename to foo
316
317 $ hg mv b foobar
318 $ hg diff --git
319 diff --git a/b b/foobar
320 rename from b
321 rename to foobar
322 $ hg amend
323
324 $ hg exp --git
325 # HG changeset patch
326 # User test
327 # Date 0 0
328 # Thu Jan 01 00:00:00 1970 +0000
329 # Node ID 9fa06fb09a83a86ec7368d15004dbc52ac1a5d2e
330 # Parent 2b41b42e192adc425b106643c3fc89170d6b8add
331 Moved a to foo
332
333 diff --git a/a b/foo
334 rename from a
335 rename to foo
336 diff --git a/b b/foobar
337 rename from b
338 rename to foobar
339
340 $ hg mv c wat
341 $ hg unamend
342
343 Retained copies in new prdecessor commit
344
345 $ hg exp --git
346 # HG changeset patch
347 # User test
348 # Date 0 0
349 # Thu Jan 01 00:00:00 1970 +0000
350 # Node ID 4cf9987c9b941f615930e7c46307b4dbf35697cf
351 # Parent 2b41b42e192adc425b106643c3fc89170d6b8add
352 Moved a to foo
353
354 diff --git a/a b/foo
355 rename from a
356 rename to foo
357
358 Retained copies in working directoy
359
360 $ hg diff --git
361 diff --git a/b b/foobar
362 rename from b
363 rename to foobar
364 diff --git a/c b/wat
365 rename from c
366 rename to wat
@@ -1,196 +1,318 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 obsutil,
32 33 pycompat,
33 34 registrar,
34 35 scmutil,
35 36 )
36 37
37 38 cmdtable = {}
38 39 command = registrar.command(cmdtable)
39 40
40 41 configtable = {}
41 42 configitem = registrar.configitem(configtable)
42 43
43 44 configitem('experimental', 'uncommitondirtywdir',
44 45 default=False,
45 46 )
46 47
47 48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
48 49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
49 50 # be specifying the version(s) of Mercurial they are tested with, or
50 51 # leave the attribute unspecified.
51 52 testedwith = 'ships-with-hg-core'
52 53
53 54 def _commitfiltered(repo, ctx, match, allowempty):
54 55 """Recommit ctx with changed files not in match. Return the new
55 56 node identifier, or None if nothing changed.
56 57 """
57 58 base = ctx.p1()
58 59 # ctx
59 60 initialfiles = set(ctx.files())
60 61 exclude = set(f for f in initialfiles if match(f))
61 62
62 63 # No files matched commit, so nothing excluded
63 64 if not exclude:
64 65 return None
65 66
66 67 files = (initialfiles - exclude)
67 68 # return the p1 so that we don't create an obsmarker later
68 69 if not files and not allowempty:
69 70 return ctx.parents()[0].node()
70 71
71 72 # Filter copies
72 73 copied = copies.pathcopies(base, ctx)
73 74 copied = dict((dst, src) for dst, src in copied.iteritems()
74 75 if dst in files)
75 76 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
76 77 if path not in contentctx:
77 78 return None
78 79 fctx = contentctx[path]
79 80 mctx = context.memfilectx(repo, fctx.path(), fctx.data(),
80 81 fctx.islink(),
81 82 fctx.isexec(),
82 83 copied=copied.get(path))
83 84 return mctx
84 85
85 86 new = context.memctx(repo,
86 87 parents=[base.node(), node.nullid],
87 88 text=ctx.description(),
88 89 files=files,
89 90 filectxfn=filectxfn,
90 91 user=ctx.user(),
91 92 date=ctx.date(),
92 93 extra=ctx.extra())
93 94 # phase handling
94 95 commitphase = ctx.phase()
95 96 overrides = {('phases', 'new-commit'): commitphase}
96 97 with repo.ui.configoverride(overrides, 'uncommit'):
97 98 newid = repo.commitctx(new)
98 99 return newid
99 100
100 101 def _uncommitdirstate(repo, oldctx, match):
101 102 """Fix the dirstate after switching the working directory from
102 103 oldctx to a copy of oldctx not containing changed files matched by
103 104 match.
104 105 """
105 106 ctx = repo['.']
106 107 ds = repo.dirstate
107 108 copies = dict(ds.copies())
108 109 s = repo.status(oldctx.p1(), oldctx, match=match)
109 110 for f in s.modified:
110 111 if ds[f] == 'r':
111 112 # modified + removed -> removed
112 113 continue
113 114 ds.normallookup(f)
114 115
115 116 for f in s.added:
116 117 if ds[f] == 'r':
117 118 # added + removed -> unknown
118 119 ds.drop(f)
119 120 elif ds[f] != 'a':
120 121 ds.add(f)
121 122
122 123 for f in s.removed:
123 124 if ds[f] == 'a':
124 125 # removed + added -> normal
125 126 ds.normallookup(f)
126 127 elif ds[f] != 'r':
127 128 ds.remove(f)
128 129
129 130 # Merge old parent and old working dir copies
130 131 oldcopies = {}
131 132 for f in (s.modified + s.added):
132 133 src = oldctx[f].renamed()
133 134 if src:
134 135 oldcopies[f] = src[0]
135 136 oldcopies.update(copies)
136 137 copies = dict((dst, oldcopies.get(src, src))
137 138 for dst, src in oldcopies.iteritems())
138 139 # Adjust the dirstate copies
139 140 for dst, src in copies.iteritems():
140 141 if (src not in ctx or dst in ctx or ds[dst] != 'a'):
141 142 src = None
142 143 ds.copy(src, dst)
143 144
144 145 @command('uncommit',
145 146 [('', 'keep', False, _('allow an empty commit after uncommiting')),
146 147 ] + commands.walkopts,
147 148 _('[OPTION]... [FILE]...'))
148 149 def uncommit(ui, repo, *pats, **opts):
149 150 """uncommit part or all of a local changeset
150 151
151 152 This command undoes the effect of a local commit, returning the affected
152 153 files to their uncommitted state. This means that files modified or
153 154 deleted in the changeset will be left unchanged, and so will remain
154 155 modified in the working directory.
155 156 """
156 157 opts = pycompat.byteskwargs(opts)
157 158
158 159 with repo.wlock(), repo.lock():
159 160 wctx = repo[None]
160 161
161 162 if not pats and not repo.ui.configbool('experimental',
162 163 'uncommitondirtywdir'):
163 164 cmdutil.bailifchanged(repo)
164 165 if wctx.parents()[0].node() == node.nullid:
165 166 raise error.Abort(_("cannot uncommit null changeset"))
166 167 if len(wctx.parents()) > 1:
167 168 raise error.Abort(_("cannot uncommit while merging"))
168 169 old = repo['.']
169 170 if not old.mutable():
170 171 raise error.Abort(_('cannot uncommit public changesets'))
171 172 if len(old.parents()) > 1:
172 173 raise error.Abort(_("cannot uncommit merge changeset"))
173 174 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
174 175 if not allowunstable and old.children():
175 176 raise error.Abort(_('cannot uncommit changeset with children'))
176 177
177 178 with repo.transaction('uncommit'):
178 179 match = scmutil.match(old, pats, opts)
179 180 newid = _commitfiltered(repo, old, match, opts.get('keep'))
180 181 if newid is None:
181 182 ui.status(_("nothing to uncommit\n"))
182 183 return 1
183 184
184 185 mapping = {}
185 186 if newid != old.p1().node():
186 187 # Move local changes on filtered changeset
187 188 mapping[old.node()] = (newid,)
188 189 else:
189 190 # Fully removed the old commit
190 191 mapping[old.node()] = ()
191 192
192 193 scmutil.cleanupnodes(repo, mapping, 'uncommit')
193 194
194 195 with repo.dirstate.parentchange():
195 196 repo.dirstate.setparents(newid, node.nullid)
196 197 _uncommitdirstate(repo, old, match)
198
199 def predecessormarkers(ctx):
200 """yields the obsolete markers marking the given changeset as a successor"""
201 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
202 yield obsutil.marker(ctx.repo(), data)
203
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 @command('^unamend', [])
246 def unamend(ui, repo, **opts):
247 """
248 undo the most recent amend operation on a current changeset
249
250 This command will roll back to the previous version of a changeset,
251 leaving working directory in state in which it was before running
252 `hg amend` (e.g. files modified as part of an amend will be
253 marked as modified `hg status`)
254 """
255
256 unfi = repo.unfiltered()
257
258 # identify the commit from which to unamend
259 curctx = repo['.']
260
261 with repo.wlock(), repo.lock(), repo.transaction('unamend'):
262 if not curctx.mutable():
263 raise error.Abort(_('cannot unamend public changesets'))
264
265 # identify the commit to which to unamend
266 markers = list(predecessormarkers(curctx))
267 if len(markers) != 1:
268 e = _("changeset must have one predecessor, found %i predecessors")
269 raise error.Abort(e % len(markers))
270
271 prednode = markers[0].prednode()
272 predctx = unfi[prednode]
273
274 if curctx.children():
275 raise error.Abort(_("cannot unamend a changeset with children"))
276
277 # add an extra so that we get a new hash
278 # note: allowing unamend to undo an unamend is an intentional feature
279 extras = predctx.extra()
280 extras['unamend_source'] = curctx.node()
281
282 def filectxfn(repo, ctx_, path):
283 try:
284 return predctx.filectx(path)
285 except KeyError:
286 return None
287
288 # Make a new commit same as predctx
289 newctx = context.memctx(repo,
290 parents=(predctx.p1(), predctx.p2()),
291 text=predctx.description(),
292 files=predctx.files(),
293 filectxfn=filectxfn,
294 user=predctx.user(),
295 date=predctx.date(),
296 extra=extras)
297 # phase handling
298 commitphase = curctx.phase()
299 overrides = {('phases', 'new-commit'): commitphase}
300 with repo.ui.configoverride(overrides, 'uncommit'):
301 newprednode = repo.commitctx(newctx)
302
303 newpredctx = repo[newprednode]
304
305 changedfiles = []
306 wctx = repo[None]
307 wm = wctx.manifest()
308 cm = newpredctx.manifest()
309 dirstate = repo.dirstate
310 diff = cm.diff(wm)
311 changedfiles.extend(diff.iterkeys())
312
313 with dirstate.parentchange():
314 dirstate.setparents(newprednode, node.nullid)
315 _unamenddirstate(repo, newpredctx, curctx)
316
317 mapping = {curctx.node(): (newprednode,)}
318 scmutil.cleanupnodes(repo, mapping, 'unamend')
General Comments 0
You need to be logged in to leave comments. Login now