##// END OF EJS Templates
histedit: limit cleanup of histedit-last-edit.txt to success
timeless -
r27546:c00924c5 default
parent child Browse files
Show More
@@ -1,1458 +1,1458 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
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
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. For
75 75 this example, let's assume that the commit message was changed to
76 76 ``Add beta and delta.`` After histedit has run and had a chance to
77 77 remove any old or temporary revisions it needed, the history looks
78 78 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. You'll be prompted for a
101 101 new commit message, but the default commit message will be the
102 102 original message for the ``edit`` ed revision.
103 103
104 104 The ``message`` operation will give you a chance to revise a commit
105 105 message without changing the contents. It's a shortcut for doing
106 106 ``edit`` immediately followed by `hg histedit --continue``.
107 107
108 108 If ``histedit`` encounters a conflict when moving a revision (while
109 109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 110 ``edit`` with the difference that it won't prompt you for a commit
111 111 message when done. If you decide at this point that you don't like how
112 112 much work it will be to rearrange history, or that you made a mistake,
113 113 you can use ``hg histedit --abort`` to abandon the new changes you
114 114 have made and return to the state before you attempted to edit your
115 115 history.
116 116
117 117 If we clone the histedit-ed example repository above and add four more
118 118 changes, such that we have the following history::
119 119
120 120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 121 | Add theta
122 122 |
123 123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 124 | Add eta
125 125 |
126 126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 127 | Add zeta
128 128 |
129 129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 130 | Add epsilon
131 131 |
132 132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 133 | Add beta and delta.
134 134 |
135 135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 136 | Add gamma
137 137 |
138 138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 139 Add alpha
140 140
141 141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 143 repository that Mercurial does not detect to be related to the source
144 144 repo, you can add a ``--force`` option.
145 145
146 146 Config
147 147 ------
148 148
149 149 Histedit rule lines are truncated to 80 characters by default. You
150 150 can customize this behavior by setting a different length in your
151 151 configuration file::
152 152
153 153 [histedit]
154 154 linelen = 120 # truncate rule lines at 120 characters
155 155
156 156 ``hg histedit`` attempts to automatically choose an appropriate base
157 157 revision to use. To change which base revision is used, define a
158 158 revset in your configuration file::
159 159
160 160 [histedit]
161 161 defaultrev = only(.) & draft()
162 162
163 163 By default each edited revision needs to be present in histedit commands.
164 164 To remove revision you need to use ``drop`` operation. You can configure
165 165 the drop to be implicit for missing commits by adding:
166 166
167 167 [histedit]
168 168 dropmissing = True
169 169
170 170 """
171 171
172 172 import pickle
173 173 import errno
174 174 import os
175 175 import sys
176 176
177 177 from mercurial import bundle2
178 178 from mercurial import cmdutil
179 179 from mercurial import discovery
180 180 from mercurial import error
181 181 from mercurial import copies
182 182 from mercurial import context
183 183 from mercurial import destutil
184 184 from mercurial import exchange
185 185 from mercurial import extensions
186 186 from mercurial import hg
187 187 from mercurial import node
188 188 from mercurial import repair
189 189 from mercurial import scmutil
190 190 from mercurial import util
191 191 from mercurial import obsolete
192 192 from mercurial import merge as mergemod
193 193 from mercurial.lock import release
194 194 from mercurial.i18n import _
195 195
196 196 cmdtable = {}
197 197 command = cmdutil.command(cmdtable)
198 198
199 199 class _constraints(object):
200 200 # aborts if there are multiple rules for one node
201 201 noduplicates = 'noduplicates'
202 202 # abort if the node does belong to edited stack
203 203 forceother = 'forceother'
204 204 # abort if the node doesn't belong to edited stack
205 205 noother = 'noother'
206 206
207 207 @classmethod
208 208 def known(cls):
209 209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
210 210
211 211 # Note for extension authors: ONLY specify testedwith = 'internal' for
212 212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
213 213 # be specifying the version(s) of Mercurial they are tested with, or
214 214 # leave the attribute unspecified.
215 215 testedwith = 'internal'
216 216
217 217 # i18n: command names and abbreviations must remain untranslated
218 218 editcomment = _("""# Edit history between %s and %s
219 219 #
220 220 # Commits are listed from least to most recent
221 221 #
222 222 # Commands:
223 223 # p, pick = use commit
224 224 # e, edit = use commit, but stop for amending
225 225 # f, fold = use commit, but combine it with the one above
226 226 # r, roll = like fold, but discard this commit's description
227 227 # d, drop = remove commit from history
228 228 # m, mess = edit commit message without changing commit content
229 229 #
230 230 """)
231 231
232 232 class histeditstate(object):
233 233 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
234 234 topmost=None, replacements=None, lock=None, wlock=None):
235 235 self.repo = repo
236 236 self.actions = actions
237 237 self.keep = keep
238 238 self.topmost = topmost
239 239 self.parentctxnode = parentctxnode
240 240 self.lock = lock
241 241 self.wlock = wlock
242 242 self.backupfile = None
243 243 if replacements is None:
244 244 self.replacements = []
245 245 else:
246 246 self.replacements = replacements
247 247
248 248 def read(self):
249 249 """Load histedit state from disk and set fields appropriately."""
250 250 try:
251 251 state = self.repo.vfs.read('histedit-state')
252 252 except IOError as err:
253 253 if err.errno != errno.ENOENT:
254 254 raise
255 255 raise error.Abort(_('no histedit in progress'))
256 256
257 257 if state.startswith('v1\n'):
258 258 data = self._load()
259 259 parentctxnode, rules, keep, topmost, replacements, backupfile = data
260 260 else:
261 261 data = pickle.loads(state)
262 262 parentctxnode, rules, keep, topmost, replacements = data
263 263 backupfile = None
264 264
265 265 self.parentctxnode = parentctxnode
266 266 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
267 267 actions = parserules(rules, self)
268 268 self.actions = actions
269 269 self.keep = keep
270 270 self.topmost = topmost
271 271 self.replacements = replacements
272 272 self.backupfile = backupfile
273 273
274 274 def write(self):
275 275 fp = self.repo.vfs('histedit-state', 'w')
276 276 fp.write('v1\n')
277 277 fp.write('%s\n' % node.hex(self.parentctxnode))
278 278 fp.write('%s\n' % node.hex(self.topmost))
279 279 fp.write('%s\n' % self.keep)
280 280 fp.write('%d\n' % len(self.actions))
281 281 for action in self.actions:
282 282 fp.write('%s\n' % action.tostate())
283 283 fp.write('%d\n' % len(self.replacements))
284 284 for replacement in self.replacements:
285 285 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
286 286 for r in replacement[1])))
287 287 backupfile = self.backupfile
288 288 if not backupfile:
289 289 backupfile = ''
290 290 fp.write('%s\n' % backupfile)
291 291 fp.close()
292 292
293 293 def _load(self):
294 294 fp = self.repo.vfs('histedit-state', 'r')
295 295 lines = [l[:-1] for l in fp.readlines()]
296 296
297 297 index = 0
298 298 lines[index] # version number
299 299 index += 1
300 300
301 301 parentctxnode = node.bin(lines[index])
302 302 index += 1
303 303
304 304 topmost = node.bin(lines[index])
305 305 index += 1
306 306
307 307 keep = lines[index] == 'True'
308 308 index += 1
309 309
310 310 # Rules
311 311 rules = []
312 312 rulelen = int(lines[index])
313 313 index += 1
314 314 for i in xrange(rulelen):
315 315 ruleaction = lines[index]
316 316 index += 1
317 317 rule = lines[index]
318 318 index += 1
319 319 rules.append((ruleaction, rule))
320 320
321 321 # Replacements
322 322 replacements = []
323 323 replacementlen = int(lines[index])
324 324 index += 1
325 325 for i in xrange(replacementlen):
326 326 replacement = lines[index]
327 327 original = node.bin(replacement[:40])
328 328 succ = [node.bin(replacement[i:i + 40]) for i in
329 329 range(40, len(replacement), 40)]
330 330 replacements.append((original, succ))
331 331 index += 1
332 332
333 333 backupfile = lines[index]
334 334 index += 1
335 335
336 336 fp.close()
337 337
338 338 return parentctxnode, rules, keep, topmost, replacements, backupfile
339 339
340 340 def clear(self):
341 341 if self.inprogress():
342 342 self.repo.vfs.unlink('histedit-state')
343 343
344 344 def inprogress(self):
345 345 return self.repo.vfs.exists('histedit-state')
346 346
347 347
348 348 class histeditaction(object):
349 349 def __init__(self, state, node):
350 350 self.state = state
351 351 self.repo = state.repo
352 352 self.node = node
353 353
354 354 @classmethod
355 355 def fromrule(cls, state, rule):
356 356 """Parses the given rule, returning an instance of the histeditaction.
357 357 """
358 358 rulehash = rule.strip().split(' ', 1)[0]
359 359 return cls(state, node.bin(rulehash))
360 360
361 361 def verify(self, prev):
362 362 """ Verifies semantic correctness of the rule"""
363 363 repo = self.repo
364 364 ha = node.hex(self.node)
365 365 try:
366 366 self.node = repo[ha].node()
367 367 except error.RepoError:
368 368 raise error.ParseError(_('unknown changeset %s listed')
369 369 % ha[:12])
370 370
371 371 def torule(self):
372 372 """build a histedit rule line for an action
373 373
374 374 by default lines are in the form:
375 375 <hash> <rev> <summary>
376 376 """
377 377 ctx = self.repo[self.node]
378 378 summary = ''
379 379 if ctx.description():
380 380 summary = ctx.description().splitlines()[0]
381 381 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
382 382 # trim to 75 columns by default so it's not stupidly wide in my editor
383 383 # (the 5 more are left for verb)
384 384 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
385 385 maxlen = max(maxlen, 22) # avoid truncating hash
386 386 return util.ellipsis(line, maxlen)
387 387
388 388 def tostate(self):
389 389 """Print an action in format used by histedit state files
390 390 (the first line is a verb, the remainder is the second)
391 391 """
392 392 return "%s\n%s" % (self.verb, node.hex(self.node))
393 393
394 394 def constraints(self):
395 395 """Return a set of constrains that this action should be verified for
396 396 """
397 397 return set([_constraints.noduplicates, _constraints.noother])
398 398
399 399 def nodetoverify(self):
400 400 """Returns a node associated with the action that will be used for
401 401 verification purposes.
402 402
403 403 If the action doesn't correspond to node it should return None
404 404 """
405 405 return self.node
406 406
407 407 def run(self):
408 408 """Runs the action. The default behavior is simply apply the action's
409 409 rulectx onto the current parentctx."""
410 410 self.applychange()
411 411 self.continuedirty()
412 412 return self.continueclean()
413 413
414 414 def applychange(self):
415 415 """Applies the changes from this action's rulectx onto the current
416 416 parentctx, but does not commit them."""
417 417 repo = self.repo
418 418 rulectx = repo[self.node]
419 419 hg.update(repo, self.state.parentctxnode, quietempty=True)
420 420 stats = applychanges(repo.ui, repo, rulectx, {})
421 421 if stats and stats[3] > 0:
422 422 raise error.InterventionRequired(_('Fix up the change and run '
423 423 'hg histedit --continue'))
424 424
425 425 def continuedirty(self):
426 426 """Continues the action when changes have been applied to the working
427 427 copy. The default behavior is to commit the dirty changes."""
428 428 repo = self.repo
429 429 rulectx = repo[self.node]
430 430
431 431 editor = self.commiteditor()
432 432 commit = commitfuncfor(repo, rulectx)
433 433
434 434 commit(text=rulectx.description(), user=rulectx.user(),
435 435 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
436 436
437 437 def commiteditor(self):
438 438 """The editor to be used to edit the commit message."""
439 439 return False
440 440
441 441 def continueclean(self):
442 442 """Continues the action when the working copy is clean. The default
443 443 behavior is to accept the current commit as the new version of the
444 444 rulectx."""
445 445 ctx = self.repo['.']
446 446 if ctx.node() == self.state.parentctxnode:
447 447 self.repo.ui.warn(_('%s: empty changeset\n') %
448 448 node.short(self.node))
449 449 return ctx, [(self.node, tuple())]
450 450 if ctx.node() == self.node:
451 451 # Nothing changed
452 452 return ctx, []
453 453 return ctx, [(self.node, (ctx.node(),))]
454 454
455 455 def commitfuncfor(repo, src):
456 456 """Build a commit function for the replacement of <src>
457 457
458 458 This function ensure we apply the same treatment to all changesets.
459 459
460 460 - Add a 'histedit_source' entry in extra.
461 461
462 462 Note that fold has its own separated logic because its handling is a bit
463 463 different and not easily factored out of the fold method.
464 464 """
465 465 phasemin = src.phase()
466 466 def commitfunc(**kwargs):
467 467 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
468 468 try:
469 469 repo.ui.setconfig('phases', 'new-commit', phasemin,
470 470 'histedit')
471 471 extra = kwargs.get('extra', {}).copy()
472 472 extra['histedit_source'] = src.hex()
473 473 kwargs['extra'] = extra
474 474 return repo.commit(**kwargs)
475 475 finally:
476 476 repo.ui.restoreconfig(phasebackup)
477 477 return commitfunc
478 478
479 479 def applychanges(ui, repo, ctx, opts):
480 480 """Merge changeset from ctx (only) in the current working directory"""
481 481 wcpar = repo.dirstate.parents()[0]
482 482 if ctx.p1().node() == wcpar:
483 483 # edits are "in place" we do not need to make any merge,
484 484 # just applies changes on parent for edition
485 485 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
486 486 stats = None
487 487 else:
488 488 try:
489 489 # ui.forcemerge is an internal variable, do not document
490 490 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
491 491 'histedit')
492 492 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
493 493 finally:
494 494 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
495 495 return stats
496 496
497 497 def collapse(repo, first, last, commitopts, skipprompt=False):
498 498 """collapse the set of revisions from first to last as new one.
499 499
500 500 Expected commit options are:
501 501 - message
502 502 - date
503 503 - username
504 504 Commit message is edited in all cases.
505 505
506 506 This function works in memory."""
507 507 ctxs = list(repo.set('%d::%d', first, last))
508 508 if not ctxs:
509 509 return None
510 510 for c in ctxs:
511 511 if not c.mutable():
512 512 raise error.ParseError(
513 513 _("cannot fold into public change %s") % node.short(c.node()))
514 514 base = first.parents()[0]
515 515
516 516 # commit a new version of the old changeset, including the update
517 517 # collect all files which might be affected
518 518 files = set()
519 519 for ctx in ctxs:
520 520 files.update(ctx.files())
521 521
522 522 # Recompute copies (avoid recording a -> b -> a)
523 523 copied = copies.pathcopies(base, last)
524 524
525 525 # prune files which were reverted by the updates
526 526 def samefile(f):
527 527 if f in last.manifest():
528 528 a = last.filectx(f)
529 529 if f in base.manifest():
530 530 b = base.filectx(f)
531 531 return (a.data() == b.data()
532 532 and a.flags() == b.flags())
533 533 else:
534 534 return False
535 535 else:
536 536 return f not in base.manifest()
537 537 files = [f for f in files if not samefile(f)]
538 538 # commit version of these files as defined by head
539 539 headmf = last.manifest()
540 540 def filectxfn(repo, ctx, path):
541 541 if path in headmf:
542 542 fctx = last[path]
543 543 flags = fctx.flags()
544 544 mctx = context.memfilectx(repo,
545 545 fctx.path(), fctx.data(),
546 546 islink='l' in flags,
547 547 isexec='x' in flags,
548 548 copied=copied.get(path))
549 549 return mctx
550 550 return None
551 551
552 552 if commitopts.get('message'):
553 553 message = commitopts['message']
554 554 else:
555 555 message = first.description()
556 556 user = commitopts.get('user')
557 557 date = commitopts.get('date')
558 558 extra = commitopts.get('extra')
559 559
560 560 parents = (first.p1().node(), first.p2().node())
561 561 editor = None
562 562 if not skipprompt:
563 563 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
564 564 new = context.memctx(repo,
565 565 parents=parents,
566 566 text=message,
567 567 files=files,
568 568 filectxfn=filectxfn,
569 569 user=user,
570 570 date=date,
571 571 extra=extra,
572 572 editor=editor)
573 573 return repo.commitctx(new)
574 574
575 575 def _isdirtywc(repo):
576 576 return repo[None].dirty(missing=True)
577 577
578 578 def abortdirty():
579 579 raise error.Abort(_('working copy has pending changes'),
580 580 hint=_('amend, commit, or revert them and run histedit '
581 581 '--continue, or abort with histedit --abort'))
582 582
583 583
584 584 actiontable = {}
585 585 actionlist = []
586 586
587 587 def addhisteditaction(verbs):
588 588 def wrap(cls):
589 589 cls.verb = verbs[0]
590 590 for verb in verbs:
591 591 actiontable[verb] = cls
592 592 actionlist.append(cls)
593 593 return cls
594 594 return wrap
595 595
596 596
597 597 @addhisteditaction(['pick', 'p'])
598 598 class pick(histeditaction):
599 599 def run(self):
600 600 rulectx = self.repo[self.node]
601 601 if rulectx.parents()[0].node() == self.state.parentctxnode:
602 602 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
603 603 return rulectx, []
604 604
605 605 return super(pick, self).run()
606 606
607 607 @addhisteditaction(['edit', 'e'])
608 608 class edit(histeditaction):
609 609 def run(self):
610 610 repo = self.repo
611 611 rulectx = repo[self.node]
612 612 hg.update(repo, self.state.parentctxnode, quietempty=True)
613 613 applychanges(repo.ui, repo, rulectx, {})
614 614 raise error.InterventionRequired(
615 615 _('Make changes as needed, you may commit or record as needed '
616 616 'now.\nWhen you are finished, run hg histedit --continue to '
617 617 'resume.'))
618 618
619 619 def commiteditor(self):
620 620 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
621 621
622 622 @addhisteditaction(['fold', 'f'])
623 623 class fold(histeditaction):
624 624 def verify(self, prev):
625 625 """ Verifies semantic correctness of the fold rule"""
626 626 super(fold, self).verify(prev)
627 627 repo = self.repo
628 628 state = self.state
629 629 if not prev:
630 630 c = repo[self.node].parents()[0]
631 631 elif not prev.verb in ('pick', 'base'):
632 632 return
633 633 else:
634 634 c = repo[prev.node]
635 635 if not c.mutable():
636 636 raise error.ParseError(
637 637 _("cannot fold into public change %s") % node.short(c.node()))
638 638
639 639
640 640 def continuedirty(self):
641 641 repo = self.repo
642 642 rulectx = repo[self.node]
643 643
644 644 commit = commitfuncfor(repo, rulectx)
645 645 commit(text='fold-temp-revision %s' % node.short(self.node),
646 646 user=rulectx.user(), date=rulectx.date(),
647 647 extra=rulectx.extra())
648 648
649 649 def continueclean(self):
650 650 repo = self.repo
651 651 ctx = repo['.']
652 652 rulectx = repo[self.node]
653 653 parentctxnode = self.state.parentctxnode
654 654 if ctx.node() == parentctxnode:
655 655 repo.ui.warn(_('%s: empty changeset\n') %
656 656 node.short(self.node))
657 657 return ctx, [(self.node, (parentctxnode,))]
658 658
659 659 parentctx = repo[parentctxnode]
660 660 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
661 661 parentctx))
662 662 if not newcommits:
663 663 repo.ui.warn(_('%s: cannot fold - working copy is not a '
664 664 'descendant of previous commit %s\n') %
665 665 (node.short(self.node), node.short(parentctxnode)))
666 666 return ctx, [(self.node, (ctx.node(),))]
667 667
668 668 middlecommits = newcommits.copy()
669 669 middlecommits.discard(ctx.node())
670 670
671 671 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
672 672 middlecommits)
673 673
674 674 def skipprompt(self):
675 675 """Returns true if the rule should skip the message editor.
676 676
677 677 For example, 'fold' wants to show an editor, but 'rollup'
678 678 doesn't want to.
679 679 """
680 680 return False
681 681
682 682 def mergedescs(self):
683 683 """Returns true if the rule should merge messages of multiple changes.
684 684
685 685 This exists mainly so that 'rollup' rules can be a subclass of
686 686 'fold'.
687 687 """
688 688 return True
689 689
690 690 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
691 691 parent = ctx.parents()[0].node()
692 692 hg.update(repo, parent)
693 693 ### prepare new commit data
694 694 commitopts = {}
695 695 commitopts['user'] = ctx.user()
696 696 # commit message
697 697 if not self.mergedescs():
698 698 newmessage = ctx.description()
699 699 else:
700 700 newmessage = '\n***\n'.join(
701 701 [ctx.description()] +
702 702 [repo[r].description() for r in internalchanges] +
703 703 [oldctx.description()]) + '\n'
704 704 commitopts['message'] = newmessage
705 705 # date
706 706 commitopts['date'] = max(ctx.date(), oldctx.date())
707 707 extra = ctx.extra().copy()
708 708 # histedit_source
709 709 # note: ctx is likely a temporary commit but that the best we can do
710 710 # here. This is sufficient to solve issue3681 anyway.
711 711 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
712 712 commitopts['extra'] = extra
713 713 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
714 714 try:
715 715 phasemin = max(ctx.phase(), oldctx.phase())
716 716 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
717 717 n = collapse(repo, ctx, repo[newnode], commitopts,
718 718 skipprompt=self.skipprompt())
719 719 finally:
720 720 repo.ui.restoreconfig(phasebackup)
721 721 if n is None:
722 722 return ctx, []
723 723 hg.update(repo, n)
724 724 replacements = [(oldctx.node(), (newnode,)),
725 725 (ctx.node(), (n,)),
726 726 (newnode, (n,)),
727 727 ]
728 728 for ich in internalchanges:
729 729 replacements.append((ich, (n,)))
730 730 return repo[n], replacements
731 731
732 732 class base(histeditaction):
733 733 def constraints(self):
734 734 return set([_constraints.forceother])
735 735
736 736 def run(self):
737 737 if self.repo['.'].node() != self.node:
738 738 mergemod.update(self.repo, self.node, False, True)
739 739 # branchmerge, force)
740 740 return self.continueclean()
741 741
742 742 def continuedirty(self):
743 743 abortdirty()
744 744
745 745 def continueclean(self):
746 746 basectx = self.repo['.']
747 747 return basectx, []
748 748
749 749 @addhisteditaction(['_multifold'])
750 750 class _multifold(fold):
751 751 """fold subclass used for when multiple folds happen in a row
752 752
753 753 We only want to fire the editor for the folded message once when
754 754 (say) four changes are folded down into a single change. This is
755 755 similar to rollup, but we should preserve both messages so that
756 756 when the last fold operation runs we can show the user all the
757 757 commit messages in their editor.
758 758 """
759 759 def skipprompt(self):
760 760 return True
761 761
762 762 @addhisteditaction(["roll", "r"])
763 763 class rollup(fold):
764 764 def mergedescs(self):
765 765 return False
766 766
767 767 def skipprompt(self):
768 768 return True
769 769
770 770 @addhisteditaction(["drop", "d"])
771 771 class drop(histeditaction):
772 772 def run(self):
773 773 parentctx = self.repo[self.state.parentctxnode]
774 774 return parentctx, [(self.node, tuple())]
775 775
776 776 @addhisteditaction(["mess", "m"])
777 777 class message(histeditaction):
778 778 def commiteditor(self):
779 779 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
780 780
781 781 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
782 782 """utility function to find the first outgoing changeset
783 783
784 784 Used by initialization code"""
785 785 if opts is None:
786 786 opts = {}
787 787 dest = ui.expandpath(remote or 'default-push', remote or 'default')
788 788 dest, revs = hg.parseurl(dest, None)[:2]
789 789 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
790 790
791 791 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
792 792 other = hg.peer(repo, opts, dest)
793 793
794 794 if revs:
795 795 revs = [repo.lookup(rev) for rev in revs]
796 796
797 797 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
798 798 if not outgoing.missing:
799 799 raise error.Abort(_('no outgoing ancestors'))
800 800 roots = list(repo.revs("roots(%ln)", outgoing.missing))
801 801 if 1 < len(roots):
802 802 msg = _('there are ambiguous outgoing revisions')
803 803 hint = _('see "hg help histedit" for more detail')
804 804 raise error.Abort(msg, hint=hint)
805 805 return repo.lookup(roots[0])
806 806
807 807
808 808 @command('histedit',
809 809 [('', 'commands', '',
810 810 _('read history edits from the specified file'), _('FILE')),
811 811 ('c', 'continue', False, _('continue an edit already in progress')),
812 812 ('', 'edit-plan', False, _('edit remaining actions list')),
813 813 ('k', 'keep', False,
814 814 _("don't strip old nodes after edit is complete")),
815 815 ('', 'abort', False, _('abort an edit in progress')),
816 816 ('o', 'outgoing', False, _('changesets not found in destination')),
817 817 ('f', 'force', False,
818 818 _('force outgoing even for unrelated repositories')),
819 819 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
820 820 _("[ANCESTOR] | --outgoing [URL]"))
821 821 def histedit(ui, repo, *freeargs, **opts):
822 822 """interactively edit changeset history
823 823
824 824 This command edits changesets between an ANCESTOR and the parent of
825 825 the working directory.
826 826
827 827 The value from the "histedit.defaultrev" config option is used as a
828 828 revset to select the base revision when ANCESTOR is not specified.
829 829 The first revision returned by the revset is used. By default, this
830 830 selects the editable history that is unique to the ancestry of the
831 831 working directory.
832 832
833 833 With --outgoing, this edits changesets not found in the
834 834 destination repository. If URL of the destination is omitted, the
835 835 'default-push' (or 'default') path will be used.
836 836
837 837 For safety, this command is also aborted if there are ambiguous
838 838 outgoing revisions which may confuse users: for example, if there
839 839 are multiple branches containing outgoing revisions.
840 840
841 841 Use "min(outgoing() and ::.)" or similar revset specification
842 842 instead of --outgoing to specify edit target revision exactly in
843 843 such ambiguous situation. See :hg:`help revsets` for detail about
844 844 selecting revisions.
845 845
846 846 .. container:: verbose
847 847
848 848 Examples:
849 849
850 850 - A number of changes have been made.
851 851 Revision 3 is no longer needed.
852 852
853 853 Start history editing from revision 3::
854 854
855 855 hg histedit -r 3
856 856
857 857 An editor opens, containing the list of revisions,
858 858 with specific actions specified::
859 859
860 860 pick 5339bf82f0ca 3 Zworgle the foobar
861 861 pick 8ef592ce7cc4 4 Bedazzle the zerlog
862 862 pick 0a9639fcda9d 5 Morgify the cromulancy
863 863
864 864 Additional information about the possible actions
865 865 to take appears below the list of revisions.
866 866
867 867 To remove revision 3 from the history,
868 868 its action (at the beginning of the relevant line)
869 869 is changed to 'drop'::
870 870
871 871 drop 5339bf82f0ca 3 Zworgle the foobar
872 872 pick 8ef592ce7cc4 4 Bedazzle the zerlog
873 873 pick 0a9639fcda9d 5 Morgify the cromulancy
874 874
875 875 - A number of changes have been made.
876 876 Revision 2 and 4 need to be swapped.
877 877
878 878 Start history editing from revision 2::
879 879
880 880 hg histedit -r 2
881 881
882 882 An editor opens, containing the list of revisions,
883 883 with specific actions specified::
884 884
885 885 pick 252a1af424ad 2 Blorb a morgwazzle
886 886 pick 5339bf82f0ca 3 Zworgle the foobar
887 887 pick 8ef592ce7cc4 4 Bedazzle the zerlog
888 888
889 889 To swap revision 2 and 4, its lines are swapped
890 890 in the editor::
891 891
892 892 pick 8ef592ce7cc4 4 Bedazzle the zerlog
893 893 pick 5339bf82f0ca 3 Zworgle the foobar
894 894 pick 252a1af424ad 2 Blorb a morgwazzle
895 895
896 896 Returns 0 on success, 1 if user intervention is required (not only
897 897 for intentional "edit" command, but also for resolving unexpected
898 898 conflicts).
899 899 """
900 900 state = histeditstate(repo)
901 901 try:
902 902 state.wlock = repo.wlock()
903 903 state.lock = repo.lock()
904 904 _histedit(ui, repo, state, *freeargs, **opts)
905 905 finally:
906 906 release(state.lock, state.wlock)
907 907
908 908 def _histedit(ui, repo, state, *freeargs, **opts):
909 909 # TODO only abort if we try to histedit mq patches, not just
910 910 # blanket if mq patches are applied somewhere
911 911 mq = getattr(repo, 'mq', None)
912 912 if mq and mq.applied:
913 913 raise error.Abort(_('source has mq patches applied'))
914 914
915 915 # basic argument incompatibility processing
916 916 outg = opts.get('outgoing')
917 917 cont = opts.get('continue')
918 918 editplan = opts.get('edit_plan')
919 919 abort = opts.get('abort')
920 920 force = opts.get('force')
921 921 rules = opts.get('commands', '')
922 922 revs = opts.get('rev', [])
923 923 goal = 'new' # This invocation goal, in new, continue, abort
924 924 if force and not outg:
925 925 raise error.Abort(_('--force only allowed with --outgoing'))
926 926 if cont:
927 927 if any((outg, abort, revs, freeargs, rules, editplan)):
928 928 raise error.Abort(_('no arguments allowed with --continue'))
929 929 goal = 'continue'
930 930 elif abort:
931 931 if any((outg, revs, freeargs, rules, editplan)):
932 932 raise error.Abort(_('no arguments allowed with --abort'))
933 933 goal = 'abort'
934 934 elif editplan:
935 935 if any((outg, revs, freeargs)):
936 936 raise error.Abort(_('only --commands argument allowed with '
937 937 '--edit-plan'))
938 938 goal = 'edit-plan'
939 939 else:
940 940 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
941 941 raise error.Abort(_('history edit already in progress, try '
942 942 '--continue or --abort'))
943 943 if outg:
944 944 if revs:
945 945 raise error.Abort(_('no revisions allowed with --outgoing'))
946 946 if len(freeargs) > 1:
947 947 raise error.Abort(
948 948 _('only one repo argument allowed with --outgoing'))
949 949 else:
950 950 revs.extend(freeargs)
951 951 if len(revs) == 0:
952 952 defaultrev = destutil.desthistedit(ui, repo)
953 953 if defaultrev is not None:
954 954 revs.append(defaultrev)
955 955
956 956 if len(revs) != 1:
957 957 raise error.Abort(
958 958 _('histedit requires exactly one ancestor revision'))
959 959
960 960
961 961 replacements = []
962 962 state.keep = opts.get('keep', False)
963 963 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
964 964
965 965 # rebuild state
966 966 if goal == 'continue':
967 967 state.read()
968 968 state = bootstrapcontinue(ui, state, opts)
969 969 elif goal == 'edit-plan':
970 970 state.read()
971 971 if not rules:
972 972 comment = editcomment % (node.short(state.parentctxnode),
973 973 node.short(state.topmost))
974 974 rules = ruleeditor(repo, ui, state.actions, comment)
975 975 else:
976 976 if rules == '-':
977 977 f = sys.stdin
978 978 else:
979 979 f = open(rules)
980 980 rules = f.read()
981 981 f.close()
982 982 actions = parserules(rules, state)
983 983 ctxs = [repo[act.nodetoverify()] \
984 984 for act in state.actions if act.nodetoverify()]
985 985 warnverifyactions(ui, repo, actions, state, ctxs)
986 986 state.actions = actions
987 987 state.write()
988 988 return
989 989 elif goal == 'abort':
990 990 try:
991 991 state.read()
992 992 tmpnodes, leafs = newnodestoabort(state)
993 993 ui.debug('restore wc to old parent %s\n'
994 994 % node.short(state.topmost))
995 995
996 996 # Recover our old commits if necessary
997 997 if not state.topmost in repo and state.backupfile:
998 998 backupfile = repo.join(state.backupfile)
999 999 f = hg.openpath(ui, backupfile)
1000 1000 gen = exchange.readbundle(ui, f, backupfile)
1001 1001 tr = repo.transaction('histedit.abort')
1002 1002 try:
1003 1003 if not isinstance(gen, bundle2.unbundle20):
1004 1004 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1005 1005 if isinstance(gen, bundle2.unbundle20):
1006 1006 bundle2.applybundle(repo, gen, tr,
1007 1007 source='histedit',
1008 1008 url='bundle:' + backupfile)
1009 1009 tr.close()
1010 1010 finally:
1011 1011 tr.release()
1012 1012
1013 1013 os.remove(backupfile)
1014 1014
1015 1015 # check whether we should update away
1016 1016 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1017 1017 state.parentctxnode, leafs | tmpnodes):
1018 1018 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1019 1019 cleanupnode(ui, repo, 'created', tmpnodes)
1020 1020 cleanupnode(ui, repo, 'temp', leafs)
1021 1021 except Exception:
1022 1022 if state.inprogress():
1023 1023 ui.warn(_('warning: encountered an exception during histedit '
1024 1024 '--abort; the repository may not have been completely '
1025 1025 'cleaned up\n'))
1026 1026 raise
1027 1027 finally:
1028 1028 state.clear()
1029 1029 return
1030 1030 else:
1031 1031 cmdutil.checkunfinished(repo)
1032 1032 cmdutil.bailifchanged(repo)
1033 1033
1034 if repo.vfs.exists('histedit-last-edit.txt'):
1035 repo.vfs.unlink('histedit-last-edit.txt')
1036 1034 topmost, empty = repo.dirstate.parents()
1037 1035 if outg:
1038 1036 if freeargs:
1039 1037 remote = freeargs[0]
1040 1038 else:
1041 1039 remote = None
1042 1040 root = findoutgoing(ui, repo, remote, force, opts)
1043 1041 else:
1044 1042 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1045 1043 if len(rr) != 1:
1046 1044 raise error.Abort(_('The specified revisions must have '
1047 1045 'exactly one common root'))
1048 1046 root = rr[0].node()
1049 1047
1050 1048 revs = between(repo, root, topmost, state.keep)
1051 1049 if not revs:
1052 1050 raise error.Abort(_('%s is not an ancestor of working directory') %
1053 1051 node.short(root))
1054 1052
1055 1053 ctxs = [repo[r] for r in revs]
1056 1054 if not rules:
1057 1055 comment = editcomment % (node.short(root), node.short(topmost))
1058 1056 actions = [pick(state, r) for r in revs]
1059 1057 rules = ruleeditor(repo, ui, actions, comment)
1060 1058 else:
1061 1059 if rules == '-':
1062 1060 f = sys.stdin
1063 1061 else:
1064 1062 f = open(rules)
1065 1063 rules = f.read()
1066 1064 f.close()
1067 1065 actions = parserules(rules, state)
1068 1066 warnverifyactions(ui, repo, actions, state, ctxs)
1069 1067
1070 1068 parentctxnode = repo[root].parents()[0].node()
1071 1069
1072 1070 state.parentctxnode = parentctxnode
1073 1071 state.actions = actions
1074 1072 state.topmost = topmost
1075 1073 state.replacements = replacements
1076 1074
1077 1075 # Create a backup so we can always abort completely.
1078 1076 backupfile = None
1079 1077 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1080 1078 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1081 1079 'histedit')
1082 1080 state.backupfile = backupfile
1083 1081
1084 1082 # preprocess rules so that we can hide inner folds from the user
1085 1083 # and only show one editor
1086 1084 actions = state.actions[:]
1087 1085 for idx, (action, nextact) in enumerate(
1088 1086 zip(actions, actions[1:] + [None])):
1089 1087 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1090 1088 state.actions[idx].__class__ = _multifold
1091 1089
1092 1090 total = len(state.actions)
1093 1091 pos = 0
1094 1092 while state.actions:
1095 1093 state.write()
1096 1094 actobj = state.actions.pop(0)
1097 1095 pos += 1
1098 1096 ui.progress(_("editing"), pos, actobj.torule(),
1099 1097 _('changes'), total)
1100 1098 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1101 1099 actobj.torule()))
1102 1100 parentctx, replacement_ = actobj.run()
1103 1101 state.parentctxnode = parentctx.node()
1104 1102 state.replacements.extend(replacement_)
1105 1103 state.write()
1106 1104 ui.progress(_("editing"), None)
1107 1105
1108 1106 hg.update(repo, state.parentctxnode, quietempty=True)
1109 1107
1110 1108 mapping, tmpnodes, created, ntm = processreplacement(state)
1111 1109 if mapping:
1112 1110 for prec, succs in mapping.iteritems():
1113 1111 if not succs:
1114 1112 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1115 1113 else:
1116 1114 ui.debug('histedit: %s is replaced by %s\n' % (
1117 1115 node.short(prec), node.short(succs[0])))
1118 1116 if len(succs) > 1:
1119 1117 m = 'histedit: %s'
1120 1118 for n in succs[1:]:
1121 1119 ui.debug(m % node.short(n))
1122 1120
1123 1121 if supportsmarkers:
1124 1122 # Only create markers if the temp nodes weren't already removed.
1125 1123 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1126 1124 if t in repo))
1127 1125 else:
1128 1126 cleanupnode(ui, repo, 'temp', tmpnodes)
1129 1127
1130 1128 if not state.keep:
1131 1129 if mapping:
1132 1130 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1133 1131 # TODO update mq state
1134 1132 if supportsmarkers:
1135 1133 markers = []
1136 1134 # sort by revision number because it sound "right"
1137 1135 for prec in sorted(mapping, key=repo.changelog.rev):
1138 1136 succs = mapping[prec]
1139 1137 markers.append((repo[prec],
1140 1138 tuple(repo[s] for s in succs)))
1141 1139 if markers:
1142 1140 obsolete.createmarkers(repo, markers)
1143 1141 else:
1144 1142 cleanupnode(ui, repo, 'replaced', mapping)
1145 1143
1146 1144 state.clear()
1147 1145 if os.path.exists(repo.sjoin('undo')):
1148 1146 os.unlink(repo.sjoin('undo'))
1147 if repo.vfs.exists('histedit-last-edit.txt'):
1148 repo.vfs.unlink('histedit-last-edit.txt')
1149 1149
1150 1150 def bootstrapcontinue(ui, state, opts):
1151 1151 repo = state.repo
1152 1152 if state.actions:
1153 1153 actobj = state.actions.pop(0)
1154 1154
1155 1155 if _isdirtywc(repo):
1156 1156 actobj.continuedirty()
1157 1157 if _isdirtywc(repo):
1158 1158 abortdirty()
1159 1159
1160 1160 parentctx, replacements = actobj.continueclean()
1161 1161
1162 1162 state.parentctxnode = parentctx.node()
1163 1163 state.replacements.extend(replacements)
1164 1164
1165 1165 return state
1166 1166
1167 1167 def between(repo, old, new, keep):
1168 1168 """select and validate the set of revision to edit
1169 1169
1170 1170 When keep is false, the specified set can't have children."""
1171 1171 ctxs = list(repo.set('%n::%n', old, new))
1172 1172 if ctxs and not keep:
1173 1173 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1174 1174 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1175 1175 raise error.Abort(_('cannot edit history that would orphan nodes'))
1176 1176 if repo.revs('(%ld) and merge()', ctxs):
1177 1177 raise error.Abort(_('cannot edit history that contains merges'))
1178 1178 root = ctxs[0] # list is already sorted by repo.set
1179 1179 if not root.mutable():
1180 1180 raise error.Abort(_('cannot edit public changeset: %s') % root,
1181 1181 hint=_('see "hg help phases" for details'))
1182 1182 return [c.node() for c in ctxs]
1183 1183
1184 1184 def ruleeditor(repo, ui, actions, editcomment=""):
1185 1185 """open an editor to edit rules
1186 1186
1187 1187 rules are in the format [ [act, ctx], ...] like in state.rules
1188 1188 """
1189 1189 rules = '\n'.join([act.torule() for act in actions])
1190 1190 rules += '\n\n'
1191 1191 rules += editcomment
1192 1192 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1193 1193
1194 1194 # Save edit rules in .hg/histedit-last-edit.txt in case
1195 1195 # the user needs to ask for help after something
1196 1196 # surprising happens.
1197 1197 f = open(repo.join('histedit-last-edit.txt'), 'w')
1198 1198 f.write(rules)
1199 1199 f.close()
1200 1200
1201 1201 return rules
1202 1202
1203 1203 def parserules(rules, state):
1204 1204 """Read the histedit rules string and return list of action objects """
1205 1205 rules = [l for l in (r.strip() for r in rules.splitlines())
1206 1206 if l and not l.startswith('#')]
1207 1207 actions = []
1208 1208 for r in rules:
1209 1209 if ' ' not in r:
1210 1210 raise error.ParseError(_('malformed line "%s"') % r)
1211 1211 verb, rest = r.split(' ', 1)
1212 1212
1213 1213 if verb not in actiontable:
1214 1214 raise error.ParseError(_('unknown action "%s"') % verb)
1215 1215
1216 1216 action = actiontable[verb].fromrule(state, rest)
1217 1217 actions.append(action)
1218 1218 return actions
1219 1219
1220 1220 def warnverifyactions(ui, repo, actions, state, ctxs):
1221 1221 try:
1222 1222 verifyactions(actions, state, ctxs)
1223 1223 except error.ParseError:
1224 1224 if repo.vfs.exists('histedit-last-edit.txt'):
1225 1225 ui.warn(_('warning: histedit rules saved '
1226 1226 'to: .hg/histedit-last-edit.txt\n'))
1227 1227 raise
1228 1228
1229 1229 def verifyactions(actions, state, ctxs):
1230 1230 """Verify that there exists exactly one action per given changeset and
1231 1231 other constraints.
1232 1232
1233 1233 Will abort if there are to many or too few rules, a malformed rule,
1234 1234 or a rule on a changeset outside of the user-given range.
1235 1235 """
1236 1236 expected = set(c.hex() for c in ctxs)
1237 1237 seen = set()
1238 1238 prev = None
1239 1239 for action in actions:
1240 1240 action.verify(prev)
1241 1241 prev = action
1242 1242 constraints = action.constraints()
1243 1243 for constraint in constraints:
1244 1244 if constraint not in _constraints.known():
1245 1245 raise error.ParseError(_('unknown constraint "%s"') %
1246 1246 constraint)
1247 1247
1248 1248 nodetoverify = action.nodetoverify()
1249 1249 if nodetoverify is not None:
1250 1250 ha = node.hex(nodetoverify)
1251 1251 if _constraints.noother in constraints and ha not in expected:
1252 1252 raise error.ParseError(
1253 1253 _('may not use "%s" with changesets '
1254 1254 'other than the ones listed') % action.verb)
1255 1255 if _constraints.forceother in constraints and ha in expected:
1256 1256 raise error.ParseError(
1257 1257 _('may not use "%s" with changesets '
1258 1258 'within the edited list') % action.verb)
1259 1259 if _constraints.noduplicates in constraints and ha in seen:
1260 1260 raise error.ParseError(_(
1261 1261 'duplicated command for changeset %s') %
1262 1262 ha[:12])
1263 1263 seen.add(ha)
1264 1264 missing = sorted(expected - seen) # sort to stabilize output
1265 1265
1266 1266 if state.repo.ui.configbool('histedit', 'dropmissing'):
1267 1267 drops = [drop(state, node.bin(n)) for n in missing]
1268 1268 # put the in the beginning so they execute immediately and
1269 1269 # don't show in the edit-plan in the future
1270 1270 actions[:0] = drops
1271 1271 elif missing:
1272 1272 raise error.ParseError(_('missing rules for changeset %s') %
1273 1273 missing[0][:12],
1274 1274 hint=_('use "drop %s" to discard, see also: '
1275 1275 '"hg help -e histedit.config"') % missing[0][:12])
1276 1276
1277 1277 def newnodestoabort(state):
1278 1278 """process the list of replacements to return
1279 1279
1280 1280 1) the list of final node
1281 1281 2) the list of temporary node
1282 1282
1283 1283 This meant to be used on abort as less data are required in this case.
1284 1284 """
1285 1285 replacements = state.replacements
1286 1286 allsuccs = set()
1287 1287 replaced = set()
1288 1288 for rep in replacements:
1289 1289 allsuccs.update(rep[1])
1290 1290 replaced.add(rep[0])
1291 1291 newnodes = allsuccs - replaced
1292 1292 tmpnodes = allsuccs & replaced
1293 1293 return newnodes, tmpnodes
1294 1294
1295 1295
1296 1296 def processreplacement(state):
1297 1297 """process the list of replacements to return
1298 1298
1299 1299 1) the final mapping between original and created nodes
1300 1300 2) the list of temporary node created by histedit
1301 1301 3) the list of new commit created by histedit"""
1302 1302 replacements = state.replacements
1303 1303 allsuccs = set()
1304 1304 replaced = set()
1305 1305 fullmapping = {}
1306 1306 # initialize basic set
1307 1307 # fullmapping records all operations recorded in replacement
1308 1308 for rep in replacements:
1309 1309 allsuccs.update(rep[1])
1310 1310 replaced.add(rep[0])
1311 1311 fullmapping.setdefault(rep[0], set()).update(rep[1])
1312 1312 new = allsuccs - replaced
1313 1313 tmpnodes = allsuccs & replaced
1314 1314 # Reduce content fullmapping into direct relation between original nodes
1315 1315 # and final node created during history edition
1316 1316 # Dropped changeset are replaced by an empty list
1317 1317 toproceed = set(fullmapping)
1318 1318 final = {}
1319 1319 while toproceed:
1320 1320 for x in list(toproceed):
1321 1321 succs = fullmapping[x]
1322 1322 for s in list(succs):
1323 1323 if s in toproceed:
1324 1324 # non final node with unknown closure
1325 1325 # We can't process this now
1326 1326 break
1327 1327 elif s in final:
1328 1328 # non final node, replace with closure
1329 1329 succs.remove(s)
1330 1330 succs.update(final[s])
1331 1331 else:
1332 1332 final[x] = succs
1333 1333 toproceed.remove(x)
1334 1334 # remove tmpnodes from final mapping
1335 1335 for n in tmpnodes:
1336 1336 del final[n]
1337 1337 # we expect all changes involved in final to exist in the repo
1338 1338 # turn `final` into list (topologically sorted)
1339 1339 nm = state.repo.changelog.nodemap
1340 1340 for prec, succs in final.items():
1341 1341 final[prec] = sorted(succs, key=nm.get)
1342 1342
1343 1343 # computed topmost element (necessary for bookmark)
1344 1344 if new:
1345 1345 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1346 1346 elif not final:
1347 1347 # Nothing rewritten at all. we won't need `newtopmost`
1348 1348 # It is the same as `oldtopmost` and `processreplacement` know it
1349 1349 newtopmost = None
1350 1350 else:
1351 1351 # every body died. The newtopmost is the parent of the root.
1352 1352 r = state.repo.changelog.rev
1353 1353 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1354 1354
1355 1355 return final, tmpnodes, new, newtopmost
1356 1356
1357 1357 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1358 1358 """Move bookmark from old to newly created node"""
1359 1359 if not mapping:
1360 1360 # if nothing got rewritten there is not purpose for this function
1361 1361 return
1362 1362 moves = []
1363 1363 for bk, old in sorted(repo._bookmarks.iteritems()):
1364 1364 if old == oldtopmost:
1365 1365 # special case ensure bookmark stay on tip.
1366 1366 #
1367 1367 # This is arguably a feature and we may only want that for the
1368 1368 # active bookmark. But the behavior is kept compatible with the old
1369 1369 # version for now.
1370 1370 moves.append((bk, newtopmost))
1371 1371 continue
1372 1372 base = old
1373 1373 new = mapping.get(base, None)
1374 1374 if new is None:
1375 1375 continue
1376 1376 while not new:
1377 1377 # base is killed, trying with parent
1378 1378 base = repo[base].p1().node()
1379 1379 new = mapping.get(base, (base,))
1380 1380 # nothing to move
1381 1381 moves.append((bk, new[-1]))
1382 1382 if moves:
1383 1383 lock = tr = None
1384 1384 try:
1385 1385 lock = repo.lock()
1386 1386 tr = repo.transaction('histedit')
1387 1387 marks = repo._bookmarks
1388 1388 for mark, new in moves:
1389 1389 old = marks[mark]
1390 1390 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1391 1391 % (mark, node.short(old), node.short(new)))
1392 1392 marks[mark] = new
1393 1393 marks.recordchange(tr)
1394 1394 tr.close()
1395 1395 finally:
1396 1396 release(tr, lock)
1397 1397
1398 1398 def cleanupnode(ui, repo, name, nodes):
1399 1399 """strip a group of nodes from the repository
1400 1400
1401 1401 The set of node to strip may contains unknown nodes."""
1402 1402 ui.debug('should strip %s nodes %s\n' %
1403 1403 (name, ', '.join([node.short(n) for n in nodes])))
1404 1404 lock = None
1405 1405 try:
1406 1406 lock = repo.lock()
1407 1407 # do not let filtering get in the way of the cleanse
1408 1408 # we should probably get rid of obsolescence marker created during the
1409 1409 # histedit, but we currently do not have such information.
1410 1410 repo = repo.unfiltered()
1411 1411 # Find all nodes that need to be stripped
1412 1412 # (we use %lr instead of %ln to silently ignore unknown items)
1413 1413 nm = repo.changelog.nodemap
1414 1414 nodes = sorted(n for n in nodes if n in nm)
1415 1415 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1416 1416 for c in roots:
1417 1417 # We should process node in reverse order to strip tip most first.
1418 1418 # but this trigger a bug in changegroup hook.
1419 1419 # This would reduce bundle overhead
1420 1420 repair.strip(ui, repo, c)
1421 1421 finally:
1422 1422 release(lock)
1423 1423
1424 1424 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1425 1425 if isinstance(nodelist, str):
1426 1426 nodelist = [nodelist]
1427 1427 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1428 1428 state = histeditstate(repo)
1429 1429 state.read()
1430 1430 histedit_nodes = set([action.nodetoverify() for action
1431 1431 in state.actions if action.nodetoverify()])
1432 1432 strip_nodes = set([repo[n].node() for n in nodelist])
1433 1433 common_nodes = histedit_nodes & strip_nodes
1434 1434 if common_nodes:
1435 1435 raise error.Abort(_("histedit in progress, can't strip %s")
1436 1436 % ', '.join(node.short(x) for x in common_nodes))
1437 1437 return orig(ui, repo, nodelist, *args, **kwargs)
1438 1438
1439 1439 extensions.wrapfunction(repair, 'strip', stripwrapper)
1440 1440
1441 1441 def summaryhook(ui, repo):
1442 1442 if not os.path.exists(repo.join('histedit-state')):
1443 1443 return
1444 1444 state = histeditstate(repo)
1445 1445 state.read()
1446 1446 if state.actions:
1447 1447 # i18n: column positioning for "hg summary"
1448 1448 ui.write(_('hist: %s (histedit --continue)\n') %
1449 1449 (ui.label(_('%d remaining'), 'histedit.remaining') %
1450 1450 len(state.actions)))
1451 1451
1452 1452 def extsetup(ui):
1453 1453 cmdutil.summaryhooks.add('histedit', summaryhook)
1454 1454 cmdutil.unfinishedstates.append(
1455 1455 ['histedit-state', False, True, _('histedit in progress'),
1456 1456 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1457 1457 if ui.configbool("experimental", "histeditng"):
1458 1458 globals()['base'] = addhisteditaction(['base', 'b'])(base)
@@ -1,443 +1,456 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 r
11 11 > cd r
12 12 > for x in a b c d e f ; do
13 13 > echo $x > $x
14 14 > hg add $x
15 15 > hg ci -m $x
16 16 > done
17 17 > }
18 18
19 19 $ initrepo
20 20
21 21 log before edit
22 22 $ hg log --graph
23 23 @ changeset: 5:652413bf663e
24 24 | tag: tip
25 25 | user: test
26 26 | date: Thu Jan 01 00:00:00 1970 +0000
27 27 | summary: f
28 28 |
29 29 o changeset: 4:e860deea161a
30 30 | user: test
31 31 | date: Thu Jan 01 00:00:00 1970 +0000
32 32 | summary: e
33 33 |
34 34 o changeset: 3:055a42cdd887
35 35 | user: test
36 36 | date: Thu Jan 01 00:00:00 1970 +0000
37 37 | summary: d
38 38 |
39 39 o changeset: 2:177f92b77385
40 40 | user: test
41 41 | date: Thu Jan 01 00:00:00 1970 +0000
42 42 | summary: c
43 43 |
44 44 o changeset: 1:d2ae7f538514
45 45 | user: test
46 46 | date: Thu Jan 01 00:00:00 1970 +0000
47 47 | summary: b
48 48 |
49 49 o changeset: 0:cb9a9f314b8b
50 50 user: test
51 51 date: Thu Jan 01 00:00:00 1970 +0000
52 52 summary: a
53 53
54 54
55 55 show the edit commands offered
56 56 $ HGEDITOR=cat hg histedit 177f92b77385
57 57 pick 177f92b77385 2 c
58 58 pick 055a42cdd887 3 d
59 59 pick e860deea161a 4 e
60 60 pick 652413bf663e 5 f
61 61
62 62 # Edit history between 177f92b77385 and 652413bf663e
63 63 #
64 64 # Commits are listed from least to most recent
65 65 #
66 66 # Commands:
67 67 # p, pick = use commit
68 68 # e, edit = use commit, but stop for amending
69 69 # f, fold = use commit, but combine it with the one above
70 70 # r, roll = like fold, but discard this commit's description
71 71 # d, drop = remove commit from history
72 72 # m, mess = edit commit message without changing commit content
73 73 #
74 74
75 75 edit the history
76 76 (use a hacky editor to check histedit-last-edit.txt backup)
77 77
78 78 $ EDITED="$TESTTMP/editedhistory"
79 79 $ cat > $EDITED <<EOF
80 > edit 177f92b77385 c
81 > pick e860deea161a e
82 > pick 652413bf663e f
83 > pick 055a42cdd887 d
84 > EOF
85 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
86 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
87 Make changes as needed, you may commit or record as needed now.
88 When you are finished, run hg histedit --continue to resume.
89
90 rules should end up in .hg/histedit-last-edit.txt:
91 $ cat .hg/histedit-last-edit.txt
92 edit 177f92b77385 c
93 pick e860deea161a e
94 pick 652413bf663e f
95 pick 055a42cdd887 d
96
97 $ hg histedit --abort
98 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 $ cat > $EDITED <<EOF
80 100 > pick 177f92b77385 c
81 101 > pick e860deea161a e
82 102 > pick 652413bf663e f
83 103 > pick 055a42cdd887 d
84 104 > EOF
85 105 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
86 106 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
87 107
88 rules should end up in .hg/histedit-last-edit.txt:
89 $ cat .hg/histedit-last-edit.txt
90 pick 177f92b77385 c
91 pick e860deea161a e
92 pick 652413bf663e f
93 pick 055a42cdd887 d
94
95 108 log after edit
96 109 $ hg log --graph
97 110 @ changeset: 5:07114f51870f
98 111 | tag: tip
99 112 | user: test
100 113 | date: Thu Jan 01 00:00:00 1970 +0000
101 114 | summary: d
102 115 |
103 116 o changeset: 4:8ade9693061e
104 117 | user: test
105 118 | date: Thu Jan 01 00:00:00 1970 +0000
106 119 | summary: f
107 120 |
108 121 o changeset: 3:d8249471110a
109 122 | user: test
110 123 | date: Thu Jan 01 00:00:00 1970 +0000
111 124 | summary: e
112 125 |
113 126 o changeset: 2:177f92b77385
114 127 | user: test
115 128 | date: Thu Jan 01 00:00:00 1970 +0000
116 129 | summary: c
117 130 |
118 131 o changeset: 1:d2ae7f538514
119 132 | user: test
120 133 | date: Thu Jan 01 00:00:00 1970 +0000
121 134 | summary: b
122 135 |
123 136 o changeset: 0:cb9a9f314b8b
124 137 user: test
125 138 date: Thu Jan 01 00:00:00 1970 +0000
126 139 summary: a
127 140
128 141
129 142 put things back
130 143
131 144 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF | fixbundle
132 145 > pick 177f92b77385 c
133 146 > pick 07114f51870f d
134 147 > pick d8249471110a e
135 148 > pick 8ade9693061e f
136 149 > EOF
137 150 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
138 151
139 152 $ hg log --graph
140 153 @ changeset: 5:7eca9b5b1148
141 154 | tag: tip
142 155 | user: test
143 156 | date: Thu Jan 01 00:00:00 1970 +0000
144 157 | summary: f
145 158 |
146 159 o changeset: 4:915da888f2de
147 160 | user: test
148 161 | date: Thu Jan 01 00:00:00 1970 +0000
149 162 | summary: e
150 163 |
151 164 o changeset: 3:10517e47bbbb
152 165 | user: test
153 166 | date: Thu Jan 01 00:00:00 1970 +0000
154 167 | summary: d
155 168 |
156 169 o changeset: 2:177f92b77385
157 170 | user: test
158 171 | date: Thu Jan 01 00:00:00 1970 +0000
159 172 | summary: c
160 173 |
161 174 o changeset: 1:d2ae7f538514
162 175 | user: test
163 176 | date: Thu Jan 01 00:00:00 1970 +0000
164 177 | summary: b
165 178 |
166 179 o changeset: 0:cb9a9f314b8b
167 180 user: test
168 181 date: Thu Jan 01 00:00:00 1970 +0000
169 182 summary: a
170 183
171 184
172 185 slightly different this time
173 186
174 187 $ hg histedit 177f92b77385 --commands - << EOF 2>&1 | fixbundle
175 188 > pick 10517e47bbbb d
176 189 > pick 7eca9b5b1148 f
177 190 > pick 915da888f2de e
178 191 > pick 177f92b77385 c
179 192 > EOF
180 193 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
181 194 $ hg log --graph
182 195 @ changeset: 5:38b92f448761
183 196 | tag: tip
184 197 | user: test
185 198 | date: Thu Jan 01 00:00:00 1970 +0000
186 199 | summary: c
187 200 |
188 201 o changeset: 4:de71b079d9ce
189 202 | user: test
190 203 | date: Thu Jan 01 00:00:00 1970 +0000
191 204 | summary: e
192 205 |
193 206 o changeset: 3:be9ae3a309c6
194 207 | user: test
195 208 | date: Thu Jan 01 00:00:00 1970 +0000
196 209 | summary: f
197 210 |
198 211 o changeset: 2:799205341b6b
199 212 | user: test
200 213 | date: Thu Jan 01 00:00:00 1970 +0000
201 214 | summary: d
202 215 |
203 216 o changeset: 1:d2ae7f538514
204 217 | user: test
205 218 | date: Thu Jan 01 00:00:00 1970 +0000
206 219 | summary: b
207 220 |
208 221 o changeset: 0:cb9a9f314b8b
209 222 user: test
210 223 date: Thu Jan 01 00:00:00 1970 +0000
211 224 summary: a
212 225
213 226
214 227 keep prevents stripping dead revs
215 228 $ hg histedit 799205341b6b --keep --commands - 2>&1 << EOF | fixbundle
216 229 > pick 799205341b6b d
217 230 > pick be9ae3a309c6 f
218 231 > pick 38b92f448761 c
219 232 > pick de71b079d9ce e
220 233 > EOF
221 234 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
222 235 $ hg log --graph
223 236 @ changeset: 7:803ef1c6fcfd
224 237 | tag: tip
225 238 | user: test
226 239 | date: Thu Jan 01 00:00:00 1970 +0000
227 240 | summary: e
228 241 |
229 242 o changeset: 6:ece0b8d93dda
230 243 | parent: 3:be9ae3a309c6
231 244 | user: test
232 245 | date: Thu Jan 01 00:00:00 1970 +0000
233 246 | summary: c
234 247 |
235 248 | o changeset: 5:38b92f448761
236 249 | | user: test
237 250 | | date: Thu Jan 01 00:00:00 1970 +0000
238 251 | | summary: c
239 252 | |
240 253 | o changeset: 4:de71b079d9ce
241 254 |/ user: test
242 255 | date: Thu Jan 01 00:00:00 1970 +0000
243 256 | summary: e
244 257 |
245 258 o changeset: 3:be9ae3a309c6
246 259 | user: test
247 260 | date: Thu Jan 01 00:00:00 1970 +0000
248 261 | summary: f
249 262 |
250 263 o changeset: 2:799205341b6b
251 264 | user: test
252 265 | date: Thu Jan 01 00:00:00 1970 +0000
253 266 | summary: d
254 267 |
255 268 o changeset: 1:d2ae7f538514
256 269 | user: test
257 270 | date: Thu Jan 01 00:00:00 1970 +0000
258 271 | summary: b
259 272 |
260 273 o changeset: 0:cb9a9f314b8b
261 274 user: test
262 275 date: Thu Jan 01 00:00:00 1970 +0000
263 276 summary: a
264 277
265 278
266 279 try with --rev
267 280 $ hg histedit --commands - --rev -2 2>&1 <<EOF | fixbundle
268 281 > pick de71b079d9ce e
269 282 > pick 38b92f448761 c
270 283 > EOF
271 284 hg: parse error: may not use "pick" with changesets other than the ones listed
272 285 $ hg log --graph
273 286 @ changeset: 7:803ef1c6fcfd
274 287 | tag: tip
275 288 | user: test
276 289 | date: Thu Jan 01 00:00:00 1970 +0000
277 290 | summary: e
278 291 |
279 292 o changeset: 6:ece0b8d93dda
280 293 | parent: 3:be9ae3a309c6
281 294 | user: test
282 295 | date: Thu Jan 01 00:00:00 1970 +0000
283 296 | summary: c
284 297 |
285 298 | o changeset: 5:38b92f448761
286 299 | | user: test
287 300 | | date: Thu Jan 01 00:00:00 1970 +0000
288 301 | | summary: c
289 302 | |
290 303 | o changeset: 4:de71b079d9ce
291 304 |/ user: test
292 305 | date: Thu Jan 01 00:00:00 1970 +0000
293 306 | summary: e
294 307 |
295 308 o changeset: 3:be9ae3a309c6
296 309 | user: test
297 310 | date: Thu Jan 01 00:00:00 1970 +0000
298 311 | summary: f
299 312 |
300 313 o changeset: 2:799205341b6b
301 314 | user: test
302 315 | date: Thu Jan 01 00:00:00 1970 +0000
303 316 | summary: d
304 317 |
305 318 o changeset: 1:d2ae7f538514
306 319 | user: test
307 320 | date: Thu Jan 01 00:00:00 1970 +0000
308 321 | summary: b
309 322 |
310 323 o changeset: 0:cb9a9f314b8b
311 324 user: test
312 325 date: Thu Jan 01 00:00:00 1970 +0000
313 326 summary: a
314 327
315 328 Verify that revsetalias entries work with histedit:
316 329 $ cat >> $HGRCPATH <<EOF
317 330 > [revsetalias]
318 331 > grandparent(ARG) = p1(p1(ARG))
319 332 > EOF
320 333 $ echo extra commit >> c
321 334 $ hg ci -m 'extra commit to c'
322 335 $ HGEDITOR=cat hg histedit 'grandparent(.)'
323 336 pick ece0b8d93dda 6 c
324 337 pick 803ef1c6fcfd 7 e
325 338 pick 9c863c565126 8 extra commit to c
326 339
327 340 # Edit history between ece0b8d93dda and 9c863c565126
328 341 #
329 342 # Commits are listed from least to most recent
330 343 #
331 344 # Commands:
332 345 # p, pick = use commit
333 346 # e, edit = use commit, but stop for amending
334 347 # f, fold = use commit, but combine it with the one above
335 348 # r, roll = like fold, but discard this commit's description
336 349 # d, drop = remove commit from history
337 350 # m, mess = edit commit message without changing commit content
338 351 #
339 352
340 353 should also work if a commit message is missing
341 354 $ BUNDLE="$TESTDIR/missing-comment.hg"
342 355 $ hg init missing
343 356 $ cd missing
344 357 $ hg unbundle $BUNDLE
345 358 adding changesets
346 359 adding manifests
347 360 adding file changes
348 361 added 3 changesets with 3 changes to 1 files
349 362 (run 'hg update' to get a working copy)
350 363 $ hg co tip
351 364 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
352 365 $ hg log --graph
353 366 @ changeset: 2:bd22688093b3
354 367 | tag: tip
355 368 | user: Robert Altman <robert.altman@telventDTN.com>
356 369 | date: Mon Nov 28 16:40:04 2011 +0000
357 370 | summary: Update file.
358 371 |
359 372 o changeset: 1:3b3e956f9171
360 373 | user: Robert Altman <robert.altman@telventDTN.com>
361 374 | date: Mon Nov 28 16:37:57 2011 +0000
362 375 |
363 376 o changeset: 0:141947992243
364 377 user: Robert Altman <robert.altman@telventDTN.com>
365 378 date: Mon Nov 28 16:35:28 2011 +0000
366 379 summary: Checked in text file
367 380
368 381 $ hg histedit 0
369 382 $ cd ..
370 383
371 384 $ cd ..
372 385
373 386
374 387 Test to make sure folding renames doesn't cause bogus conflicts (issue4251):
375 388 $ hg init issue4251
376 389 $ cd issue4251
377 390
378 391 $ mkdir initial-dir
379 392 $ echo foo > initial-dir/initial-file
380 393 $ hg add initial-dir/initial-file
381 394 $ hg commit -m "initial commit"
382 395
383 396 Move the file to a new directory, and in the same commit, change its content:
384 397 $ mkdir another-dir
385 398 $ hg mv initial-dir/initial-file another-dir/
386 399 $ echo changed > another-dir/initial-file
387 400 $ hg commit -m "moved and changed"
388 401
389 402 Rename the file:
390 403 $ hg mv another-dir/initial-file another-dir/renamed-file
391 404 $ hg commit -m "renamed"
392 405
393 406 Now, let's try to fold the second commit into the first:
394 407 $ cat > editor.sh <<EOF
395 408 > #!/bin/sh
396 409 > cat > \$1 <<ENDOF
397 410 > pick b0f4233702ca 0 initial commit
398 411 > fold 5e8704a8f2d2 1 moved and changed
399 412 > pick 40e7299e8fa7 2 renamed
400 413 > ENDOF
401 414 > EOF
402 415
403 416 $ HGEDITOR="sh ./editor.sh" hg histedit 0
404 417 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
405 418 adding another-dir/initial-file (glob)
406 419 removing initial-dir/initial-file (glob)
407 420 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
408 421 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
409 422 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/*-backup.hg (glob)
410 423 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/*-backup.hg (glob)
411 424
412 425 $ hg --config diff.git=yes export 0
413 426 # HG changeset patch
414 427 # User test
415 428 # Date 0 0
416 429 # Thu Jan 01 00:00:00 1970 +0000
417 430 # Node ID fffadc26f8f85623ce60b028a3f1ccc3730f8530
418 431 # Parent 0000000000000000000000000000000000000000
419 432 pick b0f4233702ca 0 initial commit
420 433 fold 5e8704a8f2d2 1 moved and changed
421 434 pick 40e7299e8fa7 2 renamed
422 435
423 436 diff --git a/another-dir/initial-file b/another-dir/initial-file
424 437 new file mode 100644
425 438 --- /dev/null
426 439 +++ b/another-dir/initial-file
427 440 @@ -0,0 +1,1 @@
428 441 +changed
429 442
430 443 $ hg --config diff.git=yes export 1
431 444 # HG changeset patch
432 445 # User test
433 446 # Date 0 0
434 447 # Thu Jan 01 00:00:00 1970 +0000
435 448 # Node ID 9b730d82b00af8a2766facebfa47cc124405a118
436 449 # Parent fffadc26f8f85623ce60b028a3f1ccc3730f8530
437 450 renamed
438 451
439 452 diff --git a/another-dir/initial-file b/another-dir/renamed-file
440 453 rename from another-dir/initial-file
441 454 rename to another-dir/renamed-file
442 455
443 456 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now