##// END OF EJS Templates
uncommit: don't allow bare uncommit on dirty working directory...
Pulkit Goyal -
r34285:624c53e4 default
parent child Browse files
Show More
@@ -1,183 +1,186 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 cmdutil,
25 26 commands,
26 27 context,
27 28 copies,
28 29 error,
29 30 node,
30 31 obsolete,
31 32 registrar,
32 33 scmutil,
33 34 )
34 35
35 36 cmdtable = {}
36 37 command = registrar.command(cmdtable)
37 38
38 39 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
39 40 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
40 41 # be specifying the version(s) of Mercurial they are tested with, or
41 42 # leave the attribute unspecified.
42 43 testedwith = 'ships-with-hg-core'
43 44
44 45 def _commitfiltered(repo, ctx, match, allowempty):
45 46 """Recommit ctx with changed files not in match. Return the new
46 47 node identifier, or None if nothing changed.
47 48 """
48 49 base = ctx.p1()
49 50 # ctx
50 51 initialfiles = set(ctx.files())
51 52 exclude = set(f for f in initialfiles if match(f))
52 53
53 54 # No files matched commit, so nothing excluded
54 55 if not exclude:
55 56 return None
56 57
57 58 files = (initialfiles - exclude)
58 59 # return the p1 so that we don't create an obsmarker later
59 60 if not files and not allowempty:
60 61 return ctx.parents()[0].node()
61 62
62 63 # Filter copies
63 64 copied = copies.pathcopies(base, ctx)
64 65 copied = dict((dst, src) for dst, src in copied.iteritems()
65 66 if dst in files)
66 67 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
67 68 if path not in contentctx:
68 69 return None
69 70 fctx = contentctx[path]
70 71 mctx = context.memfilectx(repo, fctx.path(), fctx.data(),
71 72 fctx.islink(),
72 73 fctx.isexec(),
73 74 copied=copied.get(path))
74 75 return mctx
75 76
76 77 new = context.memctx(repo,
77 78 parents=[base.node(), node.nullid],
78 79 text=ctx.description(),
79 80 files=files,
80 81 filectxfn=filectxfn,
81 82 user=ctx.user(),
82 83 date=ctx.date(),
83 84 extra=ctx.extra())
84 85 # phase handling
85 86 commitphase = ctx.phase()
86 87 overrides = {('phases', 'new-commit'): commitphase}
87 88 with repo.ui.configoverride(overrides, 'uncommit'):
88 89 newid = repo.commitctx(new)
89 90 return newid
90 91
91 92 def _uncommitdirstate(repo, oldctx, match):
92 93 """Fix the dirstate after switching the working directory from
93 94 oldctx to a copy of oldctx not containing changed files matched by
94 95 match.
95 96 """
96 97 ctx = repo['.']
97 98 ds = repo.dirstate
98 99 copies = dict(ds.copies())
99 100 s = repo.status(oldctx.p1(), oldctx, match=match)
100 101 for f in s.modified:
101 102 if ds[f] == 'r':
102 103 # modified + removed -> removed
103 104 continue
104 105 ds.normallookup(f)
105 106
106 107 for f in s.added:
107 108 if ds[f] == 'r':
108 109 # added + removed -> unknown
109 110 ds.drop(f)
110 111 elif ds[f] != 'a':
111 112 ds.add(f)
112 113
113 114 for f in s.removed:
114 115 if ds[f] == 'a':
115 116 # removed + added -> normal
116 117 ds.normallookup(f)
117 118 elif ds[f] != 'r':
118 119 ds.remove(f)
119 120
120 121 # Merge old parent and old working dir copies
121 122 oldcopies = {}
122 123 for f in (s.modified + s.added):
123 124 src = oldctx[f].renamed()
124 125 if src:
125 126 oldcopies[f] = src[0]
126 127 oldcopies.update(copies)
127 128 copies = dict((dst, oldcopies.get(src, src))
128 129 for dst, src in oldcopies.iteritems())
129 130 # Adjust the dirstate copies
130 131 for dst, src in copies.iteritems():
131 132 if (src not in ctx or dst in ctx or ds[dst] != 'a'):
132 133 src = None
133 134 ds.copy(src, dst)
134 135
135 136 @command('uncommit',
136 137 [('', 'keep', False, _('allow an empty commit after uncommiting')),
137 138 ] + commands.walkopts,
138 139 _('[OPTION]... [FILE]...'))
139 140 def uncommit(ui, repo, *pats, **opts):
140 141 """uncommit part or all of a local changeset
141 142
142 143 This command undoes the effect of a local commit, returning the affected
143 144 files to their uncommitted state. This means that files modified or
144 145 deleted in the changeset will be left unchanged, and so will remain
145 146 modified in the working directory.
146 147 """
147 148
148 149 with repo.wlock(), repo.lock():
149 150 wctx = repo[None]
150 151
152 if not pats:
153 cmdutil.bailifchanged(repo)
151 154 if wctx.parents()[0].node() == node.nullid:
152 155 raise error.Abort(_("cannot uncommit null changeset"))
153 156 if len(wctx.parents()) > 1:
154 157 raise error.Abort(_("cannot uncommit while merging"))
155 158 old = repo['.']
156 159 if not old.mutable():
157 160 raise error.Abort(_('cannot uncommit public changesets'))
158 161 if len(old.parents()) > 1:
159 162 raise error.Abort(_("cannot uncommit merge changeset"))
160 163 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
161 164 if not allowunstable and old.children():
162 165 raise error.Abort(_('cannot uncommit changeset with children'))
163 166
164 167 with repo.transaction('uncommit'):
165 168 match = scmutil.match(old, pats, opts)
166 169 newid = _commitfiltered(repo, old, match, opts.get('keep'))
167 170 if newid is None:
168 171 ui.status(_("nothing to uncommit\n"))
169 172 return 1
170 173
171 174 mapping = {}
172 175 if newid != old.p1().node():
173 176 # Move local changes on filtered changeset
174 177 mapping[old.node()] = (newid,)
175 178 else:
176 179 # Fully removed the old commit
177 180 mapping[old.node()] = ()
178 181
179 182 scmutil.cleanupnodes(repo, mapping, 'uncommit')
180 183
181 184 with repo.dirstate.parentchange():
182 185 repo.dirstate.setparents(newid, node.nullid)
183 186 _uncommitdirstate(repo, old, match)
@@ -1,366 +1,369 b''
1 1 Test uncommit - set up the config
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [experimental]
5 5 > evolution=createmarkers, allowunstable
6 6 > [extensions]
7 7 > uncommit =
8 8 > drawdag=$TESTDIR/drawdag.py
9 9 > EOF
10 10
11 11 Build up a repo
12 12
13 13 $ hg init repo
14 14 $ cd repo
15 15 $ hg bookmark foo
16 16
17 17 Help for uncommit
18 18
19 19 $ hg help uncommit
20 20 hg uncommit [OPTION]... [FILE]...
21 21
22 22 uncommit part or all of a local changeset
23 23
24 24 This command undoes the effect of a local commit, returning the affected
25 25 files to their uncommitted state. This means that files modified or
26 26 deleted in the changeset will be left unchanged, and so will remain
27 27 modified in the working directory.
28 28
29 29 (use 'hg help -e uncommit' to show help for the uncommit extension)
30 30
31 31 options ([+] can be repeated):
32 32
33 33 --keep allow an empty commit after uncommiting
34 34 -I --include PATTERN [+] include names matching the given patterns
35 35 -X --exclude PATTERN [+] exclude names matching the given patterns
36 36
37 37 (some details hidden, use --verbose to show complete help)
38 38
39 39 Uncommit with no commits should fail
40 40
41 41 $ hg uncommit
42 42 abort: cannot uncommit null changeset
43 43 [255]
44 44
45 45 Create some commits
46 46
47 47 $ touch files
48 48 $ hg add files
49 49 $ for i in a ab abc abcd abcde; do echo $i > files; echo $i > file-$i; hg add file-$i; hg commit -m "added file-$i"; done
50 50 $ ls
51 51 file-a
52 52 file-ab
53 53 file-abc
54 54 file-abcd
55 55 file-abcde
56 56 files
57 57
58 58 $ hg log -G -T '{rev}:{node} {desc}' --hidden
59 59 @ 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
60 60 |
61 61 o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
62 62 |
63 63 o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
64 64 |
65 65 o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
66 66 |
67 67 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
68 68
69 69 Simple uncommit off the top, also moves bookmark
70 70
71 71 $ hg bookmark
72 72 * foo 4:6c4fd43ed714
73 73 $ hg uncommit
74 74 $ hg status
75 75 M files
76 76 A file-abcde
77 77 $ hg bookmark
78 78 * foo 3:6db330d65db4
79 79
80 80 $ hg log -G -T '{rev}:{node} {desc}' --hidden
81 81 x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
82 82 |
83 83 @ 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
84 84 |
85 85 o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
86 86 |
87 87 o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
88 88 |
89 89 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
90 90
91 91
92 92 Recommit
93 93
94 94 $ hg commit -m 'new change abcde'
95 95 $ hg status
96 96 $ hg heads -T '{rev}:{node} {desc}'
97 97 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde (no-eol)
98 98
99 99 Uncommit of non-existent and unchanged files has no effect
100 100 $ hg uncommit nothinghere
101 101 nothing to uncommit
102 102 [1]
103 103 $ hg status
104 104 $ hg uncommit file-abc
105 105 nothing to uncommit
106 106 [1]
107 107 $ hg status
108 108
109 109 Try partial uncommit, also moves bookmark
110 110
111 111 $ hg bookmark
112 112 * foo 5:0c07a3ccda77
113 113 $ hg uncommit files
114 114 $ hg status
115 115 M files
116 116 $ hg bookmark
117 117 * foo 6:3727deee06f7
118 118 $ hg heads -T '{rev}:{node} {desc}'
119 119 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde (no-eol)
120 120 $ hg log -r . -p -T '{rev}:{node} {desc}'
121 121 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcdediff -r 6db330d65db4 -r 3727deee06f7 file-abcde
122 122 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
123 123 +++ b/file-abcde Thu Jan 01 00:00:00 1970 +0000
124 124 @@ -0,0 +1,1 @@
125 125 +abcde
126 126
127 127 $ hg log -G -T '{rev}:{node} {desc}' --hidden
128 128 @ 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde
129 129 |
130 130 | x 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde
131 131 |/
132 132 | x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
133 133 |/
134 134 o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
135 135 |
136 136 o 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
137 137 |
138 138 o 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
139 139 |
140 140 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
141 141
142 142 $ hg commit -m 'update files for abcde'
143 143
144 144 Uncommit with dirty state
145 145
146 146 $ echo "foo" >> files
147 147 $ cat files
148 148 abcde
149 149 foo
150 150 $ hg status
151 151 M files
152 $ hg uncommit
153 abort: uncommitted changes
154 [255]
152 155 $ hg uncommit files
153 156 $ cat files
154 157 abcde
155 158 foo
156 159 $ hg commit -m "files abcde + foo"
157 160
158 161 Uncommit in the middle of a stack, does not move bookmark
159 162
160 163 $ hg checkout '.^^^'
161 164 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
162 165 (leaving bookmark foo)
163 166 $ hg log -r . -p -T '{rev}:{node} {desc}'
164 167 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abcdiff -r 69a232e754b0 -r abf2df566fc1 file-abc
165 168 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
166 169 +++ b/file-abc Thu Jan 01 00:00:00 1970 +0000
167 170 @@ -0,0 +1,1 @@
168 171 +abc
169 172 diff -r 69a232e754b0 -r abf2df566fc1 files
170 173 --- a/files Thu Jan 01 00:00:00 1970 +0000
171 174 +++ b/files Thu Jan 01 00:00:00 1970 +0000
172 175 @@ -1,1 +1,1 @@
173 176 -ab
174 177 +abc
175 178
176 179 $ hg bookmark
177 180 foo 8:83815831694b
178 181 $ hg uncommit
179 182 $ hg status
180 183 M files
181 184 A file-abc
182 185 $ hg heads -T '{rev}:{node} {desc}'
183 186 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo (no-eol)
184 187 $ hg bookmark
185 188 foo 8:83815831694b
186 189 $ hg commit -m 'new abc'
187 190 created new head
188 191
189 192 Partial uncommit in the middle, does not move bookmark
190 193
191 194 $ hg checkout '.^'
192 195 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
193 196 $ hg log -r . -p -T '{rev}:{node} {desc}'
194 197 1:69a232e754b08d568c4899475faf2eb44b857802 added file-abdiff -r 3004d2d9b508 -r 69a232e754b0 file-ab
195 198 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
196 199 +++ b/file-ab Thu Jan 01 00:00:00 1970 +0000
197 200 @@ -0,0 +1,1 @@
198 201 +ab
199 202 diff -r 3004d2d9b508 -r 69a232e754b0 files
200 203 --- a/files Thu Jan 01 00:00:00 1970 +0000
201 204 +++ b/files Thu Jan 01 00:00:00 1970 +0000
202 205 @@ -1,1 +1,1 @@
203 206 -a
204 207 +ab
205 208
206 209 $ hg bookmark
207 210 foo 8:83815831694b
208 211 $ hg uncommit file-ab
209 212 $ hg status
210 213 A file-ab
211 214
212 215 $ hg heads -T '{rev}:{node} {desc}\n'
213 216 10:8eb87968f2edb7f27f27fe676316e179de65fff6 added file-ab
214 217 9:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
215 218 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo
216 219
217 220 $ hg bookmark
218 221 foo 8:83815831694b
219 222 $ hg commit -m 'update ab'
220 223 $ hg status
221 224 $ hg heads -T '{rev}:{node} {desc}\n'
222 225 11:f21039c59242b085491bb58f591afc4ed1c04c09 update ab
223 226 9:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
224 227 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo
225 228
226 229 $ hg log -G -T '{rev}:{node} {desc}' --hidden
227 230 @ 11:f21039c59242b085491bb58f591afc4ed1c04c09 update ab
228 231 |
229 232 o 10:8eb87968f2edb7f27f27fe676316e179de65fff6 added file-ab
230 233 |
231 234 | o 9:5dc89ca4486f8a88716c5797fa9f498d13d7c2e1 new abc
232 235 | |
233 236 | | o 8:83815831694b1271e9f207cb1b79b2b19275edcb files abcde + foo
234 237 | | |
235 238 | | | x 7:0977fa602c2fd7d8427ed4e7ee15ea13b84c9173 update files for abcde
236 239 | | |/
237 240 | | o 6:3727deee06f72f5ffa8db792ee299cf39e3e190b new change abcde
238 241 | | |
239 242 | | | x 5:0c07a3ccda771b25f1cb1edbd02e683723344ef1 new change abcde
240 243 | | |/
241 244 | | | x 4:6c4fd43ed714e7fcd8adbaa7b16c953c2e985b60 added file-abcde
242 245 | | |/
243 246 | | o 3:6db330d65db434145c0b59d291853e9a84719b24 added file-abcd
244 247 | | |
245 248 | | x 2:abf2df566fc193b3ac34d946e63c1583e4d4732b added file-abc
246 249 | |/
247 250 | x 1:69a232e754b08d568c4899475faf2eb44b857802 added file-ab
248 251 |/
249 252 o 0:3004d2d9b50883c1538fc754a3aeb55f1b4084f6 added file-a
250 253
251 254 Uncommit with draft parent
252 255
253 256 $ hg uncommit
254 257 $ hg phase -r .
255 258 10: draft
256 259 $ hg commit -m 'update ab again'
257 260
258 261 Uncommit with public parent
259 262
260 263 $ hg phase -p "::.^"
261 264 $ hg uncommit
262 265 $ hg phase -r .
263 266 10: public
264 267
265 268 Partial uncommit with public parent
266 269
267 270 $ echo xyz > xyz
268 271 $ hg add xyz
269 272 $ hg commit -m "update ab and add xyz"
270 273 $ hg uncommit xyz
271 274 $ hg status
272 275 A xyz
273 276 $ hg phase -r .
274 277 14: draft
275 278 $ hg phase -r ".^"
276 279 10: public
277 280
278 281 Uncommit leaving an empty changeset
279 282
280 283 $ cd $TESTTMP
281 284 $ hg init repo1
282 285 $ cd repo1
283 286 $ hg debugdrawdag <<'EOS'
284 287 > Q
285 288 > |
286 289 > P
287 290 > EOS
288 291 $ hg up Q -q
289 292 $ hg uncommit --keep
290 293 $ hg log -G -T '{desc} FILES: {files}'
291 294 @ Q FILES:
292 295 |
293 296 | x Q FILES: Q
294 297 |/
295 298 o P FILES: P
296 299
297 300 $ hg status
298 301 A Q
299 302
300 303 $ cd ..
301 304 $ rm -rf repo1
302 305
303 306 Testing uncommit while merge
304 307
305 308 $ hg init repo2
306 309 $ cd repo2
307 310
308 311 Create some history
309 312
310 313 $ touch a
311 314 $ hg add a
312 315 $ for i in 1 2 3; do echo $i > a; hg commit -m "a $i"; done
313 316 $ hg checkout 0
314 317 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
315 318 $ touch b
316 319 $ hg add b
317 320 $ for i in 1 2 3; do echo $i > b; hg commit -m "b $i"; done
318 321 created new head
319 322 $ hg log -G -T '{rev}:{node} {desc}' --hidden
320 323 @ 5:2cd56cdde163ded2fbb16ba2f918c96046ab0bf2 b 3
321 324 |
322 325 o 4:c3a0d5bb3b15834ffd2ef9ef603e93ec65cf2037 b 2
323 326 |
324 327 o 3:49bb009ca26078726b8870f1edb29fae8f7618f5 b 1
325 328 |
326 329 | o 2:990982b7384266e691f1bc08ca36177adcd1c8a9 a 3
327 330 | |
328 331 | o 1:24d38e3cf160c7b6f5ffe82179332229886a6d34 a 2
329 332 |/
330 333 o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
331 334
332 335
333 336 Add and expect uncommit to fail on both merge working dir and merge changeset
334 337
335 338 $ hg merge 2
336 339 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
337 340 (branch merge, don't forget to commit)
338 341
339 342 $ hg uncommit
340 abort: cannot uncommit while merging
343 abort: outstanding uncommitted merge
341 344 [255]
342 345
343 346 $ hg status
344 347 M a
345 348 $ hg commit -m 'merge a and b'
346 349
347 350 $ hg uncommit
348 351 abort: cannot uncommit merge changeset
349 352 [255]
350 353
351 354 $ hg status
352 355 $ hg log -G -T '{rev}:{node} {desc}' --hidden
353 356 @ 6:c03b9c37bc67bf504d4912061cfb527b47a63c6e merge a and b
354 357 |\
355 358 | o 5:2cd56cdde163ded2fbb16ba2f918c96046ab0bf2 b 3
356 359 | |
357 360 | o 4:c3a0d5bb3b15834ffd2ef9ef603e93ec65cf2037 b 2
358 361 | |
359 362 | o 3:49bb009ca26078726b8870f1edb29fae8f7618f5 b 1
360 363 | |
361 364 o | 2:990982b7384266e691f1bc08ca36177adcd1c8a9 a 3
362 365 | |
363 366 o | 1:24d38e3cf160c7b6f5ffe82179332229886a6d34 a 2
364 367 |/
365 368 o 0:ea4e33293d4d274a2ba73150733c2612231f398c a 1
366 369
General Comments 0
You need to be logged in to leave comments. Login now