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