##// END OF EJS Templates
bundle2: use "else" instead of checking condition again
Martin von Zweigbergk -
r32881:f65786a0 default
parent child Browse files
Show More
@@ -1,1676 +1,1676 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
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 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 #
43 43
44 44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 45 for each revision in your history. For example, if you had meant to add gamma
46 46 before beta, and then wanted to add delta in the same revision as beta, you
47 47 would reorganize the file to look like this::
48 48
49 49 pick 030b686bedc4 Add gamma
50 50 pick c561b4e977df Add beta
51 51 fold 7c2fd3b9020c Add delta
52 52
53 53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 54 #
55 55 # Commits are listed from least to most recent
56 56 #
57 57 # Commands:
58 58 # p, pick = use commit
59 59 # e, edit = use commit, but stop for amending
60 60 # f, fold = use commit, but combine it with the one above
61 61 # r, roll = like fold, but discard this commit's description and date
62 62 # d, drop = remove commit from history
63 63 # m, mess = edit commit message without changing commit content
64 64 #
65 65
66 66 At which point you close the editor and ``histedit`` starts working. When you
67 67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 68 those revisions together, offering you a chance to clean up the commit message::
69 69
70 70 Add beta
71 71 ***
72 72 Add delta
73 73
74 74 Edit the commit message to your liking, then close the editor. The date used
75 75 for the commit will be the later of the two commits' dates. For this example,
76 76 let's assume that the commit message was changed to ``Add beta and delta.``
77 77 After histedit has run and had a chance to remove any old or temporary
78 78 revisions it needed, the history looks like this::
79 79
80 80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 81 | Add beta and delta.
82 82 |
83 83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 84 | Add gamma
85 85 |
86 86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 87 Add alpha
88 88
89 89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 90 ones) until after it has completed all the editing operations, so it will
91 91 probably perform several strip operations when it's done. For the above example,
92 92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 93 so you might need to be a little patient. You can choose to keep the original
94 94 revisions by passing the ``--keep`` flag.
95 95
96 96 The ``edit`` operation will drop you back to a command prompt,
97 97 allowing you to edit files freely, or even use ``hg record`` to commit
98 98 some changes as a separate commit. When you're done, any remaining
99 99 uncommitted changes will be committed as well. When done, run ``hg
100 100 histedit --continue`` to finish this step. If there are uncommitted
101 101 changes, you'll be prompted for a new commit message, but the default
102 102 commit message will be the original message for the ``edit`` ed
103 103 revision, and the date of the original commit will be preserved.
104 104
105 105 The ``message`` operation will give you a chance to revise a commit
106 106 message without changing the contents. It's a shortcut for doing
107 107 ``edit`` immediately followed by `hg histedit --continue``.
108 108
109 109 If ``histedit`` encounters a conflict when moving a revision (while
110 110 handling ``pick`` or ``fold``), it'll stop in a similar manner to
111 111 ``edit`` with the difference that it won't prompt you for a commit
112 112 message when done. If you decide at this point that you don't like how
113 113 much work it will be to rearrange history, or that you made a mistake,
114 114 you can use ``hg histedit --abort`` to abandon the new changes you
115 115 have made and return to the state before you attempted to edit your
116 116 history.
117 117
118 118 If we clone the histedit-ed example repository above and add four more
119 119 changes, such that we have the following history::
120 120
121 121 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
122 122 | Add theta
123 123 |
124 124 o 5 140988835471 2009-04-27 18:04 -0500 stefan
125 125 | Add eta
126 126 |
127 127 o 4 122930637314 2009-04-27 18:04 -0500 stefan
128 128 | Add zeta
129 129 |
130 130 o 3 836302820282 2009-04-27 18:04 -0500 stefan
131 131 | Add epsilon
132 132 |
133 133 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
134 134 | Add beta and delta.
135 135 |
136 136 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
137 137 | Add gamma
138 138 |
139 139 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
140 140 Add alpha
141 141
142 142 If you run ``hg histedit --outgoing`` on the clone then it is the same
143 143 as running ``hg histedit 836302820282``. If you need plan to push to a
144 144 repository that Mercurial does not detect to be related to the source
145 145 repo, you can add a ``--force`` option.
146 146
147 147 Config
148 148 ------
149 149
150 150 Histedit rule lines are truncated to 80 characters by default. You
151 151 can customize this behavior by setting a different length in your
152 152 configuration file::
153 153
154 154 [histedit]
155 155 linelen = 120 # truncate rule lines at 120 characters
156 156
157 157 ``hg histedit`` attempts to automatically choose an appropriate base
158 158 revision to use. To change which base revision is used, define a
159 159 revset in your configuration file::
160 160
161 161 [histedit]
162 162 defaultrev = only(.) & draft()
163 163
164 164 By default each edited revision needs to be present in histedit commands.
165 165 To remove revision you need to use ``drop`` operation. You can configure
166 166 the drop to be implicit for missing commits by adding::
167 167
168 168 [histedit]
169 169 dropmissing = True
170 170
171 171 By default, histedit will close the transaction after each action. For
172 172 performance purposes, you can configure histedit to use a single transaction
173 173 across the entire histedit. WARNING: This setting introduces a significant risk
174 174 of losing the work you've done in a histedit if the histedit aborts
175 175 unexpectedly::
176 176
177 177 [histedit]
178 178 singletransaction = True
179 179
180 180 """
181 181
182 182 from __future__ import absolute_import
183 183
184 184 import errno
185 185 import os
186 186
187 187 from mercurial.i18n import _
188 188 from mercurial import (
189 189 bundle2,
190 190 cmdutil,
191 191 context,
192 192 copies,
193 193 destutil,
194 194 discovery,
195 195 error,
196 196 exchange,
197 197 extensions,
198 198 hg,
199 199 lock,
200 200 merge as mergemod,
201 201 mergeutil,
202 202 node,
203 203 obsolete,
204 204 registrar,
205 205 repair,
206 206 scmutil,
207 207 util,
208 208 )
209 209
210 210 pickle = util.pickle
211 211 release = lock.release
212 212 cmdtable = {}
213 213 command = registrar.command(cmdtable)
214 214
215 215 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
216 216 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
217 217 # be specifying the version(s) of Mercurial they are tested with, or
218 218 # leave the attribute unspecified.
219 219 testedwith = 'ships-with-hg-core'
220 220
221 221 actiontable = {}
222 222 primaryactions = set()
223 223 secondaryactions = set()
224 224 tertiaryactions = set()
225 225 internalactions = set()
226 226
227 227 def geteditcomment(ui, first, last):
228 228 """ construct the editor comment
229 229 The comment includes::
230 230 - an intro
231 231 - sorted primary commands
232 232 - sorted short commands
233 233 - sorted long commands
234 234 - additional hints
235 235
236 236 Commands are only included once.
237 237 """
238 238 intro = _("""Edit history between %s and %s
239 239
240 240 Commits are listed from least to most recent
241 241
242 242 You can reorder changesets by reordering the lines
243 243
244 244 Commands:
245 245 """)
246 246 actions = []
247 247 def addverb(v):
248 248 a = actiontable[v]
249 249 lines = a.message.split("\n")
250 250 if len(a.verbs):
251 251 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
252 252 actions.append(" %s = %s" % (v, lines[0]))
253 253 actions.extend([' %s' for l in lines[1:]])
254 254
255 255 for v in (
256 256 sorted(primaryactions) +
257 257 sorted(secondaryactions) +
258 258 sorted(tertiaryactions)
259 259 ):
260 260 addverb(v)
261 261 actions.append('')
262 262
263 263 hints = []
264 264 if ui.configbool('histedit', 'dropmissing'):
265 265 hints.append("Deleting a changeset from the list "
266 266 "will DISCARD it from the edited history!")
267 267
268 268 lines = (intro % (first, last)).split('\n') + actions + hints
269 269
270 270 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
271 271
272 272 class histeditstate(object):
273 273 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
274 274 topmost=None, replacements=None, lock=None, wlock=None):
275 275 self.repo = repo
276 276 self.actions = actions
277 277 self.keep = keep
278 278 self.topmost = topmost
279 279 self.parentctxnode = parentctxnode
280 280 self.lock = lock
281 281 self.wlock = wlock
282 282 self.backupfile = None
283 283 self.tr = None
284 284 if replacements is None:
285 285 self.replacements = []
286 286 else:
287 287 self.replacements = replacements
288 288
289 289 def read(self):
290 290 """Load histedit state from disk and set fields appropriately."""
291 291 try:
292 292 state = self.repo.vfs.read('histedit-state')
293 293 except IOError as err:
294 294 if err.errno != errno.ENOENT:
295 295 raise
296 296 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
297 297
298 298 if state.startswith('v1\n'):
299 299 data = self._load()
300 300 parentctxnode, rules, keep, topmost, replacements, backupfile = data
301 301 else:
302 302 data = pickle.loads(state)
303 303 parentctxnode, rules, keep, topmost, replacements = data
304 304 backupfile = None
305 305
306 306 self.parentctxnode = parentctxnode
307 307 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
308 308 actions = parserules(rules, self)
309 309 self.actions = actions
310 310 self.keep = keep
311 311 self.topmost = topmost
312 312 self.replacements = replacements
313 313 self.backupfile = backupfile
314 314
315 315 def write(self, tr=None):
316 316 if tr:
317 317 tr.addfilegenerator('histedit-state', ('histedit-state',),
318 318 self._write, location='plain')
319 319 else:
320 320 with self.repo.vfs("histedit-state", "w") as f:
321 321 self._write(f)
322 322
323 323 def _write(self, fp):
324 324 fp.write('v1\n')
325 325 fp.write('%s\n' % node.hex(self.parentctxnode))
326 326 fp.write('%s\n' % node.hex(self.topmost))
327 327 fp.write('%s\n' % self.keep)
328 328 fp.write('%d\n' % len(self.actions))
329 329 for action in self.actions:
330 330 fp.write('%s\n' % action.tostate())
331 331 fp.write('%d\n' % len(self.replacements))
332 332 for replacement in self.replacements:
333 333 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
334 334 for r in replacement[1])))
335 335 backupfile = self.backupfile
336 336 if not backupfile:
337 337 backupfile = ''
338 338 fp.write('%s\n' % backupfile)
339 339
340 340 def _load(self):
341 341 fp = self.repo.vfs('histedit-state', 'r')
342 342 lines = [l[:-1] for l in fp.readlines()]
343 343
344 344 index = 0
345 345 lines[index] # version number
346 346 index += 1
347 347
348 348 parentctxnode = node.bin(lines[index])
349 349 index += 1
350 350
351 351 topmost = node.bin(lines[index])
352 352 index += 1
353 353
354 354 keep = lines[index] == 'True'
355 355 index += 1
356 356
357 357 # Rules
358 358 rules = []
359 359 rulelen = int(lines[index])
360 360 index += 1
361 361 for i in xrange(rulelen):
362 362 ruleaction = lines[index]
363 363 index += 1
364 364 rule = lines[index]
365 365 index += 1
366 366 rules.append((ruleaction, rule))
367 367
368 368 # Replacements
369 369 replacements = []
370 370 replacementlen = int(lines[index])
371 371 index += 1
372 372 for i in xrange(replacementlen):
373 373 replacement = lines[index]
374 374 original = node.bin(replacement[:40])
375 375 succ = [node.bin(replacement[i:i + 40]) for i in
376 376 range(40, len(replacement), 40)]
377 377 replacements.append((original, succ))
378 378 index += 1
379 379
380 380 backupfile = lines[index]
381 381 index += 1
382 382
383 383 fp.close()
384 384
385 385 return parentctxnode, rules, keep, topmost, replacements, backupfile
386 386
387 387 def clear(self):
388 388 if self.inprogress():
389 389 self.repo.vfs.unlink('histedit-state')
390 390
391 391 def inprogress(self):
392 392 return self.repo.vfs.exists('histedit-state')
393 393
394 394
395 395 class histeditaction(object):
396 396 def __init__(self, state, node):
397 397 self.state = state
398 398 self.repo = state.repo
399 399 self.node = node
400 400
401 401 @classmethod
402 402 def fromrule(cls, state, rule):
403 403 """Parses the given rule, returning an instance of the histeditaction.
404 404 """
405 405 rulehash = rule.strip().split(' ', 1)[0]
406 406 try:
407 407 rev = node.bin(rulehash)
408 408 except TypeError:
409 409 raise error.ParseError("invalid changeset %s" % rulehash)
410 410 return cls(state, rev)
411 411
412 412 def verify(self, prev, expected, seen):
413 413 """ Verifies semantic correctness of the rule"""
414 414 repo = self.repo
415 415 ha = node.hex(self.node)
416 416 try:
417 417 self.node = repo[ha].node()
418 418 except error.RepoError:
419 419 raise error.ParseError(_('unknown changeset %s listed')
420 420 % ha[:12])
421 421 if self.node is not None:
422 422 self._verifynodeconstraints(prev, expected, seen)
423 423
424 424 def _verifynodeconstraints(self, prev, expected, seen):
425 425 # by default command need a node in the edited list
426 426 if self.node not in expected:
427 427 raise error.ParseError(_('%s "%s" changeset was not a candidate')
428 428 % (self.verb, node.short(self.node)),
429 429 hint=_('only use listed changesets'))
430 430 # and only one command per node
431 431 if self.node in seen:
432 432 raise error.ParseError(_('duplicated command for changeset %s') %
433 433 node.short(self.node))
434 434
435 435 def torule(self):
436 436 """build a histedit rule line for an action
437 437
438 438 by default lines are in the form:
439 439 <hash> <rev> <summary>
440 440 """
441 441 ctx = self.repo[self.node]
442 442 summary = _getsummary(ctx)
443 443 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
444 444 # trim to 75 columns by default so it's not stupidly wide in my editor
445 445 # (the 5 more are left for verb)
446 446 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
447 447 maxlen = max(maxlen, 22) # avoid truncating hash
448 448 return util.ellipsis(line, maxlen)
449 449
450 450 def tostate(self):
451 451 """Print an action in format used by histedit state files
452 452 (the first line is a verb, the remainder is the second)
453 453 """
454 454 return "%s\n%s" % (self.verb, node.hex(self.node))
455 455
456 456 def run(self):
457 457 """Runs the action. The default behavior is simply apply the action's
458 458 rulectx onto the current parentctx."""
459 459 self.applychange()
460 460 self.continuedirty()
461 461 return self.continueclean()
462 462
463 463 def applychange(self):
464 464 """Applies the changes from this action's rulectx onto the current
465 465 parentctx, but does not commit them."""
466 466 repo = self.repo
467 467 rulectx = repo[self.node]
468 468 repo.ui.pushbuffer(error=True, labeled=True)
469 469 hg.update(repo, self.state.parentctxnode, quietempty=True)
470 470 stats = applychanges(repo.ui, repo, rulectx, {})
471 471 if stats and stats[3] > 0:
472 472 buf = repo.ui.popbuffer()
473 473 repo.ui.write(*buf)
474 474 raise error.InterventionRequired(
475 475 _('Fix up the change (%s %s)') %
476 476 (self.verb, node.short(self.node)),
477 477 hint=_('hg histedit --continue to resume'))
478 478 else:
479 479 repo.ui.popbuffer()
480 480
481 481 def continuedirty(self):
482 482 """Continues the action when changes have been applied to the working
483 483 copy. The default behavior is to commit the dirty changes."""
484 484 repo = self.repo
485 485 rulectx = repo[self.node]
486 486
487 487 editor = self.commiteditor()
488 488 commit = commitfuncfor(repo, rulectx)
489 489
490 490 commit(text=rulectx.description(), user=rulectx.user(),
491 491 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
492 492
493 493 def commiteditor(self):
494 494 """The editor to be used to edit the commit message."""
495 495 return False
496 496
497 497 def continueclean(self):
498 498 """Continues the action when the working copy is clean. The default
499 499 behavior is to accept the current commit as the new version of the
500 500 rulectx."""
501 501 ctx = self.repo['.']
502 502 if ctx.node() == self.state.parentctxnode:
503 503 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
504 504 node.short(self.node))
505 505 return ctx, [(self.node, tuple())]
506 506 if ctx.node() == self.node:
507 507 # Nothing changed
508 508 return ctx, []
509 509 return ctx, [(self.node, (ctx.node(),))]
510 510
511 511 def commitfuncfor(repo, src):
512 512 """Build a commit function for the replacement of <src>
513 513
514 514 This function ensure we apply the same treatment to all changesets.
515 515
516 516 - Add a 'histedit_source' entry in extra.
517 517
518 518 Note that fold has its own separated logic because its handling is a bit
519 519 different and not easily factored out of the fold method.
520 520 """
521 521 phasemin = src.phase()
522 522 def commitfunc(**kwargs):
523 523 overrides = {('phases', 'new-commit'): phasemin}
524 524 with repo.ui.configoverride(overrides, 'histedit'):
525 525 extra = kwargs.get('extra', {}).copy()
526 526 extra['histedit_source'] = src.hex()
527 527 kwargs['extra'] = extra
528 528 return repo.commit(**kwargs)
529 529 return commitfunc
530 530
531 531 def applychanges(ui, repo, ctx, opts):
532 532 """Merge changeset from ctx (only) in the current working directory"""
533 533 wcpar = repo.dirstate.parents()[0]
534 534 if ctx.p1().node() == wcpar:
535 535 # edits are "in place" we do not need to make any merge,
536 536 # just applies changes on parent for editing
537 537 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
538 538 stats = None
539 539 else:
540 540 try:
541 541 # ui.forcemerge is an internal variable, do not document
542 542 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
543 543 'histedit')
544 544 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
545 545 finally:
546 546 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
547 547 return stats
548 548
549 549 def collapse(repo, first, last, commitopts, skipprompt=False):
550 550 """collapse the set of revisions from first to last as new one.
551 551
552 552 Expected commit options are:
553 553 - message
554 554 - date
555 555 - username
556 556 Commit message is edited in all cases.
557 557
558 558 This function works in memory."""
559 559 ctxs = list(repo.set('%d::%d', first, last))
560 560 if not ctxs:
561 561 return None
562 562 for c in ctxs:
563 563 if not c.mutable():
564 564 raise error.ParseError(
565 565 _("cannot fold into public change %s") % node.short(c.node()))
566 566 base = first.parents()[0]
567 567
568 568 # commit a new version of the old changeset, including the update
569 569 # collect all files which might be affected
570 570 files = set()
571 571 for ctx in ctxs:
572 572 files.update(ctx.files())
573 573
574 574 # Recompute copies (avoid recording a -> b -> a)
575 575 copied = copies.pathcopies(base, last)
576 576
577 577 # prune files which were reverted by the updates
578 578 files = [f for f in files if not cmdutil.samefile(f, last, base)]
579 579 # commit version of these files as defined by head
580 580 headmf = last.manifest()
581 581 def filectxfn(repo, ctx, path):
582 582 if path in headmf:
583 583 fctx = last[path]
584 584 flags = fctx.flags()
585 585 mctx = context.memfilectx(repo,
586 586 fctx.path(), fctx.data(),
587 587 islink='l' in flags,
588 588 isexec='x' in flags,
589 589 copied=copied.get(path))
590 590 return mctx
591 591 return None
592 592
593 593 if commitopts.get('message'):
594 594 message = commitopts['message']
595 595 else:
596 596 message = first.description()
597 597 user = commitopts.get('user')
598 598 date = commitopts.get('date')
599 599 extra = commitopts.get('extra')
600 600
601 601 parents = (first.p1().node(), first.p2().node())
602 602 editor = None
603 603 if not skipprompt:
604 604 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
605 605 new = context.memctx(repo,
606 606 parents=parents,
607 607 text=message,
608 608 files=files,
609 609 filectxfn=filectxfn,
610 610 user=user,
611 611 date=date,
612 612 extra=extra,
613 613 editor=editor)
614 614 return repo.commitctx(new)
615 615
616 616 def _isdirtywc(repo):
617 617 return repo[None].dirty(missing=True)
618 618
619 619 def abortdirty():
620 620 raise error.Abort(_('working copy has pending changes'),
621 621 hint=_('amend, commit, or revert them and run histedit '
622 622 '--continue, or abort with histedit --abort'))
623 623
624 624 def action(verbs, message, priority=False, internal=False):
625 625 def wrap(cls):
626 626 assert not priority or not internal
627 627 verb = verbs[0]
628 628 if priority:
629 629 primaryactions.add(verb)
630 630 elif internal:
631 631 internalactions.add(verb)
632 632 elif len(verbs) > 1:
633 633 secondaryactions.add(verb)
634 634 else:
635 635 tertiaryactions.add(verb)
636 636
637 637 cls.verb = verb
638 638 cls.verbs = verbs
639 639 cls.message = message
640 640 for verb in verbs:
641 641 actiontable[verb] = cls
642 642 return cls
643 643 return wrap
644 644
645 645 @action(['pick', 'p'],
646 646 _('use commit'),
647 647 priority=True)
648 648 class pick(histeditaction):
649 649 def run(self):
650 650 rulectx = self.repo[self.node]
651 651 if rulectx.parents()[0].node() == self.state.parentctxnode:
652 652 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
653 653 return rulectx, []
654 654
655 655 return super(pick, self).run()
656 656
657 657 @action(['edit', 'e'],
658 658 _('use commit, but stop for amending'),
659 659 priority=True)
660 660 class edit(histeditaction):
661 661 def run(self):
662 662 repo = self.repo
663 663 rulectx = repo[self.node]
664 664 hg.update(repo, self.state.parentctxnode, quietempty=True)
665 665 applychanges(repo.ui, repo, rulectx, {})
666 666 raise error.InterventionRequired(
667 667 _('Editing (%s), you may commit or record as needed now.')
668 668 % node.short(self.node),
669 669 hint=_('hg histedit --continue to resume'))
670 670
671 671 def commiteditor(self):
672 672 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
673 673
674 674 @action(['fold', 'f'],
675 675 _('use commit, but combine it with the one above'))
676 676 class fold(histeditaction):
677 677 def verify(self, prev, expected, seen):
678 678 """ Verifies semantic correctness of the fold rule"""
679 679 super(fold, self).verify(prev, expected, seen)
680 680 repo = self.repo
681 681 if not prev:
682 682 c = repo[self.node].parents()[0]
683 683 elif not prev.verb in ('pick', 'base'):
684 684 return
685 685 else:
686 686 c = repo[prev.node]
687 687 if not c.mutable():
688 688 raise error.ParseError(
689 689 _("cannot fold into public change %s") % node.short(c.node()))
690 690
691 691
692 692 def continuedirty(self):
693 693 repo = self.repo
694 694 rulectx = repo[self.node]
695 695
696 696 commit = commitfuncfor(repo, rulectx)
697 697 commit(text='fold-temp-revision %s' % node.short(self.node),
698 698 user=rulectx.user(), date=rulectx.date(),
699 699 extra=rulectx.extra())
700 700
701 701 def continueclean(self):
702 702 repo = self.repo
703 703 ctx = repo['.']
704 704 rulectx = repo[self.node]
705 705 parentctxnode = self.state.parentctxnode
706 706 if ctx.node() == parentctxnode:
707 707 repo.ui.warn(_('%s: empty changeset\n') %
708 708 node.short(self.node))
709 709 return ctx, [(self.node, (parentctxnode,))]
710 710
711 711 parentctx = repo[parentctxnode]
712 712 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
713 713 parentctx))
714 714 if not newcommits:
715 715 repo.ui.warn(_('%s: cannot fold - working copy is not a '
716 716 'descendant of previous commit %s\n') %
717 717 (node.short(self.node), node.short(parentctxnode)))
718 718 return ctx, [(self.node, (ctx.node(),))]
719 719
720 720 middlecommits = newcommits.copy()
721 721 middlecommits.discard(ctx.node())
722 722
723 723 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
724 724 middlecommits)
725 725
726 726 def skipprompt(self):
727 727 """Returns true if the rule should skip the message editor.
728 728
729 729 For example, 'fold' wants to show an editor, but 'rollup'
730 730 doesn't want to.
731 731 """
732 732 return False
733 733
734 734 def mergedescs(self):
735 735 """Returns true if the rule should merge messages of multiple changes.
736 736
737 737 This exists mainly so that 'rollup' rules can be a subclass of
738 738 'fold'.
739 739 """
740 740 return True
741 741
742 742 def firstdate(self):
743 743 """Returns true if the rule should preserve the date of the first
744 744 change.
745 745
746 746 This exists mainly so that 'rollup' rules can be a subclass of
747 747 'fold'.
748 748 """
749 749 return False
750 750
751 751 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
752 752 parent = ctx.parents()[0].node()
753 753 repo.ui.pushbuffer()
754 754 hg.update(repo, parent)
755 755 repo.ui.popbuffer()
756 756 ### prepare new commit data
757 757 commitopts = {}
758 758 commitopts['user'] = ctx.user()
759 759 # commit message
760 760 if not self.mergedescs():
761 761 newmessage = ctx.description()
762 762 else:
763 763 newmessage = '\n***\n'.join(
764 764 [ctx.description()] +
765 765 [repo[r].description() for r in internalchanges] +
766 766 [oldctx.description()]) + '\n'
767 767 commitopts['message'] = newmessage
768 768 # date
769 769 if self.firstdate():
770 770 commitopts['date'] = ctx.date()
771 771 else:
772 772 commitopts['date'] = max(ctx.date(), oldctx.date())
773 773 extra = ctx.extra().copy()
774 774 # histedit_source
775 775 # note: ctx is likely a temporary commit but that the best we can do
776 776 # here. This is sufficient to solve issue3681 anyway.
777 777 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
778 778 commitopts['extra'] = extra
779 779 phasemin = max(ctx.phase(), oldctx.phase())
780 780 overrides = {('phases', 'new-commit'): phasemin}
781 781 with repo.ui.configoverride(overrides, 'histedit'):
782 782 n = collapse(repo, ctx, repo[newnode], commitopts,
783 783 skipprompt=self.skipprompt())
784 784 if n is None:
785 785 return ctx, []
786 786 repo.ui.pushbuffer()
787 787 hg.update(repo, n)
788 788 repo.ui.popbuffer()
789 789 replacements = [(oldctx.node(), (newnode,)),
790 790 (ctx.node(), (n,)),
791 791 (newnode, (n,)),
792 792 ]
793 793 for ich in internalchanges:
794 794 replacements.append((ich, (n,)))
795 795 return repo[n], replacements
796 796
797 797 class base(histeditaction):
798 798
799 799 def run(self):
800 800 if self.repo['.'].node() != self.node:
801 801 mergemod.update(self.repo, self.node, False, True)
802 802 # branchmerge, force)
803 803 return self.continueclean()
804 804
805 805 def continuedirty(self):
806 806 abortdirty()
807 807
808 808 def continueclean(self):
809 809 basectx = self.repo['.']
810 810 return basectx, []
811 811
812 812 def _verifynodeconstraints(self, prev, expected, seen):
813 813 # base can only be use with a node not in the edited set
814 814 if self.node in expected:
815 815 msg = _('%s "%s" changeset was an edited list candidate')
816 816 raise error.ParseError(
817 817 msg % (self.verb, node.short(self.node)),
818 818 hint=_('base must only use unlisted changesets'))
819 819
820 820 @action(['_multifold'],
821 821 _(
822 822 """fold subclass used for when multiple folds happen in a row
823 823
824 824 We only want to fire the editor for the folded message once when
825 825 (say) four changes are folded down into a single change. This is
826 826 similar to rollup, but we should preserve both messages so that
827 827 when the last fold operation runs we can show the user all the
828 828 commit messages in their editor.
829 829 """),
830 830 internal=True)
831 831 class _multifold(fold):
832 832 def skipprompt(self):
833 833 return True
834 834
835 835 @action(["roll", "r"],
836 836 _("like fold, but discard this commit's description and date"))
837 837 class rollup(fold):
838 838 def mergedescs(self):
839 839 return False
840 840
841 841 def skipprompt(self):
842 842 return True
843 843
844 844 def firstdate(self):
845 845 return True
846 846
847 847 @action(["drop", "d"],
848 848 _('remove commit from history'))
849 849 class drop(histeditaction):
850 850 def run(self):
851 851 parentctx = self.repo[self.state.parentctxnode]
852 852 return parentctx, [(self.node, tuple())]
853 853
854 854 @action(["mess", "m"],
855 855 _('edit commit message without changing commit content'),
856 856 priority=True)
857 857 class message(histeditaction):
858 858 def commiteditor(self):
859 859 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
860 860
861 861 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
862 862 """utility function to find the first outgoing changeset
863 863
864 864 Used by initialization code"""
865 865 if opts is None:
866 866 opts = {}
867 867 dest = ui.expandpath(remote or 'default-push', remote or 'default')
868 868 dest, revs = hg.parseurl(dest, None)[:2]
869 869 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
870 870
871 871 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
872 872 other = hg.peer(repo, opts, dest)
873 873
874 874 if revs:
875 875 revs = [repo.lookup(rev) for rev in revs]
876 876
877 877 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
878 878 if not outgoing.missing:
879 879 raise error.Abort(_('no outgoing ancestors'))
880 880 roots = list(repo.revs("roots(%ln)", outgoing.missing))
881 881 if 1 < len(roots):
882 882 msg = _('there are ambiguous outgoing revisions')
883 883 hint = _("see 'hg help histedit' for more detail")
884 884 raise error.Abort(msg, hint=hint)
885 885 return repo.lookup(roots[0])
886 886
887 887
888 888 @command('histedit',
889 889 [('', 'commands', '',
890 890 _('read history edits from the specified file'), _('FILE')),
891 891 ('c', 'continue', False, _('continue an edit already in progress')),
892 892 ('', 'edit-plan', False, _('edit remaining actions list')),
893 893 ('k', 'keep', False,
894 894 _("don't strip old nodes after edit is complete")),
895 895 ('', 'abort', False, _('abort an edit in progress')),
896 896 ('o', 'outgoing', False, _('changesets not found in destination')),
897 897 ('f', 'force', False,
898 898 _('force outgoing even for unrelated repositories')),
899 899 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
900 900 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
901 901 def histedit(ui, repo, *freeargs, **opts):
902 902 """interactively edit changeset history
903 903
904 904 This command lets you edit a linear series of changesets (up to
905 905 and including the working directory, which should be clean).
906 906 You can:
907 907
908 908 - `pick` to [re]order a changeset
909 909
910 910 - `drop` to omit changeset
911 911
912 912 - `mess` to reword the changeset commit message
913 913
914 914 - `fold` to combine it with the preceding changeset (using the later date)
915 915
916 916 - `roll` like fold, but discarding this commit's description and date
917 917
918 918 - `edit` to edit this changeset (preserving date)
919 919
920 920 There are a number of ways to select the root changeset:
921 921
922 922 - Specify ANCESTOR directly
923 923
924 924 - Use --outgoing -- it will be the first linear changeset not
925 925 included in destination. (See :hg:`help config.paths.default-push`)
926 926
927 927 - Otherwise, the value from the "histedit.defaultrev" config option
928 928 is used as a revset to select the base revision when ANCESTOR is not
929 929 specified. The first revision returned by the revset is used. By
930 930 default, this selects the editable history that is unique to the
931 931 ancestry of the working directory.
932 932
933 933 .. container:: verbose
934 934
935 935 If you use --outgoing, this command will abort if there are ambiguous
936 936 outgoing revisions. For example, if there are multiple branches
937 937 containing outgoing revisions.
938 938
939 939 Use "min(outgoing() and ::.)" or similar revset specification
940 940 instead of --outgoing to specify edit target revision exactly in
941 941 such ambiguous situation. See :hg:`help revsets` for detail about
942 942 selecting revisions.
943 943
944 944 .. container:: verbose
945 945
946 946 Examples:
947 947
948 948 - A number of changes have been made.
949 949 Revision 3 is no longer needed.
950 950
951 951 Start history editing from revision 3::
952 952
953 953 hg histedit -r 3
954 954
955 955 An editor opens, containing the list of revisions,
956 956 with specific actions specified::
957 957
958 958 pick 5339bf82f0ca 3 Zworgle the foobar
959 959 pick 8ef592ce7cc4 4 Bedazzle the zerlog
960 960 pick 0a9639fcda9d 5 Morgify the cromulancy
961 961
962 962 Additional information about the possible actions
963 963 to take appears below the list of revisions.
964 964
965 965 To remove revision 3 from the history,
966 966 its action (at the beginning of the relevant line)
967 967 is changed to 'drop'::
968 968
969 969 drop 5339bf82f0ca 3 Zworgle the foobar
970 970 pick 8ef592ce7cc4 4 Bedazzle the zerlog
971 971 pick 0a9639fcda9d 5 Morgify the cromulancy
972 972
973 973 - A number of changes have been made.
974 974 Revision 2 and 4 need to be swapped.
975 975
976 976 Start history editing from revision 2::
977 977
978 978 hg histedit -r 2
979 979
980 980 An editor opens, containing the list of revisions,
981 981 with specific actions specified::
982 982
983 983 pick 252a1af424ad 2 Blorb a morgwazzle
984 984 pick 5339bf82f0ca 3 Zworgle the foobar
985 985 pick 8ef592ce7cc4 4 Bedazzle the zerlog
986 986
987 987 To swap revision 2 and 4, its lines are swapped
988 988 in the editor::
989 989
990 990 pick 8ef592ce7cc4 4 Bedazzle the zerlog
991 991 pick 5339bf82f0ca 3 Zworgle the foobar
992 992 pick 252a1af424ad 2 Blorb a morgwazzle
993 993
994 994 Returns 0 on success, 1 if user intervention is required (not only
995 995 for intentional "edit" command, but also for resolving unexpected
996 996 conflicts).
997 997 """
998 998 state = histeditstate(repo)
999 999 try:
1000 1000 state.wlock = repo.wlock()
1001 1001 state.lock = repo.lock()
1002 1002 _histedit(ui, repo, state, *freeargs, **opts)
1003 1003 finally:
1004 1004 release(state.lock, state.wlock)
1005 1005
1006 1006 goalcontinue = 'continue'
1007 1007 goalabort = 'abort'
1008 1008 goaleditplan = 'edit-plan'
1009 1009 goalnew = 'new'
1010 1010
1011 1011 def _getgoal(opts):
1012 1012 if opts.get('continue'):
1013 1013 return goalcontinue
1014 1014 if opts.get('abort'):
1015 1015 return goalabort
1016 1016 if opts.get('edit_plan'):
1017 1017 return goaleditplan
1018 1018 return goalnew
1019 1019
1020 1020 def _readfile(ui, path):
1021 1021 if path == '-':
1022 1022 with ui.timeblockedsection('histedit'):
1023 1023 return ui.fin.read()
1024 1024 else:
1025 1025 with open(path, 'rb') as f:
1026 1026 return f.read()
1027 1027
1028 1028 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1029 1029 # TODO only abort if we try to histedit mq patches, not just
1030 1030 # blanket if mq patches are applied somewhere
1031 1031 mq = getattr(repo, 'mq', None)
1032 1032 if mq and mq.applied:
1033 1033 raise error.Abort(_('source has mq patches applied'))
1034 1034
1035 1035 # basic argument incompatibility processing
1036 1036 outg = opts.get('outgoing')
1037 1037 editplan = opts.get('edit_plan')
1038 1038 abort = opts.get('abort')
1039 1039 force = opts.get('force')
1040 1040 if force and not outg:
1041 1041 raise error.Abort(_('--force only allowed with --outgoing'))
1042 1042 if goal == 'continue':
1043 1043 if any((outg, abort, revs, freeargs, rules, editplan)):
1044 1044 raise error.Abort(_('no arguments allowed with --continue'))
1045 1045 elif goal == 'abort':
1046 1046 if any((outg, revs, freeargs, rules, editplan)):
1047 1047 raise error.Abort(_('no arguments allowed with --abort'))
1048 1048 elif goal == 'edit-plan':
1049 1049 if any((outg, revs, freeargs)):
1050 1050 raise error.Abort(_('only --commands argument allowed with '
1051 1051 '--edit-plan'))
1052 1052 else:
1053 1053 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1054 1054 raise error.Abort(_('history edit already in progress, try '
1055 1055 '--continue or --abort'))
1056 1056 if outg:
1057 1057 if revs:
1058 1058 raise error.Abort(_('no revisions allowed with --outgoing'))
1059 1059 if len(freeargs) > 1:
1060 1060 raise error.Abort(
1061 1061 _('only one repo argument allowed with --outgoing'))
1062 1062 else:
1063 1063 revs.extend(freeargs)
1064 1064 if len(revs) == 0:
1065 1065 defaultrev = destutil.desthistedit(ui, repo)
1066 1066 if defaultrev is not None:
1067 1067 revs.append(defaultrev)
1068 1068
1069 1069 if len(revs) != 1:
1070 1070 raise error.Abort(
1071 1071 _('histedit requires exactly one ancestor revision'))
1072 1072
1073 1073 def _histedit(ui, repo, state, *freeargs, **opts):
1074 1074 goal = _getgoal(opts)
1075 1075 revs = opts.get('rev', [])
1076 1076 rules = opts.get('commands', '')
1077 1077 state.keep = opts.get('keep', False)
1078 1078
1079 1079 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1080 1080
1081 1081 # rebuild state
1082 1082 if goal == goalcontinue:
1083 1083 state.read()
1084 1084 state = bootstrapcontinue(ui, state, opts)
1085 1085 elif goal == goaleditplan:
1086 1086 _edithisteditplan(ui, repo, state, rules)
1087 1087 return
1088 1088 elif goal == goalabort:
1089 1089 _aborthistedit(ui, repo, state)
1090 1090 return
1091 1091 else:
1092 1092 # goal == goalnew
1093 1093 _newhistedit(ui, repo, state, revs, freeargs, opts)
1094 1094
1095 1095 _continuehistedit(ui, repo, state)
1096 1096 _finishhistedit(ui, repo, state)
1097 1097
1098 1098 def _continuehistedit(ui, repo, state):
1099 1099 """This function runs after either:
1100 1100 - bootstrapcontinue (if the goal is 'continue')
1101 1101 - _newhistedit (if the goal is 'new')
1102 1102 """
1103 1103 # preprocess rules so that we can hide inner folds from the user
1104 1104 # and only show one editor
1105 1105 actions = state.actions[:]
1106 1106 for idx, (action, nextact) in enumerate(
1107 1107 zip(actions, actions[1:] + [None])):
1108 1108 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1109 1109 state.actions[idx].__class__ = _multifold
1110 1110
1111 1111 total = len(state.actions)
1112 1112 pos = 0
1113 1113 state.tr = None
1114 1114
1115 1115 # Force an initial state file write, so the user can run --abort/continue
1116 1116 # even if there's an exception before the first transaction serialize.
1117 1117 state.write()
1118 1118 try:
1119 1119 # Don't use singletransaction by default since it rolls the entire
1120 1120 # transaction back if an unexpected exception happens (like a
1121 1121 # pretxncommit hook throws, or the user aborts the commit msg editor).
1122 1122 if ui.configbool("histedit", "singletransaction", False):
1123 1123 # Don't use a 'with' for the transaction, since actions may close
1124 1124 # and reopen a transaction. For example, if the action executes an
1125 1125 # external process it may choose to commit the transaction first.
1126 1126 state.tr = repo.transaction('histedit')
1127 1127
1128 1128 while state.actions:
1129 1129 state.write(tr=state.tr)
1130 1130 actobj = state.actions[0]
1131 1131 pos += 1
1132 1132 ui.progress(_("editing"), pos, actobj.torule(),
1133 1133 _('changes'), total)
1134 1134 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1135 1135 actobj.torule()))
1136 1136 parentctx, replacement_ = actobj.run()
1137 1137 state.parentctxnode = parentctx.node()
1138 1138 state.replacements.extend(replacement_)
1139 1139 state.actions.pop(0)
1140 1140
1141 1141 if state.tr is not None:
1142 1142 state.tr.close()
1143 1143 except error.InterventionRequired:
1144 1144 if state.tr is not None:
1145 1145 state.tr.close()
1146 1146 raise
1147 1147 except Exception:
1148 1148 if state.tr is not None:
1149 1149 state.tr.abort()
1150 1150 raise
1151 1151
1152 1152 state.write()
1153 1153 ui.progress(_("editing"), None)
1154 1154
1155 1155 def _finishhistedit(ui, repo, state):
1156 1156 """This action runs when histedit is finishing its session"""
1157 1157 repo.ui.pushbuffer()
1158 1158 hg.update(repo, state.parentctxnode, quietempty=True)
1159 1159 repo.ui.popbuffer()
1160 1160
1161 1161 mapping, tmpnodes, created, ntm = processreplacement(state)
1162 1162 if mapping:
1163 1163 for prec, succs in mapping.iteritems():
1164 1164 if not succs:
1165 1165 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1166 1166 else:
1167 1167 ui.debug('histedit: %s is replaced by %s\n' % (
1168 1168 node.short(prec), node.short(succs[0])))
1169 1169 if len(succs) > 1:
1170 1170 m = 'histedit: %s'
1171 1171 for n in succs[1:]:
1172 1172 ui.debug(m % node.short(n))
1173 1173
1174 1174 safecleanupnode(ui, repo, 'temp', tmpnodes)
1175 1175
1176 1176 if not state.keep:
1177 1177 if mapping:
1178 1178 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1179 1179 # TODO update mq state
1180 1180 safecleanupnode(ui, repo, 'replaced', mapping)
1181 1181
1182 1182 state.clear()
1183 1183 if os.path.exists(repo.sjoin('undo')):
1184 1184 os.unlink(repo.sjoin('undo'))
1185 1185 if repo.vfs.exists('histedit-last-edit.txt'):
1186 1186 repo.vfs.unlink('histedit-last-edit.txt')
1187 1187
1188 1188 def _aborthistedit(ui, repo, state):
1189 1189 try:
1190 1190 state.read()
1191 1191 __, leafs, tmpnodes, __ = processreplacement(state)
1192 1192 ui.debug('restore wc to old parent %s\n'
1193 1193 % node.short(state.topmost))
1194 1194
1195 1195 # Recover our old commits if necessary
1196 1196 if not state.topmost in repo and state.backupfile:
1197 1197 backupfile = repo.vfs.join(state.backupfile)
1198 1198 f = hg.openpath(ui, backupfile)
1199 1199 gen = exchange.readbundle(ui, f, backupfile)
1200 1200 with repo.transaction('histedit.abort') as tr:
1201 1201 if not isinstance(gen, bundle2.unbundle20):
1202 1202 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1203 if isinstance(gen, bundle2.unbundle20):
1203 else:
1204 1204 bundle2.applybundle(repo, gen, tr,
1205 1205 source='histedit',
1206 1206 url='bundle:' + backupfile)
1207 1207
1208 1208 os.remove(backupfile)
1209 1209
1210 1210 # check whether we should update away
1211 1211 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1212 1212 state.parentctxnode, leafs | tmpnodes):
1213 1213 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1214 1214 cleanupnode(ui, repo, 'created', tmpnodes)
1215 1215 cleanupnode(ui, repo, 'temp', leafs)
1216 1216 except Exception:
1217 1217 if state.inprogress():
1218 1218 ui.warn(_('warning: encountered an exception during histedit '
1219 1219 '--abort; the repository may not have been completely '
1220 1220 'cleaned up\n'))
1221 1221 raise
1222 1222 finally:
1223 1223 state.clear()
1224 1224
1225 1225 def _edithisteditplan(ui, repo, state, rules):
1226 1226 state.read()
1227 1227 if not rules:
1228 1228 comment = geteditcomment(ui,
1229 1229 node.short(state.parentctxnode),
1230 1230 node.short(state.topmost))
1231 1231 rules = ruleeditor(repo, ui, state.actions, comment)
1232 1232 else:
1233 1233 rules = _readfile(ui, rules)
1234 1234 actions = parserules(rules, state)
1235 1235 ctxs = [repo[act.node] \
1236 1236 for act in state.actions if act.node]
1237 1237 warnverifyactions(ui, repo, actions, state, ctxs)
1238 1238 state.actions = actions
1239 1239 state.write()
1240 1240
1241 1241 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1242 1242 outg = opts.get('outgoing')
1243 1243 rules = opts.get('commands', '')
1244 1244 force = opts.get('force')
1245 1245
1246 1246 cmdutil.checkunfinished(repo)
1247 1247 cmdutil.bailifchanged(repo)
1248 1248
1249 1249 topmost, empty = repo.dirstate.parents()
1250 1250 if outg:
1251 1251 if freeargs:
1252 1252 remote = freeargs[0]
1253 1253 else:
1254 1254 remote = None
1255 1255 root = findoutgoing(ui, repo, remote, force, opts)
1256 1256 else:
1257 1257 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1258 1258 if len(rr) != 1:
1259 1259 raise error.Abort(_('The specified revisions must have '
1260 1260 'exactly one common root'))
1261 1261 root = rr[0].node()
1262 1262
1263 1263 revs = between(repo, root, topmost, state.keep)
1264 1264 if not revs:
1265 1265 raise error.Abort(_('%s is not an ancestor of working directory') %
1266 1266 node.short(root))
1267 1267
1268 1268 ctxs = [repo[r] for r in revs]
1269 1269 if not rules:
1270 1270 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1271 1271 actions = [pick(state, r) for r in revs]
1272 1272 rules = ruleeditor(repo, ui, actions, comment)
1273 1273 else:
1274 1274 rules = _readfile(ui, rules)
1275 1275 actions = parserules(rules, state)
1276 1276 warnverifyactions(ui, repo, actions, state, ctxs)
1277 1277
1278 1278 parentctxnode = repo[root].parents()[0].node()
1279 1279
1280 1280 state.parentctxnode = parentctxnode
1281 1281 state.actions = actions
1282 1282 state.topmost = topmost
1283 1283 state.replacements = []
1284 1284
1285 1285 # Create a backup so we can always abort completely.
1286 1286 backupfile = None
1287 1287 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1288 1288 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1289 1289 'histedit')
1290 1290 state.backupfile = backupfile
1291 1291
1292 1292 def _getsummary(ctx):
1293 1293 # a common pattern is to extract the summary but default to the empty
1294 1294 # string
1295 1295 summary = ctx.description() or ''
1296 1296 if summary:
1297 1297 summary = summary.splitlines()[0]
1298 1298 return summary
1299 1299
1300 1300 def bootstrapcontinue(ui, state, opts):
1301 1301 repo = state.repo
1302 1302
1303 1303 ms = mergemod.mergestate.read(repo)
1304 1304 mergeutil.checkunresolved(ms)
1305 1305
1306 1306 if state.actions:
1307 1307 actobj = state.actions.pop(0)
1308 1308
1309 1309 if _isdirtywc(repo):
1310 1310 actobj.continuedirty()
1311 1311 if _isdirtywc(repo):
1312 1312 abortdirty()
1313 1313
1314 1314 parentctx, replacements = actobj.continueclean()
1315 1315
1316 1316 state.parentctxnode = parentctx.node()
1317 1317 state.replacements.extend(replacements)
1318 1318
1319 1319 return state
1320 1320
1321 1321 def between(repo, old, new, keep):
1322 1322 """select and validate the set of revision to edit
1323 1323
1324 1324 When keep is false, the specified set can't have children."""
1325 1325 ctxs = list(repo.set('%n::%n', old, new))
1326 1326 if ctxs and not keep:
1327 1327 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1328 1328 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1329 1329 raise error.Abort(_('can only histedit a changeset together '
1330 1330 'with all its descendants'))
1331 1331 if repo.revs('(%ld) and merge()', ctxs):
1332 1332 raise error.Abort(_('cannot edit history that contains merges'))
1333 1333 root = ctxs[0] # list is already sorted by repo.set
1334 1334 if not root.mutable():
1335 1335 raise error.Abort(_('cannot edit public changeset: %s') % root,
1336 1336 hint=_("see 'hg help phases' for details"))
1337 1337 return [c.node() for c in ctxs]
1338 1338
1339 1339 def ruleeditor(repo, ui, actions, editcomment=""):
1340 1340 """open an editor to edit rules
1341 1341
1342 1342 rules are in the format [ [act, ctx], ...] like in state.rules
1343 1343 """
1344 1344 if repo.ui.configbool("experimental", "histedit.autoverb"):
1345 1345 newact = util.sortdict()
1346 1346 for act in actions:
1347 1347 ctx = repo[act.node]
1348 1348 summary = _getsummary(ctx)
1349 1349 fword = summary.split(' ', 1)[0].lower()
1350 1350 added = False
1351 1351
1352 1352 # if it doesn't end with the special character '!' just skip this
1353 1353 if fword.endswith('!'):
1354 1354 fword = fword[:-1]
1355 1355 if fword in primaryactions | secondaryactions | tertiaryactions:
1356 1356 act.verb = fword
1357 1357 # get the target summary
1358 1358 tsum = summary[len(fword) + 1:].lstrip()
1359 1359 # safe but slow: reverse iterate over the actions so we
1360 1360 # don't clash on two commits having the same summary
1361 1361 for na, l in reversed(list(newact.iteritems())):
1362 1362 actx = repo[na.node]
1363 1363 asum = _getsummary(actx)
1364 1364 if asum == tsum:
1365 1365 added = True
1366 1366 l.append(act)
1367 1367 break
1368 1368
1369 1369 if not added:
1370 1370 newact[act] = []
1371 1371
1372 1372 # copy over and flatten the new list
1373 1373 actions = []
1374 1374 for na, l in newact.iteritems():
1375 1375 actions.append(na)
1376 1376 actions += l
1377 1377
1378 1378 rules = '\n'.join([act.torule() for act in actions])
1379 1379 rules += '\n\n'
1380 1380 rules += editcomment
1381 1381 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1382 1382 repopath=repo.path)
1383 1383
1384 1384 # Save edit rules in .hg/histedit-last-edit.txt in case
1385 1385 # the user needs to ask for help after something
1386 1386 # surprising happens.
1387 1387 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1388 1388 f.write(rules)
1389 1389 f.close()
1390 1390
1391 1391 return rules
1392 1392
1393 1393 def parserules(rules, state):
1394 1394 """Read the histedit rules string and return list of action objects """
1395 1395 rules = [l for l in (r.strip() for r in rules.splitlines())
1396 1396 if l and not l.startswith('#')]
1397 1397 actions = []
1398 1398 for r in rules:
1399 1399 if ' ' not in r:
1400 1400 raise error.ParseError(_('malformed line "%s"') % r)
1401 1401 verb, rest = r.split(' ', 1)
1402 1402
1403 1403 if verb not in actiontable:
1404 1404 raise error.ParseError(_('unknown action "%s"') % verb)
1405 1405
1406 1406 action = actiontable[verb].fromrule(state, rest)
1407 1407 actions.append(action)
1408 1408 return actions
1409 1409
1410 1410 def warnverifyactions(ui, repo, actions, state, ctxs):
1411 1411 try:
1412 1412 verifyactions(actions, state, ctxs)
1413 1413 except error.ParseError:
1414 1414 if repo.vfs.exists('histedit-last-edit.txt'):
1415 1415 ui.warn(_('warning: histedit rules saved '
1416 1416 'to: .hg/histedit-last-edit.txt\n'))
1417 1417 raise
1418 1418
1419 1419 def verifyactions(actions, state, ctxs):
1420 1420 """Verify that there exists exactly one action per given changeset and
1421 1421 other constraints.
1422 1422
1423 1423 Will abort if there are to many or too few rules, a malformed rule,
1424 1424 or a rule on a changeset outside of the user-given range.
1425 1425 """
1426 1426 expected = set(c.node() for c in ctxs)
1427 1427 seen = set()
1428 1428 prev = None
1429 1429 for action in actions:
1430 1430 action.verify(prev, expected, seen)
1431 1431 prev = action
1432 1432 if action.node is not None:
1433 1433 seen.add(action.node)
1434 1434 missing = sorted(expected - seen) # sort to stabilize output
1435 1435
1436 1436 if state.repo.ui.configbool('histedit', 'dropmissing'):
1437 1437 if len(actions) == 0:
1438 1438 raise error.ParseError(_('no rules provided'),
1439 1439 hint=_('use strip extension to remove commits'))
1440 1440
1441 1441 drops = [drop(state, n) for n in missing]
1442 1442 # put the in the beginning so they execute immediately and
1443 1443 # don't show in the edit-plan in the future
1444 1444 actions[:0] = drops
1445 1445 elif missing:
1446 1446 raise error.ParseError(_('missing rules for changeset %s') %
1447 1447 node.short(missing[0]),
1448 1448 hint=_('use "drop %s" to discard, see also: '
1449 1449 "'hg help -e histedit.config'")
1450 1450 % node.short(missing[0]))
1451 1451
1452 1452 def adjustreplacementsfrommarkers(repo, oldreplacements):
1453 1453 """Adjust replacements from obsolescence markers
1454 1454
1455 1455 Replacements structure is originally generated based on
1456 1456 histedit's state and does not account for changes that are
1457 1457 not recorded there. This function fixes that by adding
1458 1458 data read from obsolescence markers"""
1459 1459 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1460 1460 return oldreplacements
1461 1461
1462 1462 unfi = repo.unfiltered()
1463 1463 nm = unfi.changelog.nodemap
1464 1464 obsstore = repo.obsstore
1465 1465 newreplacements = list(oldreplacements)
1466 1466 oldsuccs = [r[1] for r in oldreplacements]
1467 1467 # successors that have already been added to succstocheck once
1468 1468 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1469 1469 succstocheck = list(seensuccs)
1470 1470 while succstocheck:
1471 1471 n = succstocheck.pop()
1472 1472 missing = nm.get(n) is None
1473 1473 markers = obsstore.successors.get(n, ())
1474 1474 if missing and not markers:
1475 1475 # dead end, mark it as such
1476 1476 newreplacements.append((n, ()))
1477 1477 for marker in markers:
1478 1478 nsuccs = marker[1]
1479 1479 newreplacements.append((n, nsuccs))
1480 1480 for nsucc in nsuccs:
1481 1481 if nsucc not in seensuccs:
1482 1482 seensuccs.add(nsucc)
1483 1483 succstocheck.append(nsucc)
1484 1484
1485 1485 return newreplacements
1486 1486
1487 1487 def processreplacement(state):
1488 1488 """process the list of replacements to return
1489 1489
1490 1490 1) the final mapping between original and created nodes
1491 1491 2) the list of temporary node created by histedit
1492 1492 3) the list of new commit created by histedit"""
1493 1493 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1494 1494 allsuccs = set()
1495 1495 replaced = set()
1496 1496 fullmapping = {}
1497 1497 # initialize basic set
1498 1498 # fullmapping records all operations recorded in replacement
1499 1499 for rep in replacements:
1500 1500 allsuccs.update(rep[1])
1501 1501 replaced.add(rep[0])
1502 1502 fullmapping.setdefault(rep[0], set()).update(rep[1])
1503 1503 new = allsuccs - replaced
1504 1504 tmpnodes = allsuccs & replaced
1505 1505 # Reduce content fullmapping into direct relation between original nodes
1506 1506 # and final node created during history edition
1507 1507 # Dropped changeset are replaced by an empty list
1508 1508 toproceed = set(fullmapping)
1509 1509 final = {}
1510 1510 while toproceed:
1511 1511 for x in list(toproceed):
1512 1512 succs = fullmapping[x]
1513 1513 for s in list(succs):
1514 1514 if s in toproceed:
1515 1515 # non final node with unknown closure
1516 1516 # We can't process this now
1517 1517 break
1518 1518 elif s in final:
1519 1519 # non final node, replace with closure
1520 1520 succs.remove(s)
1521 1521 succs.update(final[s])
1522 1522 else:
1523 1523 final[x] = succs
1524 1524 toproceed.remove(x)
1525 1525 # remove tmpnodes from final mapping
1526 1526 for n in tmpnodes:
1527 1527 del final[n]
1528 1528 # we expect all changes involved in final to exist in the repo
1529 1529 # turn `final` into list (topologically sorted)
1530 1530 nm = state.repo.changelog.nodemap
1531 1531 for prec, succs in final.items():
1532 1532 final[prec] = sorted(succs, key=nm.get)
1533 1533
1534 1534 # computed topmost element (necessary for bookmark)
1535 1535 if new:
1536 1536 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1537 1537 elif not final:
1538 1538 # Nothing rewritten at all. we won't need `newtopmost`
1539 1539 # It is the same as `oldtopmost` and `processreplacement` know it
1540 1540 newtopmost = None
1541 1541 else:
1542 1542 # every body died. The newtopmost is the parent of the root.
1543 1543 r = state.repo.changelog.rev
1544 1544 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1545 1545
1546 1546 return final, tmpnodes, new, newtopmost
1547 1547
1548 1548 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1549 1549 """Move bookmark from old to newly created node"""
1550 1550 if not mapping:
1551 1551 # if nothing got rewritten there is not purpose for this function
1552 1552 return
1553 1553 moves = []
1554 1554 for bk, old in sorted(repo._bookmarks.iteritems()):
1555 1555 if old == oldtopmost:
1556 1556 # special case ensure bookmark stay on tip.
1557 1557 #
1558 1558 # This is arguably a feature and we may only want that for the
1559 1559 # active bookmark. But the behavior is kept compatible with the old
1560 1560 # version for now.
1561 1561 moves.append((bk, newtopmost))
1562 1562 continue
1563 1563 base = old
1564 1564 new = mapping.get(base, None)
1565 1565 if new is None:
1566 1566 continue
1567 1567 while not new:
1568 1568 # base is killed, trying with parent
1569 1569 base = repo[base].p1().node()
1570 1570 new = mapping.get(base, (base,))
1571 1571 # nothing to move
1572 1572 moves.append((bk, new[-1]))
1573 1573 if moves:
1574 1574 lock = tr = None
1575 1575 try:
1576 1576 lock = repo.lock()
1577 1577 tr = repo.transaction('histedit')
1578 1578 marks = repo._bookmarks
1579 1579 for mark, new in moves:
1580 1580 old = marks[mark]
1581 1581 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1582 1582 % (mark, node.short(old), node.short(new)))
1583 1583 marks[mark] = new
1584 1584 marks.recordchange(tr)
1585 1585 tr.close()
1586 1586 finally:
1587 1587 release(tr, lock)
1588 1588
1589 1589 def cleanupnode(ui, repo, name, nodes):
1590 1590 """strip a group of nodes from the repository
1591 1591
1592 1592 The set of node to strip may contains unknown nodes."""
1593 1593 ui.debug('should strip %s nodes %s\n' %
1594 1594 (name, ', '.join([node.short(n) for n in nodes])))
1595 1595 with repo.lock():
1596 1596 # do not let filtering get in the way of the cleanse
1597 1597 # we should probably get rid of obsolescence marker created during the
1598 1598 # histedit, but we currently do not have such information.
1599 1599 repo = repo.unfiltered()
1600 1600 # Find all nodes that need to be stripped
1601 1601 # (we use %lr instead of %ln to silently ignore unknown items)
1602 1602 nm = repo.changelog.nodemap
1603 1603 nodes = sorted(n for n in nodes if n in nm)
1604 1604 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1605 1605 for c in roots:
1606 1606 # We should process node in reverse order to strip tip most first.
1607 1607 # but this trigger a bug in changegroup hook.
1608 1608 # This would reduce bundle overhead
1609 1609 repair.strip(ui, repo, c)
1610 1610
1611 1611 def safecleanupnode(ui, repo, name, nodes):
1612 1612 """strip or obsolete nodes
1613 1613
1614 1614 nodes could be either a set or dict which maps to replacements.
1615 1615 nodes could be unknown (outside the repo).
1616 1616 """
1617 1617 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1618 1618 if supportsmarkers:
1619 1619 if util.safehasattr(nodes, 'get'):
1620 1620 # nodes is a dict-like mapping
1621 1621 # use unfiltered repo for successors in case they are hidden
1622 1622 urepo = repo.unfiltered()
1623 1623 def getmarker(prec):
1624 1624 succs = tuple(urepo[n] for n in nodes.get(prec, ()))
1625 1625 return (repo[prec], succs)
1626 1626 else:
1627 1627 # nodes is a set-like
1628 1628 def getmarker(prec):
1629 1629 return (repo[prec], ())
1630 1630 # sort by revision number because it sound "right"
1631 1631 sortednodes = sorted([n for n in nodes if n in repo],
1632 1632 key=repo.changelog.rev)
1633 1633 markers = [getmarker(t) for t in sortednodes]
1634 1634 if markers:
1635 1635 obsolete.createmarkers(repo, markers, operation='histedit')
1636 1636 else:
1637 1637 return cleanupnode(ui, repo, name, nodes)
1638 1638
1639 1639 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1640 1640 if isinstance(nodelist, str):
1641 1641 nodelist = [nodelist]
1642 1642 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1643 1643 state = histeditstate(repo)
1644 1644 state.read()
1645 1645 histedit_nodes = {action.node for action
1646 1646 in state.actions if action.node}
1647 1647 common_nodes = histedit_nodes & set(nodelist)
1648 1648 if common_nodes:
1649 1649 raise error.Abort(_("histedit in progress, can't strip %s")
1650 1650 % ', '.join(node.short(x) for x in common_nodes))
1651 1651 return orig(ui, repo, nodelist, *args, **kwargs)
1652 1652
1653 1653 extensions.wrapfunction(repair, 'strip', stripwrapper)
1654 1654
1655 1655 def summaryhook(ui, repo):
1656 1656 if not os.path.exists(repo.vfs.join('histedit-state')):
1657 1657 return
1658 1658 state = histeditstate(repo)
1659 1659 state.read()
1660 1660 if state.actions:
1661 1661 # i18n: column positioning for "hg summary"
1662 1662 ui.write(_('hist: %s (histedit --continue)\n') %
1663 1663 (ui.label(_('%d remaining'), 'histedit.remaining') %
1664 1664 len(state.actions)))
1665 1665
1666 1666 def extsetup(ui):
1667 1667 cmdutil.summaryhooks.add('histedit', summaryhook)
1668 1668 cmdutil.unfinishedstates.append(
1669 1669 ['histedit-state', False, True, _('histedit in progress'),
1670 1670 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1671 1671 cmdutil.afterresolvedstates.append(
1672 1672 ['histedit-state', _('hg histedit --continue')])
1673 1673 if ui.configbool("experimental", "histeditng"):
1674 1674 globals()['base'] = action(['base', 'b'],
1675 1675 _('checkout changeset and apply further changesets from there')
1676 1676 )(base)
@@ -1,1049 +1,1049 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 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 bookmarks,
32 32 bundle2,
33 33 bundlerepo,
34 34 changegroup,
35 35 cmdutil,
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 registrar,
46 46 repair,
47 47 scmutil,
48 48 templatefilters,
49 49 util,
50 50 vfs as vfsmod,
51 51 )
52 52
53 53 from . import (
54 54 rebase,
55 55 )
56 56
57 57 cmdtable = {}
58 58 command = registrar.command(cmdtable)
59 59 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
60 60 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
61 61 # be specifying the version(s) of Mercurial they are tested with, or
62 62 # leave the attribute unspecified.
63 63 testedwith = 'ships-with-hg-core'
64 64
65 65 backupdir = 'shelve-backup'
66 66 shelvedir = 'shelved'
67 67 shelvefileextensions = ['hg', 'patch', 'oshelve']
68 68 # universal extension is present in all types of shelves
69 69 patchextension = 'patch'
70 70
71 71 # we never need the user, so we use a
72 72 # generic user for all shelve operations
73 73 shelveuser = 'shelve@localhost'
74 74
75 75 class shelvedfile(object):
76 76 """Helper for the file storing a single shelve
77 77
78 78 Handles common functions on shelve files (.hg/.patch) using
79 79 the vfs layer"""
80 80 def __init__(self, repo, name, filetype=None):
81 81 self.repo = repo
82 82 self.name = name
83 83 self.vfs = vfsmod.vfs(repo.vfs.join(shelvedir))
84 84 self.backupvfs = vfsmod.vfs(repo.vfs.join(backupdir))
85 85 self.ui = self.repo.ui
86 86 if filetype:
87 87 self.fname = name + '.' + filetype
88 88 else:
89 89 self.fname = name
90 90
91 91 def exists(self):
92 92 return self.vfs.exists(self.fname)
93 93
94 94 def filename(self):
95 95 return self.vfs.join(self.fname)
96 96
97 97 def backupfilename(self):
98 98 def gennames(base):
99 99 yield base
100 100 base, ext = base.rsplit('.', 1)
101 101 for i in itertools.count(1):
102 102 yield '%s-%d.%s' % (base, i, ext)
103 103
104 104 name = self.backupvfs.join(self.fname)
105 105 for n in gennames(name):
106 106 if not self.backupvfs.exists(n):
107 107 return n
108 108
109 109 def movetobackup(self):
110 110 if not self.backupvfs.isdir():
111 111 self.backupvfs.makedir()
112 112 util.rename(self.filename(), self.backupfilename())
113 113
114 114 def stat(self):
115 115 return self.vfs.stat(self.fname)
116 116
117 117 def opener(self, mode='rb'):
118 118 try:
119 119 return self.vfs(self.fname, mode)
120 120 except IOError as err:
121 121 if err.errno != errno.ENOENT:
122 122 raise
123 123 raise error.Abort(_("shelved change '%s' not found") % self.name)
124 124
125 125 def applybundle(self):
126 126 fp = self.opener()
127 127 try:
128 128 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
129 129 if not isinstance(gen, bundle2.unbundle20):
130 130 gen.apply(self.repo, 'unshelve',
131 131 'bundle:' + self.vfs.join(self.fname),
132 132 targetphase=phases.secret)
133 if isinstance(gen, bundle2.unbundle20):
133 else:
134 134 bundle2.applybundle(self.repo, gen,
135 135 self.repo.currenttransaction(),
136 136 source='unshelve',
137 137 url='bundle:' + self.vfs.join(self.fname))
138 138 finally:
139 139 fp.close()
140 140
141 141 def bundlerepo(self):
142 142 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
143 143 self.vfs.join(self.fname))
144 144 def writebundle(self, bases, node):
145 145 cgversion = changegroup.safeversion(self.repo)
146 146 if cgversion == '01':
147 147 btype = 'HG10BZ'
148 148 compression = None
149 149 else:
150 150 btype = 'HG20'
151 151 compression = 'BZ'
152 152
153 153 cg = changegroup.changegroupsubset(self.repo, bases, [node], 'shelve',
154 154 version=cgversion)
155 155 bundle2.writebundle(self.ui, cg, self.fname, btype, self.vfs,
156 156 compression=compression)
157 157
158 158 def writeobsshelveinfo(self, info):
159 159 scmutil.simplekeyvaluefile(self.vfs, self.fname).write(info)
160 160
161 161 def readobsshelveinfo(self):
162 162 return scmutil.simplekeyvaluefile(self.vfs, self.fname).read()
163 163
164 164 class shelvedstate(object):
165 165 """Handle persistence during unshelving operations.
166 166
167 167 Handles saving and restoring a shelved state. Ensures that different
168 168 versions of a shelved state are possible and handles them appropriately.
169 169 """
170 170 _version = 2
171 171 _filename = 'shelvedstate'
172 172 _keep = 'keep'
173 173 _nokeep = 'nokeep'
174 174 # colon is essential to differentiate from a real bookmark name
175 175 _noactivebook = ':no-active-bookmark'
176 176
177 177 @classmethod
178 178 def _verifyandtransform(cls, d):
179 179 """Some basic shelvestate syntactic verification and transformation"""
180 180 try:
181 181 d['originalwctx'] = nodemod.bin(d['originalwctx'])
182 182 d['pendingctx'] = nodemod.bin(d['pendingctx'])
183 183 d['parents'] = [nodemod.bin(h)
184 184 for h in d['parents'].split(' ')]
185 185 d['nodestoremove'] = [nodemod.bin(h)
186 186 for h in d['nodestoremove'].split(' ')]
187 187 except (ValueError, TypeError, KeyError) as err:
188 188 raise error.CorruptedState(str(err))
189 189
190 190 @classmethod
191 191 def _getversion(cls, repo):
192 192 """Read version information from shelvestate file"""
193 193 fp = repo.vfs(cls._filename)
194 194 try:
195 195 version = int(fp.readline().strip())
196 196 except ValueError as err:
197 197 raise error.CorruptedState(str(err))
198 198 finally:
199 199 fp.close()
200 200 return version
201 201
202 202 @classmethod
203 203 def _readold(cls, repo):
204 204 """Read the old position-based version of a shelvestate file"""
205 205 # Order is important, because old shelvestate file uses it
206 206 # to detemine values of fields (i.g. name is on the second line,
207 207 # originalwctx is on the third and so forth). Please do not change.
208 208 keys = ['version', 'name', 'originalwctx', 'pendingctx', 'parents',
209 209 'nodestoremove', 'branchtorestore', 'keep', 'activebook']
210 210 # this is executed only seldomly, so it is not a big deal
211 211 # that we open this file twice
212 212 fp = repo.vfs(cls._filename)
213 213 d = {}
214 214 try:
215 215 for key in keys:
216 216 d[key] = fp.readline().strip()
217 217 finally:
218 218 fp.close()
219 219 return d
220 220
221 221 @classmethod
222 222 def load(cls, repo):
223 223 version = cls._getversion(repo)
224 224 if version < cls._version:
225 225 d = cls._readold(repo)
226 226 elif version == cls._version:
227 227 d = scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
228 228 .read(firstlinenonkeyval=True)
229 229 else:
230 230 raise error.Abort(_('this version of shelve is incompatible '
231 231 'with the version used in this repo'))
232 232
233 233 cls._verifyandtransform(d)
234 234 try:
235 235 obj = cls()
236 236 obj.name = d['name']
237 237 obj.wctx = repo[d['originalwctx']]
238 238 obj.pendingctx = repo[d['pendingctx']]
239 239 obj.parents = d['parents']
240 240 obj.nodestoremove = d['nodestoremove']
241 241 obj.branchtorestore = d.get('branchtorestore', '')
242 242 obj.keep = d.get('keep') == cls._keep
243 243 obj.activebookmark = ''
244 244 if d.get('activebook', '') != cls._noactivebook:
245 245 obj.activebookmark = d.get('activebook', '')
246 246 except (error.RepoLookupError, KeyError) as err:
247 247 raise error.CorruptedState(str(err))
248 248
249 249 return obj
250 250
251 251 @classmethod
252 252 def save(cls, repo, name, originalwctx, pendingctx, nodestoremove,
253 253 branchtorestore, keep=False, activebook=''):
254 254 info = {
255 255 "name": name,
256 256 "originalwctx": nodemod.hex(originalwctx.node()),
257 257 "pendingctx": nodemod.hex(pendingctx.node()),
258 258 "parents": ' '.join([nodemod.hex(p)
259 259 for p in repo.dirstate.parents()]),
260 260 "nodestoremove": ' '.join([nodemod.hex(n)
261 261 for n in nodestoremove]),
262 262 "branchtorestore": branchtorestore,
263 263 "keep": cls._keep if keep else cls._nokeep,
264 264 "activebook": activebook or cls._noactivebook
265 265 }
266 266 scmutil.simplekeyvaluefile(repo.vfs, cls._filename)\
267 267 .write(info, firstline=str(cls._version))
268 268
269 269 @classmethod
270 270 def clear(cls, repo):
271 271 repo.vfs.unlinkpath(cls._filename, ignoremissing=True)
272 272
273 273 def cleanupoldbackups(repo):
274 274 vfs = vfsmod.vfs(repo.vfs.join(backupdir))
275 275 maxbackups = repo.ui.configint('shelve', 'maxbackups', 10)
276 276 hgfiles = [f for f in vfs.listdir()
277 277 if f.endswith('.' + patchextension)]
278 278 hgfiles = sorted([(vfs.stat(f).st_mtime, f) for f in hgfiles])
279 279 if 0 < maxbackups and maxbackups < len(hgfiles):
280 280 bordermtime = hgfiles[-maxbackups][0]
281 281 else:
282 282 bordermtime = None
283 283 for mtime, f in hgfiles[:len(hgfiles) - maxbackups]:
284 284 if mtime == bordermtime:
285 285 # keep it, because timestamp can't decide exact order of backups
286 286 continue
287 287 base = f[:-(1 + len(patchextension))]
288 288 for ext in shelvefileextensions:
289 289 vfs.tryunlink(base + '.' + ext)
290 290
291 291 def _backupactivebookmark(repo):
292 292 activebookmark = repo._activebookmark
293 293 if activebookmark:
294 294 bookmarks.deactivate(repo)
295 295 return activebookmark
296 296
297 297 def _restoreactivebookmark(repo, mark):
298 298 if mark:
299 299 bookmarks.activate(repo, mark)
300 300
301 301 def _aborttransaction(repo):
302 302 '''Abort current transaction for shelve/unshelve, but keep dirstate
303 303 '''
304 304 tr = repo.currenttransaction()
305 305 repo.dirstate.savebackup(tr, suffix='.shelve')
306 306 tr.abort()
307 307 repo.dirstate.restorebackup(None, suffix='.shelve')
308 308
309 309 def createcmd(ui, repo, pats, opts):
310 310 """subcommand that creates a new shelve"""
311 311 with repo.wlock():
312 312 cmdutil.checkunfinished(repo)
313 313 return _docreatecmd(ui, repo, pats, opts)
314 314
315 315 def getshelvename(repo, parent, opts):
316 316 """Decide on the name this shelve is going to have"""
317 317 def gennames():
318 318 yield label
319 319 for i in xrange(1, 100):
320 320 yield '%s-%02d' % (label, i)
321 321 name = opts.get('name')
322 322 label = repo._activebookmark or parent.branch() or 'default'
323 323 # slashes aren't allowed in filenames, therefore we rename it
324 324 label = label.replace('/', '_')
325 325 label = label.replace('\\', '_')
326 326 # filenames must not start with '.' as it should not be hidden
327 327 if label.startswith('.'):
328 328 label = label.replace('.', '_', 1)
329 329
330 330 if name:
331 331 if shelvedfile(repo, name, patchextension).exists():
332 332 e = _("a shelved change named '%s' already exists") % name
333 333 raise error.Abort(e)
334 334
335 335 # ensure we are not creating a subdirectory or a hidden file
336 336 if '/' in name or '\\' in name:
337 337 raise error.Abort(_('shelved change names can not contain slashes'))
338 338 if name.startswith('.'):
339 339 raise error.Abort(_("shelved change names can not start with '.'"))
340 340
341 341 else:
342 342 for n in gennames():
343 343 if not shelvedfile(repo, n, patchextension).exists():
344 344 name = n
345 345 break
346 346 else:
347 347 raise error.Abort(_("too many shelved changes named '%s'") % label)
348 348
349 349 return name
350 350
351 351 def mutableancestors(ctx):
352 352 """return all mutable ancestors for ctx (included)
353 353
354 354 Much faster than the revset ancestors(ctx) & draft()"""
355 355 seen = {nodemod.nullrev}
356 356 visit = collections.deque()
357 357 visit.append(ctx)
358 358 while visit:
359 359 ctx = visit.popleft()
360 360 yield ctx.node()
361 361 for parent in ctx.parents():
362 362 rev = parent.rev()
363 363 if rev not in seen:
364 364 seen.add(rev)
365 365 if parent.mutable():
366 366 visit.append(parent)
367 367
368 368 def getcommitfunc(extra, interactive, editor=False):
369 369 def commitfunc(ui, repo, message, match, opts):
370 370 hasmq = util.safehasattr(repo, 'mq')
371 371 if hasmq:
372 372 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
373 373 overrides = {('phases', 'new-commit'): phases.secret}
374 374 try:
375 375 editor_ = False
376 376 if editor:
377 377 editor_ = cmdutil.getcommiteditor(editform='shelve.shelve',
378 378 **opts)
379 379 with repo.ui.configoverride(overrides):
380 380 return repo.commit(message, shelveuser, opts.get('date'),
381 381 match, editor=editor_, extra=extra)
382 382 finally:
383 383 if hasmq:
384 384 repo.mq.checkapplied = saved
385 385
386 386 def interactivecommitfunc(ui, repo, *pats, **opts):
387 387 match = scmutil.match(repo['.'], pats, {})
388 388 message = opts['message']
389 389 return commitfunc(ui, repo, message, match, opts)
390 390
391 391 return interactivecommitfunc if interactive else commitfunc
392 392
393 393 def _nothingtoshelvemessaging(ui, repo, pats, opts):
394 394 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
395 395 if stat.deleted:
396 396 ui.status(_("nothing changed (%d missing files, see "
397 397 "'hg status')\n") % len(stat.deleted))
398 398 else:
399 399 ui.status(_("nothing changed\n"))
400 400
401 401 def _shelvecreatedcommit(repo, node, name):
402 402 bases = list(mutableancestors(repo[node]))
403 403 shelvedfile(repo, name, 'hg').writebundle(bases, node)
404 404 cmdutil.export(repo, [node],
405 405 fp=shelvedfile(repo, name, patchextension).opener('wb'),
406 406 opts=mdiff.diffopts(git=True))
407 407
408 408 def _includeunknownfiles(repo, pats, opts, extra):
409 409 s = repo.status(match=scmutil.match(repo[None], pats, opts),
410 410 unknown=True)
411 411 if s.unknown:
412 412 extra['shelve_unknown'] = '\0'.join(s.unknown)
413 413 repo[None].add(s.unknown)
414 414
415 415 def _finishshelve(repo):
416 416 _aborttransaction(repo)
417 417
418 418 def _docreatecmd(ui, repo, pats, opts):
419 419 wctx = repo[None]
420 420 parents = wctx.parents()
421 421 if len(parents) > 1:
422 422 raise error.Abort(_('cannot shelve while merging'))
423 423 parent = parents[0]
424 424 origbranch = wctx.branch()
425 425
426 426 if parent.node() != nodemod.nullid:
427 427 desc = "changes to: %s" % parent.description().split('\n', 1)[0]
428 428 else:
429 429 desc = '(changes in empty repository)'
430 430
431 431 if not opts.get('message'):
432 432 opts['message'] = desc
433 433
434 434 lock = tr = activebookmark = None
435 435 try:
436 436 lock = repo.lock()
437 437
438 438 # use an uncommitted transaction to generate the bundle to avoid
439 439 # pull races. ensure we don't print the abort message to stderr.
440 440 tr = repo.transaction('commit', report=lambda x: None)
441 441
442 442 interactive = opts.get('interactive', False)
443 443 includeunknown = (opts.get('unknown', False) and
444 444 not opts.get('addremove', False))
445 445
446 446 name = getshelvename(repo, parent, opts)
447 447 activebookmark = _backupactivebookmark(repo)
448 448 extra = {}
449 449 if includeunknown:
450 450 _includeunknownfiles(repo, pats, opts, extra)
451 451
452 452 if _iswctxonnewbranch(repo) and not _isbareshelve(pats, opts):
453 453 # In non-bare shelve we don't store newly created branch
454 454 # at bundled commit
455 455 repo.dirstate.setbranch(repo['.'].branch())
456 456
457 457 commitfunc = getcommitfunc(extra, interactive, editor=True)
458 458 if not interactive:
459 459 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
460 460 else:
461 461 node = cmdutil.dorecord(ui, repo, commitfunc, None,
462 462 False, cmdutil.recordfilter, *pats,
463 463 **opts)
464 464 if not node:
465 465 _nothingtoshelvemessaging(ui, repo, pats, opts)
466 466 return 1
467 467
468 468 _shelvecreatedcommit(repo, node, name)
469 469
470 470 if ui.formatted():
471 471 desc = util.ellipsis(desc, ui.termwidth())
472 472 ui.status(_('shelved as %s\n') % name)
473 473 hg.update(repo, parent.node())
474 474 if origbranch != repo['.'].branch() and not _isbareshelve(pats, opts):
475 475 repo.dirstate.setbranch(origbranch)
476 476
477 477 _finishshelve(repo)
478 478 finally:
479 479 _restoreactivebookmark(repo, activebookmark)
480 480 lockmod.release(tr, lock)
481 481
482 482 def _isbareshelve(pats, opts):
483 483 return (not pats
484 484 and not opts.get('interactive', False)
485 485 and not opts.get('include', False)
486 486 and not opts.get('exclude', False))
487 487
488 488 def _iswctxonnewbranch(repo):
489 489 return repo[None].branch() != repo['.'].branch()
490 490
491 491 def cleanupcmd(ui, repo):
492 492 """subcommand that deletes all shelves"""
493 493
494 494 with repo.wlock():
495 495 for (name, _type) in repo.vfs.readdir(shelvedir):
496 496 suffix = name.rsplit('.', 1)[-1]
497 497 if suffix in shelvefileextensions:
498 498 shelvedfile(repo, name).movetobackup()
499 499 cleanupoldbackups(repo)
500 500
501 501 def deletecmd(ui, repo, pats):
502 502 """subcommand that deletes a specific shelve"""
503 503 if not pats:
504 504 raise error.Abort(_('no shelved changes specified!'))
505 505 with repo.wlock():
506 506 try:
507 507 for name in pats:
508 508 for suffix in shelvefileextensions:
509 509 shfile = shelvedfile(repo, name, suffix)
510 510 # patch file is necessary, as it should
511 511 # be present for any kind of shelve,
512 512 # but the .hg file is optional as in future we
513 513 # will add obsolete shelve with does not create a
514 514 # bundle
515 515 if shfile.exists() or suffix == patchextension:
516 516 shfile.movetobackup()
517 517 cleanupoldbackups(repo)
518 518 except OSError as err:
519 519 if err.errno != errno.ENOENT:
520 520 raise
521 521 raise error.Abort(_("shelved change '%s' not found") % name)
522 522
523 523 def listshelves(repo):
524 524 """return all shelves in repo as list of (time, filename)"""
525 525 try:
526 526 names = repo.vfs.readdir(shelvedir)
527 527 except OSError as err:
528 528 if err.errno != errno.ENOENT:
529 529 raise
530 530 return []
531 531 info = []
532 532 for (name, _type) in names:
533 533 pfx, sfx = name.rsplit('.', 1)
534 534 if not pfx or sfx != patchextension:
535 535 continue
536 536 st = shelvedfile(repo, name).stat()
537 537 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
538 538 return sorted(info, reverse=True)
539 539
540 540 def listcmd(ui, repo, pats, opts):
541 541 """subcommand that displays the list of shelves"""
542 542 pats = set(pats)
543 543 width = 80
544 544 if not ui.plain():
545 545 width = ui.termwidth()
546 546 namelabel = 'shelve.newest'
547 547 ui.pager('shelve')
548 548 for mtime, name in listshelves(repo):
549 549 sname = util.split(name)[1]
550 550 if pats and sname not in pats:
551 551 continue
552 552 ui.write(sname, label=namelabel)
553 553 namelabel = 'shelve.name'
554 554 if ui.quiet:
555 555 ui.write('\n')
556 556 continue
557 557 ui.write(' ' * (16 - len(sname)))
558 558 used = 16
559 559 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
560 560 ui.write(age, label='shelve.age')
561 561 ui.write(' ' * (12 - len(age)))
562 562 used += 12
563 563 with open(name + '.' + patchextension, 'rb') as fp:
564 564 while True:
565 565 line = fp.readline()
566 566 if not line:
567 567 break
568 568 if not line.startswith('#'):
569 569 desc = line.rstrip()
570 570 if ui.formatted():
571 571 desc = util.ellipsis(desc, width - used)
572 572 ui.write(desc)
573 573 break
574 574 ui.write('\n')
575 575 if not (opts['patch'] or opts['stat']):
576 576 continue
577 577 difflines = fp.readlines()
578 578 if opts['patch']:
579 579 for chunk, label in patch.difflabel(iter, difflines):
580 580 ui.write(chunk, label=label)
581 581 if opts['stat']:
582 582 for chunk, label in patch.diffstatui(difflines, width=width):
583 583 ui.write(chunk, label=label)
584 584
585 585 def patchcmds(ui, repo, pats, opts, subcommand):
586 586 """subcommand that displays shelves"""
587 587 if len(pats) == 0:
588 588 raise error.Abort(_("--%s expects at least one shelf") % subcommand)
589 589
590 590 for shelfname in pats:
591 591 if not shelvedfile(repo, shelfname, patchextension).exists():
592 592 raise error.Abort(_("cannot find shelf %s") % shelfname)
593 593
594 594 listcmd(ui, repo, pats, opts)
595 595
596 596 def checkparents(repo, state):
597 597 """check parent while resuming an unshelve"""
598 598 if state.parents != repo.dirstate.parents():
599 599 raise error.Abort(_('working directory parents do not match unshelve '
600 600 'state'))
601 601
602 602 def pathtofiles(repo, files):
603 603 cwd = repo.getcwd()
604 604 return [repo.pathto(f, cwd) for f in files]
605 605
606 606 def unshelveabort(ui, repo, state, opts):
607 607 """subcommand that abort an in-progress unshelve"""
608 608 with repo.lock():
609 609 try:
610 610 checkparents(repo, state)
611 611
612 612 repo.vfs.rename('unshelverebasestate', 'rebasestate')
613 613 try:
614 614 rebase.rebase(ui, repo, **{
615 615 'abort' : True
616 616 })
617 617 except Exception:
618 618 repo.vfs.rename('rebasestate', 'unshelverebasestate')
619 619 raise
620 620
621 621 mergefiles(ui, repo, state.wctx, state.pendingctx)
622 622 repair.strip(ui, repo, state.nodestoremove, backup=False,
623 623 topic='shelve')
624 624 finally:
625 625 shelvedstate.clear(repo)
626 626 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
627 627
628 628 def mergefiles(ui, repo, wctx, shelvectx):
629 629 """updates to wctx and merges the changes from shelvectx into the
630 630 dirstate."""
631 631 with ui.configoverride({('ui', 'quiet'): True}):
632 632 hg.update(repo, wctx.node())
633 633 files = []
634 634 files.extend(shelvectx.files())
635 635 files.extend(shelvectx.parents()[0].files())
636 636
637 637 # revert will overwrite unknown files, so move them out of the way
638 638 for file in repo.status(unknown=True).unknown:
639 639 if file in files:
640 640 util.rename(file, scmutil.origpath(ui, repo, file))
641 641 ui.pushbuffer(True)
642 642 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
643 643 *pathtofiles(repo, files),
644 644 **{'no_backup': True})
645 645 ui.popbuffer()
646 646
647 647 def restorebranch(ui, repo, branchtorestore):
648 648 if branchtorestore and branchtorestore != repo.dirstate.branch():
649 649 repo.dirstate.setbranch(branchtorestore)
650 650 ui.status(_('marked working directory as branch %s\n')
651 651 % branchtorestore)
652 652
653 653 def unshelvecleanup(ui, repo, name, opts):
654 654 """remove related files after an unshelve"""
655 655 if not opts.get('keep'):
656 656 for filetype in shelvefileextensions:
657 657 shfile = shelvedfile(repo, name, filetype)
658 658 if shfile.exists():
659 659 shfile.movetobackup()
660 660 cleanupoldbackups(repo)
661 661
662 662 def unshelvecontinue(ui, repo, state, opts):
663 663 """subcommand to continue an in-progress unshelve"""
664 664 # We're finishing off a merge. First parent is our original
665 665 # parent, second is the temporary "fake" commit we're unshelving.
666 666 with repo.lock():
667 667 checkparents(repo, state)
668 668 ms = merge.mergestate.read(repo)
669 669 if [f for f in ms if ms[f] == 'u']:
670 670 raise error.Abort(
671 671 _("unresolved conflicts, can't continue"),
672 672 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
673 673
674 674 repo.vfs.rename('unshelverebasestate', 'rebasestate')
675 675 try:
676 676 rebase.rebase(ui, repo, **{
677 677 'continue' : True
678 678 })
679 679 except Exception:
680 680 repo.vfs.rename('rebasestate', 'unshelverebasestate')
681 681 raise
682 682
683 683 shelvectx = repo['tip']
684 684 if not shelvectx in state.pendingctx.children():
685 685 # rebase was a no-op, so it produced no child commit
686 686 shelvectx = state.pendingctx
687 687 else:
688 688 # only strip the shelvectx if the rebase produced it
689 689 state.nodestoremove.append(shelvectx.node())
690 690
691 691 mergefiles(ui, repo, state.wctx, shelvectx)
692 692 restorebranch(ui, repo, state.branchtorestore)
693 693
694 694 repair.strip(ui, repo, state.nodestoremove, backup=False,
695 695 topic='shelve')
696 696 _restoreactivebookmark(repo, state.activebookmark)
697 697 shelvedstate.clear(repo)
698 698 unshelvecleanup(ui, repo, state.name, opts)
699 699 ui.status(_("unshelve of '%s' complete\n") % state.name)
700 700
701 701 def _commitworkingcopychanges(ui, repo, opts, tmpwctx):
702 702 """Temporarily commit working copy changes before moving unshelve commit"""
703 703 # Store pending changes in a commit and remember added in case a shelve
704 704 # contains unknown files that are part of the pending change
705 705 s = repo.status()
706 706 addedbefore = frozenset(s.added)
707 707 if not (s.modified or s.added or s.removed):
708 708 return tmpwctx, addedbefore
709 709 ui.status(_("temporarily committing pending changes "
710 710 "(restore with 'hg unshelve --abort')\n"))
711 711 commitfunc = getcommitfunc(extra=None, interactive=False,
712 712 editor=False)
713 713 tempopts = {}
714 714 tempopts['message'] = "pending changes temporary commit"
715 715 tempopts['date'] = opts.get('date')
716 716 with ui.configoverride({('ui', 'quiet'): True}):
717 717 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
718 718 tmpwctx = repo[node]
719 719 return tmpwctx, addedbefore
720 720
721 721 def _unshelverestorecommit(ui, repo, basename):
722 722 """Recreate commit in the repository during the unshelve"""
723 723 with ui.configoverride({('ui', 'quiet'): True}):
724 724 shelvedfile(repo, basename, 'hg').applybundle()
725 725 shelvectx = repo['tip']
726 726 return repo, shelvectx
727 727
728 728 def _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, basename, pctx,
729 729 tmpwctx, shelvectx, branchtorestore,
730 730 activebookmark):
731 731 """Rebase restored commit from its original location to a destination"""
732 732 # If the shelve is not immediately on top of the commit
733 733 # we'll be merging with, rebase it to be on top.
734 734 if tmpwctx.node() == shelvectx.parents()[0].node():
735 735 return shelvectx
736 736
737 737 ui.status(_('rebasing shelved changes\n'))
738 738 try:
739 739 rebase.rebase(ui, repo, **{
740 740 'rev': [shelvectx.rev()],
741 741 'dest': str(tmpwctx.rev()),
742 742 'keep': True,
743 743 'tool': opts.get('tool', ''),
744 744 })
745 745 except error.InterventionRequired:
746 746 tr.close()
747 747
748 748 nodestoremove = [repo.changelog.node(rev)
749 749 for rev in xrange(oldtiprev, len(repo))]
750 750 shelvedstate.save(repo, basename, pctx, tmpwctx, nodestoremove,
751 751 branchtorestore, opts.get('keep'), activebookmark)
752 752
753 753 repo.vfs.rename('rebasestate', 'unshelverebasestate')
754 754 raise error.InterventionRequired(
755 755 _("unresolved conflicts (see 'hg resolve', then "
756 756 "'hg unshelve --continue')"))
757 757
758 758 # refresh ctx after rebase completes
759 759 shelvectx = repo['tip']
760 760
761 761 if not shelvectx in tmpwctx.children():
762 762 # rebase was a no-op, so it produced no child commit
763 763 shelvectx = tmpwctx
764 764 return shelvectx
765 765
766 766 def _forgetunknownfiles(repo, shelvectx, addedbefore):
767 767 # Forget any files that were unknown before the shelve, unknown before
768 768 # unshelve started, but are now added.
769 769 shelveunknown = shelvectx.extra().get('shelve_unknown')
770 770 if not shelveunknown:
771 771 return
772 772 shelveunknown = frozenset(shelveunknown.split('\0'))
773 773 addedafter = frozenset(repo.status().added)
774 774 toforget = (addedafter & shelveunknown) - addedbefore
775 775 repo[None].forget(toforget)
776 776
777 777 def _finishunshelve(repo, oldtiprev, tr, activebookmark):
778 778 _restoreactivebookmark(repo, activebookmark)
779 779 # The transaction aborting will strip all the commits for us,
780 780 # but it doesn't update the inmemory structures, so addchangegroup
781 781 # hooks still fire and try to operate on the missing commits.
782 782 # Clean up manually to prevent this.
783 783 repo.unfiltered().changelog.strip(oldtiprev, tr)
784 784 _aborttransaction(repo)
785 785
786 786 def _checkunshelveuntrackedproblems(ui, repo, shelvectx):
787 787 """Check potential problems which may result from working
788 788 copy having untracked changes."""
789 789 wcdeleted = set(repo.status().deleted)
790 790 shelvetouched = set(shelvectx.files())
791 791 intersection = wcdeleted.intersection(shelvetouched)
792 792 if intersection:
793 793 m = _("shelved change touches missing files")
794 794 hint = _("run hg status to see which files are missing")
795 795 raise error.Abort(m, hint=hint)
796 796
797 797 @command('unshelve',
798 798 [('a', 'abort', None,
799 799 _('abort an incomplete unshelve operation')),
800 800 ('c', 'continue', None,
801 801 _('continue an incomplete unshelve operation')),
802 802 ('k', 'keep', None,
803 803 _('keep shelve after unshelving')),
804 804 ('n', 'name', '',
805 805 _('restore shelved change with given name'), _('NAME')),
806 806 ('t', 'tool', '', _('specify merge tool')),
807 807 ('', 'date', '',
808 808 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
809 809 _('hg unshelve [[-n] SHELVED]'))
810 810 def unshelve(ui, repo, *shelved, **opts):
811 811 """restore a shelved change to the working directory
812 812
813 813 This command accepts an optional name of a shelved change to
814 814 restore. If none is given, the most recent shelved change is used.
815 815
816 816 If a shelved change is applied successfully, the bundle that
817 817 contains the shelved changes is moved to a backup location
818 818 (.hg/shelve-backup).
819 819
820 820 Since you can restore a shelved change on top of an arbitrary
821 821 commit, it is possible that unshelving will result in a conflict
822 822 between your changes and the commits you are unshelving onto. If
823 823 this occurs, you must resolve the conflict, then use
824 824 ``--continue`` to complete the unshelve operation. (The bundle
825 825 will not be moved until you successfully complete the unshelve.)
826 826
827 827 (Alternatively, you can use ``--abort`` to abandon an unshelve
828 828 that causes a conflict. This reverts the unshelved changes, and
829 829 leaves the bundle in place.)
830 830
831 831 If bare shelved change(when no files are specified, without interactive,
832 832 include and exclude option) was done on newly created branch it would
833 833 restore branch information to the working directory.
834 834
835 835 After a successful unshelve, the shelved changes are stored in a
836 836 backup directory. Only the N most recent backups are kept. N
837 837 defaults to 10 but can be overridden using the ``shelve.maxbackups``
838 838 configuration option.
839 839
840 840 .. container:: verbose
841 841
842 842 Timestamp in seconds is used to decide order of backups. More
843 843 than ``maxbackups`` backups are kept, if same timestamp
844 844 prevents from deciding exact order of them, for safety.
845 845 """
846 846 with repo.wlock():
847 847 return _dounshelve(ui, repo, *shelved, **opts)
848 848
849 849 def _dounshelve(ui, repo, *shelved, **opts):
850 850 abortf = opts.get('abort')
851 851 continuef = opts.get('continue')
852 852 if not abortf and not continuef:
853 853 cmdutil.checkunfinished(repo)
854 854 shelved = list(shelved)
855 855 if opts.get("name"):
856 856 shelved.append(opts["name"])
857 857
858 858 if abortf or continuef:
859 859 if abortf and continuef:
860 860 raise error.Abort(_('cannot use both abort and continue'))
861 861 if shelved:
862 862 raise error.Abort(_('cannot combine abort/continue with '
863 863 'naming a shelved change'))
864 864 if abortf and opts.get('tool', False):
865 865 ui.warn(_('tool option will be ignored\n'))
866 866
867 867 try:
868 868 state = shelvedstate.load(repo)
869 869 if opts.get('keep') is None:
870 870 opts['keep'] = state.keep
871 871 except IOError as err:
872 872 if err.errno != errno.ENOENT:
873 873 raise
874 874 cmdutil.wrongtooltocontinue(repo, _('unshelve'))
875 875 except error.CorruptedState as err:
876 876 ui.debug(str(err) + '\n')
877 877 if continuef:
878 878 msg = _('corrupted shelved state file')
879 879 hint = _('please run hg unshelve --abort to abort unshelve '
880 880 'operation')
881 881 raise error.Abort(msg, hint=hint)
882 882 elif abortf:
883 883 msg = _('could not read shelved state file, your working copy '
884 884 'may be in an unexpected state\nplease update to some '
885 885 'commit\n')
886 886 ui.warn(msg)
887 887 shelvedstate.clear(repo)
888 888 return
889 889
890 890 if abortf:
891 891 return unshelveabort(ui, repo, state, opts)
892 892 elif continuef:
893 893 return unshelvecontinue(ui, repo, state, opts)
894 894 elif len(shelved) > 1:
895 895 raise error.Abort(_('can only unshelve one change at a time'))
896 896 elif not shelved:
897 897 shelved = listshelves(repo)
898 898 if not shelved:
899 899 raise error.Abort(_('no shelved changes to apply!'))
900 900 basename = util.split(shelved[0][1])[1]
901 901 ui.status(_("unshelving change '%s'\n") % basename)
902 902 else:
903 903 basename = shelved[0]
904 904
905 905 if not shelvedfile(repo, basename, patchextension).exists():
906 906 raise error.Abort(_("shelved change '%s' not found") % basename)
907 907
908 908 lock = tr = None
909 909 try:
910 910 lock = repo.lock()
911 911 tr = repo.transaction('unshelve', report=lambda x: None)
912 912 oldtiprev = len(repo)
913 913
914 914 pctx = repo['.']
915 915 tmpwctx = pctx
916 916 # The goal is to have a commit structure like so:
917 917 # ...-> pctx -> tmpwctx -> shelvectx
918 918 # where tmpwctx is an optional commit with the user's pending changes
919 919 # and shelvectx is the unshelved changes. Then we merge it all down
920 920 # to the original pctx.
921 921
922 922 activebookmark = _backupactivebookmark(repo)
923 923 overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
924 924 with ui.configoverride(overrides, 'unshelve'):
925 925 tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts,
926 926 tmpwctx)
927 927 repo, shelvectx = _unshelverestorecommit(ui, repo, basename)
928 928 _checkunshelveuntrackedproblems(ui, repo, shelvectx)
929 929 branchtorestore = ''
930 930 if shelvectx.branch() != shelvectx.p1().branch():
931 931 branchtorestore = shelvectx.branch()
932 932
933 933 shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
934 934 basename, pctx, tmpwctx,
935 935 shelvectx, branchtorestore,
936 936 activebookmark)
937 937 mergefiles(ui, repo, pctx, shelvectx)
938 938 restorebranch(ui, repo, branchtorestore)
939 939 _forgetunknownfiles(repo, shelvectx, addedbefore)
940 940
941 941 shelvedstate.clear(repo)
942 942 _finishunshelve(repo, oldtiprev, tr, activebookmark)
943 943 unshelvecleanup(ui, repo, basename, opts)
944 944 finally:
945 945 if tr:
946 946 tr.release()
947 947 lockmod.release(lock)
948 948
949 949 @command('shelve',
950 950 [('A', 'addremove', None,
951 951 _('mark new/missing files as added/removed before shelving')),
952 952 ('u', 'unknown', None,
953 953 _('store unknown files in the shelve')),
954 954 ('', 'cleanup', None,
955 955 _('delete all shelved changes')),
956 956 ('', 'date', '',
957 957 _('shelve with the specified commit date'), _('DATE')),
958 958 ('d', 'delete', None,
959 959 _('delete the named shelved change(s)')),
960 960 ('e', 'edit', False,
961 961 _('invoke editor on commit messages')),
962 962 ('l', 'list', None,
963 963 _('list current shelves')),
964 964 ('m', 'message', '',
965 965 _('use text as shelve message'), _('TEXT')),
966 966 ('n', 'name', '',
967 967 _('use the given name for the shelved commit'), _('NAME')),
968 968 ('p', 'patch', None,
969 969 _('show patch')),
970 970 ('i', 'interactive', None,
971 971 _('interactive mode, only works while creating a shelve')),
972 972 ('', 'stat', None,
973 973 _('output diffstat-style summary of changes'))] + cmdutil.walkopts,
974 974 _('hg shelve [OPTION]... [FILE]...'))
975 975 def shelvecmd(ui, repo, *pats, **opts):
976 976 '''save and set aside changes from the working directory
977 977
978 978 Shelving takes files that "hg status" reports as not clean, saves
979 979 the modifications to a bundle (a shelved change), and reverts the
980 980 files so that their state in the working directory becomes clean.
981 981
982 982 To restore these changes to the working directory, using "hg
983 983 unshelve"; this will work even if you switch to a different
984 984 commit.
985 985
986 986 When no files are specified, "hg shelve" saves all not-clean
987 987 files. If specific files or directories are named, only changes to
988 988 those files are shelved.
989 989
990 990 In bare shelve (when no files are specified, without interactive,
991 991 include and exclude option), shelving remembers information if the
992 992 working directory was on newly created branch, in other words working
993 993 directory was on different branch than its first parent. In this
994 994 situation unshelving restores branch information to the working directory.
995 995
996 996 Each shelved change has a name that makes it easier to find later.
997 997 The name of a shelved change defaults to being based on the active
998 998 bookmark, or if there is no active bookmark, the current named
999 999 branch. To specify a different name, use ``--name``.
1000 1000
1001 1001 To see a list of existing shelved changes, use the ``--list``
1002 1002 option. For each shelved change, this will print its name, age,
1003 1003 and description; use ``--patch`` or ``--stat`` for more details.
1004 1004
1005 1005 To delete specific shelved changes, use ``--delete``. To delete
1006 1006 all shelved changes, use ``--cleanup``.
1007 1007 '''
1008 1008 allowables = [
1009 1009 ('addremove', {'create'}), # 'create' is pseudo action
1010 1010 ('unknown', {'create'}),
1011 1011 ('cleanup', {'cleanup'}),
1012 1012 # ('date', {'create'}), # ignored for passing '--date "0 0"' in tests
1013 1013 ('delete', {'delete'}),
1014 1014 ('edit', {'create'}),
1015 1015 ('list', {'list'}),
1016 1016 ('message', {'create'}),
1017 1017 ('name', {'create'}),
1018 1018 ('patch', {'patch', 'list'}),
1019 1019 ('stat', {'stat', 'list'}),
1020 1020 ]
1021 1021 def checkopt(opt):
1022 1022 if opts.get(opt):
1023 1023 for i, allowable in allowables:
1024 1024 if opts[i] and opt not in allowable:
1025 1025 raise error.Abort(_("options '--%s' and '--%s' may not be "
1026 1026 "used together") % (opt, i))
1027 1027 return True
1028 1028 if checkopt('cleanup'):
1029 1029 if pats:
1030 1030 raise error.Abort(_("cannot specify names when using '--cleanup'"))
1031 1031 return cleanupcmd(ui, repo)
1032 1032 elif checkopt('delete'):
1033 1033 return deletecmd(ui, repo, pats)
1034 1034 elif checkopt('list'):
1035 1035 return listcmd(ui, repo, pats, opts)
1036 1036 elif checkopt('patch'):
1037 1037 return patchcmds(ui, repo, pats, opts, subcommand='patch')
1038 1038 elif checkopt('stat'):
1039 1039 return patchcmds(ui, repo, pats, opts, subcommand='stat')
1040 1040 else:
1041 1041 return createcmd(ui, repo, pats, opts)
1042 1042
1043 1043 def extsetup(ui):
1044 1044 cmdutil.unfinishedstates.append(
1045 1045 [shelvedstate._filename, False, False,
1046 1046 _('unshelve already in progress'),
1047 1047 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
1048 1048 cmdutil.afterresolvedstates.append(
1049 1049 [shelvedstate._filename, _('hg unshelve --continue')])
General Comments 0
You need to be logged in to leave comments. Login now