##// END OF EJS Templates
histedit: move outgoing processing to its own function...
Pierre-Yves David -
r19021:26b41a90 default
parent child Browse files
Show More
@@ -1,845 +1,853 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 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
408 """utility function to find the first outgoing changeset
409
410 Used by initialisation code"""
411 dest = ui.expandpath(remote or 'default-push', remote or 'default')
412 dest, revs = hg.parseurl(dest, None)[:2]
413 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
414
415 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
416 other = hg.peer(repo, opts, dest)
417
418 if revs:
419 revs = [repo.lookup(rev) for rev in revs]
420
421 # hexlify nodes from outgoing, because we're going to parse
422 # parent[0] using revsingle below, and if the binary hash
423 # contains special revset characters like ":" the revset
424 # parser can choke.
425 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 if not outgoing.missing:
427 raise util.Abort(_('no outgoing ancestors'))
428 return outgoing.missing[0]
429
407 430 actiontable = {'p': pick,
408 431 'pick': pick,
409 432 'e': edit,
410 433 'edit': edit,
411 434 'f': fold,
412 435 'fold': fold,
413 436 'd': drop,
414 437 'drop': drop,
415 438 'm': message,
416 439 'mess': message,
417 440 }
418 441
419 442 @command('histedit',
420 443 [('', 'commands', '',
421 444 _('Read history edits from the specified file.')),
422 445 ('c', 'continue', False, _('continue an edit already in progress')),
423 446 ('k', 'keep', False,
424 447 _("don't strip old nodes after edit is complete")),
425 448 ('', 'abort', False, _('abort an edit in progress')),
426 449 ('o', 'outgoing', False, _('changesets not found in destination')),
427 450 ('f', 'force', False,
428 451 _('force outgoing even for unrelated repositories')),
429 452 ('r', 'rev', [], _('first revision to be edited'))],
430 453 _("[PARENT]"))
431 454 def histedit(ui, repo, *freeargs, **opts):
432 455 """interactively edit changeset history
433 456 """
434 457 # TODO only abort if we try and histedit mq patches, not just
435 458 # blanket if mq patches are applied somewhere
436 459 mq = getattr(repo, 'mq', None)
437 460 if mq and mq.applied:
438 461 raise util.Abort(_('source has mq patches applied'))
439 462
440 463 # basic argument incompatibility processing
441 464 outg = opts.get('outgoing')
442 465 cont = opts.get('continue')
443 466 abort = opts.get('abort')
444 467 force = opts.get('force')
445 468 rules = opts.get('commands', '')
446 469 revs = opts.get('rev', [])
447 470 goal = 'new' # This invocation goal, in new, continue, abort
448 471 if force and not outg:
449 472 raise util.Abort(_('--force only allowed with --outgoing'))
450 473 if cont:
451 474 if util.any((outg, abort, revs, freeargs, rules)):
452 475 raise util.Abort(_('no arguments allowed with --continue'))
453 476 goal = 'continue'
454 477 elif abort:
455 478 if util.any((outg, revs, freeargs, rules)):
456 479 raise util.Abort(_('no arguments allowed with --abort'))
457 480 goal = 'abort'
458 481 else:
459 482 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
460 483 raise util.Abort(_('history edit already in progress, try '
461 484 '--continue or --abort'))
462 485 if outg:
463 486 if revs:
464 487 raise util.Abort(_('no revisions allowed with --outgoing'))
465 488 if len(freeargs) > 1:
466 489 raise util.Abort(
467 490 _('only one repo argument allowed with --outgoing'))
468 491 else:
469 parent = list(freeargs) + opts.get('rev', [])
470 if len(parent) != 1:
492 revs.extend(freeargs)
493 if len(revs) != 1:
471 494 raise util.Abort(
472 495 _('histedit requires exactly one parent revision'))
473 496
474 if opts.get('outgoing'):
475 if freeargs:
476 parent = freeargs[0]
477
478 dest = ui.expandpath(parent or 'default-push', parent or 'default')
479 dest, revs = hg.parseurl(dest, None)[:2]
480 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
481
482 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
483 other = hg.peer(repo, opts, dest)
484
485 if revs:
486 revs = [repo.lookup(rev) for rev in revs]
487
488 # hexlify nodes from outgoing, because we're going to parse
489 # parent[0] using revsingle below, and if the binary hash
490 # contains special revset characters like ":" the revset
491 # parser can choke.
492 parent = [node.hex(n) for n in discovery.findcommonoutgoing(
493 repo, other, revs, force=force).missing[0:1]]
494 if not parent:
495 raise util.Abort(_('no outgoing ancestors'))
496 497
497 498 if goal == 'continue':
498 499 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
499 500 currentparent, wantnull = repo.dirstate.parents()
500 501 parentctx = repo[parentctxnode]
501 502 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
502 503 replacements.extend(repl)
503 504 elif goal == 'abort':
504 505 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
505 506 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
506 507 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
507 508 hg.clean(repo, topmost)
508 509 cleanupnode(ui, repo, 'created', tmpnodes)
509 510 cleanupnode(ui, repo, 'temp', leafs)
510 511 os.unlink(os.path.join(repo.path, 'histedit-state'))
511 512 return
512 513 else:
513 514 cmdutil.bailifchanged(repo)
514 515
515 516 topmost, empty = repo.dirstate.parents()
516
517 parent = scmutil.revsingle(repo, parent[0]).node()
517 if outg:
518 if freeargs:
519 remote = freeargs[0]
520 else:
521 remote = None
522 root = findoutgoing(ui, repo, remote, force, opts)
523 else:
524 root = revs[0]
525 root = scmutil.revsingle(repo, root).node()
518 526
519 527 keep = opts.get('keep', False)
520 revs = between(repo, parent, topmost, keep)
528 revs = between(repo, root, topmost, keep)
521 529 if not revs:
522 530 raise util.Abort(_('%s is not an ancestor of working directory') %
523 node.short(parent))
531 node.short(root))
524 532
525 533 ctxs = [repo[r] for r in revs]
526 534 if not rules:
527 535 rules = '\n'.join([makedesc(c) for c in ctxs])
528 536 rules += '\n\n'
529 rules += editcomment % (node.short(parent), node.short(topmost))
537 rules += editcomment % (node.short(root), node.short(topmost))
530 538 rules = ui.edit(rules, ui.username())
531 539 # Save edit rules in .hg/histedit-last-edit.txt in case
532 540 # the user needs to ask for help after something
533 541 # surprising happens.
534 542 f = open(repo.join('histedit-last-edit.txt'), 'w')
535 543 f.write(rules)
536 544 f.close()
537 545 else:
538 546 if rules == '-':
539 547 f = sys.stdin
540 548 else:
541 549 f = open(rules)
542 550 rules = f.read()
543 551 f.close()
544 552 rules = [l for l in (r.strip() for r in rules.splitlines())
545 553 if l and not l[0] == '#']
546 554 rules = verifyrules(rules, repo, ctxs)
547 555
548 parentctx = repo[parent].parents()[0]
556 parentctx = repo[root].parents()[0]
549 557 keep = opts.get('keep', False)
550 558 replacements = []
551 559
552 560
553 561 while rules:
554 562 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
555 563 action, ha = rules.pop(0)
556 564 ui.debug('histedit: processing %s %s\n' % (action, ha))
557 565 actfunc = actiontable[action]
558 566 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
559 567 replacements.extend(replacement_)
560 568
561 569 hg.update(repo, parentctx.node())
562 570
563 571 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
564 572 if mapping:
565 573 for prec, succs in mapping.iteritems():
566 574 if not succs:
567 575 ui.debug('histedit: %s is dropped\n' % node.short(prec))
568 576 else:
569 577 ui.debug('histedit: %s is replaced by %s\n' % (
570 578 node.short(prec), node.short(succs[0])))
571 579 if len(succs) > 1:
572 580 m = 'histedit: %s'
573 581 for n in succs[1:]:
574 582 ui.debug(m % node.short(n))
575 583
576 584 if not keep:
577 585 if mapping:
578 586 movebookmarks(ui, repo, mapping, topmost, ntm)
579 587 # TODO update mq state
580 588 if obsolete._enabled:
581 589 markers = []
582 590 # sort by revision number because it sound "right"
583 591 for prec in sorted(mapping, key=repo.changelog.rev):
584 592 succs = mapping[prec]
585 593 markers.append((repo[prec],
586 594 tuple(repo[s] for s in succs)))
587 595 if markers:
588 596 obsolete.createmarkers(repo, markers)
589 597 else:
590 598 cleanupnode(ui, repo, 'replaced', mapping)
591 599
592 600 cleanupnode(ui, repo, 'temp', tmpnodes)
593 601 os.unlink(os.path.join(repo.path, 'histedit-state'))
594 602 if os.path.exists(repo.sjoin('undo')):
595 603 os.unlink(repo.sjoin('undo'))
596 604
597 605
598 606 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
599 607 action, currentnode = rules.pop(0)
600 608 ctx = repo[currentnode]
601 609 # is there any new commit between the expected parent and "."
602 610 #
603 611 # note: does not take non linear new change in account (but previous
604 612 # implementation didn't used them anyway (issue3655)
605 613 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
606 614 if parentctx.node() != node.nullid:
607 615 if not newchildren:
608 616 # `parentctxnode` should match but no result. This means that
609 617 # currentnode is not a descendant from parentctxnode.
610 618 msg = _('%s is not an ancestor of working directory')
611 619 hint = _('update to %s or descendant and run "hg histedit '
612 620 '--continue" again') % parentctx
613 621 raise util.Abort(msg % parentctx, hint=hint)
614 622 newchildren.pop(0) # remove parentctxnode
615 623 # Commit dirty working directory if necessary
616 624 new = None
617 625 m, a, r, d = repo.status()[:4]
618 626 if m or a or r or d:
619 627 # prepare the message for the commit to comes
620 628 if action in ('f', 'fold'):
621 629 message = 'fold-temp-revision %s' % currentnode
622 630 else:
623 631 message = ctx.description() + '\n'
624 632 if action in ('e', 'edit', 'm', 'mess'):
625 633 editor = cmdutil.commitforceeditor
626 634 else:
627 635 editor = False
628 636 commit = commitfuncfor(repo, ctx)
629 637 new = commit(text=message, user=ctx.user(),
630 638 date=ctx.date(), extra=ctx.extra(),
631 639 editor=editor)
632 640 if new is not None:
633 641 newchildren.append(new)
634 642
635 643 replacements = []
636 644 # track replacements
637 645 if ctx.node() not in newchildren:
638 646 # note: new children may be empty when the changeset is dropped.
639 647 # this happen e.g during conflicting pick where we revert content
640 648 # to parent.
641 649 replacements.append((ctx.node(), tuple(newchildren)))
642 650
643 651 if action in ('f', 'fold'):
644 652 if newchildren:
645 653 # finalize fold operation if applicable
646 654 if new is None:
647 655 new = newchildren[-1]
648 656 else:
649 657 newchildren.pop() # remove new from internal changes
650 658 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
651 659 newchildren)
652 660 replacements.extend(repl)
653 661 else:
654 662 # newchildren is empty if the fold did not result in any commit
655 663 # this happen when all folded change are discarded during the
656 664 # merge.
657 665 replacements.append((ctx.node(), (parentctx.node(),)))
658 666 elif newchildren:
659 667 # otherwise update "parentctx" before proceeding to further operation
660 668 parentctx = repo[newchildren[-1]]
661 669 return parentctx, replacements
662 670
663 671
664 672 def between(repo, old, new, keep):
665 673 """select and validate the set of revision to edit
666 674
667 675 When keep is false, the specified set can't have children."""
668 676 ctxs = list(repo.set('%n::%n', old, new))
669 677 if ctxs and not keep:
670 678 if (not obsolete._enabled and
671 679 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
672 680 raise util.Abort(_('cannot edit history that would orphan nodes'))
673 681 root = ctxs[0] # list is already sorted by repo.set
674 682 if not root.phase():
675 683 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
676 684 return [c.node() for c in ctxs]
677 685
678 686
679 687 def writestate(repo, parentnode, rules, keep, topmost, replacements):
680 688 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
681 689 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
682 690 fp.close()
683 691
684 692 def readstate(repo):
685 693 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
686 694 """
687 695 fp = open(os.path.join(repo.path, 'histedit-state'))
688 696 return pickle.load(fp)
689 697
690 698
691 699 def makedesc(c):
692 700 """build a initial action line for a ctx `c`
693 701
694 702 line are in the form:
695 703
696 704 pick <hash> <rev> <summary>
697 705 """
698 706 summary = ''
699 707 if c.description():
700 708 summary = c.description().splitlines()[0]
701 709 line = 'pick %s %d %s' % (c, c.rev(), summary)
702 710 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
703 711
704 712 def verifyrules(rules, repo, ctxs):
705 713 """Verify that there exists exactly one edit rule per given changeset.
706 714
707 715 Will abort if there are to many or too few rules, a malformed rule,
708 716 or a rule on a changeset outside of the user-given range.
709 717 """
710 718 parsed = []
711 719 if len(rules) != len(ctxs):
712 720 raise util.Abort(_('must specify a rule for each changeset once'))
713 721 for r in rules:
714 722 if ' ' not in r:
715 723 raise util.Abort(_('malformed line "%s"') % r)
716 724 action, rest = r.split(' ', 1)
717 725 if ' ' in rest.strip():
718 726 ha, rest = rest.split(' ', 1)
719 727 else:
720 728 ha = r.strip()
721 729 try:
722 730 if repo[ha] not in ctxs:
723 731 raise util.Abort(
724 732 _('may not use changesets other than the ones listed'))
725 733 except error.RepoError:
726 734 raise util.Abort(_('unknown changeset %s listed') % ha)
727 735 if action not in actiontable:
728 736 raise util.Abort(_('unknown action "%s"') % action)
729 737 parsed.append([action, ha])
730 738 return parsed
731 739
732 740 def processreplacement(repo, replacements):
733 741 """process the list of replacements to return
734 742
735 743 1) the final mapping between original and created nodes
736 744 2) the list of temporary node created by histedit
737 745 3) the list of new commit created by histedit"""
738 746 allsuccs = set()
739 747 replaced = set()
740 748 fullmapping = {}
741 749 # initialise basic set
742 750 # fullmapping record all operation recorded in replacement
743 751 for rep in replacements:
744 752 allsuccs.update(rep[1])
745 753 replaced.add(rep[0])
746 754 fullmapping.setdefault(rep[0], set()).update(rep[1])
747 755 new = allsuccs - replaced
748 756 tmpnodes = allsuccs & replaced
749 757 # Reduce content fullmapping into direct relation between original nodes
750 758 # and final node created during history edition
751 759 # Dropped changeset are replaced by an empty list
752 760 toproceed = set(fullmapping)
753 761 final = {}
754 762 while toproceed:
755 763 for x in list(toproceed):
756 764 succs = fullmapping[x]
757 765 for s in list(succs):
758 766 if s in toproceed:
759 767 # non final node with unknown closure
760 768 # We can't process this now
761 769 break
762 770 elif s in final:
763 771 # non final node, replace with closure
764 772 succs.remove(s)
765 773 succs.update(final[s])
766 774 else:
767 775 final[x] = succs
768 776 toproceed.remove(x)
769 777 # remove tmpnodes from final mapping
770 778 for n in tmpnodes:
771 779 del final[n]
772 780 # we expect all changes involved in final to exist in the repo
773 781 # turn `final` into list (topologically sorted)
774 782 nm = repo.changelog.nodemap
775 783 for prec, succs in final.items():
776 784 final[prec] = sorted(succs, key=nm.get)
777 785
778 786 # computed topmost element (necessary for bookmark)
779 787 if new:
780 788 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
781 789 elif not final:
782 790 # Nothing rewritten at all. we won't need `newtopmost`
783 791 # It is the same as `oldtopmost` and `processreplacement` know it
784 792 newtopmost = None
785 793 else:
786 794 # every body died. The newtopmost is the parent of the root.
787 795 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
788 796
789 797 return final, tmpnodes, new, newtopmost
790 798
791 799 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
792 800 """Move bookmark from old to newly created node"""
793 801 if not mapping:
794 802 # if nothing got rewritten there is not purpose for this function
795 803 return
796 804 moves = []
797 805 for bk, old in sorted(repo._bookmarks.iteritems()):
798 806 if old == oldtopmost:
799 807 # special case ensure bookmark stay on tip.
800 808 #
801 809 # This is arguably a feature and we may only want that for the
802 810 # active bookmark. But the behavior is kept compatible with the old
803 811 # version for now.
804 812 moves.append((bk, newtopmost))
805 813 continue
806 814 base = old
807 815 new = mapping.get(base, None)
808 816 if new is None:
809 817 continue
810 818 while not new:
811 819 # base is killed, trying with parent
812 820 base = repo[base].p1().node()
813 821 new = mapping.get(base, (base,))
814 822 # nothing to move
815 823 moves.append((bk, new[-1]))
816 824 if moves:
817 825 marks = repo._bookmarks
818 826 for mark, new in moves:
819 827 old = marks[mark]
820 828 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
821 829 % (mark, node.short(old), node.short(new)))
822 830 marks[mark] = new
823 831 marks.write()
824 832
825 833 def cleanupnode(ui, repo, name, nodes):
826 834 """strip a group of nodes from the repository
827 835
828 836 The set of node to strip may contains unknown nodes."""
829 837 ui.debug('should strip %s nodes %s\n' %
830 838 (name, ', '.join([node.short(n) for n in nodes])))
831 839 lock = None
832 840 try:
833 841 lock = repo.lock()
834 842 # Find all node that need to be stripped
835 843 # (we hg %lr instead of %ln to silently ignore unknown item
836 844 nm = repo.changelog.nodemap
837 845 nodes = [n for n in nodes if n in nm]
838 846 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
839 847 for c in roots:
840 848 # We should process node in reverse order to strip tip most first.
841 849 # but this trigger a bug in changegroup hook.
842 850 # This would reduce bundle overhead
843 851 repair.strip(ui, repo, c)
844 852 finally:
845 853 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now