##// END OF EJS Templates
histedit: mark temporary commits as obsolete when allowed to...
Laurent Charignon -
r25809:ebb5bb9b default
parent child Browse files
Show More
@@ -1,1160 +1,1167
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 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 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 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customise this behaviour by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152 """
153 153
154 154 try:
155 155 import cPickle as pickle
156 156 pickle.dump # import now
157 157 except ImportError:
158 158 import pickle
159 159 import errno
160 160 import os
161 161 import sys
162 162
163 163 from mercurial import cmdutil
164 164 from mercurial import discovery
165 165 from mercurial import error
166 166 from mercurial import changegroup
167 167 from mercurial import copies
168 168 from mercurial import context
169 169 from mercurial import exchange
170 170 from mercurial import extensions
171 171 from mercurial import hg
172 172 from mercurial import node
173 173 from mercurial import repair
174 174 from mercurial import scmutil
175 175 from mercurial import util
176 176 from mercurial import obsolete
177 177 from mercurial import merge as mergemod
178 178 from mercurial.lock import release
179 179 from mercurial.i18n import _
180 180
181 181 cmdtable = {}
182 182 command = cmdutil.command(cmdtable)
183 183
184 184 # Note for extension authors: ONLY specify testedwith = 'internal' for
185 185 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
186 186 # be specifying the version(s) of Mercurial they are tested with, or
187 187 # leave the attribute unspecified.
188 188 testedwith = 'internal'
189 189
190 190 # i18n: command names and abbreviations must remain untranslated
191 191 editcomment = _("""# Edit history between %s and %s
192 192 #
193 193 # Commits are listed from least to most recent
194 194 #
195 195 # Commands:
196 196 # p, pick = use commit
197 197 # e, edit = use commit, but stop for amending
198 198 # f, fold = use commit, but combine it with the one above
199 199 # r, roll = like fold, but discard this commit's description
200 200 # d, drop = remove commit from history
201 201 # m, mess = edit message without changing commit content
202 202 #
203 203 """)
204 204
205 205 class histeditstate(object):
206 206 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
207 207 topmost=None, replacements=None, lock=None, wlock=None):
208 208 self.repo = repo
209 209 self.rules = rules
210 210 self.keep = keep
211 211 self.topmost = topmost
212 212 self.parentctxnode = parentctxnode
213 213 self.lock = lock
214 214 self.wlock = wlock
215 215 self.backupfile = None
216 216 if replacements is None:
217 217 self.replacements = []
218 218 else:
219 219 self.replacements = replacements
220 220
221 221 def read(self):
222 222 """Load histedit state from disk and set fields appropriately."""
223 223 try:
224 224 fp = self.repo.vfs('histedit-state', 'r')
225 225 except IOError as err:
226 226 if err.errno != errno.ENOENT:
227 227 raise
228 228 raise util.Abort(_('no histedit in progress'))
229 229
230 230 try:
231 231 data = pickle.load(fp)
232 232 parentctxnode, rules, keep, topmost, replacements = data
233 233 backupfile = None
234 234 except pickle.UnpicklingError:
235 235 data = self._load()
236 236 parentctxnode, rules, keep, topmost, replacements, backupfile = data
237 237
238 238 self.parentctxnode = parentctxnode
239 239 self.rules = rules
240 240 self.keep = keep
241 241 self.topmost = topmost
242 242 self.replacements = replacements
243 243 self.backupfile = backupfile
244 244
245 245 def write(self):
246 246 fp = self.repo.vfs('histedit-state', 'w')
247 247 fp.write('v1\n')
248 248 fp.write('%s\n' % node.hex(self.parentctxnode))
249 249 fp.write('%s\n' % node.hex(self.topmost))
250 250 fp.write('%s\n' % self.keep)
251 251 fp.write('%d\n' % len(self.rules))
252 252 for rule in self.rules:
253 253 fp.write('%s\n' % rule[0]) # action
254 254 fp.write('%s\n' % rule[1]) # remainder
255 255 fp.write('%d\n' % len(self.replacements))
256 256 for replacement in self.replacements:
257 257 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
258 258 for r in replacement[1])))
259 259 backupfile = self.backupfile
260 260 if not backupfile:
261 261 backupfile = ''
262 262 fp.write('%s\n' % backupfile)
263 263 fp.close()
264 264
265 265 def _load(self):
266 266 fp = self.repo.vfs('histedit-state', 'r')
267 267 lines = [l[:-1] for l in fp.readlines()]
268 268
269 269 index = 0
270 270 lines[index] # version number
271 271 index += 1
272 272
273 273 parentctxnode = node.bin(lines[index])
274 274 index += 1
275 275
276 276 topmost = node.bin(lines[index])
277 277 index += 1
278 278
279 279 keep = lines[index] == 'True'
280 280 index += 1
281 281
282 282 # Rules
283 283 rules = []
284 284 rulelen = int(lines[index])
285 285 index += 1
286 286 for i in xrange(rulelen):
287 287 ruleaction = lines[index]
288 288 index += 1
289 289 rule = lines[index]
290 290 index += 1
291 291 rules.append((ruleaction, rule))
292 292
293 293 # Replacements
294 294 replacements = []
295 295 replacementlen = int(lines[index])
296 296 index += 1
297 297 for i in xrange(replacementlen):
298 298 replacement = lines[index]
299 299 original = node.bin(replacement[:40])
300 300 succ = [node.bin(replacement[i:i + 40]) for i in
301 301 range(40, len(replacement), 40)]
302 302 replacements.append((original, succ))
303 303 index += 1
304 304
305 305 backupfile = lines[index]
306 306 index += 1
307 307
308 308 fp.close()
309 309
310 310 return parentctxnode, rules, keep, topmost, replacements, backupfile
311 311
312 312 def clear(self):
313 313 self.repo.vfs.unlink('histedit-state')
314 314
315 315 class histeditaction(object):
316 316 def __init__(self, state, node):
317 317 self.state = state
318 318 self.repo = state.repo
319 319 self.node = node
320 320
321 321 @classmethod
322 322 def fromrule(cls, state, rule):
323 323 """Parses the given rule, returning an instance of the histeditaction.
324 324 """
325 325 repo = state.repo
326 326 rulehash = rule.strip().split(' ', 1)[0]
327 327 try:
328 328 node = repo[rulehash].node()
329 329 except error.RepoError:
330 330 raise util.Abort(_('unknown changeset %s listed') % rulehash[:12])
331 331 return cls(state, node)
332 332
333 333 def run(self):
334 334 """Runs the action. The default behavior is simply apply the action's
335 335 rulectx onto the current parentctx."""
336 336 self.applychange()
337 337 self.continuedirty()
338 338 return self.continueclean()
339 339
340 340 def applychange(self):
341 341 """Applies the changes from this action's rulectx onto the current
342 342 parentctx, but does not commit them."""
343 343 repo = self.repo
344 344 rulectx = repo[self.node]
345 345 hg.update(repo, self.state.parentctxnode)
346 346 stats = applychanges(repo.ui, repo, rulectx, {})
347 347 if stats and stats[3] > 0:
348 348 raise error.InterventionRequired(_('Fix up the change and run '
349 349 'hg histedit --continue'))
350 350
351 351 def continuedirty(self):
352 352 """Continues the action when changes have been applied to the working
353 353 copy. The default behavior is to commit the dirty changes."""
354 354 repo = self.repo
355 355 rulectx = repo[self.node]
356 356
357 357 editor = self.commiteditor()
358 358 commit = commitfuncfor(repo, rulectx)
359 359
360 360 commit(text=rulectx.description(), user=rulectx.user(),
361 361 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
362 362
363 363 def commiteditor(self):
364 364 """The editor to be used to edit the commit message."""
365 365 return False
366 366
367 367 def continueclean(self):
368 368 """Continues the action when the working copy is clean. The default
369 369 behavior is to accept the current commit as the new version of the
370 370 rulectx."""
371 371 ctx = self.repo['.']
372 372 if ctx.node() == self.state.parentctxnode:
373 373 self.repo.ui.warn(_('%s: empty changeset\n') %
374 374 node.short(self.node))
375 375 return ctx, [(self.node, tuple())]
376 376 if ctx.node() == self.node:
377 377 # Nothing changed
378 378 return ctx, []
379 379 return ctx, [(self.node, (ctx.node(),))]
380 380
381 381 def commitfuncfor(repo, src):
382 382 """Build a commit function for the replacement of <src>
383 383
384 384 This function ensure we apply the same treatment to all changesets.
385 385
386 386 - Add a 'histedit_source' entry in extra.
387 387
388 388 Note that fold has its own separated logic because its handling is a bit
389 389 different and not easily factored out of the fold method.
390 390 """
391 391 phasemin = src.phase()
392 392 def commitfunc(**kwargs):
393 393 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
394 394 try:
395 395 repo.ui.setconfig('phases', 'new-commit', phasemin,
396 396 'histedit')
397 397 extra = kwargs.get('extra', {}).copy()
398 398 extra['histedit_source'] = src.hex()
399 399 kwargs['extra'] = extra
400 400 return repo.commit(**kwargs)
401 401 finally:
402 402 repo.ui.restoreconfig(phasebackup)
403 403 return commitfunc
404 404
405 405 def applychanges(ui, repo, ctx, opts):
406 406 """Merge changeset from ctx (only) in the current working directory"""
407 407 wcpar = repo.dirstate.parents()[0]
408 408 if ctx.p1().node() == wcpar:
409 409 # edition ar "in place" we do not need to make any merge,
410 410 # just applies changes on parent for edition
411 411 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
412 412 stats = None
413 413 else:
414 414 try:
415 415 # ui.forcemerge is an internal variable, do not document
416 416 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
417 417 'histedit')
418 418 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
419 419 finally:
420 420 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
421 421 return stats
422 422
423 423 def collapse(repo, first, last, commitopts, skipprompt=False):
424 424 """collapse the set of revisions from first to last as new one.
425 425
426 426 Expected commit options are:
427 427 - message
428 428 - date
429 429 - username
430 430 Commit message is edited in all cases.
431 431
432 432 This function works in memory."""
433 433 ctxs = list(repo.set('%d::%d', first, last))
434 434 if not ctxs:
435 435 return None
436 436 for c in ctxs:
437 437 if not c.mutable():
438 438 raise util.Abort(
439 439 _("cannot fold into public change %s") % node.short(c.node()))
440 440 base = first.parents()[0]
441 441
442 442 # commit a new version of the old changeset, including the update
443 443 # collect all files which might be affected
444 444 files = set()
445 445 for ctx in ctxs:
446 446 files.update(ctx.files())
447 447
448 448 # Recompute copies (avoid recording a -> b -> a)
449 449 copied = copies.pathcopies(base, last)
450 450
451 451 # prune files which were reverted by the updates
452 452 def samefile(f):
453 453 if f in last.manifest():
454 454 a = last.filectx(f)
455 455 if f in base.manifest():
456 456 b = base.filectx(f)
457 457 return (a.data() == b.data()
458 458 and a.flags() == b.flags())
459 459 else:
460 460 return False
461 461 else:
462 462 return f not in base.manifest()
463 463 files = [f for f in files if not samefile(f)]
464 464 # commit version of these files as defined by head
465 465 headmf = last.manifest()
466 466 def filectxfn(repo, ctx, path):
467 467 if path in headmf:
468 468 fctx = last[path]
469 469 flags = fctx.flags()
470 470 mctx = context.memfilectx(repo,
471 471 fctx.path(), fctx.data(),
472 472 islink='l' in flags,
473 473 isexec='x' in flags,
474 474 copied=copied.get(path))
475 475 return mctx
476 476 return None
477 477
478 478 if commitopts.get('message'):
479 479 message = commitopts['message']
480 480 else:
481 481 message = first.description()
482 482 user = commitopts.get('user')
483 483 date = commitopts.get('date')
484 484 extra = commitopts.get('extra')
485 485
486 486 parents = (first.p1().node(), first.p2().node())
487 487 editor = None
488 488 if not skipprompt:
489 489 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
490 490 new = context.memctx(repo,
491 491 parents=parents,
492 492 text=message,
493 493 files=files,
494 494 filectxfn=filectxfn,
495 495 user=user,
496 496 date=date,
497 497 extra=extra,
498 498 editor=editor)
499 499 return repo.commitctx(new)
500 500
501 501 class pick(histeditaction):
502 502 def run(self):
503 503 rulectx = self.repo[self.node]
504 504 if rulectx.parents()[0].node() == self.state.parentctxnode:
505 505 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
506 506 return rulectx, []
507 507
508 508 return super(pick, self).run()
509 509
510 510 class edit(histeditaction):
511 511 def run(self):
512 512 repo = self.repo
513 513 rulectx = repo[self.node]
514 514 hg.update(repo, self.state.parentctxnode)
515 515 applychanges(repo.ui, repo, rulectx, {})
516 516 raise error.InterventionRequired(
517 517 _('Make changes as needed, you may commit or record as needed '
518 518 'now.\nWhen you are finished, run hg histedit --continue to '
519 519 'resume.'))
520 520
521 521 def commiteditor(self):
522 522 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
523 523
524 524 class fold(histeditaction):
525 525 def continuedirty(self):
526 526 repo = self.repo
527 527 rulectx = repo[self.node]
528 528
529 529 commit = commitfuncfor(repo, rulectx)
530 530 commit(text='fold-temp-revision %s' % node.short(self.node),
531 531 user=rulectx.user(), date=rulectx.date(),
532 532 extra=rulectx.extra())
533 533
534 534 def continueclean(self):
535 535 repo = self.repo
536 536 ctx = repo['.']
537 537 rulectx = repo[self.node]
538 538 parentctxnode = self.state.parentctxnode
539 539 if ctx.node() == parentctxnode:
540 540 repo.ui.warn(_('%s: empty changeset\n') %
541 541 node.short(self.node))
542 542 return ctx, [(self.node, (parentctxnode,))]
543 543
544 544 parentctx = repo[parentctxnode]
545 545 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
546 546 parentctx))
547 547 if not newcommits:
548 548 repo.ui.warn(_('%s: cannot fold - working copy is not a '
549 549 'descendant of previous commit %s\n') %
550 550 (node.short(self.node), node.short(parentctxnode)))
551 551 return ctx, [(self.node, (ctx.node(),))]
552 552
553 553 middlecommits = newcommits.copy()
554 554 middlecommits.discard(ctx.node())
555 555
556 556 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
557 557 middlecommits)
558 558
559 559 def skipprompt(self):
560 560 return False
561 561
562 562 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
563 563 parent = ctx.parents()[0].node()
564 564 hg.update(repo, parent)
565 565 ### prepare new commit data
566 566 commitopts = {}
567 567 commitopts['user'] = ctx.user()
568 568 # commit message
569 569 if self.skipprompt():
570 570 newmessage = ctx.description()
571 571 else:
572 572 newmessage = '\n***\n'.join(
573 573 [ctx.description()] +
574 574 [repo[r].description() for r in internalchanges] +
575 575 [oldctx.description()]) + '\n'
576 576 commitopts['message'] = newmessage
577 577 # date
578 578 commitopts['date'] = max(ctx.date(), oldctx.date())
579 579 extra = ctx.extra().copy()
580 580 # histedit_source
581 581 # note: ctx is likely a temporary commit but that the best we can do
582 582 # here. This is sufficient to solve issue3681 anyway.
583 583 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
584 584 commitopts['extra'] = extra
585 585 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
586 586 try:
587 587 phasemin = max(ctx.phase(), oldctx.phase())
588 588 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
589 589 n = collapse(repo, ctx, repo[newnode], commitopts,
590 590 skipprompt=self.skipprompt())
591 591 finally:
592 592 repo.ui.restoreconfig(phasebackup)
593 593 if n is None:
594 594 return ctx, []
595 595 hg.update(repo, n)
596 596 replacements = [(oldctx.node(), (newnode,)),
597 597 (ctx.node(), (n,)),
598 598 (newnode, (n,)),
599 599 ]
600 600 for ich in internalchanges:
601 601 replacements.append((ich, (n,)))
602 602 return repo[n], replacements
603 603
604 604 class rollup(fold):
605 605 def skipprompt(self):
606 606 return True
607 607
608 608 class drop(histeditaction):
609 609 def run(self):
610 610 parentctx = self.repo[self.state.parentctxnode]
611 611 return parentctx, [(self.node, tuple())]
612 612
613 613 class message(histeditaction):
614 614 def commiteditor(self):
615 615 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
616 616
617 617 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
618 618 """utility function to find the first outgoing changeset
619 619
620 620 Used by initialisation code"""
621 621 dest = ui.expandpath(remote or 'default-push', remote or 'default')
622 622 dest, revs = hg.parseurl(dest, None)[:2]
623 623 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
624 624
625 625 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
626 626 other = hg.peer(repo, opts, dest)
627 627
628 628 if revs:
629 629 revs = [repo.lookup(rev) for rev in revs]
630 630
631 631 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
632 632 if not outgoing.missing:
633 633 raise util.Abort(_('no outgoing ancestors'))
634 634 roots = list(repo.revs("roots(%ln)", outgoing.missing))
635 635 if 1 < len(roots):
636 636 msg = _('there are ambiguous outgoing revisions')
637 637 hint = _('see "hg help histedit" for more detail')
638 638 raise util.Abort(msg, hint=hint)
639 639 return repo.lookup(roots[0])
640 640
641 641 actiontable = {'p': pick,
642 642 'pick': pick,
643 643 'e': edit,
644 644 'edit': edit,
645 645 'f': fold,
646 646 'fold': fold,
647 647 'r': rollup,
648 648 'roll': rollup,
649 649 'd': drop,
650 650 'drop': drop,
651 651 'm': message,
652 652 'mess': message,
653 653 }
654 654
655 655 @command('histedit',
656 656 [('', 'commands', '',
657 657 _('read history edits from the specified file'), _('FILE')),
658 658 ('c', 'continue', False, _('continue an edit already in progress')),
659 659 ('', 'edit-plan', False, _('edit remaining actions list')),
660 660 ('k', 'keep', False,
661 661 _("don't strip old nodes after edit is complete")),
662 662 ('', 'abort', False, _('abort an edit in progress')),
663 663 ('o', 'outgoing', False, _('changesets not found in destination')),
664 664 ('f', 'force', False,
665 665 _('force outgoing even for unrelated repositories')),
666 666 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
667 667 _("ANCESTOR | --outgoing [URL]"))
668 668 def histedit(ui, repo, *freeargs, **opts):
669 669 """interactively edit changeset history
670 670
671 671 This command edits changesets between ANCESTOR and the parent of
672 672 the working directory.
673 673
674 674 With --outgoing, this edits changesets not found in the
675 675 destination repository. If URL of the destination is omitted, the
676 676 'default-push' (or 'default') path will be used.
677 677
678 678 For safety, this command is aborted, also if there are ambiguous
679 679 outgoing revisions which may confuse users: for example, there are
680 680 multiple branches containing outgoing revisions.
681 681
682 682 Use "min(outgoing() and ::.)" or similar revset specification
683 683 instead of --outgoing to specify edit target revision exactly in
684 684 such ambiguous situation. See :hg:`help revsets` for detail about
685 685 selecting revisions.
686 686
687 687 Returns 0 on success, 1 if user intervention is required (not only
688 688 for intentional "edit" command, but also for resolving unexpected
689 689 conflicts).
690 690 """
691 691 state = histeditstate(repo)
692 692 try:
693 693 state.wlock = repo.wlock()
694 694 state.lock = repo.lock()
695 695 _histedit(ui, repo, state, *freeargs, **opts)
696 696 finally:
697 697 release(state.lock, state.wlock)
698 698
699 699 def _histedit(ui, repo, state, *freeargs, **opts):
700 700 # TODO only abort if we try and histedit mq patches, not just
701 701 # blanket if mq patches are applied somewhere
702 702 mq = getattr(repo, 'mq', None)
703 703 if mq and mq.applied:
704 704 raise util.Abort(_('source has mq patches applied'))
705 705
706 706 # basic argument incompatibility processing
707 707 outg = opts.get('outgoing')
708 708 cont = opts.get('continue')
709 709 editplan = opts.get('edit_plan')
710 710 abort = opts.get('abort')
711 711 force = opts.get('force')
712 712 rules = opts.get('commands', '')
713 713 revs = opts.get('rev', [])
714 714 goal = 'new' # This invocation goal, in new, continue, abort
715 715 if force and not outg:
716 716 raise util.Abort(_('--force only allowed with --outgoing'))
717 717 if cont:
718 718 if any((outg, abort, revs, freeargs, rules, editplan)):
719 719 raise util.Abort(_('no arguments allowed with --continue'))
720 720 goal = 'continue'
721 721 elif abort:
722 722 if any((outg, revs, freeargs, rules, editplan)):
723 723 raise util.Abort(_('no arguments allowed with --abort'))
724 724 goal = 'abort'
725 725 elif editplan:
726 726 if any((outg, revs, freeargs)):
727 727 raise util.Abort(_('only --commands argument allowed with '
728 728 '--edit-plan'))
729 729 goal = 'edit-plan'
730 730 else:
731 731 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
732 732 raise util.Abort(_('history edit already in progress, try '
733 733 '--continue or --abort'))
734 734 if outg:
735 735 if revs:
736 736 raise util.Abort(_('no revisions allowed with --outgoing'))
737 737 if len(freeargs) > 1:
738 738 raise util.Abort(
739 739 _('only one repo argument allowed with --outgoing'))
740 740 else:
741 741 revs.extend(freeargs)
742 742 if len(revs) == 0:
743 743 histeditdefault = ui.config('histedit', 'defaultrev')
744 744 if histeditdefault:
745 745 revs.append(histeditdefault)
746 746 if len(revs) != 1:
747 747 raise util.Abort(
748 748 _('histedit requires exactly one ancestor revision'))
749 749
750 750
751 751 replacements = []
752 752 state.keep = opts.get('keep', False)
753 753 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
754 754
755 755 # rebuild state
756 756 if goal == 'continue':
757 757 state.read()
758 758 state = bootstrapcontinue(ui, state, opts)
759 759 elif goal == 'edit-plan':
760 760 state.read()
761 761 if not rules:
762 762 comment = editcomment % (node.short(state.parentctxnode),
763 763 node.short(state.topmost))
764 764 rules = ruleeditor(repo, ui, state.rules, comment)
765 765 else:
766 766 if rules == '-':
767 767 f = sys.stdin
768 768 else:
769 769 f = open(rules)
770 770 rules = f.read()
771 771 f.close()
772 772 rules = [l for l in (r.strip() for r in rules.splitlines())
773 773 if l and not l.startswith('#')]
774 774 rules = verifyrules(rules, repo, [repo[c] for [_a, c] in state.rules])
775 775 state.rules = rules
776 776 state.write()
777 777 return
778 778 elif goal == 'abort':
779 779 state.read()
780 780 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
781 781 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
782 782
783 783 # Recover our old commits if necessary
784 784 if not state.topmost in repo and state.backupfile:
785 785 backupfile = repo.join(state.backupfile)
786 786 f = hg.openpath(ui, backupfile)
787 787 gen = exchange.readbundle(ui, f, backupfile)
788 788 changegroup.addchangegroup(repo, gen, 'histedit',
789 789 'bundle:' + backupfile)
790 790 os.remove(backupfile)
791 791
792 792 # check whether we should update away
793 793 parentnodes = [c.node() for c in repo[None].parents()]
794 794 for n in leafs | set([state.parentctxnode]):
795 795 if n in parentnodes:
796 796 hg.clean(repo, state.topmost)
797 797 break
798 798 else:
799 799 pass
800 if supportsmarkers:
801 obsolete.createmarkers(repo,
802 ((repo[t],()) for t in sorted(tmpnodes)))
803 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(leafs)))
804 else:
800 805 cleanupnode(ui, repo, 'created', tmpnodes)
801 806 cleanupnode(ui, repo, 'temp', leafs)
802 807 state.clear()
803 808 return
804 809 else:
805 810 cmdutil.checkunfinished(repo)
806 811 cmdutil.bailifchanged(repo)
807 812
808 813 topmost, empty = repo.dirstate.parents()
809 814 if outg:
810 815 if freeargs:
811 816 remote = freeargs[0]
812 817 else:
813 818 remote = None
814 819 root = findoutgoing(ui, repo, remote, force, opts)
815 820 else:
816 821 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
817 822 if len(rr) != 1:
818 823 raise util.Abort(_('The specified revisions must have '
819 824 'exactly one common root'))
820 825 root = rr[0].node()
821 826
822 827 revs = between(repo, root, topmost, state.keep)
823 828 if not revs:
824 829 raise util.Abort(_('%s is not an ancestor of working directory') %
825 830 node.short(root))
826 831
827 832 ctxs = [repo[r] for r in revs]
828 833 if not rules:
829 834 comment = editcomment % (node.short(root), node.short(topmost))
830 835 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
831 836 else:
832 837 if rules == '-':
833 838 f = sys.stdin
834 839 else:
835 840 f = open(rules)
836 841 rules = f.read()
837 842 f.close()
838 843 rules = [l for l in (r.strip() for r in rules.splitlines())
839 844 if l and not l.startswith('#')]
840 845 rules = verifyrules(rules, repo, ctxs)
841 846
842 847 parentctxnode = repo[root].parents()[0].node()
843 848
844 849 state.parentctxnode = parentctxnode
845 850 state.rules = rules
846 851 state.topmost = topmost
847 852 state.replacements = replacements
848 853
849 854 # Create a backup so we can always abort completely.
850 855 backupfile = None
851 856 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
852 857 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
853 858 'histedit')
854 859 state.backupfile = backupfile
855 860
856 861 while state.rules:
857 862 state.write()
858 863 action, ha = state.rules.pop(0)
859 864 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
860 865 actobj = actiontable[action].fromrule(state, ha)
861 866 parentctx, replacement_ = actobj.run()
862 867 state.parentctxnode = parentctx.node()
863 868 state.replacements.extend(replacement_)
864 869 state.write()
865 870
866 871 hg.update(repo, state.parentctxnode)
867 872
868 873 mapping, tmpnodes, created, ntm = processreplacement(state)
869 874 if mapping:
870 875 for prec, succs in mapping.iteritems():
871 876 if not succs:
872 877 ui.debug('histedit: %s is dropped\n' % node.short(prec))
873 878 else:
874 879 ui.debug('histedit: %s is replaced by %s\n' % (
875 880 node.short(prec), node.short(succs[0])))
876 881 if len(succs) > 1:
877 882 m = 'histedit: %s'
878 883 for n in succs[1:]:
879 884 ui.debug(m % node.short(n))
880 885
881 886 if not state.keep:
882 887 if mapping:
883 888 movebookmarks(ui, repo, mapping, state.topmost, ntm)
884 889 # TODO update mq state
885 890 if supportsmarkers:
886 891 markers = []
887 892 # sort by revision number because it sound "right"
888 893 for prec in sorted(mapping, key=repo.changelog.rev):
889 894 succs = mapping[prec]
890 895 markers.append((repo[prec],
891 896 tuple(repo[s] for s in succs)))
892 897 if markers:
893 898 obsolete.createmarkers(repo, markers)
894 899 else:
895 900 cleanupnode(ui, repo, 'replaced', mapping)
896
901 if supportsmarkers:
902 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)))
903 else:
897 904 cleanupnode(ui, repo, 'temp', tmpnodes)
898 905 state.clear()
899 906 if os.path.exists(repo.sjoin('undo')):
900 907 os.unlink(repo.sjoin('undo'))
901 908
902 909 def bootstrapcontinue(ui, state, opts):
903 910 repo = state.repo
904 911 if state.rules:
905 912 action, currentnode = state.rules.pop(0)
906 913
907 914 actobj = actiontable[action].fromrule(state, currentnode)
908 915
909 916 s = repo.status()
910 917 if s.modified or s.added or s.removed or s.deleted:
911 918 actobj.continuedirty()
912 919 s = repo.status()
913 920 if s.modified or s.added or s.removed or s.deleted:
914 921 raise util.Abort(_("working copy still dirty"))
915 922
916 923 parentctx, replacements = actobj.continueclean()
917 924
918 925 state.parentctxnode = parentctx.node()
919 926 state.replacements.extend(replacements)
920 927
921 928 return state
922 929
923 930 def between(repo, old, new, keep):
924 931 """select and validate the set of revision to edit
925 932
926 933 When keep is false, the specified set can't have children."""
927 934 ctxs = list(repo.set('%n::%n', old, new))
928 935 if ctxs and not keep:
929 936 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
930 937 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
931 938 raise util.Abort(_('cannot edit history that would orphan nodes'))
932 939 if repo.revs('(%ld) and merge()', ctxs):
933 940 raise util.Abort(_('cannot edit history that contains merges'))
934 941 root = ctxs[0] # list is already sorted by repo.set
935 942 if not root.mutable():
936 943 raise util.Abort(_('cannot edit public changeset: %s') % root,
937 944 hint=_('see "hg help phases" for details'))
938 945 return [c.node() for c in ctxs]
939 946
940 947 def makedesc(repo, action, rev):
941 948 """build a initial action line for a ctx
942 949
943 950 line are in the form:
944 951
945 952 <action> <hash> <rev> <summary>
946 953 """
947 954 ctx = repo[rev]
948 955 summary = ''
949 956 if ctx.description():
950 957 summary = ctx.description().splitlines()[0]
951 958 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
952 959 # trim to 80 columns so it's not stupidly wide in my editor
953 960 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
954 961 maxlen = max(maxlen, 22) # avoid truncating hash
955 962 return util.ellipsis(line, maxlen)
956 963
957 964 def ruleeditor(repo, ui, rules, editcomment=""):
958 965 """open an editor to edit rules
959 966
960 967 rules are in the format [ [act, ctx], ...] like in state.rules
961 968 """
962 969 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
963 970 rules += '\n\n'
964 971 rules += editcomment
965 972 rules = ui.edit(rules, ui.username())
966 973
967 974 # Save edit rules in .hg/histedit-last-edit.txt in case
968 975 # the user needs to ask for help after something
969 976 # surprising happens.
970 977 f = open(repo.join('histedit-last-edit.txt'), 'w')
971 978 f.write(rules)
972 979 f.close()
973 980
974 981 return rules
975 982
976 983 def verifyrules(rules, repo, ctxs):
977 984 """Verify that there exists exactly one edit rule per given changeset.
978 985
979 986 Will abort if there are to many or too few rules, a malformed rule,
980 987 or a rule on a changeset outside of the user-given range.
981 988 """
982 989 parsed = []
983 990 expected = set(c.hex() for c in ctxs)
984 991 seen = set()
985 992 for r in rules:
986 993 if ' ' not in r:
987 994 raise util.Abort(_('malformed line "%s"') % r)
988 995 action, rest = r.split(' ', 1)
989 996 ha = rest.strip().split(' ', 1)[0]
990 997 try:
991 998 ha = repo[ha].hex()
992 999 except error.RepoError:
993 1000 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
994 1001 if ha not in expected:
995 1002 raise util.Abort(
996 1003 _('may not use changesets other than the ones listed'))
997 1004 if ha in seen:
998 1005 raise util.Abort(_('duplicated command for changeset %s') %
999 1006 ha[:12])
1000 1007 seen.add(ha)
1001 1008 if action not in actiontable:
1002 1009 raise util.Abort(_('unknown action "%s"') % action)
1003 1010 parsed.append([action, ha])
1004 1011 missing = sorted(expected - seen) # sort to stabilize output
1005 1012 if missing:
1006 1013 raise util.Abort(_('missing rules for changeset %s') %
1007 1014 missing[0][:12],
1008 1015 hint=_('do you want to use the drop action?'))
1009 1016 return parsed
1010 1017
1011 1018 def processreplacement(state):
1012 1019 """process the list of replacements to return
1013 1020
1014 1021 1) the final mapping between original and created nodes
1015 1022 2) the list of temporary node created by histedit
1016 1023 3) the list of new commit created by histedit"""
1017 1024 replacements = state.replacements
1018 1025 allsuccs = set()
1019 1026 replaced = set()
1020 1027 fullmapping = {}
1021 1028 # initialise basic set
1022 1029 # fullmapping record all operation recorded in replacement
1023 1030 for rep in replacements:
1024 1031 allsuccs.update(rep[1])
1025 1032 replaced.add(rep[0])
1026 1033 fullmapping.setdefault(rep[0], set()).update(rep[1])
1027 1034 new = allsuccs - replaced
1028 1035 tmpnodes = allsuccs & replaced
1029 1036 # Reduce content fullmapping into direct relation between original nodes
1030 1037 # and final node created during history edition
1031 1038 # Dropped changeset are replaced by an empty list
1032 1039 toproceed = set(fullmapping)
1033 1040 final = {}
1034 1041 while toproceed:
1035 1042 for x in list(toproceed):
1036 1043 succs = fullmapping[x]
1037 1044 for s in list(succs):
1038 1045 if s in toproceed:
1039 1046 # non final node with unknown closure
1040 1047 # We can't process this now
1041 1048 break
1042 1049 elif s in final:
1043 1050 # non final node, replace with closure
1044 1051 succs.remove(s)
1045 1052 succs.update(final[s])
1046 1053 else:
1047 1054 final[x] = succs
1048 1055 toproceed.remove(x)
1049 1056 # remove tmpnodes from final mapping
1050 1057 for n in tmpnodes:
1051 1058 del final[n]
1052 1059 # we expect all changes involved in final to exist in the repo
1053 1060 # turn `final` into list (topologically sorted)
1054 1061 nm = state.repo.changelog.nodemap
1055 1062 for prec, succs in final.items():
1056 1063 final[prec] = sorted(succs, key=nm.get)
1057 1064
1058 1065 # computed topmost element (necessary for bookmark)
1059 1066 if new:
1060 1067 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1061 1068 elif not final:
1062 1069 # Nothing rewritten at all. we won't need `newtopmost`
1063 1070 # It is the same as `oldtopmost` and `processreplacement` know it
1064 1071 newtopmost = None
1065 1072 else:
1066 1073 # every body died. The newtopmost is the parent of the root.
1067 1074 r = state.repo.changelog.rev
1068 1075 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1069 1076
1070 1077 return final, tmpnodes, new, newtopmost
1071 1078
1072 1079 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1073 1080 """Move bookmark from old to newly created node"""
1074 1081 if not mapping:
1075 1082 # if nothing got rewritten there is not purpose for this function
1076 1083 return
1077 1084 moves = []
1078 1085 for bk, old in sorted(repo._bookmarks.iteritems()):
1079 1086 if old == oldtopmost:
1080 1087 # special case ensure bookmark stay on tip.
1081 1088 #
1082 1089 # This is arguably a feature and we may only want that for the
1083 1090 # active bookmark. But the behavior is kept compatible with the old
1084 1091 # version for now.
1085 1092 moves.append((bk, newtopmost))
1086 1093 continue
1087 1094 base = old
1088 1095 new = mapping.get(base, None)
1089 1096 if new is None:
1090 1097 continue
1091 1098 while not new:
1092 1099 # base is killed, trying with parent
1093 1100 base = repo[base].p1().node()
1094 1101 new = mapping.get(base, (base,))
1095 1102 # nothing to move
1096 1103 moves.append((bk, new[-1]))
1097 1104 if moves:
1098 1105 marks = repo._bookmarks
1099 1106 for mark, new in moves:
1100 1107 old = marks[mark]
1101 1108 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1102 1109 % (mark, node.short(old), node.short(new)))
1103 1110 marks[mark] = new
1104 1111 marks.write()
1105 1112
1106 1113 def cleanupnode(ui, repo, name, nodes):
1107 1114 """strip a group of nodes from the repository
1108 1115
1109 1116 The set of node to strip may contains unknown nodes."""
1110 1117 ui.debug('should strip %s nodes %s\n' %
1111 1118 (name, ', '.join([node.short(n) for n in nodes])))
1112 1119 lock = None
1113 1120 try:
1114 1121 lock = repo.lock()
1115 1122 # Find all node that need to be stripped
1116 1123 # (we hg %lr instead of %ln to silently ignore unknown item
1117 1124 nm = repo.changelog.nodemap
1118 1125 nodes = sorted(n for n in nodes if n in nm)
1119 1126 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1120 1127 for c in roots:
1121 1128 # We should process node in reverse order to strip tip most first.
1122 1129 # but this trigger a bug in changegroup hook.
1123 1130 # This would reduce bundle overhead
1124 1131 repair.strip(ui, repo, c)
1125 1132 finally:
1126 1133 release(lock)
1127 1134
1128 1135 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1129 1136 if isinstance(nodelist, str):
1130 1137 nodelist = [nodelist]
1131 1138 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1132 1139 state = histeditstate(repo)
1133 1140 state.read()
1134 1141 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1135 1142 in state.rules if rulehash in repo])
1136 1143 strip_nodes = set([repo[n].node() for n in nodelist])
1137 1144 common_nodes = histedit_nodes & strip_nodes
1138 1145 if common_nodes:
1139 1146 raise util.Abort(_("histedit in progress, can't strip %s")
1140 1147 % ', '.join(node.short(x) for x in common_nodes))
1141 1148 return orig(ui, repo, nodelist, *args, **kwargs)
1142 1149
1143 1150 extensions.wrapfunction(repair, 'strip', stripwrapper)
1144 1151
1145 1152 def summaryhook(ui, repo):
1146 1153 if not os.path.exists(repo.join('histedit-state')):
1147 1154 return
1148 1155 state = histeditstate(repo)
1149 1156 state.read()
1150 1157 if state.rules:
1151 1158 # i18n: column positioning for "hg summary"
1152 1159 ui.write(_('hist: %s (histedit --continue)\n') %
1153 1160 (ui.label(_('%d remaining'), 'histedit.remaining') %
1154 1161 len(state.rules)))
1155 1162
1156 1163 def extsetup(ui):
1157 1164 cmdutil.summaryhooks.add('histedit', summaryhook)
1158 1165 cmdutil.unfinishedstates.append(
1159 1166 ['histedit-state', False, True, _('histedit in progress'),
1160 1167 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,460 +1,463
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 Enable obsolete
4 4
5 5 $ cat >> $HGRCPATH << EOF
6 6 > [ui]
7 7 > logtemplate= {rev}:{node|short} {desc|firstline}
8 8 > [phases]
9 9 > publish=False
10 10 > [experimental]
11 11 > evolution=createmarkers,allowunstable
12 12 > [extensions]
13 13 > histedit=
14 14 > rebase=
15 15 > EOF
16 16
17 17 $ hg init base
18 18 $ cd base
19 19
20 20 $ for x in a b c d e f ; do
21 21 > echo $x > $x
22 22 > hg add $x
23 23 > hg ci -m $x
24 24 > done
25 25
26 26 $ hg log --graph
27 27 @ 5:652413bf663e f
28 28 |
29 29 o 4:e860deea161a e
30 30 |
31 31 o 3:055a42cdd887 d
32 32 |
33 33 o 2:177f92b77385 c
34 34 |
35 35 o 1:d2ae7f538514 b
36 36 |
37 37 o 0:cb9a9f314b8b a
38 38
39 39
40 40 $ HGEDITOR=cat hg histedit 1
41 41 pick d2ae7f538514 1 b
42 42 pick 177f92b77385 2 c
43 43 pick 055a42cdd887 3 d
44 44 pick e860deea161a 4 e
45 45 pick 652413bf663e 5 f
46 46
47 47 # Edit history between d2ae7f538514 and 652413bf663e
48 48 #
49 49 # Commits are listed from least to most recent
50 50 #
51 51 # Commands:
52 52 # p, pick = use commit
53 53 # e, edit = use commit, but stop for amending
54 54 # f, fold = use commit, but combine it with the one above
55 55 # r, roll = like fold, but discard this commit's description
56 56 # d, drop = remove commit from history
57 57 # m, mess = edit message without changing commit content
58 58 #
59 59 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 60 $ hg histedit 1 --commands - --verbose <<EOF | grep histedit
61 61 > pick 177f92b77385 2 c
62 62 > drop d2ae7f538514 1 b
63 63 > pick 055a42cdd887 3 d
64 64 > fold e860deea161a 4 e
65 65 > pick 652413bf663e 5 f
66 66 > EOF
67 saved backup bundle to $TESTTMP/base/.hg/strip-backup/96e494a2d553-3c6c5d92-backup.hg (glob)
67 [1]
68 68 $ hg log --graph --hidden
69 @ 8:cacdfd884a93 f
69 @ 10:cacdfd884a93 f
70 |
71 o 9:59d9f330561f d
70 72 |
71 o 7:59d9f330561f d
72 |
73 | x 8:b558abc46d09 fold-temp-revision e860deea161a
74 | |
75 | x 7:96e494a2d553 d
76 |/
73 77 o 6:b346ab9a313d c
74 78 |
75 79 | x 5:652413bf663e f
76 80 | |
77 81 | x 4:e860deea161a e
78 82 | |
79 83 | x 3:055a42cdd887 d
80 84 | |
81 85 | x 2:177f92b77385 c
82 86 | |
83 87 | x 1:d2ae7f538514 b
84 88 |/
85 89 o 0:cb9a9f314b8b a
86 90
87 91 $ hg debugobsolete
88 92 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b} (*) {'user': 'test'} (glob)
89 93 177f92b773850b59254aa5e923436f921b55483b b346ab9a313db8537ecf96fca3ca3ca984ef3bd7 0 (*) {'user': 'test'} (glob)
90 94 055a42cdd88768532f9cf79daa407fc8d138de9b 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 (*) {'user': 'test'} (glob)
91 95 e860deea161a2f77de56603b340ebbb4536308ae 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 (*) {'user': 'test'} (glob)
92 96 652413bf663ef2a641cab26574e46d5f5a64a55a cacdfd884a9321ec4e1de275ef3949fa953a1f83 0 (*) {'user': 'test'} (glob)
97 96e494a2d553dd05902ba1cee1d94d4cb7b8faed 0 {b346ab9a313db8537ecf96fca3ca3ca984ef3bd7} (*) {'user': 'test'} (glob)
98 b558abc46d09c30f57ac31e85a8a3d64d2e906e4 0 {96e494a2d553dd05902ba1cee1d94d4cb7b8faed} (*) {'user': 'test'} (glob)
93 99
94 100
95 101 Ensure hidden revision does not prevent histedit
96 102 -------------------------------------------------
97 103
98 104 create an hidden revision
99 105
100 106 $ hg histedit 6 --commands - << EOF
101 107 > pick b346ab9a313d 6 c
102 108 > drop 59d9f330561f 7 d
103 109 > pick cacdfd884a93 8 f
104 110 > EOF
105 111 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
106 112 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
107 113 $ hg log --graph
108 @ 9:c13eb81022ca f
114 @ 11:c13eb81022ca f
109 115 |
110 116 o 6:b346ab9a313d c
111 117 |
112 118 o 0:cb9a9f314b8b a
113 119
114 120 check hidden revision are ignored (6 have hidden children 7 and 8)
115 121
116 122 $ hg histedit 6 --commands - << EOF
117 123 > pick b346ab9a313d 6 c
118 124 > pick c13eb81022ca 8 f
119 125 > EOF
120 126 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 127
122 128
123 129
124 130 Test that rewriting leaving instability behind is allowed
125 131 ---------------------------------------------------------------------
126 132
127 133 $ hg up '.^'
128 134 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
129 135 $ hg log -r 'children(.)'
130 9:c13eb81022ca f (no-eol)
136 11:c13eb81022ca f (no-eol)
131 137 $ hg histedit -r '.' --commands - <<EOF
132 138 > edit b346ab9a313d 6 c
133 139 > EOF
134 140 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
135 141 adding c
136 142 Make changes as needed, you may commit or record as needed now.
137 143 When you are finished, run hg histedit --continue to resume.
138 144 [1]
139 145 $ echo c >> c
140 146 $ hg histedit --continue
141 147 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
142 148
143 149 $ hg log -r 'unstable()'
144 9:c13eb81022ca f (no-eol)
150 11:c13eb81022ca f (no-eol)
145 151
146 152 stabilise
147 153
148 154 $ hg rebase -r 'unstable()' -d .
149 rebasing 9:c13eb81022ca "f"
155 rebasing 11:c13eb81022ca "f"
150 156 $ hg up tip -q
151 157
152 158 Test dropping of changeset on the top of the stack
153 159 -------------------------------------------------------
154 160
155 161 Nothing is rewritten below, the working directory parent must be change for the
156 162 dropped changeset to be hidden.
157 163
158 164 $ cd ..
159 165 $ hg clone base droplast
160 166 updating to branch default
161 167 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
162 168 $ cd droplast
163 169 $ hg histedit -r '40db8afa467b' --commands - << EOF
164 170 > pick 40db8afa467b 10 c
165 171 > drop b449568bf7fc 11 f
166 172 > EOF
167 173 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
168 174 $ hg log -G
169 @ 10:40db8afa467b c
175 @ 12:40db8afa467b c
170 176 |
171 177 o 0:cb9a9f314b8b a
172 178
173 179
174 180 With rewritten ancestors
175 181
176 182 $ echo e > e
177 183 $ hg add e
178 184 $ hg commit -m g
179 185 $ echo f > f
180 186 $ hg add f
181 187 $ hg commit -m h
182 188 $ hg histedit -r '40db8afa467b' --commands - << EOF
183 189 > pick 47a8561c0449 12 g
184 190 > pick 40db8afa467b 10 c
185 191 > drop 1b3b05f35ff0 13 h
186 192 > EOF
187 193 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
188 194 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
189 195 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
190 196 $ hg log -G
191 @ 15:ee6544123ab8 c
197 @ 17:ee6544123ab8 c
192 198 |
193 o 14:269e713e9eae g
199 o 16:269e713e9eae g
194 200 |
195 201 o 0:cb9a9f314b8b a
196 202
197 203 $ cd ../base
198 204
199 205
200 206
201 207 Test phases support
202 208 ===========================================
203 209
204 210 Check that histedit respect immutability
205 211 -------------------------------------------
206 212
207 213 $ cat >> $HGRCPATH << EOF
208 214 > [ui]
209 215 > logtemplate= {rev}:{node|short} ({phase}) {desc|firstline}\n
210 216 > EOF
211 217
212 218 $ hg ph -pv '.^'
213 219 phase changed for 2 changesets
214 220 $ hg log -G
215 @ 11:b449568bf7fc (draft) f
221 @ 13:b449568bf7fc (draft) f
216 222 |
217 o 10:40db8afa467b (public) c
223 o 12:40db8afa467b (public) c
218 224 |
219 225 o 0:cb9a9f314b8b (public) a
220 226
221 227 $ hg histedit -r '.~2'
222 228 abort: cannot edit public changeset: cb9a9f314b8b
223 229 (see "hg help phases" for details)
224 230 [255]
225 231
226 232
227 233 Prepare further testing
228 234 -------------------------------------------
229 235
230 236 $ for x in g h i j k ; do
231 237 > echo $x > $x
232 238 > hg add $x
233 239 > hg ci -m $x
234 240 > done
235 241 $ hg phase --force --secret .~2
236 242 $ hg log -G
237 @ 16:ee118ab9fa44 (secret) k
243 @ 18:ee118ab9fa44 (secret) k
238 244 |
239 o 15:3a6c53ee7f3d (secret) j
245 o 17:3a6c53ee7f3d (secret) j
240 246 |
241 o 14:b605fb7503f2 (secret) i
247 o 16:b605fb7503f2 (secret) i
242 248 |
243 o 13:7395e1ff83bd (draft) h
249 o 15:7395e1ff83bd (draft) h
244 250 |
245 o 12:6b70183d2492 (draft) g
251 o 14:6b70183d2492 (draft) g
246 252 |
247 o 11:b449568bf7fc (draft) f
253 o 13:b449568bf7fc (draft) f
248 254 |
249 o 10:40db8afa467b (public) c
255 o 12:40db8afa467b (public) c
250 256 |
251 257 o 0:cb9a9f314b8b (public) a
252 258
253 259 $ cd ..
254 260
255 261 simple phase conservation
256 262 -------------------------------------------
257 263
258 264 Resulting changeset should conserve the phase of the original one whatever the
259 265 phases.new-commit option is.
260 266
261 267 New-commit as draft (default)
262 268
263 269 $ cp -r base simple-draft
264 270 $ cd simple-draft
265 271 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
266 272 > edit b449568bf7fc 11 f
267 273 > pick 6b70183d2492 12 g
268 274 > pick 7395e1ff83bd 13 h
269 275 > pick b605fb7503f2 14 i
270 276 > pick 3a6c53ee7f3d 15 j
271 277 > pick ee118ab9fa44 16 k
272 278 > EOF
273 279 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
274 280 adding f
275 281 Make changes as needed, you may commit or record as needed now.
276 282 When you are finished, run hg histedit --continue to resume.
277 283 [1]
278 284 $ echo f >> f
279 285 $ hg histedit --continue
280 286 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
281 287 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
282 288 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
283 289 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
284 290 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
285 291 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
286 292 $ hg log -G
287 @ 22:12e89af74238 (secret) k
293 @ 24:12e89af74238 (secret) k
288 294 |
289 o 21:636a8687b22e (secret) j
295 o 23:636a8687b22e (secret) j
290 296 |
291 o 20:ccaf0a38653f (secret) i
297 o 22:ccaf0a38653f (secret) i
292 298 |
293 o 19:11a89d1c2613 (draft) h
299 o 21:11a89d1c2613 (draft) h
294 300 |
295 o 18:c1dec7ca82ea (draft) g
301 o 20:c1dec7ca82ea (draft) g
296 302 |
297 o 17:087281e68428 (draft) f
303 o 19:087281e68428 (draft) f
298 304 |
299 o 10:40db8afa467b (public) c
305 o 12:40db8afa467b (public) c
300 306 |
301 307 o 0:cb9a9f314b8b (public) a
302 308
303 309 $ cd ..
304 310
305 311
306 312 New-commit as draft (default)
307 313
308 314 $ cp -r base simple-secret
309 315 $ cd simple-secret
310 316 $ cat >> .hg/hgrc << EOF
311 317 > [phases]
312 318 > new-commit=secret
313 319 > EOF
314 320 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
315 321 > edit b449568bf7fc 11 f
316 322 > pick 6b70183d2492 12 g
317 323 > pick 7395e1ff83bd 13 h
318 324 > pick b605fb7503f2 14 i
319 325 > pick 3a6c53ee7f3d 15 j
320 326 > pick ee118ab9fa44 16 k
321 327 > EOF
322 328 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
323 329 adding f
324 330 Make changes as needed, you may commit or record as needed now.
325 331 When you are finished, run hg histedit --continue to resume.
326 332 [1]
327 333 $ echo f >> f
328 334 $ hg histedit --continue
329 335 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
330 336 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
331 337 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
332 338 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
333 339 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 340 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
335 341 $ hg log -G
336 @ 22:12e89af74238 (secret) k
342 @ 24:12e89af74238 (secret) k
337 343 |
338 o 21:636a8687b22e (secret) j
344 o 23:636a8687b22e (secret) j
339 345 |
340 o 20:ccaf0a38653f (secret) i
346 o 22:ccaf0a38653f (secret) i
341 347 |
342 o 19:11a89d1c2613 (draft) h
348 o 21:11a89d1c2613 (draft) h
343 349 |
344 o 18:c1dec7ca82ea (draft) g
350 o 20:c1dec7ca82ea (draft) g
345 351 |
346 o 17:087281e68428 (draft) f
352 o 19:087281e68428 (draft) f
347 353 |
348 o 10:40db8afa467b (public) c
354 o 12:40db8afa467b (public) c
349 355 |
350 356 o 0:cb9a9f314b8b (public) a
351 357
352 358 $ cd ..
353 359
354 360
355 361 Changeset reordering
356 362 -------------------------------------------
357 363
358 364 If a secret changeset is put before a draft one, all descendant should be secret.
359 365 It seems more important to present the secret phase.
360 366
361 367 $ cp -r base reorder
362 368 $ cd reorder
363 369 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
364 370 > pick b449568bf7fc 11 f
365 371 > pick 3a6c53ee7f3d 15 j
366 372 > pick 6b70183d2492 12 g
367 373 > pick b605fb7503f2 14 i
368 374 > pick 7395e1ff83bd 13 h
369 375 > pick ee118ab9fa44 16 k
370 376 > EOF
371 377 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
372 378 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
373 379 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
374 380 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
375 381 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
376 382 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
377 383 $ hg log -G
378 @ 21:558246857888 (secret) k
384 @ 23:558246857888 (secret) k
379 385 |
380 o 20:28bd44768535 (secret) h
386 o 22:28bd44768535 (secret) h
381 387 |
382 o 19:d5395202aeb9 (secret) i
388 o 21:d5395202aeb9 (secret) i
383 389 |
384 o 18:21edda8e341b (secret) g
390 o 20:21edda8e341b (secret) g
385 391 |
386 o 17:5ab64f3a4832 (secret) j
392 o 19:5ab64f3a4832 (secret) j
387 393 |
388 o 11:b449568bf7fc (draft) f
394 o 13:b449568bf7fc (draft) f
389 395 |
390 o 10:40db8afa467b (public) c
396 o 12:40db8afa467b (public) c
391 397 |
392 398 o 0:cb9a9f314b8b (public) a
393 399
394 400 $ cd ..
395 401
396 402 Changeset folding
397 403 -------------------------------------------
398 404
399 405 Folding a secret changeset with a draft one turn the result secret (again,
400 406 better safe than sorry). Folding between same phase changeset still works
401 407
402 408 Note that there is a few reordering in this series for more extensive test
403 409
404 410 $ cp -r base folding
405 411 $ cd folding
406 412 $ cat >> .hg/hgrc << EOF
407 413 > [phases]
408 414 > new-commit=secret
409 415 > EOF
410 416 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
411 417 > pick 7395e1ff83bd 13 h
412 418 > fold b449568bf7fc 11 f
413 419 > pick 6b70183d2492 12 g
414 420 > fold 3a6c53ee7f3d 15 j
415 421 > pick b605fb7503f2 14 i
416 422 > fold ee118ab9fa44 16 k
417 423 > EOF
418 424 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
419 425 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
420 426 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
421 427 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
422 428 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
423 429 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
424 430 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
425 431 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
426 432 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
427 433 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
428 434 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
429 435 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
430 436 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
431 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/58019c66f35f-96092fce-backup.hg (glob)
432 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/83d1858e070b-f3469cf8-backup.hg (glob)
433 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/859969f5ed7e-d89a19d7-backup.hg (glob)
434 437 $ hg log -G
435 @ 19:f9daec13fb98 (secret) i
438 @ 27:f9daec13fb98 (secret) i
436 439 |
437 o 18:49807617f46a (secret) g
440 o 24:49807617f46a (secret) g
438 441 |
439 o 17:050280826e04 (draft) h
442 o 21:050280826e04 (draft) h
440 443 |
441 o 10:40db8afa467b (public) c
444 o 12:40db8afa467b (public) c
442 445 |
443 446 o 0:cb9a9f314b8b (public) a
444 447
445 $ hg co 18
448 $ hg co 24
446 449 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
447 450 $ echo wat >> wat
448 451 $ hg add wat
449 452 $ hg ci -m 'add wat'
450 453 created new head
451 $ hg merge 19
454 $ hg merge 27
452 455 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
453 456 (branch merge, don't forget to commit)
454 457 $ hg ci -m 'merge'
455 458 $ echo not wat > wat
456 459 $ hg ci -m 'modify wat'
457 $ hg histedit 17
460 $ hg histedit 21
458 461 abort: cannot edit history that contains merges
459 462 [255]
460 463 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now