##// END OF EJS Templates
histedit: fix --continue and --abort when curses is enabled...
Augie Fackler -
r41211:9bc7ec92 default
parent child Browse files
Show More
@@ -1,2238 +1,2241 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 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 186 # chistedit dependencies that are not available everywhere
187 187 try:
188 188 import fcntl
189 189 import termios
190 190 except ImportError:
191 191 fcntl = None
192 192 termios = None
193 193
194 194 import functools
195 195 import os
196 196 import struct
197 197
198 198 from mercurial.i18n import _
199 199 from mercurial import (
200 200 bundle2,
201 201 cmdutil,
202 202 context,
203 203 copies,
204 204 destutil,
205 205 discovery,
206 206 error,
207 207 exchange,
208 208 extensions,
209 209 hg,
210 210 logcmdutil,
211 211 merge as mergemod,
212 212 mergeutil,
213 213 node,
214 214 obsolete,
215 215 pycompat,
216 216 registrar,
217 217 repair,
218 218 scmutil,
219 219 state as statemod,
220 220 util,
221 221 )
222 222 from mercurial.utils import (
223 223 stringutil,
224 224 )
225 225
226 226 pickle = util.pickle
227 227 cmdtable = {}
228 228 command = registrar.command(cmdtable)
229 229
230 230 configtable = {}
231 231 configitem = registrar.configitem(configtable)
232 232 configitem('experimental', 'histedit.autoverb',
233 233 default=False,
234 234 )
235 235 configitem('histedit', 'defaultrev',
236 236 default=None,
237 237 )
238 238 configitem('histedit', 'dropmissing',
239 239 default=False,
240 240 )
241 241 configitem('histedit', 'linelen',
242 242 default=80,
243 243 )
244 244 configitem('histedit', 'singletransaction',
245 245 default=False,
246 246 )
247 247 configitem('ui', 'interface.histedit',
248 248 default=None,
249 249 )
250 250
251 251 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
252 252 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
253 253 # be specifying the version(s) of Mercurial they are tested with, or
254 254 # leave the attribute unspecified.
255 255 testedwith = 'ships-with-hg-core'
256 256
257 257 actiontable = {}
258 258 primaryactions = set()
259 259 secondaryactions = set()
260 260 tertiaryactions = set()
261 261 internalactions = set()
262 262
263 263 def geteditcomment(ui, first, last):
264 264 """ construct the editor comment
265 265 The comment includes::
266 266 - an intro
267 267 - sorted primary commands
268 268 - sorted short commands
269 269 - sorted long commands
270 270 - additional hints
271 271
272 272 Commands are only included once.
273 273 """
274 274 intro = _("""Edit history between %s and %s
275 275
276 276 Commits are listed from least to most recent
277 277
278 278 You can reorder changesets by reordering the lines
279 279
280 280 Commands:
281 281 """)
282 282 actions = []
283 283 def addverb(v):
284 284 a = actiontable[v]
285 285 lines = a.message.split("\n")
286 286 if len(a.verbs):
287 287 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
288 288 actions.append(" %s = %s" % (v, lines[0]))
289 289 actions.extend([' %s' for l in lines[1:]])
290 290
291 291 for v in (
292 292 sorted(primaryactions) +
293 293 sorted(secondaryactions) +
294 294 sorted(tertiaryactions)
295 295 ):
296 296 addverb(v)
297 297 actions.append('')
298 298
299 299 hints = []
300 300 if ui.configbool('histedit', 'dropmissing'):
301 301 hints.append("Deleting a changeset from the list "
302 302 "will DISCARD it from the edited history!")
303 303
304 304 lines = (intro % (first, last)).split('\n') + actions + hints
305 305
306 306 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
307 307
308 308 class histeditstate(object):
309 309 def __init__(self, repo):
310 310 self.repo = repo
311 311 self.actions = None
312 312 self.keep = None
313 313 self.topmost = None
314 314 self.parentctxnode = None
315 315 self.lock = None
316 316 self.wlock = None
317 317 self.backupfile = None
318 318 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
319 319 self.replacements = []
320 320
321 321 def read(self):
322 322 """Load histedit state from disk and set fields appropriately."""
323 323 if not self.stateobj.exists():
324 324 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
325 325
326 326 data = self._read()
327 327
328 328 self.parentctxnode = data['parentctxnode']
329 329 actions = parserules(data['rules'], self)
330 330 self.actions = actions
331 331 self.keep = data['keep']
332 332 self.topmost = data['topmost']
333 333 self.replacements = data['replacements']
334 334 self.backupfile = data['backupfile']
335 335
336 336 def _read(self):
337 337 fp = self.repo.vfs.read('histedit-state')
338 338 if fp.startswith('v1\n'):
339 339 data = self._load()
340 340 parentctxnode, rules, keep, topmost, replacements, backupfile = data
341 341 else:
342 342 data = pickle.loads(fp)
343 343 parentctxnode, rules, keep, topmost, replacements = data
344 344 backupfile = None
345 345 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
346 346
347 347 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
348 348 "topmost": topmost, "replacements": replacements,
349 349 "backupfile": backupfile}
350 350
351 351 def write(self, tr=None):
352 352 if tr:
353 353 tr.addfilegenerator('histedit-state', ('histedit-state',),
354 354 self._write, location='plain')
355 355 else:
356 356 with self.repo.vfs("histedit-state", "w") as f:
357 357 self._write(f)
358 358
359 359 def _write(self, fp):
360 360 fp.write('v1\n')
361 361 fp.write('%s\n' % node.hex(self.parentctxnode))
362 362 fp.write('%s\n' % node.hex(self.topmost))
363 363 fp.write('%s\n' % ('True' if self.keep else 'False'))
364 364 fp.write('%d\n' % len(self.actions))
365 365 for action in self.actions:
366 366 fp.write('%s\n' % action.tostate())
367 367 fp.write('%d\n' % len(self.replacements))
368 368 for replacement in self.replacements:
369 369 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
370 370 for r in replacement[1])))
371 371 backupfile = self.backupfile
372 372 if not backupfile:
373 373 backupfile = ''
374 374 fp.write('%s\n' % backupfile)
375 375
376 376 def _load(self):
377 377 fp = self.repo.vfs('histedit-state', 'r')
378 378 lines = [l[:-1] for l in fp.readlines()]
379 379
380 380 index = 0
381 381 lines[index] # version number
382 382 index += 1
383 383
384 384 parentctxnode = node.bin(lines[index])
385 385 index += 1
386 386
387 387 topmost = node.bin(lines[index])
388 388 index += 1
389 389
390 390 keep = lines[index] == 'True'
391 391 index += 1
392 392
393 393 # Rules
394 394 rules = []
395 395 rulelen = int(lines[index])
396 396 index += 1
397 397 for i in pycompat.xrange(rulelen):
398 398 ruleaction = lines[index]
399 399 index += 1
400 400 rule = lines[index]
401 401 index += 1
402 402 rules.append((ruleaction, rule))
403 403
404 404 # Replacements
405 405 replacements = []
406 406 replacementlen = int(lines[index])
407 407 index += 1
408 408 for i in pycompat.xrange(replacementlen):
409 409 replacement = lines[index]
410 410 original = node.bin(replacement[:40])
411 411 succ = [node.bin(replacement[i:i + 40]) for i in
412 412 range(40, len(replacement), 40)]
413 413 replacements.append((original, succ))
414 414 index += 1
415 415
416 416 backupfile = lines[index]
417 417 index += 1
418 418
419 419 fp.close()
420 420
421 421 return parentctxnode, rules, keep, topmost, replacements, backupfile
422 422
423 423 def clear(self):
424 424 if self.inprogress():
425 425 self.repo.vfs.unlink('histedit-state')
426 426
427 427 def inprogress(self):
428 428 return self.repo.vfs.exists('histedit-state')
429 429
430 430
431 431 class histeditaction(object):
432 432 def __init__(self, state, node):
433 433 self.state = state
434 434 self.repo = state.repo
435 435 self.node = node
436 436
437 437 @classmethod
438 438 def fromrule(cls, state, rule):
439 439 """Parses the given rule, returning an instance of the histeditaction.
440 440 """
441 441 ruleid = rule.strip().split(' ', 1)[0]
442 442 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
443 443 # Check for validation of rule ids and get the rulehash
444 444 try:
445 445 rev = node.bin(ruleid)
446 446 except TypeError:
447 447 try:
448 448 _ctx = scmutil.revsingle(state.repo, ruleid)
449 449 rulehash = _ctx.hex()
450 450 rev = node.bin(rulehash)
451 451 except error.RepoLookupError:
452 452 raise error.ParseError(_("invalid changeset %s") % ruleid)
453 453 return cls(state, rev)
454 454
455 455 def verify(self, prev, expected, seen):
456 456 """ Verifies semantic correctness of the rule"""
457 457 repo = self.repo
458 458 ha = node.hex(self.node)
459 459 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
460 460 if self.node is None:
461 461 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
462 462 self._verifynodeconstraints(prev, expected, seen)
463 463
464 464 def _verifynodeconstraints(self, prev, expected, seen):
465 465 # by default command need a node in the edited list
466 466 if self.node not in expected:
467 467 raise error.ParseError(_('%s "%s" changeset was not a candidate')
468 468 % (self.verb, node.short(self.node)),
469 469 hint=_('only use listed changesets'))
470 470 # and only one command per node
471 471 if self.node in seen:
472 472 raise error.ParseError(_('duplicated command for changeset %s') %
473 473 node.short(self.node))
474 474
475 475 def torule(self):
476 476 """build a histedit rule line for an action
477 477
478 478 by default lines are in the form:
479 479 <hash> <rev> <summary>
480 480 """
481 481 ctx = self.repo[self.node]
482 482 summary = _getsummary(ctx)
483 483 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
484 484 # trim to 75 columns by default so it's not stupidly wide in my editor
485 485 # (the 5 more are left for verb)
486 486 maxlen = self.repo.ui.configint('histedit', 'linelen')
487 487 maxlen = max(maxlen, 22) # avoid truncating hash
488 488 return stringutil.ellipsis(line, maxlen)
489 489
490 490 def tostate(self):
491 491 """Print an action in format used by histedit state files
492 492 (the first line is a verb, the remainder is the second)
493 493 """
494 494 return "%s\n%s" % (self.verb, node.hex(self.node))
495 495
496 496 def run(self):
497 497 """Runs the action. The default behavior is simply apply the action's
498 498 rulectx onto the current parentctx."""
499 499 self.applychange()
500 500 self.continuedirty()
501 501 return self.continueclean()
502 502
503 503 def applychange(self):
504 504 """Applies the changes from this action's rulectx onto the current
505 505 parentctx, but does not commit them."""
506 506 repo = self.repo
507 507 rulectx = repo[self.node]
508 508 repo.ui.pushbuffer(error=True, labeled=True)
509 509 hg.update(repo, self.state.parentctxnode, quietempty=True)
510 510 stats = applychanges(repo.ui, repo, rulectx, {})
511 511 repo.dirstate.setbranch(rulectx.branch())
512 512 if stats.unresolvedcount:
513 513 buf = repo.ui.popbuffer()
514 514 repo.ui.write(buf)
515 515 raise error.InterventionRequired(
516 516 _('Fix up the change (%s %s)') %
517 517 (self.verb, node.short(self.node)),
518 518 hint=_('hg histedit --continue to resume'))
519 519 else:
520 520 repo.ui.popbuffer()
521 521
522 522 def continuedirty(self):
523 523 """Continues the action when changes have been applied to the working
524 524 copy. The default behavior is to commit the dirty changes."""
525 525 repo = self.repo
526 526 rulectx = repo[self.node]
527 527
528 528 editor = self.commiteditor()
529 529 commit = commitfuncfor(repo, rulectx)
530 530
531 531 commit(text=rulectx.description(), user=rulectx.user(),
532 532 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
533 533
534 534 def commiteditor(self):
535 535 """The editor to be used to edit the commit message."""
536 536 return False
537 537
538 538 def continueclean(self):
539 539 """Continues the action when the working copy is clean. The default
540 540 behavior is to accept the current commit as the new version of the
541 541 rulectx."""
542 542 ctx = self.repo['.']
543 543 if ctx.node() == self.state.parentctxnode:
544 544 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
545 545 node.short(self.node))
546 546 return ctx, [(self.node, tuple())]
547 547 if ctx.node() == self.node:
548 548 # Nothing changed
549 549 return ctx, []
550 550 return ctx, [(self.node, (ctx.node(),))]
551 551
552 552 def commitfuncfor(repo, src):
553 553 """Build a commit function for the replacement of <src>
554 554
555 555 This function ensure we apply the same treatment to all changesets.
556 556
557 557 - Add a 'histedit_source' entry in extra.
558 558
559 559 Note that fold has its own separated logic because its handling is a bit
560 560 different and not easily factored out of the fold method.
561 561 """
562 562 phasemin = src.phase()
563 563 def commitfunc(**kwargs):
564 564 overrides = {('phases', 'new-commit'): phasemin}
565 565 with repo.ui.configoverride(overrides, 'histedit'):
566 566 extra = kwargs.get(r'extra', {}).copy()
567 567 extra['histedit_source'] = src.hex()
568 568 kwargs[r'extra'] = extra
569 569 return repo.commit(**kwargs)
570 570 return commitfunc
571 571
572 572 def applychanges(ui, repo, ctx, opts):
573 573 """Merge changeset from ctx (only) in the current working directory"""
574 574 wcpar = repo.dirstate.parents()[0]
575 575 if ctx.p1().node() == wcpar:
576 576 # edits are "in place" we do not need to make any merge,
577 577 # just applies changes on parent for editing
578 578 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
579 579 stats = mergemod.updateresult(0, 0, 0, 0)
580 580 else:
581 581 try:
582 582 # ui.forcemerge is an internal variable, do not document
583 583 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
584 584 'histedit')
585 585 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
586 586 finally:
587 587 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
588 588 return stats
589 589
590 590 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
591 591 """collapse the set of revisions from first to last as new one.
592 592
593 593 Expected commit options are:
594 594 - message
595 595 - date
596 596 - username
597 597 Commit message is edited in all cases.
598 598
599 599 This function works in memory."""
600 600 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
601 601 if not ctxs:
602 602 return None
603 603 for c in ctxs:
604 604 if not c.mutable():
605 605 raise error.ParseError(
606 606 _("cannot fold into public change %s") % node.short(c.node()))
607 607 base = firstctx.parents()[0]
608 608
609 609 # commit a new version of the old changeset, including the update
610 610 # collect all files which might be affected
611 611 files = set()
612 612 for ctx in ctxs:
613 613 files.update(ctx.files())
614 614
615 615 # Recompute copies (avoid recording a -> b -> a)
616 616 copied = copies.pathcopies(base, lastctx)
617 617
618 618 # prune files which were reverted by the updates
619 619 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
620 620 # commit version of these files as defined by head
621 621 headmf = lastctx.manifest()
622 622 def filectxfn(repo, ctx, path):
623 623 if path in headmf:
624 624 fctx = lastctx[path]
625 625 flags = fctx.flags()
626 626 mctx = context.memfilectx(repo, ctx,
627 627 fctx.path(), fctx.data(),
628 628 islink='l' in flags,
629 629 isexec='x' in flags,
630 630 copied=copied.get(path))
631 631 return mctx
632 632 return None
633 633
634 634 if commitopts.get('message'):
635 635 message = commitopts['message']
636 636 else:
637 637 message = firstctx.description()
638 638 user = commitopts.get('user')
639 639 date = commitopts.get('date')
640 640 extra = commitopts.get('extra')
641 641
642 642 parents = (firstctx.p1().node(), firstctx.p2().node())
643 643 editor = None
644 644 if not skipprompt:
645 645 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
646 646 new = context.memctx(repo,
647 647 parents=parents,
648 648 text=message,
649 649 files=files,
650 650 filectxfn=filectxfn,
651 651 user=user,
652 652 date=date,
653 653 extra=extra,
654 654 editor=editor)
655 655 return repo.commitctx(new)
656 656
657 657 def _isdirtywc(repo):
658 658 return repo[None].dirty(missing=True)
659 659
660 660 def abortdirty():
661 661 raise error.Abort(_('working copy has pending changes'),
662 662 hint=_('amend, commit, or revert them and run histedit '
663 663 '--continue, or abort with histedit --abort'))
664 664
665 665 def action(verbs, message, priority=False, internal=False):
666 666 def wrap(cls):
667 667 assert not priority or not internal
668 668 verb = verbs[0]
669 669 if priority:
670 670 primaryactions.add(verb)
671 671 elif internal:
672 672 internalactions.add(verb)
673 673 elif len(verbs) > 1:
674 674 secondaryactions.add(verb)
675 675 else:
676 676 tertiaryactions.add(verb)
677 677
678 678 cls.verb = verb
679 679 cls.verbs = verbs
680 680 cls.message = message
681 681 for verb in verbs:
682 682 actiontable[verb] = cls
683 683 return cls
684 684 return wrap
685 685
686 686 @action(['pick', 'p'],
687 687 _('use commit'),
688 688 priority=True)
689 689 class pick(histeditaction):
690 690 def run(self):
691 691 rulectx = self.repo[self.node]
692 692 if rulectx.parents()[0].node() == self.state.parentctxnode:
693 693 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
694 694 return rulectx, []
695 695
696 696 return super(pick, self).run()
697 697
698 698 @action(['edit', 'e'],
699 699 _('use commit, but stop for amending'),
700 700 priority=True)
701 701 class edit(histeditaction):
702 702 def run(self):
703 703 repo = self.repo
704 704 rulectx = repo[self.node]
705 705 hg.update(repo, self.state.parentctxnode, quietempty=True)
706 706 applychanges(repo.ui, repo, rulectx, {})
707 707 raise error.InterventionRequired(
708 708 _('Editing (%s), you may commit or record as needed now.')
709 709 % node.short(self.node),
710 710 hint=_('hg histedit --continue to resume'))
711 711
712 712 def commiteditor(self):
713 713 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
714 714
715 715 @action(['fold', 'f'],
716 716 _('use commit, but combine it with the one above'))
717 717 class fold(histeditaction):
718 718 def verify(self, prev, expected, seen):
719 719 """ Verifies semantic correctness of the fold rule"""
720 720 super(fold, self).verify(prev, expected, seen)
721 721 repo = self.repo
722 722 if not prev:
723 723 c = repo[self.node].parents()[0]
724 724 elif not prev.verb in ('pick', 'base'):
725 725 return
726 726 else:
727 727 c = repo[prev.node]
728 728 if not c.mutable():
729 729 raise error.ParseError(
730 730 _("cannot fold into public change %s") % node.short(c.node()))
731 731
732 732
733 733 def continuedirty(self):
734 734 repo = self.repo
735 735 rulectx = repo[self.node]
736 736
737 737 commit = commitfuncfor(repo, rulectx)
738 738 commit(text='fold-temp-revision %s' % node.short(self.node),
739 739 user=rulectx.user(), date=rulectx.date(),
740 740 extra=rulectx.extra())
741 741
742 742 def continueclean(self):
743 743 repo = self.repo
744 744 ctx = repo['.']
745 745 rulectx = repo[self.node]
746 746 parentctxnode = self.state.parentctxnode
747 747 if ctx.node() == parentctxnode:
748 748 repo.ui.warn(_('%s: empty changeset\n') %
749 749 node.short(self.node))
750 750 return ctx, [(self.node, (parentctxnode,))]
751 751
752 752 parentctx = repo[parentctxnode]
753 753 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
754 754 parentctx.rev(),
755 755 parentctx.rev()))
756 756 if not newcommits:
757 757 repo.ui.warn(_('%s: cannot fold - working copy is not a '
758 758 'descendant of previous commit %s\n') %
759 759 (node.short(self.node), node.short(parentctxnode)))
760 760 return ctx, [(self.node, (ctx.node(),))]
761 761
762 762 middlecommits = newcommits.copy()
763 763 middlecommits.discard(ctx.node())
764 764
765 765 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
766 766 middlecommits)
767 767
768 768 def skipprompt(self):
769 769 """Returns true if the rule should skip the message editor.
770 770
771 771 For example, 'fold' wants to show an editor, but 'rollup'
772 772 doesn't want to.
773 773 """
774 774 return False
775 775
776 776 def mergedescs(self):
777 777 """Returns true if the rule should merge messages of multiple changes.
778 778
779 779 This exists mainly so that 'rollup' rules can be a subclass of
780 780 'fold'.
781 781 """
782 782 return True
783 783
784 784 def firstdate(self):
785 785 """Returns true if the rule should preserve the date of the first
786 786 change.
787 787
788 788 This exists mainly so that 'rollup' rules can be a subclass of
789 789 'fold'.
790 790 """
791 791 return False
792 792
793 793 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
794 794 parent = ctx.parents()[0].node()
795 795 hg.updaterepo(repo, parent, overwrite=False)
796 796 ### prepare new commit data
797 797 commitopts = {}
798 798 commitopts['user'] = ctx.user()
799 799 # commit message
800 800 if not self.mergedescs():
801 801 newmessage = ctx.description()
802 802 else:
803 803 newmessage = '\n***\n'.join(
804 804 [ctx.description()] +
805 805 [repo[r].description() for r in internalchanges] +
806 806 [oldctx.description()]) + '\n'
807 807 commitopts['message'] = newmessage
808 808 # date
809 809 if self.firstdate():
810 810 commitopts['date'] = ctx.date()
811 811 else:
812 812 commitopts['date'] = max(ctx.date(), oldctx.date())
813 813 extra = ctx.extra().copy()
814 814 # histedit_source
815 815 # note: ctx is likely a temporary commit but that the best we can do
816 816 # here. This is sufficient to solve issue3681 anyway.
817 817 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
818 818 commitopts['extra'] = extra
819 819 phasemin = max(ctx.phase(), oldctx.phase())
820 820 overrides = {('phases', 'new-commit'): phasemin}
821 821 with repo.ui.configoverride(overrides, 'histedit'):
822 822 n = collapse(repo, ctx, repo[newnode], commitopts,
823 823 skipprompt=self.skipprompt())
824 824 if n is None:
825 825 return ctx, []
826 826 hg.updaterepo(repo, n, overwrite=False)
827 827 replacements = [(oldctx.node(), (newnode,)),
828 828 (ctx.node(), (n,)),
829 829 (newnode, (n,)),
830 830 ]
831 831 for ich in internalchanges:
832 832 replacements.append((ich, (n,)))
833 833 return repo[n], replacements
834 834
835 835 @action(['base', 'b'],
836 836 _('checkout changeset and apply further changesets from there'))
837 837 class base(histeditaction):
838 838
839 839 def run(self):
840 840 if self.repo['.'].node() != self.node:
841 841 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
842 842 return self.continueclean()
843 843
844 844 def continuedirty(self):
845 845 abortdirty()
846 846
847 847 def continueclean(self):
848 848 basectx = self.repo['.']
849 849 return basectx, []
850 850
851 851 def _verifynodeconstraints(self, prev, expected, seen):
852 852 # base can only be use with a node not in the edited set
853 853 if self.node in expected:
854 854 msg = _('%s "%s" changeset was an edited list candidate')
855 855 raise error.ParseError(
856 856 msg % (self.verb, node.short(self.node)),
857 857 hint=_('base must only use unlisted changesets'))
858 858
859 859 @action(['_multifold'],
860 860 _(
861 861 """fold subclass used for when multiple folds happen in a row
862 862
863 863 We only want to fire the editor for the folded message once when
864 864 (say) four changes are folded down into a single change. This is
865 865 similar to rollup, but we should preserve both messages so that
866 866 when the last fold operation runs we can show the user all the
867 867 commit messages in their editor.
868 868 """),
869 869 internal=True)
870 870 class _multifold(fold):
871 871 def skipprompt(self):
872 872 return True
873 873
874 874 @action(["roll", "r"],
875 875 _("like fold, but discard this commit's description and date"))
876 876 class rollup(fold):
877 877 def mergedescs(self):
878 878 return False
879 879
880 880 def skipprompt(self):
881 881 return True
882 882
883 883 def firstdate(self):
884 884 return True
885 885
886 886 @action(["drop", "d"],
887 887 _('remove commit from history'))
888 888 class drop(histeditaction):
889 889 def run(self):
890 890 parentctx = self.repo[self.state.parentctxnode]
891 891 return parentctx, [(self.node, tuple())]
892 892
893 893 @action(["mess", "m"],
894 894 _('edit commit message without changing commit content'),
895 895 priority=True)
896 896 class message(histeditaction):
897 897 def commiteditor(self):
898 898 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
899 899
900 900 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
901 901 """utility function to find the first outgoing changeset
902 902
903 903 Used by initialization code"""
904 904 if opts is None:
905 905 opts = {}
906 906 dest = ui.expandpath(remote or 'default-push', remote or 'default')
907 907 dest, branches = hg.parseurl(dest, None)[:2]
908 908 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
909 909
910 910 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
911 911 other = hg.peer(repo, opts, dest)
912 912
913 913 if revs:
914 914 revs = [repo.lookup(rev) for rev in revs]
915 915
916 916 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
917 917 if not outgoing.missing:
918 918 raise error.Abort(_('no outgoing ancestors'))
919 919 roots = list(repo.revs("roots(%ln)", outgoing.missing))
920 920 if len(roots) > 1:
921 921 msg = _('there are ambiguous outgoing revisions')
922 922 hint = _("see 'hg help histedit' for more detail")
923 923 raise error.Abort(msg, hint=hint)
924 924 return repo[roots[0]].node()
925 925
926 926 # Curses Support
927 927 try:
928 928 import curses
929 929 except ImportError:
930 930 curses = None
931 931
932 932 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
933 933 ACTION_LABELS = {
934 934 'fold': '^fold',
935 935 'roll': '^roll',
936 936 }
937 937
938 938 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4
939 939
940 940 E_QUIT, E_HISTEDIT = 1, 2
941 941 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
942 942 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
943 943
944 944 KEYTABLE = {
945 945 'global': {
946 946 'h': 'next-action',
947 947 'KEY_RIGHT': 'next-action',
948 948 'l': 'prev-action',
949 949 'KEY_LEFT': 'prev-action',
950 950 'q': 'quit',
951 951 'c': 'histedit',
952 952 'C': 'histedit',
953 953 'v': 'showpatch',
954 954 '?': 'help',
955 955 },
956 956 MODE_RULES: {
957 957 'd': 'action-drop',
958 958 'e': 'action-edit',
959 959 'f': 'action-fold',
960 960 'm': 'action-mess',
961 961 'p': 'action-pick',
962 962 'r': 'action-roll',
963 963 ' ': 'select',
964 964 'j': 'down',
965 965 'k': 'up',
966 966 'KEY_DOWN': 'down',
967 967 'KEY_UP': 'up',
968 968 'J': 'move-down',
969 969 'K': 'move-up',
970 970 'KEY_NPAGE': 'move-down',
971 971 'KEY_PPAGE': 'move-up',
972 972 '0': 'goto', # Used for 0..9
973 973 },
974 974 MODE_PATCH: {
975 975 ' ': 'page-down',
976 976 'KEY_NPAGE': 'page-down',
977 977 'KEY_PPAGE': 'page-up',
978 978 'j': 'line-down',
979 979 'k': 'line-up',
980 980 'KEY_DOWN': 'line-down',
981 981 'KEY_UP': 'line-up',
982 982 'J': 'down',
983 983 'K': 'up',
984 984 },
985 985 MODE_HELP: {
986 986 },
987 987 }
988 988
989 989 def screen_size():
990 990 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
991 991
992 992 class histeditrule(object):
993 993 def __init__(self, ctx, pos, action='pick'):
994 994 self.ctx = ctx
995 995 self.action = action
996 996 self.origpos = pos
997 997 self.pos = pos
998 998 self.conflicts = []
999 999
1000 1000 def __str__(self):
1001 1001 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1002 1002 # Add a marker showing which patch they apply to, and also omit the
1003 1003 # description for 'roll' (since it will get discarded). Example display:
1004 1004 #
1005 1005 # #10 pick 316392:06a16c25c053 add option to skip tests
1006 1006 # #11 ^roll 316393:71313c964cc5
1007 1007 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1008 1008 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1009 1009 #
1010 1010 # The carets point to the changeset being folded into ("roll this
1011 1011 # changeset into the changeset above").
1012 1012 action = ACTION_LABELS.get(self.action, self.action)
1013 1013 h = self.ctx.hex()[0:12]
1014 1014 r = self.ctx.rev()
1015 1015 desc = self.ctx.description().splitlines()[0].strip()
1016 1016 if self.action == 'roll':
1017 1017 desc = ''
1018 1018 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1019 1019 self.origpos, action, r, h, desc)
1020 1020
1021 1021 def checkconflicts(self, other):
1022 1022 if other.pos > self.pos and other.origpos <= self.origpos:
1023 1023 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1024 1024 self.conflicts.append(other)
1025 1025 return self.conflicts
1026 1026
1027 1027 if other in self.conflicts:
1028 1028 self.conflicts.remove(other)
1029 1029 return self.conflicts
1030 1030
1031 1031 # ============ EVENTS ===============
1032 1032 def movecursor(state, oldpos, newpos):
1033 1033 '''Change the rule/changeset that the cursor is pointing to, regardless of
1034 1034 current mode (you can switch between patches from the view patch window).'''
1035 1035 state['pos'] = newpos
1036 1036
1037 1037 mode, _ = state['mode']
1038 1038 if mode == MODE_RULES:
1039 1039 # Scroll through the list by updating the view for MODE_RULES, so that
1040 1040 # even if we are not currently viewing the rules, switching back will
1041 1041 # result in the cursor's rule being visible.
1042 1042 modestate = state['modes'][MODE_RULES]
1043 1043 if newpos < modestate['line_offset']:
1044 1044 modestate['line_offset'] = newpos
1045 1045 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1046 1046 modestate['line_offset'] = newpos - state['page_height'] + 1
1047 1047
1048 1048 # Reset the patch view region to the top of the new patch.
1049 1049 state['modes'][MODE_PATCH]['line_offset'] = 0
1050 1050
1051 1051 def changemode(state, mode):
1052 1052 curmode, _ = state['mode']
1053 1053 state['mode'] = (mode, curmode)
1054 1054
1055 1055 def makeselection(state, pos):
1056 1056 state['selected'] = pos
1057 1057
1058 1058 def swap(state, oldpos, newpos):
1059 1059 """Swap two positions and calculate necessary conflicts in
1060 1060 O(|newpos-oldpos|) time"""
1061 1061
1062 1062 rules = state['rules']
1063 1063 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1064 1064
1065 1065 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1066 1066
1067 1067 # TODO: swap should not know about histeditrule's internals
1068 1068 rules[newpos].pos = newpos
1069 1069 rules[oldpos].pos = oldpos
1070 1070
1071 1071 start = min(oldpos, newpos)
1072 1072 end = max(oldpos, newpos)
1073 1073 for r in pycompat.xrange(start, end + 1):
1074 1074 rules[newpos].checkconflicts(rules[r])
1075 1075 rules[oldpos].checkconflicts(rules[r])
1076 1076
1077 1077 if state['selected']:
1078 1078 makeselection(state, newpos)
1079 1079
1080 1080 def changeaction(state, pos, action):
1081 1081 """Change the action state on the given position to the new action"""
1082 1082 rules = state['rules']
1083 1083 assert 0 <= pos < len(rules)
1084 1084 rules[pos].action = action
1085 1085
1086 1086 def cycleaction(state, pos, next=False):
1087 1087 """Changes the action state the next or the previous action from
1088 1088 the action list"""
1089 1089 rules = state['rules']
1090 1090 assert 0 <= pos < len(rules)
1091 1091 current = rules[pos].action
1092 1092
1093 1093 assert current in KEY_LIST
1094 1094
1095 1095 index = KEY_LIST.index(current)
1096 1096 if next:
1097 1097 index += 1
1098 1098 else:
1099 1099 index -= 1
1100 1100 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1101 1101
1102 1102 def changeview(state, delta, unit):
1103 1103 '''Change the region of whatever is being viewed (a patch or the list of
1104 1104 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1105 1105 mode, _ = state['mode']
1106 1106 if mode != MODE_PATCH:
1107 1107 return
1108 1108 mode_state = state['modes'][mode]
1109 1109 num_lines = len(patchcontents(state))
1110 1110 page_height = state['page_height']
1111 1111 unit = page_height if unit == 'page' else 1
1112 1112 num_pages = 1 + (num_lines - 1) / page_height
1113 1113 max_offset = (num_pages - 1) * page_height
1114 1114 newline = mode_state['line_offset'] + delta * unit
1115 1115 mode_state['line_offset'] = max(0, min(max_offset, newline))
1116 1116
1117 1117 def event(state, ch):
1118 1118 """Change state based on the current character input
1119 1119
1120 1120 This takes the current state and based on the current character input from
1121 1121 the user we change the state.
1122 1122 """
1123 1123 selected = state['selected']
1124 1124 oldpos = state['pos']
1125 1125 rules = state['rules']
1126 1126
1127 1127 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1128 1128 return E_RESIZE
1129 1129
1130 1130 lookup_ch = ch
1131 1131 if '0' <= ch <= '9':
1132 1132 lookup_ch = '0'
1133 1133
1134 1134 curmode, prevmode = state['mode']
1135 1135 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1136 1136 if action is None:
1137 1137 return
1138 1138 if action in ('down', 'move-down'):
1139 1139 newpos = min(oldpos + 1, len(rules) - 1)
1140 1140 movecursor(state, oldpos, newpos)
1141 1141 if selected is not None or action == 'move-down':
1142 1142 swap(state, oldpos, newpos)
1143 1143 elif action in ('up', 'move-up'):
1144 1144 newpos = max(0, oldpos - 1)
1145 1145 movecursor(state, oldpos, newpos)
1146 1146 if selected is not None or action == 'move-up':
1147 1147 swap(state, oldpos, newpos)
1148 1148 elif action == 'next-action':
1149 1149 cycleaction(state, oldpos, next=True)
1150 1150 elif action == 'prev-action':
1151 1151 cycleaction(state, oldpos, next=False)
1152 1152 elif action == 'select':
1153 1153 selected = oldpos if selected is None else None
1154 1154 makeselection(state, selected)
1155 1155 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1156 1156 newrule = next((r for r in rules if r.origpos == int(ch)))
1157 1157 movecursor(state, oldpos, newrule.pos)
1158 1158 if selected is not None:
1159 1159 swap(state, oldpos, newrule.pos)
1160 1160 elif action.startswith('action-'):
1161 1161 changeaction(state, oldpos, action[7:])
1162 1162 elif action == 'showpatch':
1163 1163 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1164 1164 elif action == 'help':
1165 1165 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1166 1166 elif action == 'quit':
1167 1167 return E_QUIT
1168 1168 elif action == 'histedit':
1169 1169 return E_HISTEDIT
1170 1170 elif action == 'page-down':
1171 1171 return E_PAGEDOWN
1172 1172 elif action == 'page-up':
1173 1173 return E_PAGEUP
1174 1174 elif action == 'line-down':
1175 1175 return E_LINEDOWN
1176 1176 elif action == 'line-up':
1177 1177 return E_LINEUP
1178 1178
1179 1179 def makecommands(rules):
1180 1180 """Returns a list of commands consumable by histedit --commands based on
1181 1181 our list of rules"""
1182 1182 commands = []
1183 1183 for rules in rules:
1184 1184 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1185 1185 return commands
1186 1186
1187 1187 def addln(win, y, x, line, color=None):
1188 1188 """Add a line to the given window left padding but 100% filled with
1189 1189 whitespace characters, so that the color appears on the whole line"""
1190 1190 maxy, maxx = win.getmaxyx()
1191 1191 length = maxx - 1 - x
1192 1192 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1193 1193 if y < 0:
1194 1194 y = maxy + y
1195 1195 if x < 0:
1196 1196 x = maxx + x
1197 1197 if color:
1198 1198 win.addstr(y, x, line, color)
1199 1199 else:
1200 1200 win.addstr(y, x, line)
1201 1201
1202 1202 def patchcontents(state):
1203 1203 repo = state['repo']
1204 1204 rule = state['rules'][state['pos']]
1205 1205 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1206 1206 'patch': True, 'verbose': True
1207 1207 }, buffered=True)
1208 1208 displayer.show(rule.ctx)
1209 1209 displayer.close()
1210 1210 return displayer.hunk[rule.ctx.rev()].splitlines()
1211 1211
1212 1212 def _chisteditmain(repo, rules, stdscr):
1213 1213 # initialize color pattern
1214 1214 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1215 1215 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1216 1216 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1217 1217 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1218 1218
1219 1219 # don't display the cursor
1220 1220 try:
1221 1221 curses.curs_set(0)
1222 1222 except curses.error:
1223 1223 pass
1224 1224
1225 1225 def rendercommit(win, state):
1226 1226 """Renders the commit window that shows the log of the current selected
1227 1227 commit"""
1228 1228 pos = state['pos']
1229 1229 rules = state['rules']
1230 1230 rule = rules[pos]
1231 1231
1232 1232 ctx = rule.ctx
1233 1233 win.box()
1234 1234
1235 1235 maxy, maxx = win.getmaxyx()
1236 1236 length = maxx - 3
1237 1237
1238 1238 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1239 1239 win.addstr(1, 1, line[:length])
1240 1240
1241 1241 line = "user: {0}".format(stringutil.shortuser(ctx.user()))
1242 1242 win.addstr(2, 1, line[:length])
1243 1243
1244 1244 bms = repo.nodebookmarks(ctx.node())
1245 1245 line = "bookmark: {0}".format(' '.join(bms))
1246 1246 win.addstr(3, 1, line[:length])
1247 1247
1248 1248 line = "files: {0}".format(','.join(ctx.files()))
1249 1249 win.addstr(4, 1, line[:length])
1250 1250
1251 1251 line = "summary: {0}".format(ctx.description().splitlines()[0])
1252 1252 win.addstr(5, 1, line[:length])
1253 1253
1254 1254 conflicts = rule.conflicts
1255 1255 if len(conflicts) > 0:
1256 1256 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1257 1257 conflictstr = "changed files overlap with {0}".format(conflictstr)
1258 1258 else:
1259 1259 conflictstr = 'no overlap'
1260 1260
1261 1261 win.addstr(6, 1, conflictstr[:length])
1262 1262 win.noutrefresh()
1263 1263
1264 1264 def helplines(mode):
1265 1265 if mode == MODE_PATCH:
1266 1266 help = """\
1267 1267 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1268 1268 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1269 1269 """
1270 1270 else:
1271 1271 help = """\
1272 1272 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1273 1273 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1274 1274 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1275 1275 """
1276 1276 return help.splitlines()
1277 1277
1278 1278 def renderhelp(win, state):
1279 1279 maxy, maxx = win.getmaxyx()
1280 1280 mode, _ = state['mode']
1281 1281 for y, line in enumerate(helplines(mode)):
1282 1282 if y >= maxy:
1283 1283 break
1284 1284 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1285 1285 win.noutrefresh()
1286 1286
1287 1287 def renderrules(rulesscr, state):
1288 1288 rules = state['rules']
1289 1289 pos = state['pos']
1290 1290 selected = state['selected']
1291 1291 start = state['modes'][MODE_RULES]['line_offset']
1292 1292
1293 1293 conflicts = [r.ctx for r in rules if r.conflicts]
1294 1294 if len(conflicts) > 0:
1295 1295 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1296 1296 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1297 1297
1298 1298 for y, rule in enumerate(rules[start:]):
1299 1299 if y >= state['page_height']:
1300 1300 break
1301 1301 if len(rule.conflicts) > 0:
1302 1302 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1303 1303 else:
1304 1304 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1305 1305 if y + start == selected:
1306 1306 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1307 1307 elif y + start == pos:
1308 1308 addln(rulesscr, y, 2, rule, curses.A_BOLD)
1309 1309 else:
1310 1310 addln(rulesscr, y, 2, rule)
1311 1311 rulesscr.noutrefresh()
1312 1312
1313 1313 def renderstring(win, state, output):
1314 1314 maxy, maxx = win.getmaxyx()
1315 1315 length = min(maxy - 1, len(output))
1316 1316 for y in range(0, length):
1317 1317 win.addstr(y, 0, output[y])
1318 1318 win.noutrefresh()
1319 1319
1320 1320 def renderpatch(win, state):
1321 1321 start = state['modes'][MODE_PATCH]['line_offset']
1322 1322 renderstring(win, state, patchcontents(state)[start:])
1323 1323
1324 1324 def layout(mode):
1325 1325 maxy, maxx = stdscr.getmaxyx()
1326 1326 helplen = len(helplines(mode))
1327 1327 return {
1328 1328 'commit': (8, maxx),
1329 1329 'help': (helplen, maxx),
1330 1330 'main': (maxy - helplen - 8, maxx),
1331 1331 }
1332 1332
1333 1333 def drawvertwin(size, y, x):
1334 1334 win = curses.newwin(size[0], size[1], y, x)
1335 1335 y += size[0]
1336 1336 return win, y, x
1337 1337
1338 1338 state = {
1339 1339 'pos': 0,
1340 1340 'rules': rules,
1341 1341 'selected': None,
1342 1342 'mode': (MODE_INIT, MODE_INIT),
1343 1343 'page_height': None,
1344 1344 'modes': {
1345 1345 MODE_RULES: {
1346 1346 'line_offset': 0,
1347 1347 },
1348 1348 MODE_PATCH: {
1349 1349 'line_offset': 0,
1350 1350 }
1351 1351 },
1352 1352 'repo': repo,
1353 1353 }
1354 1354
1355 1355 # eventloop
1356 1356 ch = None
1357 1357 stdscr.clear()
1358 1358 stdscr.refresh()
1359 1359 while True:
1360 1360 try:
1361 1361 oldmode, _ = state['mode']
1362 1362 if oldmode == MODE_INIT:
1363 1363 changemode(state, MODE_RULES)
1364 1364 e = event(state, ch)
1365 1365
1366 1366 if e == E_QUIT:
1367 1367 return False
1368 1368 if e == E_HISTEDIT:
1369 1369 return state['rules']
1370 1370 else:
1371 1371 if e == E_RESIZE:
1372 1372 size = screen_size()
1373 1373 if size != stdscr.getmaxyx():
1374 1374 curses.resizeterm(*size)
1375 1375
1376 1376 curmode, _ = state['mode']
1377 1377 sizes = layout(curmode)
1378 1378 if curmode != oldmode:
1379 1379 state['page_height'] = sizes['main'][0]
1380 1380 # Adjust the view to fit the current screen size.
1381 1381 movecursor(state, state['pos'], state['pos'])
1382 1382
1383 1383 # Pack the windows against the top, each pane spread across the
1384 1384 # full width of the screen.
1385 1385 y, x = (0, 0)
1386 1386 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1387 1387 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1388 1388 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1389 1389
1390 1390 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1391 1391 if e == E_PAGEDOWN:
1392 1392 changeview(state, +1, 'page')
1393 1393 elif e == E_PAGEUP:
1394 1394 changeview(state, -1, 'page')
1395 1395 elif e == E_LINEDOWN:
1396 1396 changeview(state, +1, 'line')
1397 1397 elif e == E_LINEUP:
1398 1398 changeview(state, -1, 'line')
1399 1399
1400 1400 # start rendering
1401 1401 commitwin.erase()
1402 1402 helpwin.erase()
1403 1403 mainwin.erase()
1404 1404 if curmode == MODE_PATCH:
1405 1405 renderpatch(mainwin, state)
1406 1406 elif curmode == MODE_HELP:
1407 1407 renderstring(mainwin, state, __doc__.strip().splitlines())
1408 1408 else:
1409 1409 renderrules(mainwin, state)
1410 1410 rendercommit(commitwin, state)
1411 1411 renderhelp(helpwin, state)
1412 1412 curses.doupdate()
1413 1413 # done rendering
1414 1414 ch = stdscr.getkey()
1415 1415 except curses.error:
1416 1416 pass
1417 1417
1418 1418 def _chistedit(ui, repo, *freeargs, **opts):
1419 1419 """interactively edit changeset history via a curses interface
1420 1420
1421 1421 Provides a ncurses interface to histedit. Press ? in chistedit mode
1422 1422 to see an extensive help. Requires python-curses to be installed."""
1423 1423
1424 1424 if curses is None:
1425 1425 raise error.Abort(_("Python curses library required"))
1426 1426
1427 1427 # disable color
1428 1428 ui._colormode = None
1429 1429
1430 1430 try:
1431 1431 keep = opts.get('keep')
1432 1432 revs = opts.get('rev', [])[:]
1433 1433 cmdutil.checkunfinished(repo)
1434 1434 cmdutil.bailifchanged(repo)
1435 1435
1436 1436 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1437 1437 raise error.Abort(_('history edit already in progress, try '
1438 1438 '--continue or --abort'))
1439 1439 revs.extend(freeargs)
1440 1440 if not revs:
1441 1441 defaultrev = destutil.desthistedit(ui, repo)
1442 1442 if defaultrev is not None:
1443 1443 revs.append(defaultrev)
1444 1444 if len(revs) != 1:
1445 1445 raise error.Abort(
1446 1446 _('histedit requires exactly one ancestor revision'))
1447 1447
1448 1448 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1449 1449 if len(rr) != 1:
1450 1450 raise error.Abort(_('The specified revisions must have '
1451 1451 'exactly one common root'))
1452 1452 root = rr[0].node()
1453 1453
1454 1454 topmost, empty = repo.dirstate.parents()
1455 1455 revs = between(repo, root, topmost, keep)
1456 1456 if not revs:
1457 1457 raise error.Abort(_('%s is not an ancestor of working directory') %
1458 1458 node.short(root))
1459 1459
1460 1460 ctxs = []
1461 1461 for i, r in enumerate(revs):
1462 1462 ctxs.append(histeditrule(repo[r], i))
1463 1463 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1464 1464 curses.echo()
1465 1465 curses.endwin()
1466 1466 if rc is False:
1467 1467 ui.write(_("chistedit aborted\n"))
1468 1468 return 0
1469 1469 if type(rc) is list:
1470 1470 ui.status(_("running histedit\n"))
1471 1471 rules = makecommands(rc)
1472 1472 filename = repo.vfs.join('chistedit')
1473 1473 with open(filename, 'w+') as fp:
1474 1474 for r in rules:
1475 1475 fp.write(r)
1476 1476 opts['commands'] = filename
1477 1477 return _texthistedit(ui, repo, *freeargs, **opts)
1478 1478 except KeyboardInterrupt:
1479 1479 pass
1480 1480 return -1
1481 1481
1482 1482 @command('histedit',
1483 1483 [('', 'commands', '',
1484 1484 _('read history edits from the specified file'), _('FILE')),
1485 1485 ('c', 'continue', False, _('continue an edit already in progress')),
1486 1486 ('', 'edit-plan', False, _('edit remaining actions list')),
1487 1487 ('k', 'keep', False,
1488 1488 _("don't strip old nodes after edit is complete")),
1489 1489 ('', 'abort', False, _('abort an edit in progress')),
1490 1490 ('o', 'outgoing', False, _('changesets not found in destination')),
1491 1491 ('f', 'force', False,
1492 1492 _('force outgoing even for unrelated repositories')),
1493 1493 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1494 1494 cmdutil.formatteropts,
1495 1495 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1496 1496 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1497 1497 def histedit(ui, repo, *freeargs, **opts):
1498 1498 """interactively edit changeset history
1499 1499
1500 1500 This command lets you edit a linear series of changesets (up to
1501 1501 and including the working directory, which should be clean).
1502 1502 You can:
1503 1503
1504 1504 - `pick` to [re]order a changeset
1505 1505
1506 1506 - `drop` to omit changeset
1507 1507
1508 1508 - `mess` to reword the changeset commit message
1509 1509
1510 1510 - `fold` to combine it with the preceding changeset (using the later date)
1511 1511
1512 1512 - `roll` like fold, but discarding this commit's description and date
1513 1513
1514 1514 - `edit` to edit this changeset (preserving date)
1515 1515
1516 1516 - `base` to checkout changeset and apply further changesets from there
1517 1517
1518 1518 There are a number of ways to select the root changeset:
1519 1519
1520 1520 - Specify ANCESTOR directly
1521 1521
1522 1522 - Use --outgoing -- it will be the first linear changeset not
1523 1523 included in destination. (See :hg:`help config.paths.default-push`)
1524 1524
1525 1525 - Otherwise, the value from the "histedit.defaultrev" config option
1526 1526 is used as a revset to select the base revision when ANCESTOR is not
1527 1527 specified. The first revision returned by the revset is used. By
1528 1528 default, this selects the editable history that is unique to the
1529 1529 ancestry of the working directory.
1530 1530
1531 1531 .. container:: verbose
1532 1532
1533 1533 If you use --outgoing, this command will abort if there are ambiguous
1534 1534 outgoing revisions. For example, if there are multiple branches
1535 1535 containing outgoing revisions.
1536 1536
1537 1537 Use "min(outgoing() and ::.)" or similar revset specification
1538 1538 instead of --outgoing to specify edit target revision exactly in
1539 1539 such ambiguous situation. See :hg:`help revsets` for detail about
1540 1540 selecting revisions.
1541 1541
1542 1542 .. container:: verbose
1543 1543
1544 1544 Examples:
1545 1545
1546 1546 - A number of changes have been made.
1547 1547 Revision 3 is no longer needed.
1548 1548
1549 1549 Start history editing from revision 3::
1550 1550
1551 1551 hg histedit -r 3
1552 1552
1553 1553 An editor opens, containing the list of revisions,
1554 1554 with specific actions specified::
1555 1555
1556 1556 pick 5339bf82f0ca 3 Zworgle the foobar
1557 1557 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1558 1558 pick 0a9639fcda9d 5 Morgify the cromulancy
1559 1559
1560 1560 Additional information about the possible actions
1561 1561 to take appears below the list of revisions.
1562 1562
1563 1563 To remove revision 3 from the history,
1564 1564 its action (at the beginning of the relevant line)
1565 1565 is changed to 'drop'::
1566 1566
1567 1567 drop 5339bf82f0ca 3 Zworgle the foobar
1568 1568 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1569 1569 pick 0a9639fcda9d 5 Morgify the cromulancy
1570 1570
1571 1571 - A number of changes have been made.
1572 1572 Revision 2 and 4 need to be swapped.
1573 1573
1574 1574 Start history editing from revision 2::
1575 1575
1576 1576 hg histedit -r 2
1577 1577
1578 1578 An editor opens, containing the list of revisions,
1579 1579 with specific actions specified::
1580 1580
1581 1581 pick 252a1af424ad 2 Blorb a morgwazzle
1582 1582 pick 5339bf82f0ca 3 Zworgle the foobar
1583 1583 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1584 1584
1585 1585 To swap revision 2 and 4, its lines are swapped
1586 1586 in the editor::
1587 1587
1588 1588 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1589 1589 pick 5339bf82f0ca 3 Zworgle the foobar
1590 1590 pick 252a1af424ad 2 Blorb a morgwazzle
1591 1591
1592 1592 Returns 0 on success, 1 if user intervention is required (not only
1593 1593 for intentional "edit" command, but also for resolving unexpected
1594 1594 conflicts).
1595 1595 """
1596 if ui.interface('histedit') == 'curses':
1596 # kludge: _chistedit only works for starting an edit, not aborting
1597 # or continuing, so fall back to regular _texthistedit for those
1598 # operations.
1599 if ui.interface('histedit') == 'curses' and _getgoal(opts) == goalnew:
1597 1600 return _chistedit(ui, repo, *freeargs, **opts)
1598 1601 return _texthistedit(ui, repo, *freeargs, **opts)
1599 1602
1600 1603 def _texthistedit(ui, repo, *freeargs, **opts):
1601 1604 state = histeditstate(repo)
1602 1605 with repo.wlock() as wlock, repo.lock() as lock:
1603 1606 state.wlock = wlock
1604 1607 state.lock = lock
1605 1608 _histedit(ui, repo, state, *freeargs, **opts)
1606 1609
1607 1610 goalcontinue = 'continue'
1608 1611 goalabort = 'abort'
1609 1612 goaleditplan = 'edit-plan'
1610 1613 goalnew = 'new'
1611 1614
1612 1615 def _getgoal(opts):
1613 1616 if opts.get('continue'):
1614 1617 return goalcontinue
1615 1618 if opts.get('abort'):
1616 1619 return goalabort
1617 1620 if opts.get('edit_plan'):
1618 1621 return goaleditplan
1619 1622 return goalnew
1620 1623
1621 1624 def _readfile(ui, path):
1622 1625 if path == '-':
1623 1626 with ui.timeblockedsection('histedit'):
1624 1627 return ui.fin.read()
1625 1628 else:
1626 1629 with open(path, 'rb') as f:
1627 1630 return f.read()
1628 1631
1629 1632 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1630 1633 # TODO only abort if we try to histedit mq patches, not just
1631 1634 # blanket if mq patches are applied somewhere
1632 1635 mq = getattr(repo, 'mq', None)
1633 1636 if mq and mq.applied:
1634 1637 raise error.Abort(_('source has mq patches applied'))
1635 1638
1636 1639 # basic argument incompatibility processing
1637 1640 outg = opts.get('outgoing')
1638 1641 editplan = opts.get('edit_plan')
1639 1642 abort = opts.get('abort')
1640 1643 force = opts.get('force')
1641 1644 if force and not outg:
1642 1645 raise error.Abort(_('--force only allowed with --outgoing'))
1643 1646 if goal == 'continue':
1644 1647 if any((outg, abort, revs, freeargs, rules, editplan)):
1645 1648 raise error.Abort(_('no arguments allowed with --continue'))
1646 1649 elif goal == 'abort':
1647 1650 if any((outg, revs, freeargs, rules, editplan)):
1648 1651 raise error.Abort(_('no arguments allowed with --abort'))
1649 1652 elif goal == 'edit-plan':
1650 1653 if any((outg, revs, freeargs)):
1651 1654 raise error.Abort(_('only --commands argument allowed with '
1652 1655 '--edit-plan'))
1653 1656 else:
1654 1657 if state.inprogress():
1655 1658 raise error.Abort(_('history edit already in progress, try '
1656 1659 '--continue or --abort'))
1657 1660 if outg:
1658 1661 if revs:
1659 1662 raise error.Abort(_('no revisions allowed with --outgoing'))
1660 1663 if len(freeargs) > 1:
1661 1664 raise error.Abort(
1662 1665 _('only one repo argument allowed with --outgoing'))
1663 1666 else:
1664 1667 revs.extend(freeargs)
1665 1668 if len(revs) == 0:
1666 1669 defaultrev = destutil.desthistedit(ui, repo)
1667 1670 if defaultrev is not None:
1668 1671 revs.append(defaultrev)
1669 1672
1670 1673 if len(revs) != 1:
1671 1674 raise error.Abort(
1672 1675 _('histedit requires exactly one ancestor revision'))
1673 1676
1674 1677 def _histedit(ui, repo, state, *freeargs, **opts):
1675 1678 opts = pycompat.byteskwargs(opts)
1676 1679 fm = ui.formatter('histedit', opts)
1677 1680 fm.startitem()
1678 1681 goal = _getgoal(opts)
1679 1682 revs = opts.get('rev', [])
1680 1683 # experimental config: ui.history-editing-backup
1681 1684 nobackup = not ui.configbool('ui', 'history-editing-backup')
1682 1685 rules = opts.get('commands', '')
1683 1686 state.keep = opts.get('keep', False)
1684 1687
1685 1688 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1686 1689
1687 1690 hastags = False
1688 1691 if revs:
1689 1692 revs = scmutil.revrange(repo, revs)
1690 1693 ctxs = [repo[rev] for rev in revs]
1691 1694 for ctx in ctxs:
1692 1695 tags = [tag for tag in ctx.tags() if tag != 'tip']
1693 1696 if not hastags:
1694 1697 hastags = len(tags)
1695 1698 if hastags:
1696 1699 if ui.promptchoice(_('warning: tags associated with the given'
1697 1700 ' changeset will be lost after histedit. \n'
1698 1701 'do you want to continue (yN)? $$ &Yes $$ &No'), default=1):
1699 1702 raise error.Abort(_('histedit cancelled\n'))
1700 1703 # rebuild state
1701 1704 if goal == goalcontinue:
1702 1705 state.read()
1703 1706 state = bootstrapcontinue(ui, state, opts)
1704 1707 elif goal == goaleditplan:
1705 1708 _edithisteditplan(ui, repo, state, rules)
1706 1709 return
1707 1710 elif goal == goalabort:
1708 1711 _aborthistedit(ui, repo, state, nobackup=nobackup)
1709 1712 return
1710 1713 else:
1711 1714 # goal == goalnew
1712 1715 _newhistedit(ui, repo, state, revs, freeargs, opts)
1713 1716
1714 1717 _continuehistedit(ui, repo, state)
1715 1718 _finishhistedit(ui, repo, state, fm)
1716 1719 fm.end()
1717 1720
1718 1721 def _continuehistedit(ui, repo, state):
1719 1722 """This function runs after either:
1720 1723 - bootstrapcontinue (if the goal is 'continue')
1721 1724 - _newhistedit (if the goal is 'new')
1722 1725 """
1723 1726 # preprocess rules so that we can hide inner folds from the user
1724 1727 # and only show one editor
1725 1728 actions = state.actions[:]
1726 1729 for idx, (action, nextact) in enumerate(
1727 1730 zip(actions, actions[1:] + [None])):
1728 1731 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1729 1732 state.actions[idx].__class__ = _multifold
1730 1733
1731 1734 # Force an initial state file write, so the user can run --abort/continue
1732 1735 # even if there's an exception before the first transaction serialize.
1733 1736 state.write()
1734 1737
1735 1738 tr = None
1736 1739 # Don't use singletransaction by default since it rolls the entire
1737 1740 # transaction back if an unexpected exception happens (like a
1738 1741 # pretxncommit hook throws, or the user aborts the commit msg editor).
1739 1742 if ui.configbool("histedit", "singletransaction"):
1740 1743 # Don't use a 'with' for the transaction, since actions may close
1741 1744 # and reopen a transaction. For example, if the action executes an
1742 1745 # external process it may choose to commit the transaction first.
1743 1746 tr = repo.transaction('histedit')
1744 1747 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1745 1748 total=len(state.actions))
1746 1749 with progress, util.acceptintervention(tr):
1747 1750 while state.actions:
1748 1751 state.write(tr=tr)
1749 1752 actobj = state.actions[0]
1750 1753 progress.increment(item=actobj.torule())
1751 1754 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1752 1755 actobj.torule()))
1753 1756 parentctx, replacement_ = actobj.run()
1754 1757 state.parentctxnode = parentctx.node()
1755 1758 state.replacements.extend(replacement_)
1756 1759 state.actions.pop(0)
1757 1760
1758 1761 state.write()
1759 1762
1760 1763 def _finishhistedit(ui, repo, state, fm):
1761 1764 """This action runs when histedit is finishing its session"""
1762 1765 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1763 1766
1764 1767 mapping, tmpnodes, created, ntm = processreplacement(state)
1765 1768 if mapping:
1766 1769 for prec, succs in mapping.iteritems():
1767 1770 if not succs:
1768 1771 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1769 1772 else:
1770 1773 ui.debug('histedit: %s is replaced by %s\n' % (
1771 1774 node.short(prec), node.short(succs[0])))
1772 1775 if len(succs) > 1:
1773 1776 m = 'histedit: %s'
1774 1777 for n in succs[1:]:
1775 1778 ui.debug(m % node.short(n))
1776 1779
1777 1780 if not state.keep:
1778 1781 if mapping:
1779 1782 movetopmostbookmarks(repo, state.topmost, ntm)
1780 1783 # TODO update mq state
1781 1784 else:
1782 1785 mapping = {}
1783 1786
1784 1787 for n in tmpnodes:
1785 1788 if n in repo:
1786 1789 mapping[n] = ()
1787 1790
1788 1791 # remove entries about unknown nodes
1789 1792 nodemap = repo.unfiltered().changelog.nodemap
1790 1793 mapping = {k: v for k, v in mapping.items()
1791 1794 if k in nodemap and all(n in nodemap for n in v)}
1792 1795 scmutil.cleanupnodes(repo, mapping, 'histedit')
1793 1796 hf = fm.hexfunc
1794 1797 fl = fm.formatlist
1795 1798 fd = fm.formatdict
1796 1799 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1797 1800 for oldn, newn in mapping.iteritems()},
1798 1801 key="oldnode", value="newnodes")
1799 1802 fm.data(nodechanges=nodechanges)
1800 1803
1801 1804 state.clear()
1802 1805 if os.path.exists(repo.sjoin('undo')):
1803 1806 os.unlink(repo.sjoin('undo'))
1804 1807 if repo.vfs.exists('histedit-last-edit.txt'):
1805 1808 repo.vfs.unlink('histedit-last-edit.txt')
1806 1809
1807 1810 def _aborthistedit(ui, repo, state, nobackup=False):
1808 1811 try:
1809 1812 state.read()
1810 1813 __, leafs, tmpnodes, __ = processreplacement(state)
1811 1814 ui.debug('restore wc to old parent %s\n'
1812 1815 % node.short(state.topmost))
1813 1816
1814 1817 # Recover our old commits if necessary
1815 1818 if not state.topmost in repo and state.backupfile:
1816 1819 backupfile = repo.vfs.join(state.backupfile)
1817 1820 f = hg.openpath(ui, backupfile)
1818 1821 gen = exchange.readbundle(ui, f, backupfile)
1819 1822 with repo.transaction('histedit.abort') as tr:
1820 1823 bundle2.applybundle(repo, gen, tr, source='histedit',
1821 1824 url='bundle:' + backupfile)
1822 1825
1823 1826 os.remove(backupfile)
1824 1827
1825 1828 # check whether we should update away
1826 1829 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1827 1830 state.parentctxnode, leafs | tmpnodes):
1828 1831 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1829 1832 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1830 1833 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1831 1834 except Exception:
1832 1835 if state.inprogress():
1833 1836 ui.warn(_('warning: encountered an exception during histedit '
1834 1837 '--abort; the repository may not have been completely '
1835 1838 'cleaned up\n'))
1836 1839 raise
1837 1840 finally:
1838 1841 state.clear()
1839 1842
1840 1843 def _edithisteditplan(ui, repo, state, rules):
1841 1844 state.read()
1842 1845 if not rules:
1843 1846 comment = geteditcomment(ui,
1844 1847 node.short(state.parentctxnode),
1845 1848 node.short(state.topmost))
1846 1849 rules = ruleeditor(repo, ui, state.actions, comment)
1847 1850 else:
1848 1851 rules = _readfile(ui, rules)
1849 1852 actions = parserules(rules, state)
1850 1853 ctxs = [repo[act.node] \
1851 1854 for act in state.actions if act.node]
1852 1855 warnverifyactions(ui, repo, actions, state, ctxs)
1853 1856 state.actions = actions
1854 1857 state.write()
1855 1858
1856 1859 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1857 1860 outg = opts.get('outgoing')
1858 1861 rules = opts.get('commands', '')
1859 1862 force = opts.get('force')
1860 1863
1861 1864 cmdutil.checkunfinished(repo)
1862 1865 cmdutil.bailifchanged(repo)
1863 1866
1864 1867 topmost, empty = repo.dirstate.parents()
1865 1868 if outg:
1866 1869 if freeargs:
1867 1870 remote = freeargs[0]
1868 1871 else:
1869 1872 remote = None
1870 1873 root = findoutgoing(ui, repo, remote, force, opts)
1871 1874 else:
1872 1875 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1873 1876 if len(rr) != 1:
1874 1877 raise error.Abort(_('The specified revisions must have '
1875 1878 'exactly one common root'))
1876 1879 root = rr[0].node()
1877 1880
1878 1881 revs = between(repo, root, topmost, state.keep)
1879 1882 if not revs:
1880 1883 raise error.Abort(_('%s is not an ancestor of working directory') %
1881 1884 node.short(root))
1882 1885
1883 1886 ctxs = [repo[r] for r in revs]
1884 1887 if not rules:
1885 1888 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1886 1889 actions = [pick(state, r) for r in revs]
1887 1890 rules = ruleeditor(repo, ui, actions, comment)
1888 1891 else:
1889 1892 rules = _readfile(ui, rules)
1890 1893 actions = parserules(rules, state)
1891 1894 warnverifyactions(ui, repo, actions, state, ctxs)
1892 1895
1893 1896 parentctxnode = repo[root].parents()[0].node()
1894 1897
1895 1898 state.parentctxnode = parentctxnode
1896 1899 state.actions = actions
1897 1900 state.topmost = topmost
1898 1901 state.replacements = []
1899 1902
1900 1903 ui.log("histedit", "%d actions to histedit", len(actions),
1901 1904 histedit_num_actions=len(actions))
1902 1905
1903 1906 # Create a backup so we can always abort completely.
1904 1907 backupfile = None
1905 1908 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1906 1909 backupfile = repair.backupbundle(repo, [parentctxnode],
1907 1910 [topmost], root, 'histedit')
1908 1911 state.backupfile = backupfile
1909 1912
1910 1913 def _getsummary(ctx):
1911 1914 # a common pattern is to extract the summary but default to the empty
1912 1915 # string
1913 1916 summary = ctx.description() or ''
1914 1917 if summary:
1915 1918 summary = summary.splitlines()[0]
1916 1919 return summary
1917 1920
1918 1921 def bootstrapcontinue(ui, state, opts):
1919 1922 repo = state.repo
1920 1923
1921 1924 ms = mergemod.mergestate.read(repo)
1922 1925 mergeutil.checkunresolved(ms)
1923 1926
1924 1927 if state.actions:
1925 1928 actobj = state.actions.pop(0)
1926 1929
1927 1930 if _isdirtywc(repo):
1928 1931 actobj.continuedirty()
1929 1932 if _isdirtywc(repo):
1930 1933 abortdirty()
1931 1934
1932 1935 parentctx, replacements = actobj.continueclean()
1933 1936
1934 1937 state.parentctxnode = parentctx.node()
1935 1938 state.replacements.extend(replacements)
1936 1939
1937 1940 return state
1938 1941
1939 1942 def between(repo, old, new, keep):
1940 1943 """select and validate the set of revision to edit
1941 1944
1942 1945 When keep is false, the specified set can't have children."""
1943 1946 revs = repo.revs('%n::%n', old, new)
1944 1947 if revs and not keep:
1945 1948 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1946 1949 repo.revs('(%ld::) - (%ld)', revs, revs)):
1947 1950 raise error.Abort(_('can only histedit a changeset together '
1948 1951 'with all its descendants'))
1949 1952 if repo.revs('(%ld) and merge()', revs):
1950 1953 raise error.Abort(_('cannot edit history that contains merges'))
1951 1954 root = repo[revs.first()] # list is already sorted by repo.revs()
1952 1955 if not root.mutable():
1953 1956 raise error.Abort(_('cannot edit public changeset: %s') % root,
1954 1957 hint=_("see 'hg help phases' for details"))
1955 1958 return pycompat.maplist(repo.changelog.node, revs)
1956 1959
1957 1960 def ruleeditor(repo, ui, actions, editcomment=""):
1958 1961 """open an editor to edit rules
1959 1962
1960 1963 rules are in the format [ [act, ctx], ...] like in state.rules
1961 1964 """
1962 1965 if repo.ui.configbool("experimental", "histedit.autoverb"):
1963 1966 newact = util.sortdict()
1964 1967 for act in actions:
1965 1968 ctx = repo[act.node]
1966 1969 summary = _getsummary(ctx)
1967 1970 fword = summary.split(' ', 1)[0].lower()
1968 1971 added = False
1969 1972
1970 1973 # if it doesn't end with the special character '!' just skip this
1971 1974 if fword.endswith('!'):
1972 1975 fword = fword[:-1]
1973 1976 if fword in primaryactions | secondaryactions | tertiaryactions:
1974 1977 act.verb = fword
1975 1978 # get the target summary
1976 1979 tsum = summary[len(fword) + 1:].lstrip()
1977 1980 # safe but slow: reverse iterate over the actions so we
1978 1981 # don't clash on two commits having the same summary
1979 1982 for na, l in reversed(list(newact.iteritems())):
1980 1983 actx = repo[na.node]
1981 1984 asum = _getsummary(actx)
1982 1985 if asum == tsum:
1983 1986 added = True
1984 1987 l.append(act)
1985 1988 break
1986 1989
1987 1990 if not added:
1988 1991 newact[act] = []
1989 1992
1990 1993 # copy over and flatten the new list
1991 1994 actions = []
1992 1995 for na, l in newact.iteritems():
1993 1996 actions.append(na)
1994 1997 actions += l
1995 1998
1996 1999 rules = '\n'.join([act.torule() for act in actions])
1997 2000 rules += '\n\n'
1998 2001 rules += editcomment
1999 2002 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2000 2003 repopath=repo.path, action='histedit')
2001 2004
2002 2005 # Save edit rules in .hg/histedit-last-edit.txt in case
2003 2006 # the user needs to ask for help after something
2004 2007 # surprising happens.
2005 2008 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2006 2009 f.write(rules)
2007 2010
2008 2011 return rules
2009 2012
2010 2013 def parserules(rules, state):
2011 2014 """Read the histedit rules string and return list of action objects """
2012 2015 rules = [l for l in (r.strip() for r in rules.splitlines())
2013 2016 if l and not l.startswith('#')]
2014 2017 actions = []
2015 2018 for r in rules:
2016 2019 if ' ' not in r:
2017 2020 raise error.ParseError(_('malformed line "%s"') % r)
2018 2021 verb, rest = r.split(' ', 1)
2019 2022
2020 2023 if verb not in actiontable:
2021 2024 raise error.ParseError(_('unknown action "%s"') % verb)
2022 2025
2023 2026 action = actiontable[verb].fromrule(state, rest)
2024 2027 actions.append(action)
2025 2028 return actions
2026 2029
2027 2030 def warnverifyactions(ui, repo, actions, state, ctxs):
2028 2031 try:
2029 2032 verifyactions(actions, state, ctxs)
2030 2033 except error.ParseError:
2031 2034 if repo.vfs.exists('histedit-last-edit.txt'):
2032 2035 ui.warn(_('warning: histedit rules saved '
2033 2036 'to: .hg/histedit-last-edit.txt\n'))
2034 2037 raise
2035 2038
2036 2039 def verifyactions(actions, state, ctxs):
2037 2040 """Verify that there exists exactly one action per given changeset and
2038 2041 other constraints.
2039 2042
2040 2043 Will abort if there are to many or too few rules, a malformed rule,
2041 2044 or a rule on a changeset outside of the user-given range.
2042 2045 """
2043 2046 expected = set(c.node() for c in ctxs)
2044 2047 seen = set()
2045 2048 prev = None
2046 2049
2047 2050 if actions and actions[0].verb in ['roll', 'fold']:
2048 2051 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2049 2052 actions[0].verb)
2050 2053
2051 2054 for action in actions:
2052 2055 action.verify(prev, expected, seen)
2053 2056 prev = action
2054 2057 if action.node is not None:
2055 2058 seen.add(action.node)
2056 2059 missing = sorted(expected - seen) # sort to stabilize output
2057 2060
2058 2061 if state.repo.ui.configbool('histedit', 'dropmissing'):
2059 2062 if len(actions) == 0:
2060 2063 raise error.ParseError(_('no rules provided'),
2061 2064 hint=_('use strip extension to remove commits'))
2062 2065
2063 2066 drops = [drop(state, n) for n in missing]
2064 2067 # put the in the beginning so they execute immediately and
2065 2068 # don't show in the edit-plan in the future
2066 2069 actions[:0] = drops
2067 2070 elif missing:
2068 2071 raise error.ParseError(_('missing rules for changeset %s') %
2069 2072 node.short(missing[0]),
2070 2073 hint=_('use "drop %s" to discard, see also: '
2071 2074 "'hg help -e histedit.config'")
2072 2075 % node.short(missing[0]))
2073 2076
2074 2077 def adjustreplacementsfrommarkers(repo, oldreplacements):
2075 2078 """Adjust replacements from obsolescence markers
2076 2079
2077 2080 Replacements structure is originally generated based on
2078 2081 histedit's state and does not account for changes that are
2079 2082 not recorded there. This function fixes that by adding
2080 2083 data read from obsolescence markers"""
2081 2084 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2082 2085 return oldreplacements
2083 2086
2084 2087 unfi = repo.unfiltered()
2085 2088 nm = unfi.changelog.nodemap
2086 2089 obsstore = repo.obsstore
2087 2090 newreplacements = list(oldreplacements)
2088 2091 oldsuccs = [r[1] for r in oldreplacements]
2089 2092 # successors that have already been added to succstocheck once
2090 2093 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2091 2094 succstocheck = list(seensuccs)
2092 2095 while succstocheck:
2093 2096 n = succstocheck.pop()
2094 2097 missing = nm.get(n) is None
2095 2098 markers = obsstore.successors.get(n, ())
2096 2099 if missing and not markers:
2097 2100 # dead end, mark it as such
2098 2101 newreplacements.append((n, ()))
2099 2102 for marker in markers:
2100 2103 nsuccs = marker[1]
2101 2104 newreplacements.append((n, nsuccs))
2102 2105 for nsucc in nsuccs:
2103 2106 if nsucc not in seensuccs:
2104 2107 seensuccs.add(nsucc)
2105 2108 succstocheck.append(nsucc)
2106 2109
2107 2110 return newreplacements
2108 2111
2109 2112 def processreplacement(state):
2110 2113 """process the list of replacements to return
2111 2114
2112 2115 1) the final mapping between original and created nodes
2113 2116 2) the list of temporary node created by histedit
2114 2117 3) the list of new commit created by histedit"""
2115 2118 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2116 2119 allsuccs = set()
2117 2120 replaced = set()
2118 2121 fullmapping = {}
2119 2122 # initialize basic set
2120 2123 # fullmapping records all operations recorded in replacement
2121 2124 for rep in replacements:
2122 2125 allsuccs.update(rep[1])
2123 2126 replaced.add(rep[0])
2124 2127 fullmapping.setdefault(rep[0], set()).update(rep[1])
2125 2128 new = allsuccs - replaced
2126 2129 tmpnodes = allsuccs & replaced
2127 2130 # Reduce content fullmapping into direct relation between original nodes
2128 2131 # and final node created during history edition
2129 2132 # Dropped changeset are replaced by an empty list
2130 2133 toproceed = set(fullmapping)
2131 2134 final = {}
2132 2135 while toproceed:
2133 2136 for x in list(toproceed):
2134 2137 succs = fullmapping[x]
2135 2138 for s in list(succs):
2136 2139 if s in toproceed:
2137 2140 # non final node with unknown closure
2138 2141 # We can't process this now
2139 2142 break
2140 2143 elif s in final:
2141 2144 # non final node, replace with closure
2142 2145 succs.remove(s)
2143 2146 succs.update(final[s])
2144 2147 else:
2145 2148 final[x] = succs
2146 2149 toproceed.remove(x)
2147 2150 # remove tmpnodes from final mapping
2148 2151 for n in tmpnodes:
2149 2152 del final[n]
2150 2153 # we expect all changes involved in final to exist in the repo
2151 2154 # turn `final` into list (topologically sorted)
2152 2155 nm = state.repo.changelog.nodemap
2153 2156 for prec, succs in final.items():
2154 2157 final[prec] = sorted(succs, key=nm.get)
2155 2158
2156 2159 # computed topmost element (necessary for bookmark)
2157 2160 if new:
2158 2161 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2159 2162 elif not final:
2160 2163 # Nothing rewritten at all. we won't need `newtopmost`
2161 2164 # It is the same as `oldtopmost` and `processreplacement` know it
2162 2165 newtopmost = None
2163 2166 else:
2164 2167 # every body died. The newtopmost is the parent of the root.
2165 2168 r = state.repo.changelog.rev
2166 2169 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2167 2170
2168 2171 return final, tmpnodes, new, newtopmost
2169 2172
2170 2173 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2171 2174 """Move bookmark from oldtopmost to newly created topmost
2172 2175
2173 2176 This is arguably a feature and we may only want that for the active
2174 2177 bookmark. But the behavior is kept compatible with the old version for now.
2175 2178 """
2176 2179 if not oldtopmost or not newtopmost:
2177 2180 return
2178 2181 oldbmarks = repo.nodebookmarks(oldtopmost)
2179 2182 if oldbmarks:
2180 2183 with repo.lock(), repo.transaction('histedit') as tr:
2181 2184 marks = repo._bookmarks
2182 2185 changes = []
2183 2186 for name in oldbmarks:
2184 2187 changes.append((name, newtopmost))
2185 2188 marks.applychanges(repo, tr, changes)
2186 2189
2187 2190 def cleanupnode(ui, repo, nodes, nobackup=False):
2188 2191 """strip a group of nodes from the repository
2189 2192
2190 2193 The set of node to strip may contains unknown nodes."""
2191 2194 with repo.lock():
2192 2195 # do not let filtering get in the way of the cleanse
2193 2196 # we should probably get rid of obsolescence marker created during the
2194 2197 # histedit, but we currently do not have such information.
2195 2198 repo = repo.unfiltered()
2196 2199 # Find all nodes that need to be stripped
2197 2200 # (we use %lr instead of %ln to silently ignore unknown items)
2198 2201 nm = repo.changelog.nodemap
2199 2202 nodes = sorted(n for n in nodes if n in nm)
2200 2203 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2201 2204 if roots:
2202 2205 backup = not nobackup
2203 2206 repair.strip(ui, repo, roots, backup=backup)
2204 2207
2205 2208 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2206 2209 if isinstance(nodelist, str):
2207 2210 nodelist = [nodelist]
2208 2211 state = histeditstate(repo)
2209 2212 if state.inprogress():
2210 2213 state.read()
2211 2214 histedit_nodes = {action.node for action
2212 2215 in state.actions if action.node}
2213 2216 common_nodes = histedit_nodes & set(nodelist)
2214 2217 if common_nodes:
2215 2218 raise error.Abort(_("histedit in progress, can't strip %s")
2216 2219 % ', '.join(node.short(x) for x in common_nodes))
2217 2220 return orig(ui, repo, nodelist, *args, **kwargs)
2218 2221
2219 2222 extensions.wrapfunction(repair, 'strip', stripwrapper)
2220 2223
2221 2224 def summaryhook(ui, repo):
2222 2225 state = histeditstate(repo)
2223 2226 if not state.inprogress():
2224 2227 return
2225 2228 state.read()
2226 2229 if state.actions:
2227 2230 # i18n: column positioning for "hg summary"
2228 2231 ui.write(_('hist: %s (histedit --continue)\n') %
2229 2232 (ui.label(_('%d remaining'), 'histedit.remaining') %
2230 2233 len(state.actions)))
2231 2234
2232 2235 def extsetup(ui):
2233 2236 cmdutil.summaryhooks.add('histedit', summaryhook)
2234 2237 cmdutil.unfinishedstates.append(
2235 2238 ['histedit-state', False, True, _('histedit in progress'),
2236 2239 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2237 2240 cmdutil.afterresolvedstates.append(
2238 2241 ['histedit-state', _('hg histedit --continue')])
@@ -1,303 +1,306 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > histedit=
6 6 > EOF
7 7
8 8 $ initrepo ()
9 9 > {
10 10 > hg init $1
11 11 > cd $1
12 12 > for x in a b c d e f ; do
13 13 > echo $x$x$x$x$x > $x
14 14 > hg add $x
15 15 > done
16 16 > hg ci -m 'Initial commit'
17 17 > for x in a b c d e f ; do
18 18 > echo $x > $x
19 19 > hg ci -m $x
20 20 > done
21 21 > echo 'I can haz no commute' > e
22 22 > hg ci -m 'does not commute with e'
23 23 > cd ..
24 24 > }
25 25
26 26 $ initrepo r1
27 27 $ cd r1
28 28
29 29 Initial generation of the command files
30 30
31 31 $ EDITED="$TESTTMP/editedhistory"
32 32 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
33 33 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
34 34 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 7 >> $EDITED
35 35 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
36 36 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
37 37 $ cat $EDITED
38 38 pick 65a9a84f33fd 3 c
39 39 pick 00f1c5383965 4 d
40 40 pick 39522b764e3d 7 does not commute with e
41 41 pick 7b4e2f4b7bcd 5 e
42 42 pick 500cac37a696 6 f
43 43
44 44 log before edit
45 45 $ hg log --graph
46 46 @ changeset: 7:39522b764e3d
47 47 | tag: tip
48 48 | user: test
49 49 | date: Thu Jan 01 00:00:00 1970 +0000
50 50 | summary: does not commute with e
51 51 |
52 52 o changeset: 6:500cac37a696
53 53 | user: test
54 54 | date: Thu Jan 01 00:00:00 1970 +0000
55 55 | summary: f
56 56 |
57 57 o changeset: 5:7b4e2f4b7bcd
58 58 | user: test
59 59 | date: Thu Jan 01 00:00:00 1970 +0000
60 60 | summary: e
61 61 |
62 62 o changeset: 4:00f1c5383965
63 63 | user: test
64 64 | date: Thu Jan 01 00:00:00 1970 +0000
65 65 | summary: d
66 66 |
67 67 o changeset: 3:65a9a84f33fd
68 68 | user: test
69 69 | date: Thu Jan 01 00:00:00 1970 +0000
70 70 | summary: c
71 71 |
72 72 o changeset: 2:da6535b52e45
73 73 | user: test
74 74 | date: Thu Jan 01 00:00:00 1970 +0000
75 75 | summary: b
76 76 |
77 77 o changeset: 1:c1f09da44841
78 78 | user: test
79 79 | date: Thu Jan 01 00:00:00 1970 +0000
80 80 | summary: a
81 81 |
82 82 o changeset: 0:1715188a53c7
83 83 user: test
84 84 date: Thu Jan 01 00:00:00 1970 +0000
85 85 summary: Initial commit
86 86
87 87
88 88 edit the history
89 89 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
90 90 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
91 91 merging e
92 92 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
93 93 Fix up the change (pick 39522b764e3d)
94 94 (hg histedit --continue to resume)
95 95
96 96 abort the edit
97 97 $ hg histedit --abort 2>&1 | fixbundle
98 98 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 99
100 100
101 101 second edit set
102 102
103 103 $ hg log --graph
104 104 @ changeset: 7:39522b764e3d
105 105 | tag: tip
106 106 | user: test
107 107 | date: Thu Jan 01 00:00:00 1970 +0000
108 108 | summary: does not commute with e
109 109 |
110 110 o changeset: 6:500cac37a696
111 111 | user: test
112 112 | date: Thu Jan 01 00:00:00 1970 +0000
113 113 | summary: f
114 114 |
115 115 o changeset: 5:7b4e2f4b7bcd
116 116 | user: test
117 117 | date: Thu Jan 01 00:00:00 1970 +0000
118 118 | summary: e
119 119 |
120 120 o changeset: 4:00f1c5383965
121 121 | user: test
122 122 | date: Thu Jan 01 00:00:00 1970 +0000
123 123 | summary: d
124 124 |
125 125 o changeset: 3:65a9a84f33fd
126 126 | user: test
127 127 | date: Thu Jan 01 00:00:00 1970 +0000
128 128 | summary: c
129 129 |
130 130 o changeset: 2:da6535b52e45
131 131 | user: test
132 132 | date: Thu Jan 01 00:00:00 1970 +0000
133 133 | summary: b
134 134 |
135 135 o changeset: 1:c1f09da44841
136 136 | user: test
137 137 | date: Thu Jan 01 00:00:00 1970 +0000
138 138 | summary: a
139 139 |
140 140 o changeset: 0:1715188a53c7
141 141 user: test
142 142 date: Thu Jan 01 00:00:00 1970 +0000
143 143 summary: Initial commit
144 144
145 145
146 146 edit the history
147 147 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
148 148 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
149 149 merging e
150 150 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
151 151 Fix up the change (pick 39522b764e3d)
152 152 (hg histedit --continue to resume)
153 153
154 154 fix up
155 155 $ echo 'I can haz no commute' > e
156 156 $ hg resolve --mark e
157 157 (no more unresolved files)
158 158 continue: hg histedit --continue
159 159 $ hg histedit --continue 2>&1 | fixbundle
160 160 merging e
161 161 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
162 162 Fix up the change (pick 7b4e2f4b7bcd)
163 163 (hg histedit --continue to resume)
164 $ hg histedit --continue 2>&1 | fixbundle
164 We forcibly enable curses here so we can verify that continuing works
165 with curses enabled.
166 $ hg histedit --continue --config ui.interactive=true \
167 > --config ui.interface=curses 2>&1 | fixbundle
165 168 abort: unresolved merge conflicts (see 'hg help resolve')
166 169
167 170 This failure is caused by 7b4e2f4b7bcd "e" not rebasing the non commutative
168 171 former children.
169 172
170 173 just continue this time
171 174 $ hg revert -r 'p1()' e
172 175 make sure the to-be-empty commit doesn't screw up the state (issue5545)
173 176 $ hg histedit --continue 2>&1 | fixbundle
174 177 abort: unresolved merge conflicts (see 'hg help resolve')
175 178 $ hg resolve --mark e
176 179 (no more unresolved files)
177 180 continue: hg histedit --continue
178 181 $ hg histedit --continue 2>&1 | fixbundle
179 182 7b4e2f4b7bcd: skipping changeset (no changes)
180 183
181 184 log after edit
182 185 $ hg log --graph
183 186 @ changeset: 6:7efe1373e4bc
184 187 | tag: tip
185 188 | user: test
186 189 | date: Thu Jan 01 00:00:00 1970 +0000
187 190 | summary: f
188 191 |
189 192 o changeset: 5:e334d87a1e55
190 193 | user: test
191 194 | date: Thu Jan 01 00:00:00 1970 +0000
192 195 | summary: does not commute with e
193 196 |
194 197 o changeset: 4:00f1c5383965
195 198 | user: test
196 199 | date: Thu Jan 01 00:00:00 1970 +0000
197 200 | summary: d
198 201 |
199 202 o changeset: 3:65a9a84f33fd
200 203 | user: test
201 204 | date: Thu Jan 01 00:00:00 1970 +0000
202 205 | summary: c
203 206 |
204 207 o changeset: 2:da6535b52e45
205 208 | user: test
206 209 | date: Thu Jan 01 00:00:00 1970 +0000
207 210 | summary: b
208 211 |
209 212 o changeset: 1:c1f09da44841
210 213 | user: test
211 214 | date: Thu Jan 01 00:00:00 1970 +0000
212 215 | summary: a
213 216 |
214 217 o changeset: 0:1715188a53c7
215 218 user: test
216 219 date: Thu Jan 01 00:00:00 1970 +0000
217 220 summary: Initial commit
218 221
219 222
220 223 start over
221 224
222 225 $ cd ..
223 226
224 227 $ initrepo r2
225 228 $ cd r2
226 229 $ rm $EDITED
227 230 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 3 >> $EDITED
228 231 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 4 >> $EDITED
229 232 $ hg log --template 'mess {node|short} {rev} {desc}\n' -r 7 >> $EDITED
230 233 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 5 >> $EDITED
231 234 $ hg log --template 'pick {node|short} {rev} {desc}\n' -r 6 >> $EDITED
232 235 $ cat $EDITED
233 236 pick 65a9a84f33fd 3 c
234 237 pick 00f1c5383965 4 d
235 238 mess 39522b764e3d 7 does not commute with e
236 239 pick 7b4e2f4b7bcd 5 e
237 240 pick 500cac37a696 6 f
238 241
239 242 edit the history, this time with a fold action
240 243 $ hg histedit 3 --commands $EDITED 2>&1 | fixbundle
241 244 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
242 245 merging e
243 246 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
244 247 Fix up the change (mess 39522b764e3d)
245 248 (hg histedit --continue to resume)
246 249
247 250 $ echo 'I can haz no commute' > e
248 251 $ hg resolve --mark e
249 252 (no more unresolved files)
250 253 continue: hg histedit --continue
251 254 $ hg histedit --continue 2>&1 | fixbundle
252 255 merging e
253 256 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
254 257 Fix up the change (pick 7b4e2f4b7bcd)
255 258 (hg histedit --continue to resume)
256 259 second edit also fails, but just continue
257 260 $ hg revert -r 'p1()' e
258 261 $ hg resolve --mark e
259 262 (no more unresolved files)
260 263 continue: hg histedit --continue
261 264 $ hg histedit --continue 2>&1 | fixbundle
262 265 7b4e2f4b7bcd: skipping changeset (no changes)
263 266
264 267 post message fix
265 268 $ hg log --graph
266 269 @ changeset: 6:7efe1373e4bc
267 270 | tag: tip
268 271 | user: test
269 272 | date: Thu Jan 01 00:00:00 1970 +0000
270 273 | summary: f
271 274 |
272 275 o changeset: 5:e334d87a1e55
273 276 | user: test
274 277 | date: Thu Jan 01 00:00:00 1970 +0000
275 278 | summary: does not commute with e
276 279 |
277 280 o changeset: 4:00f1c5383965
278 281 | user: test
279 282 | date: Thu Jan 01 00:00:00 1970 +0000
280 283 | summary: d
281 284 |
282 285 o changeset: 3:65a9a84f33fd
283 286 | user: test
284 287 | date: Thu Jan 01 00:00:00 1970 +0000
285 288 | summary: c
286 289 |
287 290 o changeset: 2:da6535b52e45
288 291 | user: test
289 292 | date: Thu Jan 01 00:00:00 1970 +0000
290 293 | summary: b
291 294 |
292 295 o changeset: 1:c1f09da44841
293 296 | user: test
294 297 | date: Thu Jan 01 00:00:00 1970 +0000
295 298 | summary: a
296 299 |
297 300 o changeset: 0:1715188a53c7
298 301 user: test
299 302 date: Thu Jan 01 00:00:00 1970 +0000
300 303 summary: Initial commit
301 304
302 305
303 306 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now