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