##// END OF EJS Templates
histedit: refuse to edit public changeset...
Pierre-Yves David -
r17762:57f27cbf default
parent child Browse files
Show More
@@ -1,761 +1,765 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 633536316234 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 633536316234 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 example repository above and add three more changes, such that
112 112 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
147 147 from mercurial import bookmarks
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 applychanges(ui, repo, ctx, opts):
181 181 """Merge changeset from ctx (only) in the current working directory"""
182 182 wcpar = repo.dirstate.parents()[0]
183 183 if ctx.p1().node() == wcpar:
184 184 # edition ar "in place" we do not need to make any merge,
185 185 # just applies changes on parent for edition
186 186 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
187 187 stats = None
188 188 else:
189 189 try:
190 190 # ui.forcemerge is an internal variable, do not document
191 191 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
192 192 stats = mergemod.update(repo, ctx.node(), True, True, False,
193 193 ctx.p1().node())
194 194 finally:
195 195 repo.ui.setconfig('ui', 'forcemerge', '')
196 196 repo.setparents(wcpar, node.nullid)
197 197 repo.dirstate.write()
198 198 # fix up dirstate for copies and renames
199 199 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
200 200 return stats
201 201
202 202 def collapse(repo, first, last, commitopts):
203 203 """collapse the set of revisions from first to last as new one.
204 204
205 205 Expected commit options are:
206 206 - message
207 207 - date
208 208 - username
209 209 Commit message is edited in all cases.
210 210
211 211 This function works in memory."""
212 212 ctxs = list(repo.set('%d::%d', first, last))
213 213 if not ctxs:
214 214 return None
215 215 base = first.parents()[0]
216 216
217 217 # commit a new version of the old changeset, including the update
218 218 # collect all files which might be affected
219 219 files = set()
220 220 for ctx in ctxs:
221 221 files.update(ctx.files())
222 222
223 223 # Recompute copies (avoid recording a -> b -> a)
224 224 copied = copies.pathcopies(first, last)
225 225
226 226 # prune files which were reverted by the updates
227 227 def samefile(f):
228 228 if f in last.manifest():
229 229 a = last.filectx(f)
230 230 if f in base.manifest():
231 231 b = base.filectx(f)
232 232 return (a.data() == b.data()
233 233 and a.flags() == b.flags())
234 234 else:
235 235 return False
236 236 else:
237 237 return f not in base.manifest()
238 238 files = [f for f in files if not samefile(f)]
239 239 # commit version of these files as defined by head
240 240 headmf = last.manifest()
241 241 def filectxfn(repo, ctx, path):
242 242 if path in headmf:
243 243 fctx = last[path]
244 244 flags = fctx.flags()
245 245 mctx = context.memfilectx(fctx.path(), fctx.data(),
246 246 islink='l' in flags,
247 247 isexec='x' in flags,
248 248 copied=copied.get(path))
249 249 return mctx
250 250 raise IOError()
251 251
252 252 if commitopts.get('message'):
253 253 message = commitopts['message']
254 254 else:
255 255 message = first.description()
256 256 user = commitopts.get('user')
257 257 date = commitopts.get('date')
258 258 extra = first.extra()
259 259
260 260 parents = (first.p1().node(), first.p2().node())
261 261 new = context.memctx(repo,
262 262 parents=parents,
263 263 text=message,
264 264 files=files,
265 265 filectxfn=filectxfn,
266 266 user=user,
267 267 date=date,
268 268 extra=extra)
269 269 new._text = cmdutil.commitforceeditor(repo, new, [])
270 270 return repo.commitctx(new)
271 271
272 272 def pick(ui, repo, ctx, ha, opts):
273 273 oldctx = repo[ha]
274 274 if oldctx.parents()[0] == ctx:
275 275 ui.debug('node %s unchanged\n' % ha)
276 276 return oldctx, []
277 277 hg.update(repo, ctx.node())
278 278 stats = applychanges(ui, repo, oldctx, opts)
279 279 if stats and stats[3] > 0:
280 280 raise util.Abort(_('Fix up the change and run '
281 281 'hg histedit --continue'))
282 282 # drop the second merge parent
283 283 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
284 284 date=oldctx.date(), extra=oldctx.extra())
285 285 if n is None:
286 286 ui.warn(_('%s: empty changeset\n')
287 287 % node.hex(ha))
288 288 return ctx, []
289 289 new = repo[n]
290 290 return new, [(oldctx.node(), (n,))]
291 291
292 292
293 293 def edit(ui, repo, ctx, ha, opts):
294 294 oldctx = repo[ha]
295 295 hg.update(repo, ctx.node())
296 296 applychanges(ui, repo, oldctx, opts)
297 297 raise util.Abort(_('Make changes as needed, you may commit or record as '
298 298 'needed now.\nWhen you are finished, run hg'
299 299 ' histedit --continue to resume.'))
300 300
301 301 def fold(ui, repo, ctx, ha, opts):
302 302 oldctx = repo[ha]
303 303 hg.update(repo, ctx.node())
304 304 stats = applychanges(ui, repo, oldctx, opts)
305 305 if stats and stats[3] > 0:
306 306 raise util.Abort(_('Fix up the change and run '
307 307 'hg histedit --continue'))
308 308 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
309 309 date=oldctx.date(), extra=oldctx.extra())
310 310 if n is None:
311 311 ui.warn(_('%s: empty changeset')
312 312 % node.hex(ha))
313 313 return ctx, []
314 314 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
315 315
316 316 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
317 317 parent = ctx.parents()[0].node()
318 318 hg.update(repo, parent)
319 319 ### prepare new commit data
320 320 commitopts = opts.copy()
321 321 # username
322 322 if ctx.user() == oldctx.user():
323 323 username = ctx.user()
324 324 else:
325 325 username = ui.username()
326 326 commitopts['user'] = username
327 327 # commit message
328 328 newmessage = '\n***\n'.join(
329 329 [ctx.description()] +
330 330 [repo[r].description() for r in internalchanges] +
331 331 [oldctx.description()]) + '\n'
332 332 commitopts['message'] = newmessage
333 333 # date
334 334 commitopts['date'] = max(ctx.date(), oldctx.date())
335 335 n = collapse(repo, ctx, repo[newnode], commitopts)
336 336 if n is None:
337 337 return ctx, []
338 338 hg.update(repo, n)
339 339 replacements = [(oldctx.node(), (newnode,)),
340 340 (ctx.node(), (n,)),
341 341 (newnode, (n,)),
342 342 ]
343 343 for ich in internalchanges:
344 344 replacements.append((ich, (n,)))
345 345 return repo[n], replacements
346 346
347 347 def drop(ui, repo, ctx, ha, opts):
348 348 return ctx, [(repo[ha].node(), ())]
349 349
350 350
351 351 def message(ui, repo, ctx, ha, opts):
352 352 oldctx = repo[ha]
353 353 hg.update(repo, ctx.node())
354 354 stats = applychanges(ui, repo, oldctx, opts)
355 355 if stats and stats[3] > 0:
356 356 raise util.Abort(_('Fix up the change and run '
357 357 'hg histedit --continue'))
358 358 message = oldctx.description() + '\n'
359 359 message = ui.edit(message, ui.username())
360 360 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
361 361 extra=oldctx.extra())
362 362 newctx = repo[new]
363 363 if oldctx.node() != newctx.node():
364 364 return newctx, [(oldctx.node(), (new,))]
365 365 # We didn't make an edit, so just indicate no replaced nodes
366 366 return newctx, []
367 367
368 368 actiontable = {'p': pick,
369 369 'pick': pick,
370 370 'e': edit,
371 371 'edit': edit,
372 372 'f': fold,
373 373 'fold': fold,
374 374 'd': drop,
375 375 'drop': drop,
376 376 'm': message,
377 377 'mess': message,
378 378 }
379 379
380 380 @command('histedit',
381 381 [('', 'commands', '',
382 382 _('Read history edits from the specified file.')),
383 383 ('c', 'continue', False, _('continue an edit already in progress')),
384 384 ('k', 'keep', False,
385 385 _("don't strip old nodes after edit is complete")),
386 386 ('', 'abort', False, _('abort an edit in progress')),
387 387 ('o', 'outgoing', False, _('changesets not found in destination')),
388 388 ('f', 'force', False,
389 389 _('force outgoing even for unrelated repositories')),
390 390 ('r', 'rev', [], _('first revision to be edited'))],
391 391 _("[PARENT]"))
392 392 def histedit(ui, repo, *parent, **opts):
393 393 """interactively edit changeset history
394 394 """
395 395 # TODO only abort if we try and histedit mq patches, not just
396 396 # blanket if mq patches are applied somewhere
397 397 mq = getattr(repo, 'mq', None)
398 398 if mq and mq.applied:
399 399 raise util.Abort(_('source has mq patches applied'))
400 400
401 401 parent = list(parent) + opts.get('rev', [])
402 402 if opts.get('outgoing'):
403 403 if len(parent) > 1:
404 404 raise util.Abort(
405 405 _('only one repo argument allowed with --outgoing'))
406 406 elif parent:
407 407 parent = parent[0]
408 408
409 409 dest = ui.expandpath(parent or 'default-push', parent or 'default')
410 410 dest, revs = hg.parseurl(dest, None)[:2]
411 411 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
412 412
413 413 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
414 414 other = hg.peer(repo, opts, dest)
415 415
416 416 if revs:
417 417 revs = [repo.lookup(rev) for rev in revs]
418 418
419 419 parent = discovery.findcommonoutgoing(
420 420 repo, other, [], force=opts.get('force')).missing[0:1]
421 421 else:
422 422 if opts.get('force'):
423 423 raise util.Abort(_('--force only allowed with --outgoing'))
424 424
425 425 if opts.get('continue', False):
426 426 if len(parent) != 0:
427 427 raise util.Abort(_('no arguments allowed with --continue'))
428 428 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
429 429 currentparent, wantnull = repo.dirstate.parents()
430 430 parentctx = repo[parentctxnode]
431 431 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
432 432 replacements.extend(repl)
433 433 elif opts.get('abort', False):
434 434 if len(parent) != 0:
435 435 raise util.Abort(_('no arguments allowed with --abort'))
436 436 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
437 437 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
438 438 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
439 439 hg.clean(repo, topmost)
440 440 cleanupnode(ui, repo, 'created', tmpnodes)
441 441 cleanupnode(ui, repo, 'temp', leafs)
442 442 os.unlink(os.path.join(repo.path, 'histedit-state'))
443 443 return
444 444 else:
445 445 cmdutil.bailifchanged(repo)
446 446 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
447 447 raise util.Abort(_('history edit already in progress, try '
448 448 '--continue or --abort'))
449 449
450 450 topmost, empty = repo.dirstate.parents()
451 451
452 452 if len(parent) != 1:
453 453 raise util.Abort(_('histedit requires exactly one parent revision'))
454 454 parent = scmutil.revsingle(repo, parent[0]).node()
455 455
456 456 keep = opts.get('keep', False)
457 457 revs = between(repo, parent, topmost, keep)
458 458
459 459 ctxs = [repo[r] for r in revs]
460 460 rules = opts.get('commands', '')
461 461 if not rules:
462 462 rules = '\n'.join([makedesc(c) for c in ctxs])
463 463 rules += '\n\n'
464 464 rules += editcomment % (node.short(parent), node.short(topmost))
465 465 rules = ui.edit(rules, ui.username())
466 466 # Save edit rules in .hg/histedit-last-edit.txt in case
467 467 # the user needs to ask for help after something
468 468 # surprising happens.
469 469 f = open(repo.join('histedit-last-edit.txt'), 'w')
470 470 f.write(rules)
471 471 f.close()
472 472 else:
473 473 f = open(rules)
474 474 rules = f.read()
475 475 f.close()
476 476 rules = [l for l in (r.strip() for r in rules.splitlines())
477 477 if l and not l[0] == '#']
478 478 rules = verifyrules(rules, repo, ctxs)
479 479
480 480 parentctx = repo[parent].parents()[0]
481 481 keep = opts.get('keep', False)
482 482 replacements = []
483 483
484 484
485 485 while rules:
486 486 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
487 487 action, ha = rules.pop(0)
488 488 ui.debug('histedit: processing %s %s\n' % (action, ha))
489 489 actfunc = actiontable[action]
490 490 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
491 491 replacements.extend(replacement_)
492 492
493 493 hg.update(repo, parentctx.node())
494 494
495 495 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
496 496 if mapping:
497 497 for prec, succs in mapping.iteritems():
498 498 if not succs:
499 499 ui.debug('histedit: %s is dropped\n' % node.short(prec))
500 500 else:
501 501 ui.debug('histedit: %s is replaced by %s\n' % (
502 502 node.short(prec), node.short(succs[0])))
503 503 if len(succs) > 1:
504 504 m = 'histedit: %s'
505 505 for n in succs[1:]:
506 506 ui.debug(m % node.short(n))
507 507
508 508 if not keep:
509 509 if mapping:
510 510 movebookmarks(ui, repo, mapping, topmost, ntm)
511 511 # TODO update mq state
512 512 if obsolete._enabled:
513 513 markers = []
514 514 for prec, succs in mapping.iteritems():
515 515 markers.append((repo[prec],
516 516 tuple(repo[s] for s in succs)))
517 517 if markers:
518 518 obsolete.createmarkers(repo, markers)
519 519 else:
520 520 cleanupnode(ui, repo, 'replaced', mapping)
521 521
522 522 cleanupnode(ui, repo, 'temp', tmpnodes)
523 523 os.unlink(os.path.join(repo.path, 'histedit-state'))
524 524 if os.path.exists(repo.sjoin('undo')):
525 525 os.unlink(repo.sjoin('undo'))
526 526
527 527
528 528 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
529 529 action, currentnode = rules.pop(0)
530 530 ctx = repo[currentnode]
531 531 # is there any new commit between the expected parent and "."
532 532 #
533 533 # note: does not take non linear new change in account (but previous
534 534 # implementation didn't used them anyway (issue3655)
535 535 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
536 536 if not newchildren:
537 537 # `parentctxnode` should match but no result. This means that
538 538 # currentnode is not a descendant from parentctxnode.
539 539 msg = _('working directory parent is not a descendant of %s')
540 540 hint = _('update to %s or descendant and run "hg histedit '
541 541 '--continue" again') % parentctx
542 542 raise util.Abort(msg % parentctx, hint=hint)
543 543 newchildren.pop(0) # remove parentctxnode
544 544 # Commit dirty working directory if necessary
545 545 new = None
546 546 m, a, r, d = repo.status()[:4]
547 547 if m or a or r or d:
548 548 # prepare the message for the commit to comes
549 549 if action in ('f', 'fold'):
550 550 message = 'fold-temp-revision %s' % currentnode
551 551 else:
552 552 message = ctx.description() + '\n'
553 553 if action in ('e', 'edit', 'm', 'mess'):
554 554 editor = cmdutil.commitforceeditor
555 555 else:
556 556 editor = False
557 557 new = repo.commit(text=message, user=ctx.user(),
558 558 date=ctx.date(), extra=ctx.extra(),
559 559 editor=editor)
560 560 if new is not None:
561 561 newchildren.append(new)
562 562
563 563 replacements = []
564 564 # track replacements
565 565 if ctx.node() not in newchildren:
566 566 # note: new children may be empty when the changeset is dropped.
567 567 # this happen e.g during conflicting pick where we revert content
568 568 # to parent.
569 569 replacements.append((ctx.node(), tuple(newchildren)))
570 570
571 571 if action in ('f', 'fold'):
572 572 # finalize fold operation if applicable
573 573 if new is None:
574 574 new = newchildren[-1]
575 575 else:
576 576 newchildren.pop() # remove new from internal changes
577 577 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
578 578 newchildren)
579 579 replacements.extend(repl)
580 580 elif newchildren:
581 581 # otherwize update "parentctx" before proceding to further operation
582 582 parentctx = repo[newchildren[-1]]
583 583 return parentctx, replacements
584 584
585 585
586 586 def between(repo, old, new, keep):
587 587 """select and validate the set of revision to edit
588 588
589 589 When keep is false, the specified set can't have children."""
590 590 revs = list(repo.set('%n::%n', old, new))
591 if not keep and repo.revs('(%ld::) - (%ld + hidden())', revs, revs):
591 if not keep:
592 if repo.revs('(%ld::) - (%ld + hidden())', revs, revs):
592 593 raise util.Abort(_('cannot edit history that would orphan nodes'))
594 root = min(revs)
595 if not root.phase():
596 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
593 597 return [c.node() for c in revs]
594 598
595 599
596 600 def writestate(repo, parentnode, rules, keep, topmost, replacements):
597 601 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
598 602 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
599 603 fp.close()
600 604
601 605 def readstate(repo):
602 606 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
603 607 """
604 608 fp = open(os.path.join(repo.path, 'histedit-state'))
605 609 return pickle.load(fp)
606 610
607 611
608 612 def makedesc(c):
609 613 """build a initial action line for a ctx `c`
610 614
611 615 line are in the form:
612 616
613 617 pick <hash> <rev> <summary>
614 618 """
615 619 summary = ''
616 620 if c.description():
617 621 summary = c.description().splitlines()[0]
618 622 line = 'pick %s %d %s' % (c, c.rev(), summary)
619 623 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
620 624
621 625 def verifyrules(rules, repo, ctxs):
622 626 """Verify that there exists exactly one edit rule per given changeset.
623 627
624 628 Will abort if there are to many or too few rules, a malformed rule,
625 629 or a rule on a changeset outside of the user-given range.
626 630 """
627 631 parsed = []
628 632 if len(rules) != len(ctxs):
629 633 raise util.Abort(_('must specify a rule for each changeset once'))
630 634 for r in rules:
631 635 if ' ' not in r:
632 636 raise util.Abort(_('malformed line "%s"') % r)
633 637 action, rest = r.split(' ', 1)
634 638 if ' ' in rest.strip():
635 639 ha, rest = rest.split(' ', 1)
636 640 else:
637 641 ha = r.strip()
638 642 try:
639 643 if repo[ha] not in ctxs:
640 644 raise util.Abort(
641 645 _('may not use changesets other than the ones listed'))
642 646 except error.RepoError:
643 647 raise util.Abort(_('unknown changeset %s listed') % ha)
644 648 if action not in actiontable:
645 649 raise util.Abort(_('unknown action "%s"') % action)
646 650 parsed.append([action, ha])
647 651 return parsed
648 652
649 653 def processreplacement(repo, replacements):
650 654 """process the list of replacements to return
651 655
652 656 1) the final mapping between original and created nodes
653 657 2) the list of temporary node created by histedit
654 658 3) the list of new commit created by histedit"""
655 659 allsuccs = set()
656 660 replaced = set()
657 661 fullmapping = {}
658 662 # initialise basic set
659 663 # fullmapping record all operation recorded in replacement
660 664 for rep in replacements:
661 665 allsuccs.update(rep[1])
662 666 replaced.add(rep[0])
663 667 fullmapping.setdefault(rep[0], set()).update(rep[1])
664 668 new = allsuccs - replaced
665 669 tmpnodes = allsuccs & replaced
666 670 # Reduce content fullmapping into direct relation between original nodes
667 671 # and final node created during history edition
668 672 # Dropped changeset are replaced by an empty list
669 673 toproceed = set(fullmapping)
670 674 final = {}
671 675 while toproceed:
672 676 for x in list(toproceed):
673 677 succs = fullmapping[x]
674 678 for s in list(succs):
675 679 if s in toproceed:
676 680 # non final node with unknown closure
677 681 # We can't process this now
678 682 break
679 683 elif s in final:
680 684 # non final node, replace with closure
681 685 succs.remove(s)
682 686 succs.update(final[s])
683 687 else:
684 688 final[x] = succs
685 689 toproceed.remove(x)
686 690 # remove tmpnodes from final mapping
687 691 for n in tmpnodes:
688 692 del final[n]
689 693 # we expect all changes involved in final to exist in the repo
690 694 # turn `final` into list (topologically sorted)
691 695 nm = repo.changelog.nodemap
692 696 for prec, succs in final.items():
693 697 final[prec] = sorted(succs, key=nm.get)
694 698
695 699 # computed topmost element (necessary for bookmark)
696 700 if new:
697 701 newtopmost = max(new, key=repo.changelog.rev)
698 702 elif not final:
699 703 # Nothing rewritten at all. we won't need `newtopmost`
700 704 # It is the same as `oldtopmost` and `processreplacement` know it
701 705 newtopmost = None
702 706 else:
703 707 # every body died. The newtopmost is the parent of the root.
704 708 newtopmost = repo[min(final, key=repo.changelog.rev)].p1().node()
705 709
706 710 return final, tmpnodes, new, newtopmost
707 711
708 712 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
709 713 """Move bookmark from old to newly created node"""
710 714 if not mapping:
711 715 # if nothing got rewritten there is not purpose for this function
712 716 return
713 717 moves = []
714 718 for bk, old in repo._bookmarks.iteritems():
715 719 if old == oldtopmost:
716 720 # special case ensure bookmark stay on tip.
717 721 #
718 722 # This is arguably a feature and we may only want that for the
719 723 # active bookmark. But the behavior is kept compatible with the old
720 724 # version for now.
721 725 moves.append((bk, newtopmost))
722 726 continue
723 727 base = old
724 728 new = mapping.get(base, None)
725 729 if new is None:
726 730 continue
727 731 while not new:
728 732 # base is killed, trying with parent
729 733 base = repo[base].p1().node()
730 734 new = mapping.get(base, (base,))
731 735 # nothing to move
732 736 moves.append((bk, new[-1]))
733 737 if moves:
734 738 for mark, new in moves:
735 739 old = repo._bookmarks[mark]
736 740 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
737 741 % (mark, node.short(old), node.short(new)))
738 742 repo._bookmarks[mark] = new
739 743 bookmarks.write(repo)
740 744
741 745 def cleanupnode(ui, repo, name, nodes):
742 746 """strip a group of nodes from the repository
743 747
744 748 The set of node to strip may contains unknown nodes."""
745 749 ui.debug('should strip %s nodes %s\n' %
746 750 (name, ', '.join([node.short(n) for n in nodes])))
747 751 lock = None
748 752 try:
749 753 lock = repo.lock()
750 754 # Find all node that need to be stripped
751 755 # (we hg %lr instead of %ln to silently ignore unknown item
752 756 nm = repo.changelog.nodemap
753 757 nodes = [n for n in nodes if n in nm]
754 758 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
755 759 for c in roots:
756 760 # We should process node in reverse order to strip tip most first.
757 761 # but this trigger a bug in changegroup hook.
758 762 # This would reduce bundle overhead
759 763 repair.strip(ui, repo, c)
760 764 finally:
761 765 lockmod.release(lock)
@@ -1,123 +1,142 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 Enable obsolete
4 4
5 5 $ cat > ${TESTTMP}/obs.py << EOF
6 6 > import mercurial.obsolete
7 7 > mercurial.obsolete._enabled = True
8 8 > EOF
9 9
10 10 $ cat >> $HGRCPATH << EOF
11 11 > [ui]
12 12 > logtemplate= {rev}:{node|short} {desc|firstline}
13 13 > [phases]
14 14 > publish=False
15 15 > [extensions]'
16 16 > histedit=
17 17 >
18 18 > obs=${TESTTMP}/obs.py
19 19 > EOF
20 20
21 21 $ hg init base
22 22 $ cd base
23 23
24 24 $ for x in a b c d e f ; do
25 25 > echo $x > $x
26 26 > hg add $x
27 27 > hg ci -m $x
28 28 > done
29 29
30 30 $ hg log --graph
31 31 @ 5:652413bf663e f
32 32 |
33 33 o 4:e860deea161a e
34 34 |
35 35 o 3:055a42cdd887 d
36 36 |
37 37 o 2:177f92b77385 c
38 38 |
39 39 o 1:d2ae7f538514 b
40 40 |
41 41 o 0:cb9a9f314b8b a
42 42
43 43
44 44 $ HGEDITOR=cat hg histedit 1
45 45 pick d2ae7f538514 1 b
46 46 pick 177f92b77385 2 c
47 47 pick 055a42cdd887 3 d
48 48 pick e860deea161a 4 e
49 49 pick 652413bf663e 5 f
50 50
51 51 # Edit history between d2ae7f538514 and 652413bf663e
52 52 #
53 53 # Commands:
54 54 # p, pick = use commit
55 55 # e, edit = use commit, but stop for amending
56 56 # f, fold = use commit, but fold into previous commit (combines N and N-1)
57 57 # d, drop = remove commit from history
58 58 # m, mess = edit message without changing commit content
59 59 #
60 60 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 61 $ cat >> commands.txt <<EOF
62 62 > pick 177f92b77385 2 c
63 63 > drop d2ae7f538514 1 b
64 64 > pick 055a42cdd887 3 d
65 65 > fold e860deea161a 4 e
66 66 > pick 652413bf663e 5 f
67 67 > EOF
68 68 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
69 69 saved backup bundle to $TESTTMP/base/.hg/strip-backup/34a9919932c1-backup.hg (glob)
70 70 $ hg log --graph --hidden
71 71 @ 8:0efacef7cb48 f
72 72 |
73 73 o 7:ae467701c500 d
74 74 |
75 75 o 6:d36c0562f908 c
76 76 |
77 77 | x 5:652413bf663e f
78 78 | |
79 79 | x 4:e860deea161a e
80 80 | |
81 81 | x 3:055a42cdd887 d
82 82 | |
83 83 | x 2:177f92b77385 c
84 84 | |
85 85 | x 1:d2ae7f538514 b
86 86 |/
87 87 o 0:cb9a9f314b8b a
88 88
89 89 $ hg debugobsolete
90 90 e860deea161a2f77de56603b340ebbb4536308ae ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '*', 'user': 'test'} (glob)
91 91 652413bf663ef2a641cab26574e46d5f5a64a55a 0efacef7cb481bf574f69075b82d044fdbe5c20f 0 {'date': '*': 'test'} (glob)
92 92 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {'date': '*', 'user': 'test'} (glob)
93 93 055a42cdd88768532f9cf79daa407fc8d138de9b ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '*': 'test'} (glob)
94 94 177f92b773850b59254aa5e923436f921b55483b d36c0562f908c692f5204d606d4ff3537d41f1bf 0 {'date': '*', 'user': 'test'} (glob)
95 95
96 96 Ensure hidden revision does not prevent histedit
97 97 -------------------------------------------------
98 98
99 99 create an hidden revision
100 100
101 101 $ cat > commands.txt <<EOF
102 102 > pick d36c0562f908 6 c
103 103 > drop ae467701c500 7 d
104 104 > pick 0efacef7cb48 8 f
105 105 > EOF
106 106 $ hg histedit 6 --commands commands.txt
107 107 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
108 108 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
109 109 $ hg log --graph
110 110 @ 9:7c044e3e33a9 f
111 111 |
112 112 o 6:d36c0562f908 c
113 113 |
114 114 o 0:cb9a9f314b8b a
115 115
116 116 check hidden revision are ignored (6 have hidden children 7 and 8)
117 117
118 118 $ cat > commands.txt <<EOF
119 119 > pick d36c0562f908 6 c
120 120 > pick 7c044e3e33a9 8 f
121 121 > EOF
122 122 $ hg histedit 6 --commands commands.txt
123 123 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
124
125
126 Check that histedit respect phases
127 =========================================
128
129 (not directly related to the test file but doesn't deserve it's own test case)
130
131 $ hg log -G
132 @ 9:7c044e3e33a9 f
133 |
134 o 6:d36c0562f908 c
135 |
136 o 0:cb9a9f314b8b a
137
138 $ hg ph -pv '.^'
139 phase changed for 2 changesets
140 $ hg histedit -r '.~2'
141 abort: cannot edit immutable changeset: cb9a9f314b8b
142 [255]
General Comments 0
You need to be logged in to leave comments. Login now