##// END OF EJS Templates
histedit: move all arguments checks to the beginning of the command...
Pierre-Yves David -
r19020:12c06686 default
parent child Browse files
Show More
@@ -1,826 +1,845 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 actiontable = {'p': pick,
408 408 'pick': pick,
409 409 'e': edit,
410 410 'edit': edit,
411 411 'f': fold,
412 412 'fold': fold,
413 413 'd': drop,
414 414 'drop': drop,
415 415 'm': message,
416 416 'mess': message,
417 417 }
418 418
419 419 @command('histedit',
420 420 [('', 'commands', '',
421 421 _('Read history edits from the specified file.')),
422 422 ('c', 'continue', False, _('continue an edit already in progress')),
423 423 ('k', 'keep', False,
424 424 _("don't strip old nodes after edit is complete")),
425 425 ('', 'abort', False, _('abort an edit in progress')),
426 426 ('o', 'outgoing', False, _('changesets not found in destination')),
427 427 ('f', 'force', False,
428 428 _('force outgoing even for unrelated repositories')),
429 429 ('r', 'rev', [], _('first revision to be edited'))],
430 430 _("[PARENT]"))
431 def histedit(ui, repo, *parent, **opts):
431 def histedit(ui, repo, *freeargs, **opts):
432 432 """interactively edit changeset history
433 433 """
434 434 # TODO only abort if we try and histedit mq patches, not just
435 435 # blanket if mq patches are applied somewhere
436 436 mq = getattr(repo, 'mq', None)
437 437 if mq and mq.applied:
438 438 raise util.Abort(_('source has mq patches applied'))
439 439
440 parent = list(parent) + opts.get('rev', [])
440 # basic argument incompatibility processing
441 outg = opts.get('outgoing')
442 cont = opts.get('continue')
443 abort = opts.get('abort')
444 force = opts.get('force')
445 rules = opts.get('commands', '')
446 revs = opts.get('rev', [])
447 goal = 'new' # This invocation goal, in new, continue, abort
448 if force and not outg:
449 raise util.Abort(_('--force only allowed with --outgoing'))
450 if cont:
451 if util.any((outg, abort, revs, freeargs, rules)):
452 raise util.Abort(_('no arguments allowed with --continue'))
453 goal = 'continue'
454 elif abort:
455 if util.any((outg, revs, freeargs, rules)):
456 raise util.Abort(_('no arguments allowed with --abort'))
457 goal = 'abort'
458 else:
459 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
460 raise util.Abort(_('history edit already in progress, try '
461 '--continue or --abort'))
462 if outg:
463 if revs:
464 raise util.Abort(_('no revisions allowed with --outgoing'))
465 if len(freeargs) > 1:
466 raise util.Abort(
467 _('only one repo argument allowed with --outgoing'))
468 else:
469 parent = list(freeargs) + opts.get('rev', [])
470 if len(parent) != 1:
471 raise util.Abort(
472 _('histedit requires exactly one parent revision'))
473
441 474 if opts.get('outgoing'):
442 if len(parent) > 1:
443 raise util.Abort(
444 _('only one repo argument allowed with --outgoing'))
445 elif parent:
446 parent = parent[0]
475 if freeargs:
476 parent = freeargs[0]
447 477
448 478 dest = ui.expandpath(parent or 'default-push', parent or 'default')
449 479 dest, revs = hg.parseurl(dest, None)[:2]
450 480 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
451 481
452 482 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
453 483 other = hg.peer(repo, opts, dest)
454 484
455 485 if revs:
456 486 revs = [repo.lookup(rev) for rev in revs]
457 487
458 488 # hexlify nodes from outgoing, because we're going to parse
459 489 # parent[0] using revsingle below, and if the binary hash
460 490 # contains special revset characters like ":" the revset
461 491 # parser can choke.
462 492 parent = [node.hex(n) for n in discovery.findcommonoutgoing(
463 repo, other, revs, force=opts.get('force')).missing[0:1]]
464 else:
465 if opts.get('force'):
466 raise util.Abort(_('--force only allowed with --outgoing'))
493 repo, other, revs, force=force).missing[0:1]]
494 if not parent:
495 raise util.Abort(_('no outgoing ancestors'))
467 496
468 if opts.get('continue', False):
469 if len(parent) != 0:
470 raise util.Abort(_('no arguments allowed with --continue'))
497 if goal == 'continue':
471 498 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
472 499 currentparent, wantnull = repo.dirstate.parents()
473 500 parentctx = repo[parentctxnode]
474 501 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
475 502 replacements.extend(repl)
476 elif opts.get('abort', False):
477 if len(parent) != 0:
478 raise util.Abort(_('no arguments allowed with --abort'))
503 elif goal == 'abort':
479 504 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
480 505 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
481 506 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
482 507 hg.clean(repo, topmost)
483 508 cleanupnode(ui, repo, 'created', tmpnodes)
484 509 cleanupnode(ui, repo, 'temp', leafs)
485 510 os.unlink(os.path.join(repo.path, 'histedit-state'))
486 511 return
487 512 else:
488 513 cmdutil.bailifchanged(repo)
489 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
490 raise util.Abort(_('history edit already in progress, try '
491 '--continue or --abort'))
492 514
493 515 topmost, empty = repo.dirstate.parents()
494 516
495 if len(parent) != 1:
496 raise util.Abort(_('histedit requires exactly one parent revision'))
497 517 parent = scmutil.revsingle(repo, parent[0]).node()
498 518
499 519 keep = opts.get('keep', False)
500 520 revs = between(repo, parent, topmost, keep)
501 521 if not revs:
502 522 raise util.Abort(_('%s is not an ancestor of working directory') %
503 523 node.short(parent))
504 524
505 525 ctxs = [repo[r] for r in revs]
506 rules = opts.get('commands', '')
507 526 if not rules:
508 527 rules = '\n'.join([makedesc(c) for c in ctxs])
509 528 rules += '\n\n'
510 529 rules += editcomment % (node.short(parent), node.short(topmost))
511 530 rules = ui.edit(rules, ui.username())
512 531 # Save edit rules in .hg/histedit-last-edit.txt in case
513 532 # the user needs to ask for help after something
514 533 # surprising happens.
515 534 f = open(repo.join('histedit-last-edit.txt'), 'w')
516 535 f.write(rules)
517 536 f.close()
518 537 else:
519 538 if rules == '-':
520 539 f = sys.stdin
521 540 else:
522 541 f = open(rules)
523 542 rules = f.read()
524 543 f.close()
525 544 rules = [l for l in (r.strip() for r in rules.splitlines())
526 545 if l and not l[0] == '#']
527 546 rules = verifyrules(rules, repo, ctxs)
528 547
529 548 parentctx = repo[parent].parents()[0]
530 549 keep = opts.get('keep', False)
531 550 replacements = []
532 551
533 552
534 553 while rules:
535 554 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
536 555 action, ha = rules.pop(0)
537 556 ui.debug('histedit: processing %s %s\n' % (action, ha))
538 557 actfunc = actiontable[action]
539 558 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
540 559 replacements.extend(replacement_)
541 560
542 561 hg.update(repo, parentctx.node())
543 562
544 563 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
545 564 if mapping:
546 565 for prec, succs in mapping.iteritems():
547 566 if not succs:
548 567 ui.debug('histedit: %s is dropped\n' % node.short(prec))
549 568 else:
550 569 ui.debug('histedit: %s is replaced by %s\n' % (
551 570 node.short(prec), node.short(succs[0])))
552 571 if len(succs) > 1:
553 572 m = 'histedit: %s'
554 573 for n in succs[1:]:
555 574 ui.debug(m % node.short(n))
556 575
557 576 if not keep:
558 577 if mapping:
559 578 movebookmarks(ui, repo, mapping, topmost, ntm)
560 579 # TODO update mq state
561 580 if obsolete._enabled:
562 581 markers = []
563 582 # sort by revision number because it sound "right"
564 583 for prec in sorted(mapping, key=repo.changelog.rev):
565 584 succs = mapping[prec]
566 585 markers.append((repo[prec],
567 586 tuple(repo[s] for s in succs)))
568 587 if markers:
569 588 obsolete.createmarkers(repo, markers)
570 589 else:
571 590 cleanupnode(ui, repo, 'replaced', mapping)
572 591
573 592 cleanupnode(ui, repo, 'temp', tmpnodes)
574 593 os.unlink(os.path.join(repo.path, 'histedit-state'))
575 594 if os.path.exists(repo.sjoin('undo')):
576 595 os.unlink(repo.sjoin('undo'))
577 596
578 597
579 598 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
580 599 action, currentnode = rules.pop(0)
581 600 ctx = repo[currentnode]
582 601 # is there any new commit between the expected parent and "."
583 602 #
584 603 # note: does not take non linear new change in account (but previous
585 604 # implementation didn't used them anyway (issue3655)
586 605 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
587 606 if parentctx.node() != node.nullid:
588 607 if not newchildren:
589 608 # `parentctxnode` should match but no result. This means that
590 609 # currentnode is not a descendant from parentctxnode.
591 610 msg = _('%s is not an ancestor of working directory')
592 611 hint = _('update to %s or descendant and run "hg histedit '
593 612 '--continue" again') % parentctx
594 613 raise util.Abort(msg % parentctx, hint=hint)
595 614 newchildren.pop(0) # remove parentctxnode
596 615 # Commit dirty working directory if necessary
597 616 new = None
598 617 m, a, r, d = repo.status()[:4]
599 618 if m or a or r or d:
600 619 # prepare the message for the commit to comes
601 620 if action in ('f', 'fold'):
602 621 message = 'fold-temp-revision %s' % currentnode
603 622 else:
604 623 message = ctx.description() + '\n'
605 624 if action in ('e', 'edit', 'm', 'mess'):
606 625 editor = cmdutil.commitforceeditor
607 626 else:
608 627 editor = False
609 628 commit = commitfuncfor(repo, ctx)
610 629 new = commit(text=message, user=ctx.user(),
611 630 date=ctx.date(), extra=ctx.extra(),
612 631 editor=editor)
613 632 if new is not None:
614 633 newchildren.append(new)
615 634
616 635 replacements = []
617 636 # track replacements
618 637 if ctx.node() not in newchildren:
619 638 # note: new children may be empty when the changeset is dropped.
620 639 # this happen e.g during conflicting pick where we revert content
621 640 # to parent.
622 641 replacements.append((ctx.node(), tuple(newchildren)))
623 642
624 643 if action in ('f', 'fold'):
625 644 if newchildren:
626 645 # finalize fold operation if applicable
627 646 if new is None:
628 647 new = newchildren[-1]
629 648 else:
630 649 newchildren.pop() # remove new from internal changes
631 650 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
632 651 newchildren)
633 652 replacements.extend(repl)
634 653 else:
635 654 # newchildren is empty if the fold did not result in any commit
636 655 # this happen when all folded change are discarded during the
637 656 # merge.
638 657 replacements.append((ctx.node(), (parentctx.node(),)))
639 658 elif newchildren:
640 659 # otherwise update "parentctx" before proceeding to further operation
641 660 parentctx = repo[newchildren[-1]]
642 661 return parentctx, replacements
643 662
644 663
645 664 def between(repo, old, new, keep):
646 665 """select and validate the set of revision to edit
647 666
648 667 When keep is false, the specified set can't have children."""
649 668 ctxs = list(repo.set('%n::%n', old, new))
650 669 if ctxs and not keep:
651 670 if (not obsolete._enabled and
652 671 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
653 672 raise util.Abort(_('cannot edit history that would orphan nodes'))
654 673 root = ctxs[0] # list is already sorted by repo.set
655 674 if not root.phase():
656 675 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
657 676 return [c.node() for c in ctxs]
658 677
659 678
660 679 def writestate(repo, parentnode, rules, keep, topmost, replacements):
661 680 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
662 681 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
663 682 fp.close()
664 683
665 684 def readstate(repo):
666 685 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
667 686 """
668 687 fp = open(os.path.join(repo.path, 'histedit-state'))
669 688 return pickle.load(fp)
670 689
671 690
672 691 def makedesc(c):
673 692 """build a initial action line for a ctx `c`
674 693
675 694 line are in the form:
676 695
677 696 pick <hash> <rev> <summary>
678 697 """
679 698 summary = ''
680 699 if c.description():
681 700 summary = c.description().splitlines()[0]
682 701 line = 'pick %s %d %s' % (c, c.rev(), summary)
683 702 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
684 703
685 704 def verifyrules(rules, repo, ctxs):
686 705 """Verify that there exists exactly one edit rule per given changeset.
687 706
688 707 Will abort if there are to many or too few rules, a malformed rule,
689 708 or a rule on a changeset outside of the user-given range.
690 709 """
691 710 parsed = []
692 711 if len(rules) != len(ctxs):
693 712 raise util.Abort(_('must specify a rule for each changeset once'))
694 713 for r in rules:
695 714 if ' ' not in r:
696 715 raise util.Abort(_('malformed line "%s"') % r)
697 716 action, rest = r.split(' ', 1)
698 717 if ' ' in rest.strip():
699 718 ha, rest = rest.split(' ', 1)
700 719 else:
701 720 ha = r.strip()
702 721 try:
703 722 if repo[ha] not in ctxs:
704 723 raise util.Abort(
705 724 _('may not use changesets other than the ones listed'))
706 725 except error.RepoError:
707 726 raise util.Abort(_('unknown changeset %s listed') % ha)
708 727 if action not in actiontable:
709 728 raise util.Abort(_('unknown action "%s"') % action)
710 729 parsed.append([action, ha])
711 730 return parsed
712 731
713 732 def processreplacement(repo, replacements):
714 733 """process the list of replacements to return
715 734
716 735 1) the final mapping between original and created nodes
717 736 2) the list of temporary node created by histedit
718 737 3) the list of new commit created by histedit"""
719 738 allsuccs = set()
720 739 replaced = set()
721 740 fullmapping = {}
722 741 # initialise basic set
723 742 # fullmapping record all operation recorded in replacement
724 743 for rep in replacements:
725 744 allsuccs.update(rep[1])
726 745 replaced.add(rep[0])
727 746 fullmapping.setdefault(rep[0], set()).update(rep[1])
728 747 new = allsuccs - replaced
729 748 tmpnodes = allsuccs & replaced
730 749 # Reduce content fullmapping into direct relation between original nodes
731 750 # and final node created during history edition
732 751 # Dropped changeset are replaced by an empty list
733 752 toproceed = set(fullmapping)
734 753 final = {}
735 754 while toproceed:
736 755 for x in list(toproceed):
737 756 succs = fullmapping[x]
738 757 for s in list(succs):
739 758 if s in toproceed:
740 759 # non final node with unknown closure
741 760 # We can't process this now
742 761 break
743 762 elif s in final:
744 763 # non final node, replace with closure
745 764 succs.remove(s)
746 765 succs.update(final[s])
747 766 else:
748 767 final[x] = succs
749 768 toproceed.remove(x)
750 769 # remove tmpnodes from final mapping
751 770 for n in tmpnodes:
752 771 del final[n]
753 772 # we expect all changes involved in final to exist in the repo
754 773 # turn `final` into list (topologically sorted)
755 774 nm = repo.changelog.nodemap
756 775 for prec, succs in final.items():
757 776 final[prec] = sorted(succs, key=nm.get)
758 777
759 778 # computed topmost element (necessary for bookmark)
760 779 if new:
761 780 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
762 781 elif not final:
763 782 # Nothing rewritten at all. we won't need `newtopmost`
764 783 # It is the same as `oldtopmost` and `processreplacement` know it
765 784 newtopmost = None
766 785 else:
767 786 # every body died. The newtopmost is the parent of the root.
768 787 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
769 788
770 789 return final, tmpnodes, new, newtopmost
771 790
772 791 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
773 792 """Move bookmark from old to newly created node"""
774 793 if not mapping:
775 794 # if nothing got rewritten there is not purpose for this function
776 795 return
777 796 moves = []
778 797 for bk, old in sorted(repo._bookmarks.iteritems()):
779 798 if old == oldtopmost:
780 799 # special case ensure bookmark stay on tip.
781 800 #
782 801 # This is arguably a feature and we may only want that for the
783 802 # active bookmark. But the behavior is kept compatible with the old
784 803 # version for now.
785 804 moves.append((bk, newtopmost))
786 805 continue
787 806 base = old
788 807 new = mapping.get(base, None)
789 808 if new is None:
790 809 continue
791 810 while not new:
792 811 # base is killed, trying with parent
793 812 base = repo[base].p1().node()
794 813 new = mapping.get(base, (base,))
795 814 # nothing to move
796 815 moves.append((bk, new[-1]))
797 816 if moves:
798 817 marks = repo._bookmarks
799 818 for mark, new in moves:
800 819 old = marks[mark]
801 820 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
802 821 % (mark, node.short(old), node.short(new)))
803 822 marks[mark] = new
804 823 marks.write()
805 824
806 825 def cleanupnode(ui, repo, name, nodes):
807 826 """strip a group of nodes from the repository
808 827
809 828 The set of node to strip may contains unknown nodes."""
810 829 ui.debug('should strip %s nodes %s\n' %
811 830 (name, ', '.join([node.short(n) for n in nodes])))
812 831 lock = None
813 832 try:
814 833 lock = repo.lock()
815 834 # Find all node that need to be stripped
816 835 # (we hg %lr instead of %ln to silently ignore unknown item
817 836 nm = repo.changelog.nodemap
818 837 nodes = [n for n in nodes if n in nm]
819 838 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
820 839 for c in roots:
821 840 # We should process node in reverse order to strip tip most first.
822 841 # but this trigger a bug in changegroup hook.
823 842 # This would reduce bundle overhead
824 843 repair.strip(ui, repo, c)
825 844 finally:
826 845 lockmod.release(lock)
@@ -1,295 +1,295 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > graphlog=
6 6 > histedit=
7 7 > EOF
8 8
9 9 $ initrepo ()
10 10 > {
11 11 > hg init $1
12 12 > cd $1
13 13 > for x in a b c d e f ; do
14 14 > echo $x$x$x$x$x > $x
15 15 > hg add $x
16 16 > done
17 17 > hg ci -m 'Initial commit'
18 18 > for x in a b c d e f ; do
19 19 > echo $x > $x
20 20 > hg ci -m $x
21 21 > done
22 22 > echo 'I can haz no commute' > e
23 23 > hg ci -m 'does not commute with e'
24 24 > cd ..
25 25 > }
26 26
27 27 $ initrepo r1
28 28 $ cd r1
29 29
30 30 Initial generation of the command files
31 31
32 32 $ EDITED="$TESTTMP/editedhistory"
33 33 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
34 34 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
35 35 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 7 >> $EDITED
36 36 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
37 37 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
38 38 $ cat $EDITED
39 39 pick 65a9a84f33fd 3 c
40 40 pick 00f1c5383965 4 d
41 41 pick 39522b764e3d 7 does not commute with e
42 42 pick 7b4e2f4b7bcd 5 e
43 43 pick 500cac37a696 6 f
44 44
45 45 log before edit
46 46 $ hg log --graph
47 47 @ changeset: 7:39522b764e3d
48 48 | tag: tip
49 49 | user: test
50 50 | date: Thu Jan 01 00:00:00 1970 +0000
51 51 | summary: does not commute with e
52 52 |
53 53 o changeset: 6:500cac37a696
54 54 | user: test
55 55 | date: Thu Jan 01 00:00:00 1970 +0000
56 56 | summary: f
57 57 |
58 58 o changeset: 5:7b4e2f4b7bcd
59 59 | user: test
60 60 | date: Thu Jan 01 00:00:00 1970 +0000
61 61 | summary: e
62 62 |
63 63 o changeset: 4:00f1c5383965
64 64 | user: test
65 65 | date: Thu Jan 01 00:00:00 1970 +0000
66 66 | summary: d
67 67 |
68 68 o changeset: 3:65a9a84f33fd
69 69 | user: test
70 70 | date: Thu Jan 01 00:00:00 1970 +0000
71 71 | summary: c
72 72 |
73 73 o changeset: 2:da6535b52e45
74 74 | user: test
75 75 | date: Thu Jan 01 00:00:00 1970 +0000
76 76 | summary: b
77 77 |
78 78 o changeset: 1:c1f09da44841
79 79 | user: test
80 80 | date: Thu Jan 01 00:00:00 1970 +0000
81 81 | summary: a
82 82 |
83 83 o changeset: 0:1715188a53c7
84 84 user: test
85 85 date: Thu Jan 01 00:00:00 1970 +0000
86 86 summary: Initial commit
87 87
88 88
89 89 edit the history
90 90 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
91 91 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 92 merging e
93 93 warning: conflicts during merge.
94 94 merging e incomplete! (edit conflicts, then use 'hg resolve --mark')
95 95 Fix up the change and run hg histedit --continue
96 96
97 97 abort the edit
98 98 $ hg histedit --abort 2>&1 | fixbundle
99 99 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
100 100
101 101
102 102 second edit set
103 103
104 104 $ hg log --graph
105 105 @ changeset: 7:39522b764e3d
106 106 | tag: tip
107 107 | user: test
108 108 | date: Thu Jan 01 00:00:00 1970 +0000
109 109 | summary: does not commute with e
110 110 |
111 111 o changeset: 6:500cac37a696
112 112 | user: test
113 113 | date: Thu Jan 01 00:00:00 1970 +0000
114 114 | summary: f
115 115 |
116 116 o changeset: 5:7b4e2f4b7bcd
117 117 | user: test
118 118 | date: Thu Jan 01 00:00:00 1970 +0000
119 119 | summary: e
120 120 |
121 121 o changeset: 4:00f1c5383965
122 122 | user: test
123 123 | date: Thu Jan 01 00:00:00 1970 +0000
124 124 | summary: d
125 125 |
126 126 o changeset: 3:65a9a84f33fd
127 127 | user: test
128 128 | date: Thu Jan 01 00:00:00 1970 +0000
129 129 | summary: c
130 130 |
131 131 o changeset: 2:da6535b52e45
132 132 | user: test
133 133 | date: Thu Jan 01 00:00:00 1970 +0000
134 134 | summary: b
135 135 |
136 136 o changeset: 1:c1f09da44841
137 137 | user: test
138 138 | date: Thu Jan 01 00:00:00 1970 +0000
139 139 | summary: a
140 140 |
141 141 o changeset: 0:1715188a53c7
142 142 user: test
143 143 date: Thu Jan 01 00:00:00 1970 +0000
144 144 summary: Initial commit
145 145
146 146
147 147 edit the history
148 148 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
149 149 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
150 150 merging e
151 151 warning: conflicts during merge.
152 152 merging e incomplete! (edit conflicts, then use 'hg resolve --mark')
153 153 Fix up the change and run hg histedit --continue
154 154
155 155 fix up
156 156 $ echo 'I can haz no commute' > e
157 157 $ hg resolve --mark e
158 158 $ hg histedit --continue 2>&1 | fixbundle
159 159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 merging e
161 161 warning: conflicts during merge.
162 162 merging e incomplete! (edit conflicts, then use 'hg resolve --mark')
163 163 Fix up the change and run hg histedit --continue
164 164
165 165 This failure is caused by 7b4e2f4b7bcd "e" not rebasing the non commutative
166 166 former children.
167 167
168 168 just continue this time
169 169 $ hg revert -r 'p1()' e
170 170 $ hg resolve --mark e
171 171 $ hg histedit --continue 2>&1 | fixbundle
172 172 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
173 173 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
174 174
175 175 log after edit
176 176 $ hg log --graph
177 177 @ changeset: 6:7efe1373e4bc
178 178 | tag: tip
179 179 | user: test
180 180 | date: Thu Jan 01 00:00:00 1970 +0000
181 181 | summary: f
182 182 |
183 183 o changeset: 5:e334d87a1e55
184 184 | user: test
185 185 | date: Thu Jan 01 00:00:00 1970 +0000
186 186 | summary: does not commute with e
187 187 |
188 188 o changeset: 4:00f1c5383965
189 189 | user: test
190 190 | date: Thu Jan 01 00:00:00 1970 +0000
191 191 | summary: d
192 192 |
193 193 o changeset: 3:65a9a84f33fd
194 194 | user: test
195 195 | date: Thu Jan 01 00:00:00 1970 +0000
196 196 | summary: c
197 197 |
198 198 o changeset: 2:da6535b52e45
199 199 | user: test
200 200 | date: Thu Jan 01 00:00:00 1970 +0000
201 201 | summary: b
202 202 |
203 203 o changeset: 1:c1f09da44841
204 204 | user: test
205 205 | date: Thu Jan 01 00:00:00 1970 +0000
206 206 | summary: a
207 207 |
208 208 o changeset: 0:1715188a53c7
209 209 user: test
210 210 date: Thu Jan 01 00:00:00 1970 +0000
211 211 summary: Initial commit
212 212
213 213
214 214 start over
215 215
216 216 $ cd ..
217 217
218 218 $ initrepo r2
219 219 $ cd r2
220 220 $ rm $EDITED
221 221 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
222 222 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
223 223 $ hg log --template 'mess {node|short} {rev} {desc}\n' -r 7 >> $EDITED
224 224 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
225 225 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
226 226 $ cat $EDITED
227 227 pick 65a9a84f33fd 3 c
228 228 pick 00f1c5383965 4 d
229 229 mess 39522b764e3d 7 does not commute with e
230 230 pick 7b4e2f4b7bcd 5 e
231 231 pick 500cac37a696 6 f
232 232
233 233 edit the history, this time with a fold action
234 234 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
235 235 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
236 236 merging e
237 237 warning: conflicts during merge.
238 238 merging e incomplete! (edit conflicts, then use 'hg resolve --mark')
239 239 Fix up the change and run hg histedit --continue
240 240
241 241 $ echo 'I can haz no commute' > e
242 242 $ hg resolve --mark e
243 $ hg histedit --commands $EDITED --continue 2>&1 | fixbundle
243 $ hg histedit --continue 2>&1 | fixbundle
244 244 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 245 merging e
246 246 warning: conflicts during merge.
247 247 merging e incomplete! (edit conflicts, then use 'hg resolve --mark')
248 248 Fix up the change and run hg histedit --continue
249 249 second edit also fails, but just continue
250 250 $ hg revert -r 'p1()' e
251 251 $ hg resolve --mark e
252 252 $ hg histedit --continue 2>&1 | fixbundle
253 253 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
255 255
256 256 post message fix
257 257 $ hg log --graph
258 258 @ changeset: 6:7efe1373e4bc
259 259 | tag: tip
260 260 | user: test
261 261 | date: Thu Jan 01 00:00:00 1970 +0000
262 262 | summary: f
263 263 |
264 264 o changeset: 5:e334d87a1e55
265 265 | user: test
266 266 | date: Thu Jan 01 00:00:00 1970 +0000
267 267 | summary: does not commute with e
268 268 |
269 269 o changeset: 4:00f1c5383965
270 270 | user: test
271 271 | date: Thu Jan 01 00:00:00 1970 +0000
272 272 | summary: d
273 273 |
274 274 o changeset: 3:65a9a84f33fd
275 275 | user: test
276 276 | date: Thu Jan 01 00:00:00 1970 +0000
277 277 | summary: c
278 278 |
279 279 o changeset: 2:da6535b52e45
280 280 | user: test
281 281 | date: Thu Jan 01 00:00:00 1970 +0000
282 282 | summary: b
283 283 |
284 284 o changeset: 1:c1f09da44841
285 285 | user: test
286 286 | date: Thu Jan 01 00:00:00 1970 +0000
287 287 | summary: a
288 288 |
289 289 o changeset: 0:1715188a53c7
290 290 user: test
291 291 date: Thu Jan 01 00:00:00 1970 +0000
292 292 summary: Initial commit
293 293
294 294
295 295 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now