##// END OF EJS Templates
shelve: move commitfunc closer to use site...
Simon Farnsworth -
r27888:6c740218 default
parent child Browse files
Show More
@@ -1,833 +1,834 b''
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 import collections
25 25 import itertools
26 26 from mercurial.i18n import _
27 27 from mercurial.node import nullid, nullrev, bin, hex
28 28 from mercurial import changegroup, cmdutil, scmutil, phases, commands
29 29 from mercurial import error, hg, mdiff, merge, patch, repair, util
30 30 from mercurial import templatefilters, exchange, bundlerepo, bundle2
31 31 from mercurial import lock as lockmod
32 32 from hgext import rebase
33 33 import errno
34 34
35 35 cmdtable = {}
36 36 command = cmdutil.command(cmdtable)
37 37 # Note for extension authors: ONLY specify testedwith = 'internal' for
38 38 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
39 39 # be specifying the version(s) of Mercurial they are tested with, or
40 40 # leave the attribute unspecified.
41 41 testedwith = 'internal'
42 42
43 43 backupdir = 'shelve-backup'
44 44
45 45 class shelvedfile(object):
46 46 """Helper for the file storing a single shelve
47 47
48 48 Handles common functions on shelve files (.hg/.patch) using
49 49 the vfs layer"""
50 50 def __init__(self, repo, name, filetype=None):
51 51 self.repo = repo
52 52 self.name = name
53 53 self.vfs = scmutil.vfs(repo.join('shelved'))
54 54 self.backupvfs = scmutil.vfs(repo.join(backupdir))
55 55 self.ui = self.repo.ui
56 56 if filetype:
57 57 self.fname = name + '.' + filetype
58 58 else:
59 59 self.fname = name
60 60
61 61 def exists(self):
62 62 return self.vfs.exists(self.fname)
63 63
64 64 def filename(self):
65 65 return self.vfs.join(self.fname)
66 66
67 67 def backupfilename(self):
68 68 def gennames(base):
69 69 yield base
70 70 base, ext = base.rsplit('.', 1)
71 71 for i in itertools.count(1):
72 72 yield '%s-%d.%s' % (base, i, ext)
73 73
74 74 name = self.backupvfs.join(self.fname)
75 75 for n in gennames(name):
76 76 if not self.backupvfs.exists(n):
77 77 return n
78 78
79 79 def movetobackup(self):
80 80 if not self.backupvfs.isdir():
81 81 self.backupvfs.makedir()
82 82 util.rename(self.filename(), self.backupfilename())
83 83
84 84 def stat(self):
85 85 return self.vfs.stat(self.fname)
86 86
87 87 def opener(self, mode='rb'):
88 88 try:
89 89 return self.vfs(self.fname, mode)
90 90 except IOError as err:
91 91 if err.errno != errno.ENOENT:
92 92 raise
93 93 raise error.Abort(_("shelved change '%s' not found") % self.name)
94 94
95 95 def applybundle(self):
96 96 fp = self.opener()
97 97 try:
98 98 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
99 99 if not isinstance(gen, bundle2.unbundle20):
100 100 gen.apply(self.repo, 'unshelve',
101 101 'bundle:' + self.vfs.join(self.fname),
102 102 targetphase=phases.secret)
103 103 if isinstance(gen, bundle2.unbundle20):
104 104 bundle2.applybundle(self.repo, gen,
105 105 self.repo.currenttransaction(),
106 106 source='unshelve',
107 107 url='bundle:' + self.vfs.join(self.fname))
108 108 finally:
109 109 fp.close()
110 110
111 111 def bundlerepo(self):
112 112 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
113 113 self.vfs.join(self.fname))
114 114 def writebundle(self, bases, node):
115 115 btype = 'HG10BZ'
116 116 cgversion = '01'
117 117 compression = None
118 118 if 'generaldelta' in self.repo.requirements:
119 119 btype = 'HG20'
120 120 cgversion = '02'
121 121 compression = 'BZ'
122 122
123 123 cg = changegroup.changegroupsubset(self.repo, bases, [node], 'shelve',
124 124 version=cgversion)
125 125 changegroup.writebundle(self.ui, cg, self.fname, btype, self.vfs,
126 126 compression=compression)
127 127
128 128 class shelvedstate(object):
129 129 """Handle persistence during unshelving operations.
130 130
131 131 Handles saving and restoring a shelved state. Ensures that different
132 132 versions of a shelved state are possible and handles them appropriately.
133 133 """
134 134 _version = 1
135 135 _filename = 'shelvedstate'
136 136
137 137 @classmethod
138 138 def load(cls, repo):
139 139 fp = repo.vfs(cls._filename)
140 140 try:
141 141 version = int(fp.readline().strip())
142 142
143 143 if version != cls._version:
144 144 raise error.Abort(_('this version of shelve is incompatible '
145 145 'with the version used in this repo'))
146 146 name = fp.readline().strip()
147 147 wctx = fp.readline().strip()
148 148 pendingctx = fp.readline().strip()
149 149 parents = [bin(h) for h in fp.readline().split()]
150 150 stripnodes = [bin(h) for h in fp.readline().split()]
151 151 finally:
152 152 fp.close()
153 153
154 154 obj = cls()
155 155 obj.name = name
156 156 obj.wctx = repo[bin(wctx)]
157 157 obj.pendingctx = repo[bin(pendingctx)]
158 158 obj.parents = parents
159 159 obj.stripnodes = stripnodes
160 160
161 161 return obj
162 162
163 163 @classmethod
164 164 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
165 165 fp = repo.vfs(cls._filename, 'wb')
166 166 fp.write('%i\n' % cls._version)
167 167 fp.write('%s\n' % name)
168 168 fp.write('%s\n' % hex(originalwctx.node()))
169 169 fp.write('%s\n' % hex(pendingctx.node()))
170 170 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
171 171 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
172 172 fp.close()
173 173
174 174 @classmethod
175 175 def clear(cls, repo):
176 176 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
177 177
178 178 def cleanupoldbackups(repo):
179 179 vfs = scmutil.vfs(repo.join(backupdir))
180 180 maxbackups = repo.ui.configint('shelve', 'maxbackups', 10)
181 181 hgfiles = [f for f in vfs.listdir() if f.endswith('.hg')]
182 182 hgfiles = sorted([(vfs.stat(f).st_mtime, f) for f in hgfiles])
183 183 if 0 < maxbackups and maxbackups < len(hgfiles):
184 184 bordermtime = hgfiles[-maxbackups][0]
185 185 else:
186 186 bordermtime = None
187 187 for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
188 188 if mtime == bordermtime:
189 189 # keep it, because timestamp can't decide exact order of backups
190 190 continue
191 191 base = f[:-3]
192 192 for ext in 'hg patch'.split():
193 193 try:
194 194 vfs.unlink(base + '.' + ext)
195 195 except OSError as err:
196 196 if err.errno != errno.ENOENT:
197 197 raise
198 198
199 199 def _aborttransaction(repo):
200 200 '''Abort current transaction for shelve/unshelve, but keep dirstate
201 201 '''
202 202 backupname = 'dirstate.shelve'
203 203 dirstatebackup = None
204 204 try:
205 205 # create backup of (un)shelved dirstate, because aborting transaction
206 206 # should restore dirstate to one at the beginning of the
207 207 # transaction, which doesn't include the result of (un)shelving
208 208 fp = repo.vfs.open(backupname, "w")
209 209 dirstatebackup = backupname
210 210 # clearing _dirty/_dirtypl of dirstate by _writedirstate below
211 211 # is unintentional. but it doesn't cause problem in this case,
212 212 # because no code path refers them until transaction is aborted.
213 213 repo.dirstate._writedirstate(fp) # write in-memory changes forcibly
214 214
215 215 tr = repo.currenttransaction()
216 216 tr.abort()
217 217
218 218 # restore to backuped dirstate
219 219 repo.vfs.rename(dirstatebackup, 'dirstate')
220 220 dirstatebackup = None
221 221 finally:
222 222 if dirstatebackup:
223 223 repo.vfs.unlink(dirstatebackup)
224 224
225 225 def createcmd(ui, repo, pats, opts):
226 226 """subcommand that creates a new shelve"""
227 227 with repo.wlock():
228 228 cmdutil.checkunfinished(repo)
229 229 return _docreatecmd(ui, repo, pats, opts)
230 230
231 231 def _docreatecmd(ui, repo, pats, opts):
232 232 def mutableancestors(ctx):
233 233 """return all mutable ancestors for ctx (included)
234 234
235 235 Much faster than the revset ancestors(ctx) & draft()"""
236 236 seen = set([nullrev])
237 237 visit = collections.deque()
238 238 visit.append(ctx)
239 239 while visit:
240 240 ctx = visit.popleft()
241 241 yield ctx.node()
242 242 for parent in ctx.parents():
243 243 rev = parent.rev()
244 244 if rev not in seen:
245 245 seen.add(rev)
246 246 if parent.mutable():
247 247 visit.append(parent)
248 248
249 249 wctx = repo[None]
250 250 parents = wctx.parents()
251 251 if len(parents) > 1:
252 252 raise error.Abort(_('cannot shelve while merging'))
253 253 parent = parents[0]
254 254
255 255 # we never need the user, so we use a generic user for all shelve operations
256 256 user = 'shelve@localhost'
257 257 label = repo._activebookmark or parent.branch() or 'default'
258 258
259 259 # slashes aren't allowed in filenames, therefore we rename it
260 260 label = label.replace('/', '_')
261 261
262 262 def gennames():
263 263 yield label
264 264 for i in xrange(1, 100):
265 265 yield '%s-%02d' % (label, i)
266 266
267 def commitfunc(ui, repo, message, match, opts):
268 hasmq = util.safehasattr(repo, 'mq')
269 if hasmq:
270 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
271 backup = repo.ui.backupconfig('phases', 'new-commit')
272 try:
273 repo.ui. setconfig('phases', 'new-commit', phases.secret)
274 editor = cmdutil.getcommiteditor(editform='shelve.shelve', **opts)
275 return repo.commit(message, user, opts.get('date'), match,
276 editor=editor)
277 finally:
278 repo.ui.restoreconfig(backup)
279 if hasmq:
280 repo.mq.checkapplied = saved
281
282 267 if parent.node() != nullid:
283 268 desc = "changes to: %s" % parent.description().split('\n', 1)[0]
284 269 else:
285 270 desc = '(changes in empty repository)'
286 271
287 272 if not opts['message']:
288 273 opts['message'] = desc
289 274
290 275 name = opts['name']
291 276
292 277 lock = tr = None
293 278 try:
294 279 lock = repo.lock()
295 280
296 281 # use an uncommitted transaction to generate the bundle to avoid
297 282 # pull races. ensure we don't print the abort message to stderr.
298 283 tr = repo.transaction('commit', report=lambda x: None)
299 284
300 285 if name:
301 286 if shelvedfile(repo, name, 'hg').exists():
302 287 raise error.Abort(_("a shelved change named '%s' already exists"
303 288 ) % name)
304 289 else:
305 290 for n in gennames():
306 291 if not shelvedfile(repo, n, 'hg').exists():
307 292 name = n
308 293 break
309 294 else:
310 295 raise error.Abort(_("too many shelved changes named '%s'") %
311 296 label)
312 297
313 298 # ensure we are not creating a subdirectory or a hidden file
314 299 if '/' in name or '\\' in name:
315 300 raise error.Abort(_('shelved change names may not contain slashes'))
316 301 if name.startswith('.'):
317 302 raise error.Abort(_("shelved change names may not start with '.'"))
318 303 interactive = opts.get('interactive', False)
319 304
305 def commitfunc(ui, repo, message, match, opts):
306 hasmq = util.safehasattr(repo, 'mq')
307 if hasmq:
308 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
309 backup = repo.ui.backupconfig('phases', 'new-commit')
310 try:
311 repo.ui. setconfig('phases', 'new-commit', phases.secret)
312 editor = cmdutil.getcommiteditor(editform='shelve.shelve',
313 **opts)
314 return repo.commit(message, user, opts.get('date'), match,
315 editor=editor)
316 finally:
317 repo.ui.restoreconfig(backup)
318 if hasmq:
319 repo.mq.checkapplied = saved
320
320 321 def interactivecommitfunc(ui, repo, *pats, **opts):
321 322 match = scmutil.match(repo['.'], pats, {})
322 323 message = opts['message']
323 324 return commitfunc(ui, repo, message, match, opts)
324 325 if not interactive:
325 326 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
326 327 else:
327 328 node = cmdutil.dorecord(ui, repo, interactivecommitfunc, None,
328 329 False, cmdutil.recordfilter, *pats, **opts)
329 330 if not node:
330 331 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
331 332 if stat.deleted:
332 333 ui.status(_("nothing changed (%d missing files, see "
333 334 "'hg status')\n") % len(stat.deleted))
334 335 else:
335 336 ui.status(_("nothing changed\n"))
336 337 return 1
337 338
338 339 bases = list(mutableancestors(repo[node]))
339 340 shelvedfile(repo, name, 'hg').writebundle(bases, node)
340 341 cmdutil.export(repo, [node],
341 342 fp=shelvedfile(repo, name, 'patch').opener('wb'),
342 343 opts=mdiff.diffopts(git=True))
343 344
344 345
345 346 if ui.formatted():
346 347 desc = util.ellipsis(desc, ui.termwidth())
347 348 ui.status(_('shelved as %s\n') % name)
348 349 hg.update(repo, parent.node())
349 350
350 351 _aborttransaction(repo)
351 352 finally:
352 353 lockmod.release(tr, lock)
353 354
354 355 def cleanupcmd(ui, repo):
355 356 """subcommand that deletes all shelves"""
356 357
357 358 with repo.wlock():
358 359 for (name, _type) in repo.vfs.readdir('shelved'):
359 360 suffix = name.rsplit('.', 1)[-1]
360 361 if suffix in ('hg', 'patch'):
361 362 shelvedfile(repo, name).movetobackup()
362 363 cleanupoldbackups(repo)
363 364
364 365 def deletecmd(ui, repo, pats):
365 366 """subcommand that deletes a specific shelve"""
366 367 if not pats:
367 368 raise error.Abort(_('no shelved changes specified!'))
368 369 with repo.wlock():
369 370 try:
370 371 for name in pats:
371 372 for suffix in 'hg patch'.split():
372 373 shelvedfile(repo, name, suffix).movetobackup()
373 374 cleanupoldbackups(repo)
374 375 except OSError as err:
375 376 if err.errno != errno.ENOENT:
376 377 raise
377 378 raise error.Abort(_("shelved change '%s' not found") % name)
378 379
379 380 def listshelves(repo):
380 381 """return all shelves in repo as list of (time, filename)"""
381 382 try:
382 383 names = repo.vfs.readdir('shelved')
383 384 except OSError as err:
384 385 if err.errno != errno.ENOENT:
385 386 raise
386 387 return []
387 388 info = []
388 389 for (name, _type) in names:
389 390 pfx, sfx = name.rsplit('.', 1)
390 391 if not pfx or sfx != 'patch':
391 392 continue
392 393 st = shelvedfile(repo, name).stat()
393 394 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
394 395 return sorted(info, reverse=True)
395 396
396 397 def listcmd(ui, repo, pats, opts):
397 398 """subcommand that displays the list of shelves"""
398 399 pats = set(pats)
399 400 width = 80
400 401 if not ui.plain():
401 402 width = ui.termwidth()
402 403 namelabel = 'shelve.newest'
403 404 for mtime, name in listshelves(repo):
404 405 sname = util.split(name)[1]
405 406 if pats and sname not in pats:
406 407 continue
407 408 ui.write(sname, label=namelabel)
408 409 namelabel = 'shelve.name'
409 410 if ui.quiet:
410 411 ui.write('\n')
411 412 continue
412 413 ui.write(' ' * (16 - len(sname)))
413 414 used = 16
414 415 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
415 416 ui.write(age, label='shelve.age')
416 417 ui.write(' ' * (12 - len(age)))
417 418 used += 12
418 419 with open(name + '.patch', 'rb') as fp:
419 420 while True:
420 421 line = fp.readline()
421 422 if not line:
422 423 break
423 424 if not line.startswith('#'):
424 425 desc = line.rstrip()
425 426 if ui.formatted():
426 427 desc = util.ellipsis(desc, width - used)
427 428 ui.write(desc)
428 429 break
429 430 ui.write('\n')
430 431 if not (opts['patch'] or opts['stat']):
431 432 continue
432 433 difflines = fp.readlines()
433 434 if opts['patch']:
434 435 for chunk, label in patch.difflabel(iter, difflines):
435 436 ui.write(chunk, label=label)
436 437 if opts['stat']:
437 438 for chunk, label in patch.diffstatui(difflines, width=width,
438 439 git=True):
439 440 ui.write(chunk, label=label)
440 441
441 442 def singlepatchcmds(ui, repo, pats, opts, subcommand):
442 443 """subcommand that displays a single shelf"""
443 444 if len(pats) != 1:
444 445 raise error.Abort(_("--%s expects a single shelf") % subcommand)
445 446 shelfname = pats[0]
446 447
447 448 if not shelvedfile(repo, shelfname, 'patch').exists():
448 449 raise error.Abort(_("cannot find shelf %s") % shelfname)
449 450
450 451 listcmd(ui, repo, pats, opts)
451 452
452 453 def checkparents(repo, state):
453 454 """check parent while resuming an unshelve"""
454 455 if state.parents != repo.dirstate.parents():
455 456 raise error.Abort(_('working directory parents do not match unshelve '
456 457 'state'))
457 458
458 459 def pathtofiles(repo, files):
459 460 cwd = repo.getcwd()
460 461 return [repo.pathto(f, cwd) for f in files]
461 462
462 463 def unshelveabort(ui, repo, state, opts):
463 464 """subcommand that abort an in-progress unshelve"""
464 465 with repo.lock():
465 466 try:
466 467 checkparents(repo, state)
467 468
468 469 util.rename(repo.join('unshelverebasestate'),
469 470 repo.join('rebasestate'))
470 471 try:
471 472 rebase.rebase(ui, repo, **{
472 473 'abort' : True
473 474 })
474 475 except Exception:
475 476 util.rename(repo.join('rebasestate'),
476 477 repo.join('unshelverebasestate'))
477 478 raise
478 479
479 480 mergefiles(ui, repo, state.wctx, state.pendingctx)
480 481 repair.strip(ui, repo, state.stripnodes, backup=False,
481 482 topic='shelve')
482 483 finally:
483 484 shelvedstate.clear(repo)
484 485 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
485 486
486 487 def mergefiles(ui, repo, wctx, shelvectx):
487 488 """updates to wctx and merges the changes from shelvectx into the
488 489 dirstate."""
489 490 oldquiet = ui.quiet
490 491 try:
491 492 ui.quiet = True
492 493 hg.update(repo, wctx.node())
493 494 files = []
494 495 files.extend(shelvectx.files())
495 496 files.extend(shelvectx.parents()[0].files())
496 497
497 498 # revert will overwrite unknown files, so move them out of the way
498 499 for file in repo.status(unknown=True).unknown:
499 500 if file in files:
500 501 util.rename(file, scmutil.origpath(ui, repo, file))
501 502 ui.pushbuffer(True)
502 503 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
503 504 *pathtofiles(repo, files),
504 505 **{'no_backup': True})
505 506 ui.popbuffer()
506 507 finally:
507 508 ui.quiet = oldquiet
508 509
509 510 def unshelvecleanup(ui, repo, name, opts):
510 511 """remove related files after an unshelve"""
511 512 if not opts['keep']:
512 513 for filetype in 'hg patch'.split():
513 514 shelvedfile(repo, name, filetype).movetobackup()
514 515 cleanupoldbackups(repo)
515 516
516 517 def unshelvecontinue(ui, repo, state, opts):
517 518 """subcommand to continue an in-progress unshelve"""
518 519 # We're finishing off a merge. First parent is our original
519 520 # parent, second is the temporary "fake" commit we're unshelving.
520 521 with repo.lock():
521 522 checkparents(repo, state)
522 523 ms = merge.mergestate.read(repo)
523 524 if [f for f in ms if ms[f] == 'u']:
524 525 raise error.Abort(
525 526 _("unresolved conflicts, can't continue"),
526 527 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
527 528
528 529 util.rename(repo.join('unshelverebasestate'),
529 530 repo.join('rebasestate'))
530 531 try:
531 532 rebase.rebase(ui, repo, **{
532 533 'continue' : True
533 534 })
534 535 except Exception:
535 536 util.rename(repo.join('rebasestate'),
536 537 repo.join('unshelverebasestate'))
537 538 raise
538 539
539 540 shelvectx = repo['tip']
540 541 if not shelvectx in state.pendingctx.children():
541 542 # rebase was a no-op, so it produced no child commit
542 543 shelvectx = state.pendingctx
543 544 else:
544 545 # only strip the shelvectx if the rebase produced it
545 546 state.stripnodes.append(shelvectx.node())
546 547
547 548 mergefiles(ui, repo, state.wctx, shelvectx)
548 549
549 550 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
550 551 shelvedstate.clear(repo)
551 552 unshelvecleanup(ui, repo, state.name, opts)
552 553 ui.status(_("unshelve of '%s' complete\n") % state.name)
553 554
554 555 @command('unshelve',
555 556 [('a', 'abort', None,
556 557 _('abort an incomplete unshelve operation')),
557 558 ('c', 'continue', None,
558 559 _('continue an incomplete unshelve operation')),
559 560 ('k', 'keep', None,
560 561 _('keep shelve after unshelving')),
561 562 ('t', 'tool', '', _('specify merge tool')),
562 563 ('', 'date', '',
563 564 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
564 565 _('hg unshelve [SHELVED]'))
565 566 def unshelve(ui, repo, *shelved, **opts):
566 567 """restore a shelved change to the working directory
567 568
568 569 This command accepts an optional name of a shelved change to
569 570 restore. If none is given, the most recent shelved change is used.
570 571
571 572 If a shelved change is applied successfully, the bundle that
572 573 contains the shelved changes is moved to a backup location
573 574 (.hg/shelve-backup).
574 575
575 576 Since you can restore a shelved change on top of an arbitrary
576 577 commit, it is possible that unshelving will result in a conflict
577 578 between your changes and the commits you are unshelving onto. If
578 579 this occurs, you must resolve the conflict, then use
579 580 ``--continue`` to complete the unshelve operation. (The bundle
580 581 will not be moved until you successfully complete the unshelve.)
581 582
582 583 (Alternatively, you can use ``--abort`` to abandon an unshelve
583 584 that causes a conflict. This reverts the unshelved changes, and
584 585 leaves the bundle in place.)
585 586
586 587 After a successful unshelve, the shelved changes are stored in a
587 588 backup directory. Only the N most recent backups are kept. N
588 589 defaults to 10 but can be overridden using the ``shelve.maxbackups``
589 590 configuration option.
590 591
591 592 .. container:: verbose
592 593
593 594 Timestamp in seconds is used to decide order of backups. More
594 595 than ``maxbackups`` backups are kept, if same timestamp
595 596 prevents from deciding exact order of them, for safety.
596 597 """
597 598 with repo.wlock():
598 599 return _dounshelve(ui, repo, *shelved, **opts)
599 600
600 601 def _dounshelve(ui, repo, *shelved, **opts):
601 602 abortf = opts['abort']
602 603 continuef = opts['continue']
603 604 if not abortf and not continuef:
604 605 cmdutil.checkunfinished(repo)
605 606
606 607 if abortf or continuef:
607 608 if abortf and continuef:
608 609 raise error.Abort(_('cannot use both abort and continue'))
609 610 if shelved:
610 611 raise error.Abort(_('cannot combine abort/continue with '
611 612 'naming a shelved change'))
612 613 if abortf and opts.get('tool', False):
613 614 ui.warn(_('tool option will be ignored\n'))
614 615
615 616 try:
616 617 state = shelvedstate.load(repo)
617 618 except IOError as err:
618 619 if err.errno != errno.ENOENT:
619 620 raise
620 621 raise error.Abort(_('no unshelve operation underway'))
621 622
622 623 if abortf:
623 624 return unshelveabort(ui, repo, state, opts)
624 625 elif continuef:
625 626 return unshelvecontinue(ui, repo, state, opts)
626 627 elif len(shelved) > 1:
627 628 raise error.Abort(_('can only unshelve one change at a time'))
628 629 elif not shelved:
629 630 shelved = listshelves(repo)
630 631 if not shelved:
631 632 raise error.Abort(_('no shelved changes to apply!'))
632 633 basename = util.split(shelved[0][1])[1]
633 634 ui.status(_("unshelving change '%s'\n") % basename)
634 635 else:
635 636 basename = shelved[0]
636 637
637 638 if not shelvedfile(repo, basename, 'patch').exists():
638 639 raise error.Abort(_("shelved change '%s' not found") % basename)
639 640
640 641 oldquiet = ui.quiet
641 642 lock = tr = None
642 643 forcemerge = ui.backupconfig('ui', 'forcemerge')
643 644 try:
644 645 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'unshelve')
645 646 lock = repo.lock()
646 647
647 648 tr = repo.transaction('unshelve', report=lambda x: None)
648 649 oldtiprev = len(repo)
649 650
650 651 pctx = repo['.']
651 652 tmpwctx = pctx
652 653 # The goal is to have a commit structure like so:
653 654 # ...-> pctx -> tmpwctx -> shelvectx
654 655 # where tmpwctx is an optional commit with the user's pending changes
655 656 # and shelvectx is the unshelved changes. Then we merge it all down
656 657 # to the original pctx.
657 658
658 659 # Store pending changes in a commit
659 660 s = repo.status()
660 661 if s.modified or s.added or s.removed or s.deleted:
661 662 ui.status(_("temporarily committing pending changes "
662 663 "(restore with 'hg unshelve --abort')\n"))
663 664 def commitfunc(ui, repo, message, match, opts):
664 665 hasmq = util.safehasattr(repo, 'mq')
665 666 if hasmq:
666 667 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
667 668
668 669 backup = repo.ui.backupconfig('phases', 'new-commit')
669 670 try:
670 671 repo.ui.setconfig('phases', 'new-commit', phases.secret)
671 672 return repo.commit(message, 'shelve@localhost',
672 673 opts.get('date'), match)
673 674 finally:
674 675 repo.ui.restoreconfig(backup)
675 676 if hasmq:
676 677 repo.mq.checkapplied = saved
677 678
678 679 tempopts = {}
679 680 tempopts['message'] = "pending changes temporary commit"
680 681 tempopts['date'] = opts.get('date')
681 682 ui.quiet = True
682 683 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
683 684 tmpwctx = repo[node]
684 685
685 686 ui.quiet = True
686 687 shelvedfile(repo, basename, 'hg').applybundle()
687 688
688 689 ui.quiet = oldquiet
689 690
690 691 shelvectx = repo['tip']
691 692
692 693 # If the shelve is not immediately on top of the commit
693 694 # we'll be merging with, rebase it to be on top.
694 695 if tmpwctx.node() != shelvectx.parents()[0].node():
695 696 ui.status(_('rebasing shelved changes\n'))
696 697 try:
697 698 rebase.rebase(ui, repo, **{
698 699 'rev' : [shelvectx.rev()],
699 700 'dest' : str(tmpwctx.rev()),
700 701 'keep' : True,
701 702 'tool' : opts.get('tool', ''),
702 703 })
703 704 except error.InterventionRequired:
704 705 tr.close()
705 706
706 707 stripnodes = [repo.changelog.node(rev)
707 708 for rev in xrange(oldtiprev, len(repo))]
708 709 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
709 710
710 711 util.rename(repo.join('rebasestate'),
711 712 repo.join('unshelverebasestate'))
712 713 raise error.InterventionRequired(
713 714 _("unresolved conflicts (see 'hg resolve', then "
714 715 "'hg unshelve --continue')"))
715 716
716 717 # refresh ctx after rebase completes
717 718 shelvectx = repo['tip']
718 719
719 720 if not shelvectx in tmpwctx.children():
720 721 # rebase was a no-op, so it produced no child commit
721 722 shelvectx = tmpwctx
722 723
723 724 mergefiles(ui, repo, pctx, shelvectx)
724 725 shelvedstate.clear(repo)
725 726
726 727 # The transaction aborting will strip all the commits for us,
727 728 # but it doesn't update the inmemory structures, so addchangegroup
728 729 # hooks still fire and try to operate on the missing commits.
729 730 # Clean up manually to prevent this.
730 731 repo.unfiltered().changelog.strip(oldtiprev, tr)
731 732
732 733 unshelvecleanup(ui, repo, basename, opts)
733 734
734 735 _aborttransaction(repo)
735 736 finally:
736 737 ui.quiet = oldquiet
737 738 if tr:
738 739 tr.release()
739 740 lockmod.release(lock)
740 741 ui.restoreconfig(forcemerge)
741 742
742 743 @command('shelve',
743 744 [('A', 'addremove', None,
744 745 _('mark new/missing files as added/removed before shelving')),
745 746 ('', 'cleanup', None,
746 747 _('delete all shelved changes')),
747 748 ('', 'date', '',
748 749 _('shelve with the specified commit date'), _('DATE')),
749 750 ('d', 'delete', None,
750 751 _('delete the named shelved change(s)')),
751 752 ('e', 'edit', False,
752 753 _('invoke editor on commit messages')),
753 754 ('l', 'list', None,
754 755 _('list current shelves')),
755 756 ('m', 'message', '',
756 757 _('use text as shelve message'), _('TEXT')),
757 758 ('n', 'name', '',
758 759 _('use the given name for the shelved commit'), _('NAME')),
759 760 ('p', 'patch', None,
760 761 _('show patch')),
761 762 ('i', 'interactive', None,
762 763 _('interactive mode, only works while creating a shelve')),
763 764 ('', 'stat', None,
764 765 _('output diffstat-style summary of changes'))] + commands.walkopts,
765 766 _('hg shelve [OPTION]... [FILE]...'))
766 767 def shelvecmd(ui, repo, *pats, **opts):
767 768 '''save and set aside changes from the working directory
768 769
769 770 Shelving takes files that "hg status" reports as not clean, saves
770 771 the modifications to a bundle (a shelved change), and reverts the
771 772 files so that their state in the working directory becomes clean.
772 773
773 774 To restore these changes to the working directory, using "hg
774 775 unshelve"; this will work even if you switch to a different
775 776 commit.
776 777
777 778 When no files are specified, "hg shelve" saves all not-clean
778 779 files. If specific files or directories are named, only changes to
779 780 those files are shelved.
780 781
781 782 Each shelved change has a name that makes it easier to find later.
782 783 The name of a shelved change defaults to being based on the active
783 784 bookmark, or if there is no active bookmark, the current named
784 785 branch. To specify a different name, use ``--name``.
785 786
786 787 To see a list of existing shelved changes, use the ``--list``
787 788 option. For each shelved change, this will print its name, age,
788 789 and description; use ``--patch`` or ``--stat`` for more details.
789 790
790 791 To delete specific shelved changes, use ``--delete``. To delete
791 792 all shelved changes, use ``--cleanup``.
792 793 '''
793 794 allowables = [
794 795 ('addremove', set(['create'])), # 'create' is pseudo action
795 796 ('cleanup', set(['cleanup'])),
796 797 # ('date', set(['create'])), # ignored for passing '--date "0 0"' in tests
797 798 ('delete', set(['delete'])),
798 799 ('edit', set(['create'])),
799 800 ('list', set(['list'])),
800 801 ('message', set(['create'])),
801 802 ('name', set(['create'])),
802 803 ('patch', set(['patch', 'list'])),
803 804 ('stat', set(['stat', 'list'])),
804 805 ]
805 806 def checkopt(opt):
806 807 if opts[opt]:
807 808 for i, allowable in allowables:
808 809 if opts[i] and opt not in allowable:
809 810 raise error.Abort(_("options '--%s' and '--%s' may not be "
810 811 "used together") % (opt, i))
811 812 return True
812 813 if checkopt('cleanup'):
813 814 if pats:
814 815 raise error.Abort(_("cannot specify names when using '--cleanup'"))
815 816 return cleanupcmd(ui, repo)
816 817 elif checkopt('delete'):
817 818 return deletecmd(ui, repo, pats)
818 819 elif checkopt('list'):
819 820 return listcmd(ui, repo, pats, opts)
820 821 elif checkopt('patch'):
821 822 return singlepatchcmds(ui, repo, pats, opts, subcommand='patch')
822 823 elif checkopt('stat'):
823 824 return singlepatchcmds(ui, repo, pats, opts, subcommand='stat')
824 825 else:
825 826 return createcmd(ui, repo, pats, opts)
826 827
827 828 def extsetup(ui):
828 829 cmdutil.unfinishedstates.append(
829 830 [shelvedstate._filename, False, False,
830 831 _('unshelve already in progress'),
831 832 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
832 833 cmdutil.afterresolvedstates.append(
833 834 [shelvedstate._filename, _('hg unshelve --continue')])
General Comments 0
You need to be logged in to leave comments. Login now