##// END OF EJS Templates
histedit: allow configuring default behavior...
Durham Goode -
r24009:00d33176 default
parent child Browse files
Show More
@@ -1,985 +1,989 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 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
147 147 try:
148 148 import cPickle as pickle
149 149 pickle.dump # import now
150 150 except ImportError:
151 151 import pickle
152 152 import errno
153 153 import os
154 154 import sys
155 155
156 156 from mercurial import cmdutil
157 157 from mercurial import discovery
158 158 from mercurial import error
159 159 from mercurial import copies
160 160 from mercurial import context
161 161 from mercurial import hg
162 162 from mercurial import node
163 163 from mercurial import repair
164 164 from mercurial import scmutil
165 165 from mercurial import util
166 166 from mercurial import obsolete
167 167 from mercurial import merge as mergemod
168 168 from mercurial.lock import release
169 169 from mercurial.i18n import _
170 170
171 171 cmdtable = {}
172 172 command = cmdutil.command(cmdtable)
173 173
174 174 testedwith = 'internal'
175 175
176 176 # i18n: command names and abbreviations must remain untranslated
177 177 editcomment = _("""# Edit history between %s and %s
178 178 #
179 179 # Commits are listed from least to most recent
180 180 #
181 181 # Commands:
182 182 # p, pick = use commit
183 183 # e, edit = use commit, but stop for amending
184 184 # f, fold = use commit, but combine it with the one above
185 185 # r, roll = like fold, but discard this commit's description
186 186 # d, drop = remove commit from history
187 187 # m, mess = edit message without changing commit content
188 188 #
189 189 """)
190 190
191 191 class histeditstate(object):
192 192 def __init__(self, repo, parentctx=None, rules=None, keep=None,
193 193 topmost=None, replacements=None, lock=None, wlock=None):
194 194 self.repo = repo
195 195 self.rules = rules
196 196 self.keep = keep
197 197 self.topmost = topmost
198 198 self.parentctx = parentctx
199 199 self.lock = lock
200 200 self.wlock = wlock
201 201 if replacements is None:
202 202 self.replacements = []
203 203 else:
204 204 self.replacements = replacements
205 205
206 206 def read(self):
207 207 """Load histedit state from disk and set fields appropriately."""
208 208 try:
209 209 fp = self.repo.vfs('histedit-state', 'r')
210 210 except IOError, err:
211 211 if err.errno != errno.ENOENT:
212 212 raise
213 213 raise util.Abort(_('no histedit in progress'))
214 214
215 215 parentctxnode, rules, keep, topmost, replacements = pickle.load(fp)
216 216
217 217 self.parentctx = self.repo[parentctxnode]
218 218 self.rules = rules
219 219 self.keep = keep
220 220 self.topmost = topmost
221 221 self.replacements = replacements
222 222
223 223 def write(self):
224 224 fp = self.repo.vfs('histedit-state', 'w')
225 225 pickle.dump((self.parentctx.node(), self.rules, self.keep,
226 226 self.topmost, self.replacements), fp)
227 227 fp.close()
228 228
229 229 def clear(self):
230 230 self.repo.vfs.unlink('histedit-state')
231 231
232 232 def commitfuncfor(repo, src):
233 233 """Build a commit function for the replacement of <src>
234 234
235 235 This function ensure we apply the same treatment to all changesets.
236 236
237 237 - Add a 'histedit_source' entry in extra.
238 238
239 239 Note that fold have its own separated logic because its handling is a bit
240 240 different and not easily factored out of the fold method.
241 241 """
242 242 phasemin = src.phase()
243 243 def commitfunc(**kwargs):
244 244 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
245 245 try:
246 246 repo.ui.setconfig('phases', 'new-commit', phasemin,
247 247 'histedit')
248 248 extra = kwargs.get('extra', {}).copy()
249 249 extra['histedit_source'] = src.hex()
250 250 kwargs['extra'] = extra
251 251 return repo.commit(**kwargs)
252 252 finally:
253 253 repo.ui.restoreconfig(phasebackup)
254 254 return commitfunc
255 255
256 256 def applychanges(ui, repo, ctx, opts):
257 257 """Merge changeset from ctx (only) in the current working directory"""
258 258 wcpar = repo.dirstate.parents()[0]
259 259 if ctx.p1().node() == wcpar:
260 260 # edition ar "in place" we do not need to make any merge,
261 261 # just applies changes on parent for edition
262 262 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
263 263 stats = None
264 264 else:
265 265 try:
266 266 # ui.forcemerge is an internal variable, do not document
267 267 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
268 268 'histedit')
269 269 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
270 270 finally:
271 271 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
272 272 return stats
273 273
274 274 def collapse(repo, first, last, commitopts):
275 275 """collapse the set of revisions from first to last as new one.
276 276
277 277 Expected commit options are:
278 278 - message
279 279 - date
280 280 - username
281 281 Commit message is edited in all cases.
282 282
283 283 This function works in memory."""
284 284 ctxs = list(repo.set('%d::%d', first, last))
285 285 if not ctxs:
286 286 return None
287 287 base = first.parents()[0]
288 288
289 289 # commit a new version of the old changeset, including the update
290 290 # collect all files which might be affected
291 291 files = set()
292 292 for ctx in ctxs:
293 293 files.update(ctx.files())
294 294
295 295 # Recompute copies (avoid recording a -> b -> a)
296 296 copied = copies.pathcopies(base, last)
297 297
298 298 # prune files which were reverted by the updates
299 299 def samefile(f):
300 300 if f in last.manifest():
301 301 a = last.filectx(f)
302 302 if f in base.manifest():
303 303 b = base.filectx(f)
304 304 return (a.data() == b.data()
305 305 and a.flags() == b.flags())
306 306 else:
307 307 return False
308 308 else:
309 309 return f not in base.manifest()
310 310 files = [f for f in files if not samefile(f)]
311 311 # commit version of these files as defined by head
312 312 headmf = last.manifest()
313 313 def filectxfn(repo, ctx, path):
314 314 if path in headmf:
315 315 fctx = last[path]
316 316 flags = fctx.flags()
317 317 mctx = context.memfilectx(repo,
318 318 fctx.path(), fctx.data(),
319 319 islink='l' in flags,
320 320 isexec='x' in flags,
321 321 copied=copied.get(path))
322 322 return mctx
323 323 return None
324 324
325 325 if commitopts.get('message'):
326 326 message = commitopts['message']
327 327 else:
328 328 message = first.description()
329 329 user = commitopts.get('user')
330 330 date = commitopts.get('date')
331 331 extra = commitopts.get('extra')
332 332
333 333 parents = (first.p1().node(), first.p2().node())
334 334 editor = None
335 335 if not commitopts.get('rollup'):
336 336 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
337 337 new = context.memctx(repo,
338 338 parents=parents,
339 339 text=message,
340 340 files=files,
341 341 filectxfn=filectxfn,
342 342 user=user,
343 343 date=date,
344 344 extra=extra,
345 345 editor=editor)
346 346 return repo.commitctx(new)
347 347
348 348 def pick(ui, state, ha, opts):
349 349 repo, ctx = state.repo, state.parentctx
350 350 oldctx = repo[ha]
351 351 if oldctx.parents()[0] == ctx:
352 352 ui.debug('node %s unchanged\n' % ha[:12])
353 353 return oldctx, []
354 354 hg.update(repo, ctx.node())
355 355 stats = applychanges(ui, repo, oldctx, opts)
356 356 if stats and stats[3] > 0:
357 357 raise error.InterventionRequired(_('Fix up the change and run '
358 358 'hg histedit --continue'))
359 359 # drop the second merge parent
360 360 commit = commitfuncfor(repo, oldctx)
361 361 n = commit(text=oldctx.description(), user=oldctx.user(),
362 362 date=oldctx.date(), extra=oldctx.extra())
363 363 if n is None:
364 364 ui.warn(_('%s: empty changeset\n') % ha[:12])
365 365 return ctx, []
366 366 new = repo[n]
367 367 return new, [(oldctx.node(), (n,))]
368 368
369 369
370 370 def edit(ui, state, ha, opts):
371 371 repo, ctx = state.repo, state.parentctx
372 372 oldctx = repo[ha]
373 373 hg.update(repo, ctx.node())
374 374 applychanges(ui, repo, oldctx, opts)
375 375 raise error.InterventionRequired(
376 376 _('Make changes as needed, you may commit or record as needed now.\n'
377 377 'When you are finished, run hg histedit --continue to resume.'))
378 378
379 379 def rollup(ui, state, ha, opts):
380 380 rollupopts = opts.copy()
381 381 rollupopts['rollup'] = True
382 382 return fold(ui, state, ha, rollupopts)
383 383
384 384 def fold(ui, state, ha, opts):
385 385 repo, ctx = state.repo, state.parentctx
386 386 oldctx = repo[ha]
387 387 hg.update(repo, ctx.node())
388 388 stats = applychanges(ui, repo, oldctx, opts)
389 389 if stats and stats[3] > 0:
390 390 raise error.InterventionRequired(
391 391 _('Fix up the change and run hg histedit --continue'))
392 392 n = repo.commit(text='fold-temp-revision %s' % ha[:12], user=oldctx.user(),
393 393 date=oldctx.date(), extra=oldctx.extra())
394 394 if n is None:
395 395 ui.warn(_('%s: empty changeset') % ha[:12])
396 396 return ctx, []
397 397 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
398 398
399 399 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
400 400 parent = ctx.parents()[0].node()
401 401 hg.update(repo, parent)
402 402 ### prepare new commit data
403 403 commitopts = opts.copy()
404 404 commitopts['user'] = ctx.user()
405 405 # commit message
406 406 if opts.get('rollup'):
407 407 newmessage = ctx.description()
408 408 else:
409 409 newmessage = '\n***\n'.join(
410 410 [ctx.description()] +
411 411 [repo[r].description() for r in internalchanges] +
412 412 [oldctx.description()]) + '\n'
413 413 commitopts['message'] = newmessage
414 414 # date
415 415 commitopts['date'] = max(ctx.date(), oldctx.date())
416 416 extra = ctx.extra().copy()
417 417 # histedit_source
418 418 # note: ctx is likely a temporary commit but that the best we can do here
419 419 # This is sufficient to solve issue3681 anyway
420 420 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
421 421 commitopts['extra'] = extra
422 422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
423 423 try:
424 424 phasemin = max(ctx.phase(), oldctx.phase())
425 425 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
426 426 n = collapse(repo, ctx, repo[newnode], commitopts)
427 427 finally:
428 428 repo.ui.restoreconfig(phasebackup)
429 429 if n is None:
430 430 return ctx, []
431 431 hg.update(repo, n)
432 432 replacements = [(oldctx.node(), (newnode,)),
433 433 (ctx.node(), (n,)),
434 434 (newnode, (n,)),
435 435 ]
436 436 for ich in internalchanges:
437 437 replacements.append((ich, (n,)))
438 438 return repo[n], replacements
439 439
440 440 def drop(ui, state, ha, opts):
441 441 repo, ctx = state.repo, state.parentctx
442 442 return ctx, [(repo[ha].node(), ())]
443 443
444 444
445 445 def message(ui, state, ha, opts):
446 446 repo, ctx = state.repo, state.parentctx
447 447 oldctx = repo[ha]
448 448 hg.update(repo, ctx.node())
449 449 stats = applychanges(ui, repo, oldctx, opts)
450 450 if stats and stats[3] > 0:
451 451 raise error.InterventionRequired(
452 452 _('Fix up the change and run hg histedit --continue'))
453 453 message = oldctx.description()
454 454 commit = commitfuncfor(repo, oldctx)
455 455 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
456 456 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
457 457 extra=oldctx.extra(), editor=editor)
458 458 newctx = repo[new]
459 459 if oldctx.node() != newctx.node():
460 460 return newctx, [(oldctx.node(), (new,))]
461 461 # We didn't make an edit, so just indicate no replaced nodes
462 462 return newctx, []
463 463
464 464 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
465 465 """utility function to find the first outgoing changeset
466 466
467 467 Used by initialisation code"""
468 468 dest = ui.expandpath(remote or 'default-push', remote or 'default')
469 469 dest, revs = hg.parseurl(dest, None)[:2]
470 470 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
471 471
472 472 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
473 473 other = hg.peer(repo, opts, dest)
474 474
475 475 if revs:
476 476 revs = [repo.lookup(rev) for rev in revs]
477 477
478 478 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
479 479 if not outgoing.missing:
480 480 raise util.Abort(_('no outgoing ancestors'))
481 481 roots = list(repo.revs("roots(%ln)", outgoing.missing))
482 482 if 1 < len(roots):
483 483 msg = _('there are ambiguous outgoing revisions')
484 484 hint = _('see "hg help histedit" for more detail')
485 485 raise util.Abort(msg, hint=hint)
486 486 return repo.lookup(roots[0])
487 487
488 488 actiontable = {'p': pick,
489 489 'pick': pick,
490 490 'e': edit,
491 491 'edit': edit,
492 492 'f': fold,
493 493 'fold': fold,
494 494 'r': rollup,
495 495 'roll': rollup,
496 496 'd': drop,
497 497 'drop': drop,
498 498 'm': message,
499 499 'mess': message,
500 500 }
501 501
502 502 @command('histedit',
503 503 [('', 'commands', '',
504 504 _('Read history edits from the specified file.')),
505 505 ('c', 'continue', False, _('continue an edit already in progress')),
506 506 ('k', 'keep', False,
507 507 _("don't strip old nodes after edit is complete")),
508 508 ('', 'abort', False, _('abort an edit in progress')),
509 509 ('o', 'outgoing', False, _('changesets not found in destination')),
510 510 ('f', 'force', False,
511 511 _('force outgoing even for unrelated repositories')),
512 512 ('r', 'rev', [], _('first revision to be edited'))],
513 513 _("ANCESTOR | --outgoing [URL]"))
514 514 def histedit(ui, repo, *freeargs, **opts):
515 515 """interactively edit changeset history
516 516
517 517 This command edits changesets between ANCESTOR and the parent of
518 518 the working directory.
519 519
520 520 With --outgoing, this edits changesets not found in the
521 521 destination repository. If URL of the destination is omitted, the
522 522 'default-push' (or 'default') path will be used.
523 523
524 524 For safety, this command is aborted, also if there are ambiguous
525 525 outgoing revisions which may confuse users: for example, there are
526 526 multiple branches containing outgoing revisions.
527 527
528 528 Use "min(outgoing() and ::.)" or similar revset specification
529 529 instead of --outgoing to specify edit target revision exactly in
530 530 such ambiguous situation. See :hg:`help revsets` for detail about
531 531 selecting revisions.
532 532
533 533 Returns 0 on success, 1 if user intervention is required (not only
534 534 for intentional "edit" command, but also for resolving unexpected
535 535 conflicts).
536 536 """
537 537 state = histeditstate(repo)
538 538 try:
539 539 state.wlock = repo.wlock()
540 540 state.lock = repo.lock()
541 541 _histedit(ui, repo, state, *freeargs, **opts)
542 542 finally:
543 543 release(state.lock, state.wlock)
544 544
545 545 def _histedit(ui, repo, state, *freeargs, **opts):
546 546 # TODO only abort if we try and histedit mq patches, not just
547 547 # blanket if mq patches are applied somewhere
548 548 mq = getattr(repo, 'mq', None)
549 549 if mq and mq.applied:
550 550 raise util.Abort(_('source has mq patches applied'))
551 551
552 552 # basic argument incompatibility processing
553 553 outg = opts.get('outgoing')
554 554 cont = opts.get('continue')
555 555 abort = opts.get('abort')
556 556 force = opts.get('force')
557 557 rules = opts.get('commands', '')
558 558 revs = opts.get('rev', [])
559 559 goal = 'new' # This invocation goal, in new, continue, abort
560 560 if force and not outg:
561 561 raise util.Abort(_('--force only allowed with --outgoing'))
562 562 if cont:
563 563 if util.any((outg, abort, revs, freeargs, rules)):
564 564 raise util.Abort(_('no arguments allowed with --continue'))
565 565 goal = 'continue'
566 566 elif abort:
567 567 if util.any((outg, revs, freeargs, rules)):
568 568 raise util.Abort(_('no arguments allowed with --abort'))
569 569 goal = 'abort'
570 570 else:
571 571 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
572 572 raise util.Abort(_('history edit already in progress, try '
573 573 '--continue or --abort'))
574 574 if outg:
575 575 if revs:
576 576 raise util.Abort(_('no revisions allowed with --outgoing'))
577 577 if len(freeargs) > 1:
578 578 raise util.Abort(
579 579 _('only one repo argument allowed with --outgoing'))
580 580 else:
581 581 revs.extend(freeargs)
582 if len(revs) == 0:
583 histeditdefault = ui.config('histedit', 'defaultrev')
584 if histeditdefault:
585 revs.append(histeditdefault)
582 586 if len(revs) != 1:
583 587 raise util.Abort(
584 588 _('histedit requires exactly one ancestor revision'))
585 589
586 590
587 591 replacements = []
588 592 keep = opts.get('keep', False)
589 593
590 594 # rebuild state
591 595 if goal == 'continue':
592 596 state = histeditstate(repo)
593 597 state.read()
594 598 state = bootstrapcontinue(ui, state, opts)
595 599 elif goal == 'abort':
596 600 state = histeditstate(repo)
597 601 state.read()
598 602 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
599 603 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
600 604 # check whether we should update away
601 605 parentnodes = [c.node() for c in repo[None].parents()]
602 606 for n in leafs | set([state.parentctx.node()]):
603 607 if n in parentnodes:
604 608 hg.clean(repo, state.topmost)
605 609 break
606 610 else:
607 611 pass
608 612 cleanupnode(ui, repo, 'created', tmpnodes)
609 613 cleanupnode(ui, repo, 'temp', leafs)
610 614 state.clear()
611 615 return
612 616 else:
613 617 cmdutil.checkunfinished(repo)
614 618 cmdutil.bailifchanged(repo)
615 619
616 620 topmost, empty = repo.dirstate.parents()
617 621 if outg:
618 622 if freeargs:
619 623 remote = freeargs[0]
620 624 else:
621 625 remote = None
622 626 root = findoutgoing(ui, repo, remote, force, opts)
623 627 else:
624 628 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
625 629 if len(rr) != 1:
626 630 raise util.Abort(_('The specified revisions must have '
627 631 'exactly one common root'))
628 632 root = rr[0].node()
629 633
630 634 revs = between(repo, root, topmost, keep)
631 635 if not revs:
632 636 raise util.Abort(_('%s is not an ancestor of working directory') %
633 637 node.short(root))
634 638
635 639 ctxs = [repo[r] for r in revs]
636 640 if not rules:
637 641 rules = '\n'.join([makedesc(c) for c in ctxs])
638 642 rules += '\n\n'
639 643 rules += editcomment % (node.short(root), node.short(topmost))
640 644 rules = ui.edit(rules, ui.username())
641 645 # Save edit rules in .hg/histedit-last-edit.txt in case
642 646 # the user needs to ask for help after something
643 647 # surprising happens.
644 648 f = open(repo.join('histedit-last-edit.txt'), 'w')
645 649 f.write(rules)
646 650 f.close()
647 651 else:
648 652 if rules == '-':
649 653 f = sys.stdin
650 654 else:
651 655 f = open(rules)
652 656 rules = f.read()
653 657 f.close()
654 658 rules = [l for l in (r.strip() for r in rules.splitlines())
655 659 if l and not l.startswith('#')]
656 660 rules = verifyrules(rules, repo, ctxs)
657 661
658 662 parentctx = repo[root].parents()[0]
659 663
660 664 state.parentctx = parentctx
661 665 state.rules = rules
662 666 state.keep = keep
663 667 state.topmost = topmost
664 668 state.replacements = replacements
665 669
666 670 while state.rules:
667 671 state.write()
668 672 action, ha = state.rules.pop(0)
669 673 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
670 674 actfunc = actiontable[action]
671 675 state.parentctx, replacement_ = actfunc(ui, state, ha, opts)
672 676 state.replacements.extend(replacement_)
673 677
674 678 hg.update(repo, state.parentctx.node())
675 679
676 680 mapping, tmpnodes, created, ntm = processreplacement(state)
677 681 if mapping:
678 682 for prec, succs in mapping.iteritems():
679 683 if not succs:
680 684 ui.debug('histedit: %s is dropped\n' % node.short(prec))
681 685 else:
682 686 ui.debug('histedit: %s is replaced by %s\n' % (
683 687 node.short(prec), node.short(succs[0])))
684 688 if len(succs) > 1:
685 689 m = 'histedit: %s'
686 690 for n in succs[1:]:
687 691 ui.debug(m % node.short(n))
688 692
689 693 if not keep:
690 694 if mapping:
691 695 movebookmarks(ui, repo, mapping, state.topmost, ntm)
692 696 # TODO update mq state
693 697 if obsolete.isenabled(repo, obsolete.createmarkersopt):
694 698 markers = []
695 699 # sort by revision number because it sound "right"
696 700 for prec in sorted(mapping, key=repo.changelog.rev):
697 701 succs = mapping[prec]
698 702 markers.append((repo[prec],
699 703 tuple(repo[s] for s in succs)))
700 704 if markers:
701 705 obsolete.createmarkers(repo, markers)
702 706 else:
703 707 cleanupnode(ui, repo, 'replaced', mapping)
704 708
705 709 cleanupnode(ui, repo, 'temp', tmpnodes)
706 710 state.clear()
707 711 if os.path.exists(repo.sjoin('undo')):
708 712 os.unlink(repo.sjoin('undo'))
709 713
710 714 def gatherchildren(repo, ctx):
711 715 # is there any new commit between the expected parent and "."
712 716 #
713 717 # note: does not take non linear new change in account (but previous
714 718 # implementation didn't used them anyway (issue3655)
715 719 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
716 720 if ctx.node() != node.nullid:
717 721 if not newchildren:
718 722 # `ctx` should match but no result. This means that
719 723 # currentnode is not a descendant from ctx.
720 724 msg = _('%s is not an ancestor of working directory')
721 725 hint = _('use "histedit --abort" to clear broken state')
722 726 raise util.Abort(msg % ctx, hint=hint)
723 727 newchildren.pop(0) # remove ctx
724 728 return newchildren
725 729
726 730 def bootstrapcontinue(ui, state, opts):
727 731 repo, parentctx = state.repo, state.parentctx
728 732 action, currentnode = state.rules.pop(0)
729 733 ctx = repo[currentnode]
730 734
731 735 newchildren = gatherchildren(repo, parentctx)
732 736
733 737 # Commit dirty working directory if necessary
734 738 new = None
735 739 s = repo.status()
736 740 if s.modified or s.added or s.removed or s.deleted:
737 741 # prepare the message for the commit to comes
738 742 if action in ('f', 'fold', 'r', 'roll'):
739 743 message = 'fold-temp-revision %s' % currentnode[:12]
740 744 else:
741 745 message = ctx.description()
742 746 editopt = action in ('e', 'edit', 'm', 'mess')
743 747 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
744 748 editform = 'histedit.%s' % canonaction.get(action, action)
745 749 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
746 750 commit = commitfuncfor(repo, ctx)
747 751 new = commit(text=message, user=ctx.user(), date=ctx.date(),
748 752 extra=ctx.extra(), editor=editor)
749 753 if new is not None:
750 754 newchildren.append(new)
751 755
752 756 replacements = []
753 757 # track replacements
754 758 if ctx.node() not in newchildren:
755 759 # note: new children may be empty when the changeset is dropped.
756 760 # this happen e.g during conflicting pick where we revert content
757 761 # to parent.
758 762 replacements.append((ctx.node(), tuple(newchildren)))
759 763
760 764 if action in ('f', 'fold', 'r', 'roll'):
761 765 if newchildren:
762 766 # finalize fold operation if applicable
763 767 if new is None:
764 768 new = newchildren[-1]
765 769 else:
766 770 newchildren.pop() # remove new from internal changes
767 771 foldopts = opts
768 772 if action in ('r', 'roll'):
769 773 foldopts = foldopts.copy()
770 774 foldopts['rollup'] = True
771 775 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
772 776 foldopts, newchildren)
773 777 replacements.extend(repl)
774 778 else:
775 779 # newchildren is empty if the fold did not result in any commit
776 780 # this happen when all folded change are discarded during the
777 781 # merge.
778 782 replacements.append((ctx.node(), (parentctx.node(),)))
779 783 elif newchildren:
780 784 # otherwise update "parentctx" before proceeding to further operation
781 785 parentctx = repo[newchildren[-1]]
782 786
783 787 state.parentctx = parentctx
784 788 state.replacements.extend(replacements)
785 789
786 790 return state
787 791
788 792 def between(repo, old, new, keep):
789 793 """select and validate the set of revision to edit
790 794
791 795 When keep is false, the specified set can't have children."""
792 796 ctxs = list(repo.set('%n::%n', old, new))
793 797 if ctxs and not keep:
794 798 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
795 799 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
796 800 raise util.Abort(_('cannot edit history that would orphan nodes'))
797 801 if repo.revs('(%ld) and merge()', ctxs):
798 802 raise util.Abort(_('cannot edit history that contains merges'))
799 803 root = ctxs[0] # list is already sorted by repo.set
800 804 if not root.mutable():
801 805 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
802 806 return [c.node() for c in ctxs]
803 807
804 808 def makedesc(c):
805 809 """build a initial action line for a ctx `c`
806 810
807 811 line are in the form:
808 812
809 813 pick <hash> <rev> <summary>
810 814 """
811 815 summary = ''
812 816 if c.description():
813 817 summary = c.description().splitlines()[0]
814 818 line = 'pick %s %d %s' % (c, c.rev(), summary)
815 819 # trim to 80 columns so it's not stupidly wide in my editor
816 820 return util.ellipsis(line, 80)
817 821
818 822 def verifyrules(rules, repo, ctxs):
819 823 """Verify that there exists exactly one edit rule per given changeset.
820 824
821 825 Will abort if there are to many or too few rules, a malformed rule,
822 826 or a rule on a changeset outside of the user-given range.
823 827 """
824 828 parsed = []
825 829 expected = set(c.hex() for c in ctxs)
826 830 seen = set()
827 831 for r in rules:
828 832 if ' ' not in r:
829 833 raise util.Abort(_('malformed line "%s"') % r)
830 834 action, rest = r.split(' ', 1)
831 835 ha = rest.strip().split(' ', 1)[0]
832 836 try:
833 837 ha = repo[ha].hex()
834 838 except error.RepoError:
835 839 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
836 840 if ha not in expected:
837 841 raise util.Abort(
838 842 _('may not use changesets other than the ones listed'))
839 843 if ha in seen:
840 844 raise util.Abort(_('duplicated command for changeset %s') %
841 845 ha[:12])
842 846 seen.add(ha)
843 847 if action not in actiontable:
844 848 raise util.Abort(_('unknown action "%s"') % action)
845 849 parsed.append([action, ha])
846 850 missing = sorted(expected - seen) # sort to stabilize output
847 851 if missing:
848 852 raise util.Abort(_('missing rules for changeset %s') %
849 853 missing[0][:12],
850 854 hint=_('do you want to use the drop action?'))
851 855 return parsed
852 856
853 857 def processreplacement(state):
854 858 """process the list of replacements to return
855 859
856 860 1) the final mapping between original and created nodes
857 861 2) the list of temporary node created by histedit
858 862 3) the list of new commit created by histedit"""
859 863 replacements = state.replacements
860 864 allsuccs = set()
861 865 replaced = set()
862 866 fullmapping = {}
863 867 # initialise basic set
864 868 # fullmapping record all operation recorded in replacement
865 869 for rep in replacements:
866 870 allsuccs.update(rep[1])
867 871 replaced.add(rep[0])
868 872 fullmapping.setdefault(rep[0], set()).update(rep[1])
869 873 new = allsuccs - replaced
870 874 tmpnodes = allsuccs & replaced
871 875 # Reduce content fullmapping into direct relation between original nodes
872 876 # and final node created during history edition
873 877 # Dropped changeset are replaced by an empty list
874 878 toproceed = set(fullmapping)
875 879 final = {}
876 880 while toproceed:
877 881 for x in list(toproceed):
878 882 succs = fullmapping[x]
879 883 for s in list(succs):
880 884 if s in toproceed:
881 885 # non final node with unknown closure
882 886 # We can't process this now
883 887 break
884 888 elif s in final:
885 889 # non final node, replace with closure
886 890 succs.remove(s)
887 891 succs.update(final[s])
888 892 else:
889 893 final[x] = succs
890 894 toproceed.remove(x)
891 895 # remove tmpnodes from final mapping
892 896 for n in tmpnodes:
893 897 del final[n]
894 898 # we expect all changes involved in final to exist in the repo
895 899 # turn `final` into list (topologically sorted)
896 900 nm = state.repo.changelog.nodemap
897 901 for prec, succs in final.items():
898 902 final[prec] = sorted(succs, key=nm.get)
899 903
900 904 # computed topmost element (necessary for bookmark)
901 905 if new:
902 906 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
903 907 elif not final:
904 908 # Nothing rewritten at all. we won't need `newtopmost`
905 909 # It is the same as `oldtopmost` and `processreplacement` know it
906 910 newtopmost = None
907 911 else:
908 912 # every body died. The newtopmost is the parent of the root.
909 913 r = state.repo.changelog.rev
910 914 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
911 915
912 916 return final, tmpnodes, new, newtopmost
913 917
914 918 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
915 919 """Move bookmark from old to newly created node"""
916 920 if not mapping:
917 921 # if nothing got rewritten there is not purpose for this function
918 922 return
919 923 moves = []
920 924 for bk, old in sorted(repo._bookmarks.iteritems()):
921 925 if old == oldtopmost:
922 926 # special case ensure bookmark stay on tip.
923 927 #
924 928 # This is arguably a feature and we may only want that for the
925 929 # active bookmark. But the behavior is kept compatible with the old
926 930 # version for now.
927 931 moves.append((bk, newtopmost))
928 932 continue
929 933 base = old
930 934 new = mapping.get(base, None)
931 935 if new is None:
932 936 continue
933 937 while not new:
934 938 # base is killed, trying with parent
935 939 base = repo[base].p1().node()
936 940 new = mapping.get(base, (base,))
937 941 # nothing to move
938 942 moves.append((bk, new[-1]))
939 943 if moves:
940 944 marks = repo._bookmarks
941 945 for mark, new in moves:
942 946 old = marks[mark]
943 947 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
944 948 % (mark, node.short(old), node.short(new)))
945 949 marks[mark] = new
946 950 marks.write()
947 951
948 952 def cleanupnode(ui, repo, name, nodes):
949 953 """strip a group of nodes from the repository
950 954
951 955 The set of node to strip may contains unknown nodes."""
952 956 ui.debug('should strip %s nodes %s\n' %
953 957 (name, ', '.join([node.short(n) for n in nodes])))
954 958 lock = None
955 959 try:
956 960 lock = repo.lock()
957 961 # Find all node that need to be stripped
958 962 # (we hg %lr instead of %ln to silently ignore unknown item
959 963 nm = repo.changelog.nodemap
960 964 nodes = sorted(n for n in nodes if n in nm)
961 965 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
962 966 for c in roots:
963 967 # We should process node in reverse order to strip tip most first.
964 968 # but this trigger a bug in changegroup hook.
965 969 # This would reduce bundle overhead
966 970 repair.strip(ui, repo, c)
967 971 finally:
968 972 release(lock)
969 973
970 974 def summaryhook(ui, repo):
971 975 if not os.path.exists(repo.join('histedit-state')):
972 976 return
973 977 state = histeditstate(repo)
974 978 state.read()
975 979 if state.rules:
976 980 # i18n: column positioning for "hg summary"
977 981 ui.write(_('hist: %s (histedit --continue)\n') %
978 982 (ui.label(_('%d remaining'), 'histedit.remaining') %
979 983 len(state.rules)))
980 984
981 985 def extsetup(ui):
982 986 cmdutil.summaryhooks.add('histedit', summaryhook)
983 987 cmdutil.unfinishedstates.append(
984 988 ['histedit-state', False, True, _('histedit in progress'),
985 989 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,273 +1,282 b''
1 1 Test argument handling and various data parsing
2 2 ==================================================
3 3
4 4
5 5 Enable extensions used by this test.
6 6 $ cat >>$HGRCPATH <<EOF
7 7 > [extensions]
8 8 > histedit=
9 9 > EOF
10 10
11 11 Repo setup.
12 12 $ hg init foo
13 13 $ cd foo
14 14 $ echo alpha >> alpha
15 15 $ hg addr
16 16 adding alpha
17 17 $ hg ci -m one
18 18 $ echo alpha >> alpha
19 19 $ hg ci -m two
20 20 $ echo alpha >> alpha
21 21 $ hg ci -m three
22 22 $ echo alpha >> alpha
23 23 $ hg ci -m four
24 24 $ echo alpha >> alpha
25 25 $ hg ci -m five
26 26
27 27 $ hg log --style compact --graph
28 28 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
29 29 | five
30 30 |
31 31 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
32 32 | four
33 33 |
34 34 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
35 35 | three
36 36 |
37 37 o 1 579e40513370 1970-01-01 00:00 +0000 test
38 38 | two
39 39 |
40 40 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
41 41 one
42 42
43 43
44 44 histedit --continue/--abort with no existing state
45 45 --------------------------------------------------
46 46
47 47 $ hg histedit --continue
48 48 abort: no histedit in progress
49 49 [255]
50 50 $ hg histedit --abort
51 51 abort: no histedit in progress
52 52 [255]
53 53
54 54 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
55 55 --------------------------------------------------------------------
56 56
57 57 $ HGEDITOR=cat hg histedit "tip^^"
58 58 pick eb57da33312f 2 three
59 59 pick c8e68270e35a 3 four
60 60 pick 08d98a8350f3 4 five
61 61
62 62 # Edit history between eb57da33312f and 08d98a8350f3
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 message without changing commit content
73 73 #
74 74 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
75 75
76 76 Run on a revision not ancestors of the current working directory.
77 77 --------------------------------------------------------------------
78 78
79 79 $ hg up 2
80 80 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
81 81 $ hg histedit -r 4
82 82 abort: 08d98a8350f3 is not an ancestor of working directory
83 83 [255]
84 84 $ hg up --quiet
85 85
86 86
87 87 Test that we pick the minimum of a revrange
88 88 ---------------------------------------
89 89
90 90 $ HGEDITOR=cat hg histedit '2::' --commands - << EOF
91 91 > pick eb57da33312f 2 three
92 92 > pick c8e68270e35a 3 four
93 93 > pick 08d98a8350f3 4 five
94 94 > EOF
95 95 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
96 96 $ hg up --quiet
97 97
98 98 $ HGEDITOR=cat hg histedit 'tip:2' --commands - << EOF
99 99 > pick eb57da33312f 2 three
100 100 > pick c8e68270e35a 3 four
101 101 > pick 08d98a8350f3 4 five
102 102 > EOF
103 103 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
104 104 $ hg up --quiet
105 105
106 Test config specified default
107 -----------------------------
108
109 $ HGEDITOR=cat hg histedit --config "histedit.defaultrev=only(.) - ::eb57da33312f" --commands - << EOF
110 > pick c8e68270e35a 3 four
111 > pick 08d98a8350f3 4 five
112 > EOF
113 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
114
106 115 Run on a revision not descendants of the initial parent
107 116 --------------------------------------------------------------------
108 117
109 118 Test the message shown for inconsistent histedit state, which may be
110 119 created (and forgotten) by Mercurial earlier than 2.7. This emulates
111 120 Mercurial earlier than 2.7 by renaming ".hg/histedit-state"
112 121 temporarily.
113 122
114 123 $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF
115 124 > edit 08d98a8350f3 4 five
116 125 > EOF
117 126 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
118 127 reverting alpha
119 128 Make changes as needed, you may commit or record as needed now.
120 129 When you are finished, run hg histedit --continue to resume.
121 130 [1]
122 131
123 132 $ mv .hg/histedit-state .hg/histedit-state.back
124 133 $ hg update --quiet --clean 2
125 134 $ mv .hg/histedit-state.back .hg/histedit-state
126 135
127 136 $ hg histedit --continue
128 137 abort: c8e68270e35a is not an ancestor of working directory
129 138 (use "histedit --abort" to clear broken state)
130 139 [255]
131 140
132 141 $ hg histedit --abort
133 142 $ hg update --quiet --clean
134 143
135 144 Test that missing revisions are detected
136 145 ---------------------------------------
137 146
138 147 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
139 148 > pick eb57da33312f 2 three
140 149 > pick 08d98a8350f3 4 five
141 150 > EOF
142 151 abort: missing rules for changeset c8e68270e35a
143 152 (do you want to use the drop action?)
144 153 [255]
145 154
146 155 Test that extra revisions are detected
147 156 ---------------------------------------
148 157
149 158 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
150 159 > pick 6058cbb6cfd7 0 one
151 160 > pick c8e68270e35a 3 four
152 161 > pick 08d98a8350f3 4 five
153 162 > EOF
154 163 abort: may not use changesets other than the ones listed
155 164 [255]
156 165
157 166 Test malformed line
158 167 ---------------------------------------
159 168
160 169 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
161 170 > pickeb57da33312f2three
162 171 > pick c8e68270e35a 3 four
163 172 > pick 08d98a8350f3 4 five
164 173 > EOF
165 174 abort: malformed line "pickeb57da33312f2three"
166 175 [255]
167 176
168 177 Test unknown changeset
169 178 ---------------------------------------
170 179
171 180 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
172 181 > pick 0123456789ab 2 three
173 182 > pick c8e68270e35a 3 four
174 183 > pick 08d98a8350f3 4 five
175 184 > EOF
176 185 abort: unknown changeset 0123456789ab listed
177 186 [255]
178 187
179 188 Test unknown command
180 189 ---------------------------------------
181 190
182 191 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
183 192 > coin eb57da33312f 2 three
184 193 > pick c8e68270e35a 3 four
185 194 > pick 08d98a8350f3 4 five
186 195 > EOF
187 196 abort: unknown action "coin"
188 197 [255]
189 198
190 199 Test duplicated changeset
191 200 ---------------------------------------
192 201
193 202 So one is missing and one appear twice.
194 203
195 204 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
196 205 > pick eb57da33312f 2 three
197 206 > pick eb57da33312f 2 three
198 207 > pick 08d98a8350f3 4 five
199 208 > EOF
200 209 abort: duplicated command for changeset eb57da33312f
201 210 [255]
202 211
203 212 Test short version of command
204 213 ---------------------------------------
205 214
206 215 Note: we use varying amounts of white space between command name and changeset
207 216 short hash. This tests issue3893.
208 217
209 218 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
210 219 > pick eb57da33312f 2 three
211 220 > p c8e68270e35a 3 four
212 221 > f 08d98a8350f3 4 five
213 222 > EOF
214 223 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
215 224 reverting alpha
216 225 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
217 226 four
218 227 ***
219 228 five
220 229
221 230
222 231
223 232 HG: Enter commit message. Lines beginning with 'HG:' are removed.
224 233 HG: Leave message empty to abort commit.
225 234 HG: --
226 235 HG: user: test
227 236 HG: branch 'default'
228 237 HG: changed alpha
229 238 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
230 239 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
231 240 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/*-backup.hg (glob)
232 241
233 242 $ hg update -q 2
234 243 $ echo x > x
235 244 $ hg add x
236 245 $ hg commit -m'x' x
237 246 created new head
238 247 $ hg histedit -r 'heads(all())'
239 248 abort: The specified revisions must have exactly one common root
240 249 [255]
241 250
242 251 Test that trimming description using multi-byte characters
243 252 --------------------------------------------------------------------
244 253
245 254 $ python <<EOF
246 255 > fp = open('logfile', 'w')
247 256 > fp.write('12345678901234567890123456789012345678901234567890' +
248 257 > '12345') # there are 5 more columns for 80 columns
249 258 >
250 259 > # 2 x 4 = 8 columns, but 3 x 4 = 12 bytes
251 260 > fp.write(u'\u3042\u3044\u3046\u3048'.encode('utf-8'))
252 261 >
253 262 > fp.close()
254 263 > EOF
255 264 $ echo xx >> x
256 265 $ hg --encoding utf-8 commit --logfile logfile
257 266
258 267 $ HGEDITOR=cat hg --encoding utf-8 histedit tip
259 268 pick 3d3ea1f3a10b 5 1234567890123456789012345678901234567890123456789012345\xe3\x81\x82... (esc)
260 269
261 270 # Edit history between 3d3ea1f3a10b and 3d3ea1f3a10b
262 271 #
263 272 # Commits are listed from least to most recent
264 273 #
265 274 # Commands:
266 275 # p, pick = use commit
267 276 # e, edit = use commit, but stop for amending
268 277 # f, fold = use commit, but combine it with the one above
269 278 # r, roll = like fold, but discard this commit's description
270 279 # d, drop = remove commit from history
271 280 # m, mess = edit message without changing commit content
272 281 #
273 282 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
General Comments 0
You need to be logged in to leave comments. Login now