##// END OF EJS Templates
histedit: handle multiple spaces between action and hash (issue3893)...
Pierre-Yves David -
r19039:41669a18 default
parent child Browse files
Show More
@@ -1,853 +1,850 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 # Commands:
34 34 # p, pick = use commit
35 35 # e, edit = use commit, but stop for amending
36 36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
37 37 # d, drop = remove commit from history
38 38 # m, mess = edit message without changing commit content
39 39 #
40 40
41 41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 42 for each revision in your history. For example, if you had meant to add gamma
43 43 before beta, and then wanted to add delta in the same revision as beta, you
44 44 would reorganize the file to look like this::
45 45
46 46 pick 030b686bedc4 Add gamma
47 47 pick c561b4e977df Add beta
48 48 fold 7c2fd3b9020c Add delta
49 49
50 50 # Edit history between c561b4e977df and 7c2fd3b9020c
51 51 #
52 52 # Commands:
53 53 # p, pick = use commit
54 54 # e, edit = use commit, but stop for amending
55 55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 56 # d, drop = remove commit from history
57 57 # m, mess = edit message without changing commit content
58 58 #
59 59
60 60 At which point you close the editor and ``histedit`` starts working. When you
61 61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
62 62 those revisions together, offering you a chance to clean up the commit message::
63 63
64 64 Add beta
65 65 ***
66 66 Add delta
67 67
68 68 Edit the commit message to your liking, then close the editor. For
69 69 this example, let's assume that the commit message was changed to
70 70 ``Add beta and delta.`` After histedit has run and had a chance to
71 71 remove any old or temporary revisions it needed, the history looks
72 72 like this::
73 73
74 74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
75 75 | Add beta and delta.
76 76 |
77 77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
78 78 | Add gamma
79 79 |
80 80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
81 81 Add alpha
82 82
83 83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
84 84 ones) until after it has completed all the editing operations, so it will
85 85 probably perform several strip operations when it's done. For the above example,
86 86 it had to run strip twice. Strip can be slow depending on a variety of factors,
87 87 so you might need to be a little patient. You can choose to keep the original
88 88 revisions by passing the ``--keep`` flag.
89 89
90 90 The ``edit`` operation will drop you back to a command prompt,
91 91 allowing you to edit files freely, or even use ``hg record`` to commit
92 92 some changes as a separate commit. When you're done, any remaining
93 93 uncommitted changes will be committed as well. When done, run ``hg
94 94 histedit --continue`` to finish this step. You'll be prompted for a
95 95 new commit message, but the default commit message will be the
96 96 original message for the ``edit`` ed revision.
97 97
98 98 The ``message`` operation will give you a chance to revise a commit
99 99 message without changing the contents. It's a shortcut for doing
100 100 ``edit`` immediately followed by `hg histedit --continue``.
101 101
102 102 If ``histedit`` encounters a conflict when moving a revision (while
103 103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
104 104 ``edit`` with the difference that it won't prompt you for a commit
105 105 message when done. If you decide at this point that you don't like how
106 106 much work it will be to rearrange history, or that you made a mistake,
107 107 you can use ``hg histedit --abort`` to abandon the new changes you
108 108 have made and return to the state before you attempted to edit your
109 109 history.
110 110
111 111 If we clone the histedit-ed example repository above and add four more
112 112 changes, such that we have the following history::
113 113
114 114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
115 115 | Add theta
116 116 |
117 117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
118 118 | Add eta
119 119 |
120 120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
121 121 | Add zeta
122 122 |
123 123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
124 124 | Add epsilon
125 125 |
126 126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
127 127 | Add beta and delta.
128 128 |
129 129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
130 130 | Add gamma
131 131 |
132 132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
133 133 Add alpha
134 134
135 135 If you run ``hg histedit --outgoing`` on the clone then it is the same
136 136 as running ``hg histedit 836302820282``. If you need plan to push to a
137 137 repository that Mercurial does not detect to be related to the source
138 138 repo, you can add a ``--force`` option.
139 139 """
140 140
141 141 try:
142 142 import cPickle as pickle
143 143 except ImportError:
144 144 import pickle
145 145 import os
146 146 import sys
147 147
148 148 from mercurial import cmdutil
149 149 from mercurial import discovery
150 150 from mercurial import error
151 151 from mercurial import copies
152 152 from mercurial import context
153 153 from mercurial import hg
154 154 from mercurial import lock as lockmod
155 155 from mercurial import node
156 156 from mercurial import repair
157 157 from mercurial import scmutil
158 158 from mercurial import util
159 159 from mercurial import obsolete
160 160 from mercurial import merge as mergemod
161 161 from mercurial.i18n import _
162 162
163 163 cmdtable = {}
164 164 command = cmdutil.command(cmdtable)
165 165
166 166 testedwith = 'internal'
167 167
168 168 # i18n: command names and abbreviations must remain untranslated
169 169 editcomment = _("""# Edit history between %s and %s
170 170 #
171 171 # Commands:
172 172 # p, pick = use commit
173 173 # e, edit = use commit, but stop for amending
174 174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
175 175 # d, drop = remove commit from history
176 176 # m, mess = edit message without changing commit content
177 177 #
178 178 """)
179 179
180 180 def commitfuncfor(repo, src):
181 181 """Build a commit function for the replacement of <src>
182 182
183 183 This function ensure we apply the same treatment to all changesets.
184 184
185 185 - Add a 'histedit_source' entry in extra.
186 186
187 187 Note that fold have its own separated logic because its handling is a bit
188 188 different and not easily factored out of the fold method.
189 189 """
190 190 phasemin = src.phase()
191 191 def commitfunc(**kwargs):
192 192 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
193 193 try:
194 194 repo.ui.setconfig('phases', 'new-commit', phasemin)
195 195 extra = kwargs.get('extra', {}).copy()
196 196 extra['histedit_source'] = src.hex()
197 197 kwargs['extra'] = extra
198 198 return repo.commit(**kwargs)
199 199 finally:
200 200 repo.ui.restoreconfig(phasebackup)
201 201 return commitfunc
202 202
203 203
204 204
205 205 def applychanges(ui, repo, ctx, opts):
206 206 """Merge changeset from ctx (only) in the current working directory"""
207 207 wcpar = repo.dirstate.parents()[0]
208 208 if ctx.p1().node() == wcpar:
209 209 # edition ar "in place" we do not need to make any merge,
210 210 # just applies changes on parent for edition
211 211 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
212 212 stats = None
213 213 else:
214 214 try:
215 215 # ui.forcemerge is an internal variable, do not document
216 216 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
217 217 stats = mergemod.update(repo, ctx.node(), True, True, False,
218 218 ctx.p1().node())
219 219 finally:
220 220 repo.ui.setconfig('ui', 'forcemerge', '')
221 221 repo.setparents(wcpar, node.nullid)
222 222 repo.dirstate.write()
223 223 # fix up dirstate for copies and renames
224 224 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
225 225 return stats
226 226
227 227 def collapse(repo, first, last, commitopts):
228 228 """collapse the set of revisions from first to last as new one.
229 229
230 230 Expected commit options are:
231 231 - message
232 232 - date
233 233 - username
234 234 Commit message is edited in all cases.
235 235
236 236 This function works in memory."""
237 237 ctxs = list(repo.set('%d::%d', first, last))
238 238 if not ctxs:
239 239 return None
240 240 base = first.parents()[0]
241 241
242 242 # commit a new version of the old changeset, including the update
243 243 # collect all files which might be affected
244 244 files = set()
245 245 for ctx in ctxs:
246 246 files.update(ctx.files())
247 247
248 248 # Recompute copies (avoid recording a -> b -> a)
249 249 copied = copies.pathcopies(first, last)
250 250
251 251 # prune files which were reverted by the updates
252 252 def samefile(f):
253 253 if f in last.manifest():
254 254 a = last.filectx(f)
255 255 if f in base.manifest():
256 256 b = base.filectx(f)
257 257 return (a.data() == b.data()
258 258 and a.flags() == b.flags())
259 259 else:
260 260 return False
261 261 else:
262 262 return f not in base.manifest()
263 263 files = [f for f in files if not samefile(f)]
264 264 # commit version of these files as defined by head
265 265 headmf = last.manifest()
266 266 def filectxfn(repo, ctx, path):
267 267 if path in headmf:
268 268 fctx = last[path]
269 269 flags = fctx.flags()
270 270 mctx = context.memfilectx(fctx.path(), fctx.data(),
271 271 islink='l' in flags,
272 272 isexec='x' in flags,
273 273 copied=copied.get(path))
274 274 return mctx
275 275 raise IOError()
276 276
277 277 if commitopts.get('message'):
278 278 message = commitopts['message']
279 279 else:
280 280 message = first.description()
281 281 user = commitopts.get('user')
282 282 date = commitopts.get('date')
283 283 extra = commitopts.get('extra')
284 284
285 285 parents = (first.p1().node(), first.p2().node())
286 286 new = context.memctx(repo,
287 287 parents=parents,
288 288 text=message,
289 289 files=files,
290 290 filectxfn=filectxfn,
291 291 user=user,
292 292 date=date,
293 293 extra=extra)
294 294 new._text = cmdutil.commitforceeditor(repo, new, [])
295 295 return repo.commitctx(new)
296 296
297 297 def pick(ui, repo, ctx, ha, opts):
298 298 oldctx = repo[ha]
299 299 if oldctx.parents()[0] == ctx:
300 300 ui.debug('node %s unchanged\n' % ha)
301 301 return oldctx, []
302 302 hg.update(repo, ctx.node())
303 303 stats = applychanges(ui, repo, oldctx, opts)
304 304 if stats and stats[3] > 0:
305 305 raise error.InterventionRequired(_('Fix up the change and run '
306 306 'hg histedit --continue'))
307 307 # drop the second merge parent
308 308 commit = commitfuncfor(repo, oldctx)
309 309 n = commit(text=oldctx.description(), user=oldctx.user(),
310 310 date=oldctx.date(), extra=oldctx.extra())
311 311 if n is None:
312 312 ui.warn(_('%s: empty changeset\n')
313 313 % node.hex(ha))
314 314 return ctx, []
315 315 new = repo[n]
316 316 return new, [(oldctx.node(), (n,))]
317 317
318 318
319 319 def edit(ui, repo, ctx, ha, opts):
320 320 oldctx = repo[ha]
321 321 hg.update(repo, ctx.node())
322 322 applychanges(ui, repo, oldctx, opts)
323 323 raise error.InterventionRequired(
324 324 _('Make changes as needed, you may commit or record as needed now.\n'
325 325 'When you are finished, run hg histedit --continue to resume.'))
326 326
327 327 def fold(ui, repo, ctx, ha, opts):
328 328 oldctx = repo[ha]
329 329 hg.update(repo, ctx.node())
330 330 stats = applychanges(ui, repo, oldctx, opts)
331 331 if stats and stats[3] > 0:
332 332 raise error.InterventionRequired(
333 333 _('Fix up the change and run hg histedit --continue'))
334 334 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
335 335 date=oldctx.date(), extra=oldctx.extra())
336 336 if n is None:
337 337 ui.warn(_('%s: empty changeset')
338 338 % node.hex(ha))
339 339 return ctx, []
340 340 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
341 341
342 342 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
343 343 parent = ctx.parents()[0].node()
344 344 hg.update(repo, parent)
345 345 ### prepare new commit data
346 346 commitopts = opts.copy()
347 347 # username
348 348 if ctx.user() == oldctx.user():
349 349 username = ctx.user()
350 350 else:
351 351 username = ui.username()
352 352 commitopts['user'] = username
353 353 # commit message
354 354 newmessage = '\n***\n'.join(
355 355 [ctx.description()] +
356 356 [repo[r].description() for r in internalchanges] +
357 357 [oldctx.description()]) + '\n'
358 358 commitopts['message'] = newmessage
359 359 # date
360 360 commitopts['date'] = max(ctx.date(), oldctx.date())
361 361 extra = ctx.extra().copy()
362 362 # histedit_source
363 363 # note: ctx is likely a temporary commit but that the best we can do here
364 364 # This is sufficient to solve issue3681 anyway
365 365 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
366 366 commitopts['extra'] = extra
367 367 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
368 368 try:
369 369 phasemin = max(ctx.phase(), oldctx.phase())
370 370 repo.ui.setconfig('phases', 'new-commit', phasemin)
371 371 n = collapse(repo, ctx, repo[newnode], commitopts)
372 372 finally:
373 373 repo.ui.restoreconfig(phasebackup)
374 374 if n is None:
375 375 return ctx, []
376 376 hg.update(repo, n)
377 377 replacements = [(oldctx.node(), (newnode,)),
378 378 (ctx.node(), (n,)),
379 379 (newnode, (n,)),
380 380 ]
381 381 for ich in internalchanges:
382 382 replacements.append((ich, (n,)))
383 383 return repo[n], replacements
384 384
385 385 def drop(ui, repo, ctx, ha, opts):
386 386 return ctx, [(repo[ha].node(), ())]
387 387
388 388
389 389 def message(ui, repo, ctx, ha, opts):
390 390 oldctx = repo[ha]
391 391 hg.update(repo, ctx.node())
392 392 stats = applychanges(ui, repo, oldctx, opts)
393 393 if stats and stats[3] > 0:
394 394 raise error.InterventionRequired(
395 395 _('Fix up the change and run hg histedit --continue'))
396 396 message = oldctx.description() + '\n'
397 397 message = ui.edit(message, ui.username())
398 398 commit = commitfuncfor(repo, oldctx)
399 399 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
400 400 extra=oldctx.extra())
401 401 newctx = repo[new]
402 402 if oldctx.node() != newctx.node():
403 403 return newctx, [(oldctx.node(), (new,))]
404 404 # We didn't make an edit, so just indicate no replaced nodes
405 405 return newctx, []
406 406
407 407 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
408 408 """utility function to find the first outgoing changeset
409 409
410 410 Used by initialisation code"""
411 411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
412 412 dest, revs = hg.parseurl(dest, None)[:2]
413 413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
414 414
415 415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
416 416 other = hg.peer(repo, opts, dest)
417 417
418 418 if revs:
419 419 revs = [repo.lookup(rev) for rev in revs]
420 420
421 421 # hexlify nodes from outgoing, because we're going to parse
422 422 # parent[0] using revsingle below, and if the binary hash
423 423 # contains special revset characters like ":" the revset
424 424 # parser can choke.
425 425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 426 if not outgoing.missing:
427 427 raise util.Abort(_('no outgoing ancestors'))
428 428 return outgoing.missing[0]
429 429
430 430 actiontable = {'p': pick,
431 431 'pick': pick,
432 432 'e': edit,
433 433 'edit': edit,
434 434 'f': fold,
435 435 'fold': fold,
436 436 'd': drop,
437 437 'drop': drop,
438 438 'm': message,
439 439 'mess': message,
440 440 }
441 441
442 442 @command('histedit',
443 443 [('', 'commands', '',
444 444 _('Read history edits from the specified file.')),
445 445 ('c', 'continue', False, _('continue an edit already in progress')),
446 446 ('k', 'keep', False,
447 447 _("don't strip old nodes after edit is complete")),
448 448 ('', 'abort', False, _('abort an edit in progress')),
449 449 ('o', 'outgoing', False, _('changesets not found in destination')),
450 450 ('f', 'force', False,
451 451 _('force outgoing even for unrelated repositories')),
452 452 ('r', 'rev', [], _('first revision to be edited'))],
453 453 _("[PARENT]"))
454 454 def histedit(ui, repo, *freeargs, **opts):
455 455 """interactively edit changeset history
456 456 """
457 457 # TODO only abort if we try and histedit mq patches, not just
458 458 # blanket if mq patches are applied somewhere
459 459 mq = getattr(repo, 'mq', None)
460 460 if mq and mq.applied:
461 461 raise util.Abort(_('source has mq patches applied'))
462 462
463 463 # basic argument incompatibility processing
464 464 outg = opts.get('outgoing')
465 465 cont = opts.get('continue')
466 466 abort = opts.get('abort')
467 467 force = opts.get('force')
468 468 rules = opts.get('commands', '')
469 469 revs = opts.get('rev', [])
470 470 goal = 'new' # This invocation goal, in new, continue, abort
471 471 if force and not outg:
472 472 raise util.Abort(_('--force only allowed with --outgoing'))
473 473 if cont:
474 474 if util.any((outg, abort, revs, freeargs, rules)):
475 475 raise util.Abort(_('no arguments allowed with --continue'))
476 476 goal = 'continue'
477 477 elif abort:
478 478 if util.any((outg, revs, freeargs, rules)):
479 479 raise util.Abort(_('no arguments allowed with --abort'))
480 480 goal = 'abort'
481 481 else:
482 482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
483 483 raise util.Abort(_('history edit already in progress, try '
484 484 '--continue or --abort'))
485 485 if outg:
486 486 if revs:
487 487 raise util.Abort(_('no revisions allowed with --outgoing'))
488 488 if len(freeargs) > 1:
489 489 raise util.Abort(
490 490 _('only one repo argument allowed with --outgoing'))
491 491 else:
492 492 revs.extend(freeargs)
493 493 if len(revs) != 1:
494 494 raise util.Abort(
495 495 _('histedit requires exactly one parent revision'))
496 496
497 497
498 498 if goal == 'continue':
499 499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
500 500 currentparent, wantnull = repo.dirstate.parents()
501 501 parentctx = repo[parentctxnode]
502 502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
503 503 replacements.extend(repl)
504 504 elif goal == 'abort':
505 505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
506 506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
507 507 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
508 508 hg.clean(repo, topmost)
509 509 cleanupnode(ui, repo, 'created', tmpnodes)
510 510 cleanupnode(ui, repo, 'temp', leafs)
511 511 os.unlink(os.path.join(repo.path, 'histedit-state'))
512 512 return
513 513 else:
514 514 cmdutil.bailifchanged(repo)
515 515
516 516 topmost, empty = repo.dirstate.parents()
517 517 if outg:
518 518 if freeargs:
519 519 remote = freeargs[0]
520 520 else:
521 521 remote = None
522 522 root = findoutgoing(ui, repo, remote, force, opts)
523 523 else:
524 524 root = revs[0]
525 525 root = scmutil.revsingle(repo, root).node()
526 526
527 527 keep = opts.get('keep', False)
528 528 revs = between(repo, root, topmost, keep)
529 529 if not revs:
530 530 raise util.Abort(_('%s is not an ancestor of working directory') %
531 531 node.short(root))
532 532
533 533 ctxs = [repo[r] for r in revs]
534 534 if not rules:
535 535 rules = '\n'.join([makedesc(c) for c in ctxs])
536 536 rules += '\n\n'
537 537 rules += editcomment % (node.short(root), node.short(topmost))
538 538 rules = ui.edit(rules, ui.username())
539 539 # Save edit rules in .hg/histedit-last-edit.txt in case
540 540 # the user needs to ask for help after something
541 541 # surprising happens.
542 542 f = open(repo.join('histedit-last-edit.txt'), 'w')
543 543 f.write(rules)
544 544 f.close()
545 545 else:
546 546 if rules == '-':
547 547 f = sys.stdin
548 548 else:
549 549 f = open(rules)
550 550 rules = f.read()
551 551 f.close()
552 552 rules = [l for l in (r.strip() for r in rules.splitlines())
553 553 if l and not l[0] == '#']
554 554 rules = verifyrules(rules, repo, ctxs)
555 555
556 556 parentctx = repo[root].parents()[0]
557 557 keep = opts.get('keep', False)
558 558 replacements = []
559 559
560 560
561 561 while rules:
562 562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
563 563 action, ha = rules.pop(0)
564 564 ui.debug('histedit: processing %s %s\n' % (action, ha))
565 565 actfunc = actiontable[action]
566 566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
567 567 replacements.extend(replacement_)
568 568
569 569 hg.update(repo, parentctx.node())
570 570
571 571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
572 572 if mapping:
573 573 for prec, succs in mapping.iteritems():
574 574 if not succs:
575 575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
576 576 else:
577 577 ui.debug('histedit: %s is replaced by %s\n' % (
578 578 node.short(prec), node.short(succs[0])))
579 579 if len(succs) > 1:
580 580 m = 'histedit: %s'
581 581 for n in succs[1:]:
582 582 ui.debug(m % node.short(n))
583 583
584 584 if not keep:
585 585 if mapping:
586 586 movebookmarks(ui, repo, mapping, topmost, ntm)
587 587 # TODO update mq state
588 588 if obsolete._enabled:
589 589 markers = []
590 590 # sort by revision number because it sound "right"
591 591 for prec in sorted(mapping, key=repo.changelog.rev):
592 592 succs = mapping[prec]
593 593 markers.append((repo[prec],
594 594 tuple(repo[s] for s in succs)))
595 595 if markers:
596 596 obsolete.createmarkers(repo, markers)
597 597 else:
598 598 cleanupnode(ui, repo, 'replaced', mapping)
599 599
600 600 cleanupnode(ui, repo, 'temp', tmpnodes)
601 601 os.unlink(os.path.join(repo.path, 'histedit-state'))
602 602 if os.path.exists(repo.sjoin('undo')):
603 603 os.unlink(repo.sjoin('undo'))
604 604
605 605
606 606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
607 607 action, currentnode = rules.pop(0)
608 608 ctx = repo[currentnode]
609 609 # is there any new commit between the expected parent and "."
610 610 #
611 611 # note: does not take non linear new change in account (but previous
612 612 # implementation didn't used them anyway (issue3655)
613 613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
614 614 if parentctx.node() != node.nullid:
615 615 if not newchildren:
616 616 # `parentctxnode` should match but no result. This means that
617 617 # currentnode is not a descendant from parentctxnode.
618 618 msg = _('%s is not an ancestor of working directory')
619 619 hint = _('update to %s or descendant and run "hg histedit '
620 620 '--continue" again') % parentctx
621 621 raise util.Abort(msg % parentctx, hint=hint)
622 622 newchildren.pop(0) # remove parentctxnode
623 623 # Commit dirty working directory if necessary
624 624 new = None
625 625 m, a, r, d = repo.status()[:4]
626 626 if m or a or r or d:
627 627 # prepare the message for the commit to comes
628 628 if action in ('f', 'fold'):
629 629 message = 'fold-temp-revision %s' % currentnode
630 630 else:
631 631 message = ctx.description() + '\n'
632 632 if action in ('e', 'edit', 'm', 'mess'):
633 633 editor = cmdutil.commitforceeditor
634 634 else:
635 635 editor = False
636 636 commit = commitfuncfor(repo, ctx)
637 637 new = commit(text=message, user=ctx.user(),
638 638 date=ctx.date(), extra=ctx.extra(),
639 639 editor=editor)
640 640 if new is not None:
641 641 newchildren.append(new)
642 642
643 643 replacements = []
644 644 # track replacements
645 645 if ctx.node() not in newchildren:
646 646 # note: new children may be empty when the changeset is dropped.
647 647 # this happen e.g during conflicting pick where we revert content
648 648 # to parent.
649 649 replacements.append((ctx.node(), tuple(newchildren)))
650 650
651 651 if action in ('f', 'fold'):
652 652 if newchildren:
653 653 # finalize fold operation if applicable
654 654 if new is None:
655 655 new = newchildren[-1]
656 656 else:
657 657 newchildren.pop() # remove new from internal changes
658 658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
659 659 newchildren)
660 660 replacements.extend(repl)
661 661 else:
662 662 # newchildren is empty if the fold did not result in any commit
663 663 # this happen when all folded change are discarded during the
664 664 # merge.
665 665 replacements.append((ctx.node(), (parentctx.node(),)))
666 666 elif newchildren:
667 667 # otherwise update "parentctx" before proceeding to further operation
668 668 parentctx = repo[newchildren[-1]]
669 669 return parentctx, replacements
670 670
671 671
672 672 def between(repo, old, new, keep):
673 673 """select and validate the set of revision to edit
674 674
675 675 When keep is false, the specified set can't have children."""
676 676 ctxs = list(repo.set('%n::%n', old, new))
677 677 if ctxs and not keep:
678 678 if (not obsolete._enabled and
679 679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
680 680 raise util.Abort(_('cannot edit history that would orphan nodes'))
681 681 root = ctxs[0] # list is already sorted by repo.set
682 682 if not root.phase():
683 683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
684 684 return [c.node() for c in ctxs]
685 685
686 686
687 687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
688 688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
689 689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
690 690 fp.close()
691 691
692 692 def readstate(repo):
693 693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
694 694 """
695 695 fp = open(os.path.join(repo.path, 'histedit-state'))
696 696 return pickle.load(fp)
697 697
698 698
699 699 def makedesc(c):
700 700 """build a initial action line for a ctx `c`
701 701
702 702 line are in the form:
703 703
704 704 pick <hash> <rev> <summary>
705 705 """
706 706 summary = ''
707 707 if c.description():
708 708 summary = c.description().splitlines()[0]
709 709 line = 'pick %s %d %s' % (c, c.rev(), summary)
710 710 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
711 711
712 712 def verifyrules(rules, repo, ctxs):
713 713 """Verify that there exists exactly one edit rule per given changeset.
714 714
715 715 Will abort if there are to many or too few rules, a malformed rule,
716 716 or a rule on a changeset outside of the user-given range.
717 717 """
718 718 parsed = []
719 719 if len(rules) != len(ctxs):
720 720 raise util.Abort(_('must specify a rule for each changeset once'))
721 721 for r in rules:
722 722 if ' ' not in r:
723 723 raise util.Abort(_('malformed line "%s"') % r)
724 724 action, rest = r.split(' ', 1)
725 if ' ' in rest.strip():
726 ha, rest = rest.split(' ', 1)
727 else:
728 ha = r.strip()
725 ha = rest.strip().split(' ', 1)[0]
729 726 try:
730 727 if repo[ha] not in ctxs:
731 728 raise util.Abort(
732 729 _('may not use changesets other than the ones listed'))
733 730 except error.RepoError:
734 731 raise util.Abort(_('unknown changeset %s listed') % ha)
735 732 if action not in actiontable:
736 733 raise util.Abort(_('unknown action "%s"') % action)
737 734 parsed.append([action, ha])
738 735 return parsed
739 736
740 737 def processreplacement(repo, replacements):
741 738 """process the list of replacements to return
742 739
743 740 1) the final mapping between original and created nodes
744 741 2) the list of temporary node created by histedit
745 742 3) the list of new commit created by histedit"""
746 743 allsuccs = set()
747 744 replaced = set()
748 745 fullmapping = {}
749 746 # initialise basic set
750 747 # fullmapping record all operation recorded in replacement
751 748 for rep in replacements:
752 749 allsuccs.update(rep[1])
753 750 replaced.add(rep[0])
754 751 fullmapping.setdefault(rep[0], set()).update(rep[1])
755 752 new = allsuccs - replaced
756 753 tmpnodes = allsuccs & replaced
757 754 # Reduce content fullmapping into direct relation between original nodes
758 755 # and final node created during history edition
759 756 # Dropped changeset are replaced by an empty list
760 757 toproceed = set(fullmapping)
761 758 final = {}
762 759 while toproceed:
763 760 for x in list(toproceed):
764 761 succs = fullmapping[x]
765 762 for s in list(succs):
766 763 if s in toproceed:
767 764 # non final node with unknown closure
768 765 # We can't process this now
769 766 break
770 767 elif s in final:
771 768 # non final node, replace with closure
772 769 succs.remove(s)
773 770 succs.update(final[s])
774 771 else:
775 772 final[x] = succs
776 773 toproceed.remove(x)
777 774 # remove tmpnodes from final mapping
778 775 for n in tmpnodes:
779 776 del final[n]
780 777 # we expect all changes involved in final to exist in the repo
781 778 # turn `final` into list (topologically sorted)
782 779 nm = repo.changelog.nodemap
783 780 for prec, succs in final.items():
784 781 final[prec] = sorted(succs, key=nm.get)
785 782
786 783 # computed topmost element (necessary for bookmark)
787 784 if new:
788 785 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
789 786 elif not final:
790 787 # Nothing rewritten at all. we won't need `newtopmost`
791 788 # It is the same as `oldtopmost` and `processreplacement` know it
792 789 newtopmost = None
793 790 else:
794 791 # every body died. The newtopmost is the parent of the root.
795 792 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
796 793
797 794 return final, tmpnodes, new, newtopmost
798 795
799 796 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
800 797 """Move bookmark from old to newly created node"""
801 798 if not mapping:
802 799 # if nothing got rewritten there is not purpose for this function
803 800 return
804 801 moves = []
805 802 for bk, old in sorted(repo._bookmarks.iteritems()):
806 803 if old == oldtopmost:
807 804 # special case ensure bookmark stay on tip.
808 805 #
809 806 # This is arguably a feature and we may only want that for the
810 807 # active bookmark. But the behavior is kept compatible with the old
811 808 # version for now.
812 809 moves.append((bk, newtopmost))
813 810 continue
814 811 base = old
815 812 new = mapping.get(base, None)
816 813 if new is None:
817 814 continue
818 815 while not new:
819 816 # base is killed, trying with parent
820 817 base = repo[base].p1().node()
821 818 new = mapping.get(base, (base,))
822 819 # nothing to move
823 820 moves.append((bk, new[-1]))
824 821 if moves:
825 822 marks = repo._bookmarks
826 823 for mark, new in moves:
827 824 old = marks[mark]
828 825 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
829 826 % (mark, node.short(old), node.short(new)))
830 827 marks[mark] = new
831 828 marks.write()
832 829
833 830 def cleanupnode(ui, repo, name, nodes):
834 831 """strip a group of nodes from the repository
835 832
836 833 The set of node to strip may contains unknown nodes."""
837 834 ui.debug('should strip %s nodes %s\n' %
838 835 (name, ', '.join([node.short(n) for n in nodes])))
839 836 lock = None
840 837 try:
841 838 lock = repo.lock()
842 839 # Find all node that need to be stripped
843 840 # (we hg %lr instead of %ln to silently ignore unknown item
844 841 nm = repo.changelog.nodemap
845 842 nodes = [n for n in nodes if n in nm]
846 843 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
847 844 for c in roots:
848 845 # We should process node in reverse order to strip tip most first.
849 846 # but this trigger a bug in changegroup hook.
850 847 # This would reduce bundle overhead
851 848 repair.strip(ui, repo, c)
852 849 finally:
853 850 lockmod.release(lock)
@@ -1,70 +1,101 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 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
45 45 --------------------------------------------------------------------
46 46
47 47 $ HGEDITOR=cat hg histedit "tip^^"
48 48 pick eb57da33312f 2 three
49 49 pick c8e68270e35a 3 four
50 50 pick 08d98a8350f3 4 five
51 51
52 52 # Edit history between eb57da33312f and 08d98a8350f3
53 53 #
54 54 # Commands:
55 55 # p, pick = use commit
56 56 # e, edit = use commit, but stop for amending
57 57 # f, fold = use commit, but fold into previous commit (combines N and N-1)
58 58 # d, drop = remove commit from history
59 59 # m, mess = edit message without changing commit content
60 60 #
61 61 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 62
63 63 Run on a revision not ancestors of the current working directory.
64 64 --------------------------------------------------------------------
65 65
66 66 $ hg up 2
67 67 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 68 $ hg histedit -r 4
69 69 abort: 08d98a8350f3 is not an ancestor of working directory
70 70 [255]
71 $ hg up --quiet
72
73 Test short version of command
74 ---------------------------------------
75
76 Note: we use varying amounts of white space between command name and changeset
77 short hash. This tests issue3893.
78
79 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
80 > pick eb57da33312f 2 three
81 > p c8e68270e35a 3 four
82 > f 08d98a8350f3 4 five
83 > EOF
84 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 reverting alpha
86 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 four
88 ***
89 five
90
91
92
93 HG: Enter commit message. Lines beginning with 'HG:' are removed.
94 HG: Leave message empty to abort commit.
95 HG: --
96 HG: user: test
97 HG: branch 'default'
98 HG: changed alpha
99 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
100 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/*-backup.hg (glob)
General Comments 0
You need to be logged in to leave comments. Login now