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