##// END OF EJS Templates
shelve: disallow commit while unshelve is in progress...
FUJIWARA Katsunori -
r19963:6f29cc56 stable
parent child Browse files
Show More
@@ -1,708 +1,709
1 1 # shelve.py - save/restore working directory state
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """save and restore changes to the working directory
9 9
10 10 The "hg shelve" command saves changes made to the working directory
11 11 and reverts those changes, resetting the working directory to a clean
12 12 state.
13 13
14 14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 15 shelve". Changes can be restored even after updating to a different
16 16 parent, in which case Mercurial's merge machinery will resolve any
17 17 conflicts if necessary.
18 18
19 19 You can have more than one shelved change outstanding at a time; each
20 20 shelved change has a distinct name. For details, see the help for "hg
21 21 shelve".
22 22 """
23 23
24 24 from mercurial.i18n import _
25 25 from mercurial.node import nullid, bin, hex
26 26 from mercurial import changegroup, cmdutil, scmutil, phases
27 27 from mercurial import error, hg, mdiff, merge, patch, repair, util
28 28 from mercurial import templatefilters
29 29 from mercurial import lock as lockmod
30 30 from hgext import rebase
31 31 import errno
32 32
33 33 cmdtable = {}
34 34 command = cmdutil.command(cmdtable)
35 35 testedwith = 'internal'
36 36
37 37 class shelvedfile(object):
38 38 """Helper for the file storing a single shelve
39 39
40 40 Handles common functions on shelve files (.hg/.files/.patch) using
41 41 the vfs layer"""
42 42 def __init__(self, repo, name, filetype=None):
43 43 self.repo = repo
44 44 self.name = name
45 45 self.vfs = scmutil.vfs(repo.join('shelved'))
46 46 if filetype:
47 47 self.fname = name + '.' + filetype
48 48 else:
49 49 self.fname = name
50 50
51 51 def exists(self):
52 52 return self.vfs.exists(self.fname)
53 53
54 54 def filename(self):
55 55 return self.vfs.join(self.fname)
56 56
57 57 def unlink(self):
58 58 util.unlink(self.filename())
59 59
60 60 def stat(self):
61 61 return self.vfs.stat(self.fname)
62 62
63 63 def opener(self, mode='rb'):
64 64 try:
65 65 return self.vfs(self.fname, mode)
66 66 except IOError, err:
67 67 if err.errno != errno.ENOENT:
68 68 raise
69 69 if mode[0] in 'wa':
70 70 try:
71 71 self.vfs.mkdir()
72 72 return self.vfs(self.fname, mode)
73 73 except IOError, err:
74 74 if err.errno != errno.EEXIST:
75 75 raise
76 76 elif mode[0] == 'r':
77 77 raise util.Abort(_("shelved change '%s' not found") %
78 78 self.name)
79 79
80 80 class shelvedstate(object):
81 81 """Handle persistence during unshelving operations.
82 82
83 83 Handles saving and restoring a shelved state. Ensures that different
84 84 versions of a shelved state are possible and handles them appropriately.
85 85 """
86 86 _version = 1
87 87 _filename = 'shelvedstate'
88 88
89 89 @classmethod
90 90 def load(cls, repo):
91 91 fp = repo.opener(cls._filename)
92 92 try:
93 93 version = int(fp.readline().strip())
94 94
95 95 if version != cls._version:
96 96 raise util.Abort(_('this version of shelve is incompatible '
97 97 'with the version used in this repo'))
98 98 name = fp.readline().strip()
99 99 wctx = fp.readline().strip()
100 100 pendingctx = fp.readline().strip()
101 101 parents = [bin(h) for h in fp.readline().split()]
102 102 stripnodes = [bin(h) for h in fp.readline().split()]
103 103 unknownfiles = fp.readline()[:-1].split('\0')
104 104 finally:
105 105 fp.close()
106 106
107 107 obj = cls()
108 108 obj.name = name
109 109 obj.wctx = repo[bin(wctx)]
110 110 obj.pendingctx = repo[bin(pendingctx)]
111 111 obj.parents = parents
112 112 obj.stripnodes = stripnodes
113 113 obj.unknownfiles = unknownfiles
114 114
115 115 return obj
116 116
117 117 @classmethod
118 118 def save(cls, repo, name, originalwctx, pendingctx, stripnodes,
119 119 unknownfiles):
120 120 fp = repo.opener(cls._filename, 'wb')
121 121 fp.write('%i\n' % cls._version)
122 122 fp.write('%s\n' % name)
123 123 fp.write('%s\n' % hex(originalwctx.node()))
124 124 fp.write('%s\n' % hex(pendingctx.node()))
125 125 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
126 126 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
127 127 fp.write('%s\n' % '\0'.join(unknownfiles))
128 128 fp.close()
129 129
130 130 @classmethod
131 131 def clear(cls, repo):
132 132 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
133 133
134 134 def createcmd(ui, repo, pats, opts):
135 135 """subcommand that creates a new shelve"""
136 136
137 137 def publicancestors(ctx):
138 138 """Compute the heads of the public ancestors of a commit.
139 139
140 140 Much faster than the revset heads(ancestors(ctx) - draft())"""
141 141 seen = set()
142 142 visit = util.deque()
143 143 visit.append(ctx)
144 144 while visit:
145 145 ctx = visit.popleft()
146 146 for parent in ctx.parents():
147 147 rev = parent.rev()
148 148 if rev not in seen:
149 149 seen.add(rev)
150 150 if parent.mutable():
151 151 visit.append(parent)
152 152 else:
153 153 yield parent.node()
154 154
155 155 wctx = repo[None]
156 156 parents = wctx.parents()
157 157 if len(parents) > 1:
158 158 raise util.Abort(_('cannot shelve while merging'))
159 159 parent = parents[0]
160 160
161 161 # we never need the user, so we use a generic user for all shelve operations
162 162 user = 'shelve@localhost'
163 163 label = repo._bookmarkcurrent or parent.branch() or 'default'
164 164
165 165 # slashes aren't allowed in filenames, therefore we rename it
166 166 origlabel, label = label, label.replace('/', '_')
167 167
168 168 def gennames():
169 169 yield label
170 170 for i in xrange(1, 100):
171 171 yield '%s-%02d' % (label, i)
172 172
173 173 shelvedfiles = []
174 174
175 175 def commitfunc(ui, repo, message, match, opts):
176 176 # check modified, added, removed, deleted only
177 177 for flist in repo.status(match=match)[:4]:
178 178 shelvedfiles.extend(flist)
179 179 hasmq = util.safehasattr(repo, 'mq')
180 180 if hasmq:
181 181 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
182 182 try:
183 183 return repo.commit(message, user, opts.get('date'), match)
184 184 finally:
185 185 if hasmq:
186 186 repo.mq.checkapplied = saved
187 187
188 188 if parent.node() != nullid:
189 189 desc = parent.description().split('\n', 1)[0]
190 190 else:
191 191 desc = '(empty repository)'
192 192
193 193 if not opts['message']:
194 194 opts['message'] = desc
195 195
196 196 name = opts['name']
197 197
198 198 wlock = lock = tr = bms = None
199 199 try:
200 200 wlock = repo.wlock()
201 201 lock = repo.lock()
202 202
203 203 bms = repo._bookmarks.copy()
204 204 # use an uncommitted transaction to generate the bundle to avoid
205 205 # pull races. ensure we don't print the abort message to stderr.
206 206 tr = repo.transaction('commit', report=lambda x: None)
207 207
208 208 if name:
209 209 if shelvedfile(repo, name, 'hg').exists():
210 210 raise util.Abort(_("a shelved change named '%s' already exists")
211 211 % name)
212 212 else:
213 213 for n in gennames():
214 214 if not shelvedfile(repo, n, 'hg').exists():
215 215 name = n
216 216 break
217 217 else:
218 218 raise util.Abort(_("too many shelved changes named '%s'") %
219 219 label)
220 220
221 221 # ensure we are not creating a subdirectory or a hidden file
222 222 if '/' in name or '\\' in name:
223 223 raise util.Abort(_('shelved change names may not contain slashes'))
224 224 if name.startswith('.'):
225 225 raise util.Abort(_("shelved change names may not start with '.'"))
226 226
227 227 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
228 228
229 229 if not node:
230 230 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
231 231 if stat[3]:
232 232 ui.status(_("nothing changed (%d missing files, see "
233 233 "'hg status')\n") % len(stat[3]))
234 234 else:
235 235 ui.status(_("nothing changed\n"))
236 236 return 1
237 237
238 238 phases.retractboundary(repo, phases.secret, [node])
239 239
240 240 fp = shelvedfile(repo, name, 'files').opener('wb')
241 241 fp.write('\0'.join(shelvedfiles))
242 242
243 243 bases = list(publicancestors(repo[node]))
244 244 cg = repo.changegroupsubset(bases, [node], 'shelve')
245 245 changegroup.writebundle(cg, shelvedfile(repo, name, 'hg').filename(),
246 246 'HG10UN')
247 247 cmdutil.export(repo, [node],
248 248 fp=shelvedfile(repo, name, 'patch').opener('wb'),
249 249 opts=mdiff.diffopts(git=True))
250 250
251 251
252 252 if ui.formatted():
253 253 desc = util.ellipsis(desc, ui.termwidth())
254 254 ui.status(_('shelved as %s\n') % name)
255 255 hg.update(repo, parent.node())
256 256 finally:
257 257 if bms:
258 258 # restore old bookmarks
259 259 repo._bookmarks.update(bms)
260 260 repo._bookmarks.write()
261 261 if tr:
262 262 tr.abort()
263 263 lockmod.release(lock, wlock)
264 264
265 265 def cleanupcmd(ui, repo):
266 266 """subcommand that deletes all shelves"""
267 267
268 268 wlock = None
269 269 try:
270 270 wlock = repo.wlock()
271 271 for (name, _) in repo.vfs.readdir('shelved'):
272 272 suffix = name.rsplit('.', 1)[-1]
273 273 if suffix in ('hg', 'files', 'patch'):
274 274 shelvedfile(repo, name).unlink()
275 275 finally:
276 276 lockmod.release(wlock)
277 277
278 278 def deletecmd(ui, repo, pats):
279 279 """subcommand that deletes a specific shelve"""
280 280 if not pats:
281 281 raise util.Abort(_('no shelved changes specified!'))
282 282 wlock = None
283 283 try:
284 284 wlock = repo.wlock()
285 285 try:
286 286 for name in pats:
287 287 for suffix in 'hg files patch'.split():
288 288 shelvedfile(repo, name, suffix).unlink()
289 289 except OSError, err:
290 290 if err.errno != errno.ENOENT:
291 291 raise
292 292 raise util.Abort(_("shelved change '%s' not found") % name)
293 293 finally:
294 294 lockmod.release(wlock)
295 295
296 296 def listshelves(repo):
297 297 """return all shelves in repo as list of (time, filename)"""
298 298 try:
299 299 names = repo.vfs.readdir('shelved')
300 300 except OSError, err:
301 301 if err.errno != errno.ENOENT:
302 302 raise
303 303 return []
304 304 info = []
305 305 for (name, _) in names:
306 306 pfx, sfx = name.rsplit('.', 1)
307 307 if not pfx or sfx != 'patch':
308 308 continue
309 309 st = shelvedfile(repo, name).stat()
310 310 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
311 311 return sorted(info, reverse=True)
312 312
313 313 def listcmd(ui, repo, pats, opts):
314 314 """subcommand that displays the list of shelves"""
315 315 pats = set(pats)
316 316 width = 80
317 317 if not ui.plain():
318 318 width = ui.termwidth()
319 319 namelabel = 'shelve.newest'
320 320 for mtime, name in listshelves(repo):
321 321 sname = util.split(name)[1]
322 322 if pats and sname not in pats:
323 323 continue
324 324 ui.write(sname, label=namelabel)
325 325 namelabel = 'shelve.name'
326 326 if ui.quiet:
327 327 ui.write('\n')
328 328 continue
329 329 ui.write(' ' * (16 - len(sname)))
330 330 used = 16
331 331 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
332 332 ui.write(age, label='shelve.age')
333 333 ui.write(' ' * (12 - len(age)))
334 334 used += 12
335 335 fp = open(name + '.patch', 'rb')
336 336 try:
337 337 while True:
338 338 line = fp.readline()
339 339 if not line:
340 340 break
341 341 if not line.startswith('#'):
342 342 desc = line.rstrip()
343 343 if ui.formatted():
344 344 desc = util.ellipsis(desc, width - used)
345 345 ui.write(desc)
346 346 break
347 347 ui.write('\n')
348 348 if not (opts['patch'] or opts['stat']):
349 349 continue
350 350 difflines = fp.readlines()
351 351 if opts['patch']:
352 352 for chunk, label in patch.difflabel(iter, difflines):
353 353 ui.write(chunk, label=label)
354 354 if opts['stat']:
355 355 for chunk, label in patch.diffstatui(difflines, width=width,
356 356 git=True):
357 357 ui.write(chunk, label=label)
358 358 finally:
359 359 fp.close()
360 360
361 361 def readshelvedfiles(repo, basename):
362 362 """return the list of files touched in a shelve"""
363 363 fp = shelvedfile(repo, basename, 'files').opener()
364 364 return fp.read().split('\0')
365 365
366 366 def checkparents(repo, state):
367 367 """check parent while resuming an unshelve"""
368 368 if state.parents != repo.dirstate.parents():
369 369 raise util.Abort(_('working directory parents do not match unshelve '
370 370 'state'))
371 371
372 372 def pathtofiles(repo, files):
373 373 cwd = repo.getcwd()
374 374 return [repo.pathto(f, cwd) for f in files]
375 375
376 376 def unshelveabort(ui, repo, state, opts):
377 377 """subcommand that abort an in-progress unshelve"""
378 378 wlock = repo.wlock()
379 379 lock = None
380 380 try:
381 381 checkparents(repo, state)
382 382
383 383 util.rename(repo.join('unshelverebasestate'),
384 384 repo.join('rebasestate'))
385 385 try:
386 386 rebase.rebase(ui, repo, **{
387 387 'abort' : True
388 388 })
389 389 except Exception:
390 390 util.rename(repo.join('rebasestate'),
391 391 repo.join('unshelverebasestate'))
392 392 raise
393 393
394 394 lock = repo.lock()
395 395
396 396 mergefiles(ui, repo, state.wctx, state.pendingctx, state.unknownfiles)
397 397
398 398 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
399 399 shelvedstate.clear(repo)
400 400 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
401 401 finally:
402 402 lockmod.release(lock, wlock)
403 403
404 404 def mergefiles(ui, repo, wctx, shelvectx, unknownfiles):
405 405 """updates to wctx and merges the changes from shelvectx into the
406 406 dirstate. drops any files in unknownfiles from the dirstate."""
407 407 oldquiet = ui.quiet
408 408 try:
409 409 ui.quiet = True
410 410 hg.update(repo, wctx.node())
411 411 files = []
412 412 files.extend(shelvectx.files())
413 413 files.extend(shelvectx.parents()[0].files())
414 414 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
415 415 *pathtofiles(repo, files),
416 416 **{'no_backup': True})
417 417 finally:
418 418 ui.quiet = oldquiet
419 419
420 420 # Send untracked files back to being untracked
421 421 dirstate = repo.dirstate
422 422 for f in unknownfiles:
423 423 dirstate.drop(f)
424 424
425 425 def unshelvecleanup(ui, repo, name, opts):
426 426 """remove related files after an unshelve"""
427 427 if not opts['keep']:
428 428 for filetype in 'hg files patch'.split():
429 429 shelvedfile(repo, name, filetype).unlink()
430 430
431 431 def unshelvecontinue(ui, repo, state, opts):
432 432 """subcommand to continue an in-progress unshelve"""
433 433 # We're finishing off a merge. First parent is our original
434 434 # parent, second is the temporary "fake" commit we're unshelving.
435 435 wlock = repo.wlock()
436 436 lock = None
437 437 try:
438 438 checkparents(repo, state)
439 439 ms = merge.mergestate(repo)
440 440 if [f for f in ms if ms[f] == 'u']:
441 441 raise util.Abort(
442 442 _("unresolved conflicts, can't continue"),
443 443 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
444 444
445 445 lock = repo.lock()
446 446
447 447 util.rename(repo.join('unshelverebasestate'),
448 448 repo.join('rebasestate'))
449 449 try:
450 450 rebase.rebase(ui, repo, **{
451 451 'continue' : True
452 452 })
453 453 except Exception:
454 454 util.rename(repo.join('rebasestate'),
455 455 repo.join('unshelverebasestate'))
456 456 raise
457 457
458 458 shelvectx = repo['tip']
459 459 if not shelvectx in state.pendingctx.children():
460 460 # rebase was a no-op, so it produced no child commit
461 461 shelvectx = state.pendingctx
462 462
463 463 mergefiles(ui, repo, state.wctx, shelvectx, state.unknownfiles)
464 464
465 465 state.stripnodes.append(shelvectx.node())
466 466 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
467 467 shelvedstate.clear(repo)
468 468 unshelvecleanup(ui, repo, state.name, opts)
469 469 ui.status(_("unshelve of '%s' complete\n") % state.name)
470 470 finally:
471 471 lockmod.release(lock, wlock)
472 472
473 473 @command('unshelve',
474 474 [('a', 'abort', None,
475 475 _('abort an incomplete unshelve operation')),
476 476 ('c', 'continue', None,
477 477 _('continue an incomplete unshelve operation')),
478 478 ('', 'keep', None,
479 479 _('keep shelve after unshelving'))],
480 480 _('hg unshelve [SHELVED]'))
481 481 def unshelve(ui, repo, *shelved, **opts):
482 482 """restore a shelved change to the working directory
483 483
484 484 This command accepts an optional name of a shelved change to
485 485 restore. If none is given, the most recent shelved change is used.
486 486
487 487 If a shelved change is applied successfully, the bundle that
488 488 contains the shelved changes is deleted afterwards.
489 489
490 490 Since you can restore a shelved change on top of an arbitrary
491 491 commit, it is possible that unshelving will result in a conflict
492 492 between your changes and the commits you are unshelving onto. If
493 493 this occurs, you must resolve the conflict, then use
494 494 ``--continue`` to complete the unshelve operation. (The bundle
495 495 will not be deleted until you successfully complete the unshelve.)
496 496
497 497 (Alternatively, you can use ``--abort`` to abandon an unshelve
498 498 that causes a conflict. This reverts the unshelved changes, and
499 499 does not delete the bundle.)
500 500 """
501 501 abortf = opts['abort']
502 502 continuef = opts['continue']
503 503 if not abortf and not continuef:
504 504 cmdutil.checkunfinished(repo)
505 505
506 506 if abortf or continuef:
507 507 if abortf and continuef:
508 508 raise util.Abort(_('cannot use both abort and continue'))
509 509 if shelved:
510 510 raise util.Abort(_('cannot combine abort/continue with '
511 511 'naming a shelved change'))
512 512
513 513 try:
514 514 state = shelvedstate.load(repo)
515 515 except IOError, err:
516 516 if err.errno != errno.ENOENT:
517 517 raise
518 518 raise util.Abort(_('no unshelve operation underway'))
519 519
520 520 if abortf:
521 521 return unshelveabort(ui, repo, state, opts)
522 522 elif continuef:
523 523 return unshelvecontinue(ui, repo, state, opts)
524 524 elif len(shelved) > 1:
525 525 raise util.Abort(_('can only unshelve one change at a time'))
526 526 elif not shelved:
527 527 shelved = listshelves(repo)
528 528 if not shelved:
529 529 raise util.Abort(_('no shelved changes to apply!'))
530 530 basename = util.split(shelved[0][1])[1]
531 531 ui.status(_("unshelving change '%s'\n") % basename)
532 532 else:
533 533 basename = shelved[0]
534 534
535 535 shelvedfiles = readshelvedfiles(repo, basename)
536 536
537 537 wlock = lock = tr = None
538 538 try:
539 539 lock = repo.lock()
540 540 wlock = repo.wlock()
541 541
542 542 tr = repo.transaction('unshelve', report=lambda x: None)
543 543 oldtiprev = len(repo)
544 544
545 545 wctx = repo['.']
546 546 tmpwctx = wctx
547 547 # The goal is to have a commit structure like so:
548 548 # ...-> wctx -> tmpwctx -> shelvectx
549 549 # where tmpwctx is an optional commit with the user's pending changes
550 550 # and shelvectx is the unshelved changes. Then we merge it all down
551 551 # to the original wctx.
552 552
553 553 # Store pending changes in a commit
554 554 m, a, r, d, u = repo.status(unknown=True)[:5]
555 555 if m or a or r or d or u:
556 556 def commitfunc(ui, repo, message, match, opts):
557 557 hasmq = util.safehasattr(repo, 'mq')
558 558 if hasmq:
559 559 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
560 560
561 561 try:
562 562 return repo.commit(message, 'shelve@localhost',
563 563 opts.get('date'), match)
564 564 finally:
565 565 if hasmq:
566 566 repo.mq.checkapplied = saved
567 567
568 568 tempopts = {}
569 569 tempopts['message'] = "pending changes temporary commit"
570 570 tempopts['addremove'] = True
571 571 oldquiet = ui.quiet
572 572 try:
573 573 ui.quiet = True
574 574 node = cmdutil.commit(ui, repo, commitfunc, None, tempopts)
575 575 finally:
576 576 ui.quiet = oldquiet
577 577 tmpwctx = repo[node]
578 578
579 579 try:
580 580 fp = shelvedfile(repo, basename, 'hg').opener()
581 581 gen = changegroup.readbundle(fp, fp.name)
582 582 repo.addchangegroup(gen, 'unshelve', 'bundle:' + fp.name)
583 583 nodes = [ctx.node() for ctx in repo.set('%d:', oldtiprev)]
584 584 phases.retractboundary(repo, phases.secret, nodes)
585 585 finally:
586 586 fp.close()
587 587
588 588 shelvectx = repo['tip']
589 589
590 590 # If the shelve is not immediately on top of the commit
591 591 # we'll be merging with, rebase it to be on top.
592 592 if tmpwctx.node() != shelvectx.parents()[0].node():
593 593 try:
594 594 rebase.rebase(ui, repo, **{
595 595 'rev' : [shelvectx.rev()],
596 596 'dest' : str(tmpwctx.rev()),
597 597 'keep' : True,
598 598 })
599 599 except error.InterventionRequired:
600 600 tr.close()
601 601
602 602 stripnodes = [repo.changelog.node(rev)
603 603 for rev in xrange(oldtiprev, len(repo))]
604 604 shelvedstate.save(repo, basename, wctx, tmpwctx, stripnodes, u)
605 605
606 606 util.rename(repo.join('rebasestate'),
607 607 repo.join('unshelverebasestate'))
608 608 raise error.InterventionRequired(
609 609 _("unresolved conflicts (see 'hg resolve', then "
610 610 "'hg unshelve --continue')"))
611 611
612 612 # refresh ctx after rebase completes
613 613 shelvectx = repo['tip']
614 614
615 615 if not shelvectx in tmpwctx.children():
616 616 # rebase was a no-op, so it produced no child commit
617 617 shelvectx = tmpwctx
618 618
619 619 mergefiles(ui, repo, wctx, shelvectx, u)
620 620 shelvedstate.clear(repo)
621 621
622 622 # The transaction aborting will strip all the commits for us,
623 623 # but it doesn't update the inmemory structures, so addchangegroup
624 624 # hooks still fire and try to operate on the missing commits.
625 625 # Clean up manually to prevent this.
626 626 repo.changelog.strip(oldtiprev, tr)
627 627
628 628 unshelvecleanup(ui, repo, basename, opts)
629 629 finally:
630 630 if tr:
631 631 tr.release()
632 632 lockmod.release(lock, wlock)
633 633
634 634 @command('shelve',
635 635 [('A', 'addremove', None,
636 636 _('mark new/missing files as added/removed before shelving')),
637 637 ('', 'cleanup', None,
638 638 _('delete all shelved changes')),
639 639 ('', 'date', '',
640 640 _('shelve with the specified commit date'), _('DATE')),
641 641 ('d', 'delete', None,
642 642 _('delete the named shelved change(s)')),
643 643 ('l', 'list', None,
644 644 _('list current shelves')),
645 645 ('m', 'message', '',
646 646 _('use text as shelve message'), _('TEXT')),
647 647 ('n', 'name', '',
648 648 _('use the given name for the shelved commit'), _('NAME')),
649 649 ('p', 'patch', None,
650 650 _('show patch')),
651 651 ('', 'stat', None,
652 652 _('output diffstat-style summary of changes'))],
653 653 _('hg shelve'))
654 654 def shelvecmd(ui, repo, *pats, **opts):
655 655 '''save and set aside changes from the working directory
656 656
657 657 Shelving takes files that "hg status" reports as not clean, saves
658 658 the modifications to a bundle (a shelved change), and reverts the
659 659 files so that their state in the working directory becomes clean.
660 660
661 661 To restore these changes to the working directory, using "hg
662 662 unshelve"; this will work even if you switch to a different
663 663 commit.
664 664
665 665 When no files are specified, "hg shelve" saves all not-clean
666 666 files. If specific files or directories are named, only changes to
667 667 those files are shelved.
668 668
669 669 Each shelved change has a name that makes it easier to find later.
670 670 The name of a shelved change defaults to being based on the active
671 671 bookmark, or if there is no active bookmark, the current named
672 672 branch. To specify a different name, use ``--name``.
673 673
674 674 To see a list of existing shelved changes, use the ``--list``
675 675 option. For each shelved change, this will print its name, age,
676 676 and description; use ``--patch`` or ``--stat`` for more details.
677 677
678 678 To delete specific shelved changes, use ``--delete``. To delete
679 679 all shelved changes, use ``--cleanup``.
680 680 '''
681 681 cmdutil.checkunfinished(repo)
682 682
683 683 def checkopt(opt, incompatible):
684 684 if opts[opt]:
685 685 for i in incompatible.split():
686 686 if opts[i]:
687 687 raise util.Abort(_("options '--%s' and '--%s' may not be "
688 688 "used together") % (opt, i))
689 689 return True
690 690 if checkopt('cleanup', 'addremove delete list message name patch stat'):
691 691 if pats:
692 692 raise util.Abort(_("cannot specify names when using '--cleanup'"))
693 693 return cleanupcmd(ui, repo)
694 694 elif checkopt('delete', 'addremove cleanup list message name patch stat'):
695 695 return deletecmd(ui, repo, pats)
696 696 elif checkopt('list', 'addremove cleanup delete message name'):
697 697 return listcmd(ui, repo, pats, opts)
698 698 else:
699 699 for i in ('patch', 'stat'):
700 700 if opts[i]:
701 701 raise util.Abort(_("option '--%s' may not be "
702 702 "used when shelving a change") % (i,))
703 703 return createcmd(ui, repo, pats, opts)
704 704
705 705 def extsetup(ui):
706 706 cmdutil.unfinishedstates.append(
707 [shelvedstate._filename, False, True, _('unshelve already in progress'),
707 [shelvedstate._filename, False, False,
708 _('unshelve already in progress'),
708 709 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
@@ -1,516 +1,521
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "mq=" >> $HGRCPATH
3 3 $ echo "shelve=" >> $HGRCPATH
4 4 $ echo "[defaults]" >> $HGRCPATH
5 5 $ echo "diff = --nodates --git" >> $HGRCPATH
6 6 $ echo "qnew = --date '0 0'" >> $HGRCPATH
7 7
8 8 $ hg init repo
9 9 $ cd repo
10 10 $ mkdir a b
11 11 $ echo a > a/a
12 12 $ echo b > b/b
13 13 $ echo c > c
14 14 $ echo d > d
15 15 $ echo x > x
16 16 $ hg addremove -q
17 17
18 18 shelving in an empty repo should be possible
19 19
20 20 $ hg shelve
21 21 shelved as default
22 22 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
23 23
24 24 $ hg unshelve
25 25 unshelving change 'default'
26 26 adding changesets
27 27 adding manifests
28 28 adding file changes
29 29 added 1 changesets with 5 changes to 5 files
30 30
31 31 $ hg commit -q -m 'initial commit'
32 32
33 33 $ hg shelve
34 34 nothing changed
35 35 [1]
36 36
37 37 create an mq patch - shelving should work fine with a patch applied
38 38
39 39 $ echo n > n
40 40 $ hg add n
41 41 $ hg commit n -m second
42 42 $ hg qnew second.patch
43 43
44 44 shelve a change that we will delete later
45 45
46 46 $ echo a >> a/a
47 47 $ hg shelve
48 48 shelved as default
49 49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
50 50
51 51 set up some more complex changes to shelve
52 52
53 53 $ echo a >> a/a
54 54 $ hg mv b b.rename
55 55 moving b/b to b.rename/b (glob)
56 56 $ hg cp c c.copy
57 57 $ hg status -C
58 58 M a/a
59 59 A b.rename/b
60 60 b/b
61 61 A c.copy
62 62 c
63 63 R b/b
64 64
65 65 prevent some foot-shooting
66 66
67 67 $ hg shelve -n foo/bar
68 68 abort: shelved change names may not contain slashes
69 69 [255]
70 70 $ hg shelve -n .baz
71 71 abort: shelved change names may not start with '.'
72 72 [255]
73 73
74 74 the common case - no options or filenames
75 75
76 76 $ hg shelve
77 77 shelved as default-01
78 78 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
79 79 $ hg status -C
80 80
81 81 ensure that our shelved changes exist
82 82
83 83 $ hg shelve -l
84 84 default-01 (*) [mq]: second.patch (glob)
85 85 default (*) [mq]: second.patch (glob)
86 86
87 87 $ hg shelve -l -p default
88 88 default (*) [mq]: second.patch (glob)
89 89
90 90 diff --git a/a/a b/a/a
91 91 --- a/a/a
92 92 +++ b/a/a
93 93 @@ -1,1 +1,2 @@
94 94 a
95 95 +a
96 96
97 97 delete our older shelved change
98 98
99 99 $ hg shelve -d default
100 100 $ hg qfinish -a -q
101 101
102 102 local edits should not prevent a shelved change from applying
103 103
104 104 $ printf "z\na\n" > a/a
105 105 $ hg unshelve --keep
106 106 unshelving change 'default-01'
107 107 adding changesets
108 108 adding manifests
109 109 adding file changes
110 110 added 1 changesets with 3 changes to 8 files (+1 heads)
111 111 merging a/a
112 112
113 113 $ hg revert --all -q
114 114 $ rm a/a.orig b.rename/b c.copy
115 115
116 116 apply it and make sure our state is as expected
117 117
118 118 $ hg unshelve
119 119 unshelving change 'default-01'
120 120 adding changesets
121 121 adding manifests
122 122 adding file changes
123 123 added 1 changesets with 3 changes to 8 files
124 124 $ hg status -C
125 125 M a/a
126 126 A b.rename/b
127 127 b/b
128 128 A c.copy
129 129 c
130 130 R b/b
131 131 $ hg shelve -l
132 132
133 133 $ hg unshelve
134 134 abort: no shelved changes to apply!
135 135 [255]
136 136 $ hg unshelve foo
137 137 abort: shelved change 'foo' not found
138 138 [255]
139 139
140 140 named shelves, specific filenames, and "commit messages" should all work
141 141
142 142 $ hg status -C
143 143 M a/a
144 144 A b.rename/b
145 145 b/b
146 146 A c.copy
147 147 c
148 148 R b/b
149 149 $ hg shelve -q -n wibble -m wat a
150 150
151 151 expect "a" to no longer be present, but status otherwise unchanged
152 152
153 153 $ hg status -C
154 154 A b.rename/b
155 155 b/b
156 156 A c.copy
157 157 c
158 158 R b/b
159 159 $ hg shelve -l --stat
160 160 wibble (*) wat (glob)
161 161 a/a | 1 +
162 162 1 files changed, 1 insertions(+), 0 deletions(-)
163 163
164 164 and now "a/a" should reappear
165 165
166 166 $ cd a
167 167 $ hg unshelve -q wibble
168 168 $ cd ..
169 169 $ hg status -C
170 170 M a/a
171 171 A b.rename/b
172 172 b/b
173 173 A c.copy
174 174 c
175 175 R b/b
176 176
177 177 cause unshelving to result in a merge with 'a' conflicting
178 178
179 179 $ hg shelve -q
180 180 $ echo c>>a/a
181 181 $ hg commit -m second
182 182 $ hg tip --template '{files}\n'
183 183 a/a
184 184
185 185 add an unrelated change that should be preserved
186 186
187 187 $ mkdir foo
188 188 $ echo foo > foo/foo
189 189 $ hg add foo/foo
190 190
191 191 force a conflicted merge to occur
192 192
193 193 $ hg unshelve
194 194 unshelving change 'default'
195 195 adding changesets
196 196 adding manifests
197 197 adding file changes
198 198 added 1 changesets with 3 changes to 8 files (+1 heads)
199 199 merging a/a
200 200 warning: conflicts during merge.
201 201 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
202 202 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
203 203 [1]
204 204
205 205 ensure that we have a merge with unresolved conflicts
206 206
207 207 $ hg heads -q --template '{rev}\n'
208 208 5
209 209 4
210 210 $ hg parents -q --template '{rev}\n'
211 211 4
212 212 5
213 213 $ hg status
214 214 M a/a
215 215 M b.rename/b
216 216 M c.copy
217 217 R b/b
218 218 ? a/a.orig
219 219 $ hg diff
220 220 diff --git a/a/a b/a/a
221 221 --- a/a/a
222 222 +++ b/a/a
223 223 @@ -1,2 +1,6 @@
224 224 a
225 225 +<<<<<<< local
226 226 c
227 227 +=======
228 228 +a
229 229 +>>>>>>> other
230 230 diff --git a/b.rename/b b/b.rename/b
231 231 --- /dev/null
232 232 +++ b/b.rename/b
233 233 @@ -0,0 +1,1 @@
234 234 +b
235 235 diff --git a/b/b b/b/b
236 236 deleted file mode 100644
237 237 --- a/b/b
238 238 +++ /dev/null
239 239 @@ -1,1 +0,0 @@
240 240 -b
241 241 diff --git a/c.copy b/c.copy
242 242 --- /dev/null
243 243 +++ b/c.copy
244 244 @@ -0,0 +1,1 @@
245 245 +c
246 246 $ hg resolve -l
247 247 U a/a
248 248
249 249 $ hg shelve
250 250 abort: unshelve already in progress
251 251 (use 'hg unshelve --continue' or 'hg unshelve --abort')
252 252 [255]
253 253
254 254 abort the unshelve and be happy
255 255
256 256 $ hg status
257 257 M a/a
258 258 M b.rename/b
259 259 M c.copy
260 260 R b/b
261 261 ? a/a.orig
262 262 $ hg unshelve -a
263 263 rebase aborted
264 264 unshelve of 'default' aborted
265 265 $ hg heads -q
266 266 3:2e69b451d1ea
267 267 $ hg parents
268 268 changeset: 3:2e69b451d1ea
269 269 tag: tip
270 270 user: test
271 271 date: Thu Jan 01 00:00:00 1970 +0000
272 272 summary: second
273 273
274 274 $ hg resolve -l
275 275 $ hg status
276 276 A foo/foo
277 277 ? a/a.orig
278 278
279 279 try to continue with no unshelve underway
280 280
281 281 $ hg unshelve -c
282 282 abort: no unshelve operation underway
283 283 [255]
284 284 $ hg status
285 285 A foo/foo
286 286 ? a/a.orig
287 287
288 288 redo the unshelve to get a conflict
289 289
290 290 $ hg unshelve -q
291 291 warning: conflicts during merge.
292 292 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
293 293 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
294 294 [1]
295 295
296 296 attempt to continue
297 297
298 298 $ hg unshelve -c
299 299 abort: unresolved conflicts, can't continue
300 300 (see 'hg resolve', then 'hg unshelve --continue')
301 301 [255]
302 302
303 303 $ hg revert -r . a/a
304 304 $ hg resolve -m a/a
305 305
306 $ hg commit -m 'commit while unshelve in progress'
307 abort: unshelve already in progress
308 (use 'hg unshelve --continue' or 'hg unshelve --abort')
309 [255]
310
306 311 $ hg unshelve -c
307 312 unshelve of 'default' complete
308 313
309 314 ensure the repo is as we hope
310 315
311 316 $ hg parents
312 317 changeset: 3:2e69b451d1ea
313 318 tag: tip
314 319 user: test
315 320 date: Thu Jan 01 00:00:00 1970 +0000
316 321 summary: second
317 322
318 323 $ hg heads -q
319 324 3:2e69b451d1ea
320 325
321 326 $ hg status -C
322 327 A b.rename/b
323 328 b/b
324 329 A c.copy
325 330 c
326 331 A foo/foo
327 332 R b/b
328 333 ? a/a.orig
329 334
330 335 there should be no shelves left
331 336
332 337 $ hg shelve -l
333 338
334 339 #if execbit
335 340
336 341 ensure that metadata-only changes are shelved
337 342
338 343 $ chmod +x a/a
339 344 $ hg shelve -q -n execbit a/a
340 345 $ hg status a/a
341 346 $ hg unshelve -q execbit
342 347 $ hg status a/a
343 348 M a/a
344 349 $ hg revert a/a
345 350
346 351 #endif
347 352
348 353 #if symlink
349 354
350 355 $ rm a/a
351 356 $ ln -s foo a/a
352 357 $ hg shelve -q -n symlink a/a
353 358 $ hg status a/a
354 359 $ hg unshelve -q symlink
355 360 $ hg status a/a
356 361 M a/a
357 362 $ hg revert a/a
358 363
359 364 #endif
360 365
361 366 set up another conflict between a commit and a shelved change
362 367
363 368 $ hg revert -q -C -a
364 369 $ rm a/a.orig b.rename/b c.copy
365 370 $ echo a >> a/a
366 371 $ hg shelve -q
367 372 $ echo x >> a/a
368 373 $ hg ci -m 'create conflict'
369 374 $ hg add foo/foo
370 375
371 376 if we resolve a conflict while unshelving, the unshelve should succeed
372 377
373 378 $ HGMERGE=true hg unshelve
374 379 unshelving change 'default'
375 380 adding changesets
376 381 adding manifests
377 382 adding file changes
378 383 added 1 changesets with 1 changes to 6 files (+1 heads)
379 384 merging a/a
380 385 $ hg parents -q
381 386 4:33f7f61e6c5e
382 387 $ hg shelve -l
383 388 $ hg status
384 389 A foo/foo
385 390 $ cat a/a
386 391 a
387 392 c
388 393 x
389 394
390 395 test keep and cleanup
391 396
392 397 $ hg shelve
393 398 shelved as default
394 399 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
395 400 $ hg shelve --list
396 401 default (*) create conflict (glob)
397 402 $ hg unshelve --keep
398 403 unshelving change 'default'
399 404 adding changesets
400 405 adding manifests
401 406 adding file changes
402 407 added 1 changesets with 1 changes to 7 files
403 408 $ hg shelve --list
404 409 default (*) create conflict (glob)
405 410 $ hg shelve --cleanup
406 411 $ hg shelve --list
407 412
408 413 test bookmarks
409 414
410 415 $ hg bookmark test
411 416 $ hg bookmark
412 417 * test 4:33f7f61e6c5e
413 418 $ hg shelve
414 419 shelved as test
415 420 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
416 421 $ hg bookmark
417 422 * test 4:33f7f61e6c5e
418 423 $ hg unshelve
419 424 unshelving change 'test'
420 425 adding changesets
421 426 adding manifests
422 427 adding file changes
423 428 added 1 changesets with 1 changes to 7 files
424 429 $ hg bookmark
425 430 * test 4:33f7f61e6c5e
426 431
427 432 shelve should still work even if mq is disabled
428 433
429 434 $ hg --config extensions.mq=! shelve
430 435 shelved as test
431 436 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
432 437 $ hg --config extensions.mq=! shelve --list
433 438 test (1s ago) create conflict
434 439 $ hg --config extensions.mq=! unshelve
435 440 unshelving change 'test'
436 441 adding changesets
437 442 adding manifests
438 443 adding file changes
439 444 added 1 changesets with 1 changes to 7 files
440 445
441 446 shelve should leave dirstate clean (issue 4055)
442 447
443 448 $ cd ..
444 449 $ hg init shelverebase
445 450 $ cd shelverebase
446 451 $ printf 'x\ny\n' > x
447 452 $ echo z > z
448 453 $ hg commit -Aqm xy
449 454 $ echo z >> x
450 455 $ hg commit -Aqm z
451 456 $ hg up 0
452 457 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
453 458 $ printf 'a\nx\ny\nz\n' > x
454 459 $ hg commit -Aqm xyz
455 460 $ echo c >> z
456 461 $ hg shelve
457 462 shelved as default
458 463 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
459 464 $ hg rebase -d 1 --config extensions.rebase=
460 465 merging x
461 466 saved backup bundle to $TESTTMP/shelverebase/.hg/strip-backup/323bfa07f744-backup.hg (glob)
462 467 $ hg unshelve
463 468 unshelving change 'default'
464 469 adding changesets
465 470 adding manifests
466 471 adding file changes
467 472 added 2 changesets with 2 changes to 2 files (+1 heads)
468 473 $ hg status
469 474 M z
470 475
471 476 $ cd ..
472 477
473 478 shelve should only unshelve pending changes (issue 4068)
474 479
475 480 $ hg init onlypendingchanges
476 481 $ cd onlypendingchanges
477 482 $ touch a
478 483 $ hg ci -Aqm a
479 484 $ touch b
480 485 $ hg ci -Aqm b
481 486 $ hg up -q 0
482 487 $ touch c
483 488 $ hg ci -Aqm c
484 489
485 490 $ touch d
486 491 $ hg add d
487 492 $ hg shelve
488 493 shelved as default
489 494 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
490 495 $ hg up -q 1
491 496 $ hg unshelve
492 497 unshelving change 'default'
493 498 adding changesets
494 499 adding manifests
495 500 adding file changes
496 501 added 1 changesets with 1 changes to 3 files
497 502 $ hg status
498 503 A d
499 504
500 505 unshelve should work on an ancestor of the original commit
501 506
502 507 $ hg shelve
503 508 shelved as default
504 509 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
505 510 $ hg up 0
506 511 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
507 512 $ hg unshelve
508 513 unshelving change 'default'
509 514 adding changesets
510 515 adding manifests
511 516 adding file changes
512 517 added 1 changesets with 1 changes to 3 files
513 518 $ hg status
514 519 A d
515 520
516 521 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now