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