##// END OF EJS Templates
histedit: remove trailing space from warning message
Yuya Nishihara -
r41248:240f8e49 default
parent child Browse files
Show More
@@ -1,2241 +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 1596 # kludge: _chistedit only works for starting an edit, not aborting
1597 1597 # or continuing, so fall back to regular _texthistedit for those
1598 1598 # operations.
1599 1599 if ui.interface('histedit') == 'curses' and _getgoal(opts) == goalnew:
1600 1600 return _chistedit(ui, repo, *freeargs, **opts)
1601 1601 return _texthistedit(ui, repo, *freeargs, **opts)
1602 1602
1603 1603 def _texthistedit(ui, repo, *freeargs, **opts):
1604 1604 state = histeditstate(repo)
1605 1605 with repo.wlock() as wlock, repo.lock() as lock:
1606 1606 state.wlock = wlock
1607 1607 state.lock = lock
1608 1608 _histedit(ui, repo, state, *freeargs, **opts)
1609 1609
1610 1610 goalcontinue = 'continue'
1611 1611 goalabort = 'abort'
1612 1612 goaleditplan = 'edit-plan'
1613 1613 goalnew = 'new'
1614 1614
1615 1615 def _getgoal(opts):
1616 1616 if opts.get('continue'):
1617 1617 return goalcontinue
1618 1618 if opts.get('abort'):
1619 1619 return goalabort
1620 1620 if opts.get('edit_plan'):
1621 1621 return goaleditplan
1622 1622 return goalnew
1623 1623
1624 1624 def _readfile(ui, path):
1625 1625 if path == '-':
1626 1626 with ui.timeblockedsection('histedit'):
1627 1627 return ui.fin.read()
1628 1628 else:
1629 1629 with open(path, 'rb') as f:
1630 1630 return f.read()
1631 1631
1632 1632 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1633 1633 # TODO only abort if we try to histedit mq patches, not just
1634 1634 # blanket if mq patches are applied somewhere
1635 1635 mq = getattr(repo, 'mq', None)
1636 1636 if mq and mq.applied:
1637 1637 raise error.Abort(_('source has mq patches applied'))
1638 1638
1639 1639 # basic argument incompatibility processing
1640 1640 outg = opts.get('outgoing')
1641 1641 editplan = opts.get('edit_plan')
1642 1642 abort = opts.get('abort')
1643 1643 force = opts.get('force')
1644 1644 if force and not outg:
1645 1645 raise error.Abort(_('--force only allowed with --outgoing'))
1646 1646 if goal == 'continue':
1647 1647 if any((outg, abort, revs, freeargs, rules, editplan)):
1648 1648 raise error.Abort(_('no arguments allowed with --continue'))
1649 1649 elif goal == 'abort':
1650 1650 if any((outg, revs, freeargs, rules, editplan)):
1651 1651 raise error.Abort(_('no arguments allowed with --abort'))
1652 1652 elif goal == 'edit-plan':
1653 1653 if any((outg, revs, freeargs)):
1654 1654 raise error.Abort(_('only --commands argument allowed with '
1655 1655 '--edit-plan'))
1656 1656 else:
1657 1657 if state.inprogress():
1658 1658 raise error.Abort(_('history edit already in progress, try '
1659 1659 '--continue or --abort'))
1660 1660 if outg:
1661 1661 if revs:
1662 1662 raise error.Abort(_('no revisions allowed with --outgoing'))
1663 1663 if len(freeargs) > 1:
1664 1664 raise error.Abort(
1665 1665 _('only one repo argument allowed with --outgoing'))
1666 1666 else:
1667 1667 revs.extend(freeargs)
1668 1668 if len(revs) == 0:
1669 1669 defaultrev = destutil.desthistedit(ui, repo)
1670 1670 if defaultrev is not None:
1671 1671 revs.append(defaultrev)
1672 1672
1673 1673 if len(revs) != 1:
1674 1674 raise error.Abort(
1675 1675 _('histedit requires exactly one ancestor revision'))
1676 1676
1677 1677 def _histedit(ui, repo, state, *freeargs, **opts):
1678 1678 opts = pycompat.byteskwargs(opts)
1679 1679 fm = ui.formatter('histedit', opts)
1680 1680 fm.startitem()
1681 1681 goal = _getgoal(opts)
1682 1682 revs = opts.get('rev', [])
1683 1683 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1684 1684 rules = opts.get('commands', '')
1685 1685 state.keep = opts.get('keep', False)
1686 1686
1687 1687 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1688 1688
1689 1689 hastags = False
1690 1690 if revs:
1691 1691 revs = scmutil.revrange(repo, revs)
1692 1692 ctxs = [repo[rev] for rev in revs]
1693 1693 for ctx in ctxs:
1694 1694 tags = [tag for tag in ctx.tags() if tag != 'tip']
1695 1695 if not hastags:
1696 1696 hastags = len(tags)
1697 1697 if hastags:
1698 1698 if ui.promptchoice(_('warning: tags associated with the given'
1699 ' changeset will be lost after histedit. \n'
1699 ' changeset will be lost after histedit.\n'
1700 1700 'do you want to continue (yN)? $$ &Yes $$ &No'),
1701 1701 default=1):
1702 1702 raise error.Abort(_('histedit cancelled\n'))
1703 1703 # rebuild state
1704 1704 if goal == goalcontinue:
1705 1705 state.read()
1706 1706 state = bootstrapcontinue(ui, state, opts)
1707 1707 elif goal == goaleditplan:
1708 1708 _edithisteditplan(ui, repo, state, rules)
1709 1709 return
1710 1710 elif goal == goalabort:
1711 1711 _aborthistedit(ui, repo, state, nobackup=nobackup)
1712 1712 return
1713 1713 else:
1714 1714 # goal == goalnew
1715 1715 _newhistedit(ui, repo, state, revs, freeargs, opts)
1716 1716
1717 1717 _continuehistedit(ui, repo, state)
1718 1718 _finishhistedit(ui, repo, state, fm)
1719 1719 fm.end()
1720 1720
1721 1721 def _continuehistedit(ui, repo, state):
1722 1722 """This function runs after either:
1723 1723 - bootstrapcontinue (if the goal is 'continue')
1724 1724 - _newhistedit (if the goal is 'new')
1725 1725 """
1726 1726 # preprocess rules so that we can hide inner folds from the user
1727 1727 # and only show one editor
1728 1728 actions = state.actions[:]
1729 1729 for idx, (action, nextact) in enumerate(
1730 1730 zip(actions, actions[1:] + [None])):
1731 1731 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1732 1732 state.actions[idx].__class__ = _multifold
1733 1733
1734 1734 # Force an initial state file write, so the user can run --abort/continue
1735 1735 # even if there's an exception before the first transaction serialize.
1736 1736 state.write()
1737 1737
1738 1738 tr = None
1739 1739 # Don't use singletransaction by default since it rolls the entire
1740 1740 # transaction back if an unexpected exception happens (like a
1741 1741 # pretxncommit hook throws, or the user aborts the commit msg editor).
1742 1742 if ui.configbool("histedit", "singletransaction"):
1743 1743 # Don't use a 'with' for the transaction, since actions may close
1744 1744 # and reopen a transaction. For example, if the action executes an
1745 1745 # external process it may choose to commit the transaction first.
1746 1746 tr = repo.transaction('histedit')
1747 1747 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1748 1748 total=len(state.actions))
1749 1749 with progress, util.acceptintervention(tr):
1750 1750 while state.actions:
1751 1751 state.write(tr=tr)
1752 1752 actobj = state.actions[0]
1753 1753 progress.increment(item=actobj.torule())
1754 1754 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1755 1755 actobj.torule()))
1756 1756 parentctx, replacement_ = actobj.run()
1757 1757 state.parentctxnode = parentctx.node()
1758 1758 state.replacements.extend(replacement_)
1759 1759 state.actions.pop(0)
1760 1760
1761 1761 state.write()
1762 1762
1763 1763 def _finishhistedit(ui, repo, state, fm):
1764 1764 """This action runs when histedit is finishing its session"""
1765 1765 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1766 1766
1767 1767 mapping, tmpnodes, created, ntm = processreplacement(state)
1768 1768 if mapping:
1769 1769 for prec, succs in mapping.iteritems():
1770 1770 if not succs:
1771 1771 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1772 1772 else:
1773 1773 ui.debug('histedit: %s is replaced by %s\n' % (
1774 1774 node.short(prec), node.short(succs[0])))
1775 1775 if len(succs) > 1:
1776 1776 m = 'histedit: %s'
1777 1777 for n in succs[1:]:
1778 1778 ui.debug(m % node.short(n))
1779 1779
1780 1780 if not state.keep:
1781 1781 if mapping:
1782 1782 movetopmostbookmarks(repo, state.topmost, ntm)
1783 1783 # TODO update mq state
1784 1784 else:
1785 1785 mapping = {}
1786 1786
1787 1787 for n in tmpnodes:
1788 1788 if n in repo:
1789 1789 mapping[n] = ()
1790 1790
1791 1791 # remove entries about unknown nodes
1792 1792 nodemap = repo.unfiltered().changelog.nodemap
1793 1793 mapping = {k: v for k, v in mapping.items()
1794 1794 if k in nodemap and all(n in nodemap for n in v)}
1795 1795 scmutil.cleanupnodes(repo, mapping, 'histedit')
1796 1796 hf = fm.hexfunc
1797 1797 fl = fm.formatlist
1798 1798 fd = fm.formatdict
1799 1799 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1800 1800 for oldn, newn in mapping.iteritems()},
1801 1801 key="oldnode", value="newnodes")
1802 1802 fm.data(nodechanges=nodechanges)
1803 1803
1804 1804 state.clear()
1805 1805 if os.path.exists(repo.sjoin('undo')):
1806 1806 os.unlink(repo.sjoin('undo'))
1807 1807 if repo.vfs.exists('histedit-last-edit.txt'):
1808 1808 repo.vfs.unlink('histedit-last-edit.txt')
1809 1809
1810 1810 def _aborthistedit(ui, repo, state, nobackup=False):
1811 1811 try:
1812 1812 state.read()
1813 1813 __, leafs, tmpnodes, __ = processreplacement(state)
1814 1814 ui.debug('restore wc to old parent %s\n'
1815 1815 % node.short(state.topmost))
1816 1816
1817 1817 # Recover our old commits if necessary
1818 1818 if not state.topmost in repo and state.backupfile:
1819 1819 backupfile = repo.vfs.join(state.backupfile)
1820 1820 f = hg.openpath(ui, backupfile)
1821 1821 gen = exchange.readbundle(ui, f, backupfile)
1822 1822 with repo.transaction('histedit.abort') as tr:
1823 1823 bundle2.applybundle(repo, gen, tr, source='histedit',
1824 1824 url='bundle:' + backupfile)
1825 1825
1826 1826 os.remove(backupfile)
1827 1827
1828 1828 # check whether we should update away
1829 1829 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1830 1830 state.parentctxnode, leafs | tmpnodes):
1831 1831 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1832 1832 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1833 1833 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1834 1834 except Exception:
1835 1835 if state.inprogress():
1836 1836 ui.warn(_('warning: encountered an exception during histedit '
1837 1837 '--abort; the repository may not have been completely '
1838 1838 'cleaned up\n'))
1839 1839 raise
1840 1840 finally:
1841 1841 state.clear()
1842 1842
1843 1843 def _edithisteditplan(ui, repo, state, rules):
1844 1844 state.read()
1845 1845 if not rules:
1846 1846 comment = geteditcomment(ui,
1847 1847 node.short(state.parentctxnode),
1848 1848 node.short(state.topmost))
1849 1849 rules = ruleeditor(repo, ui, state.actions, comment)
1850 1850 else:
1851 1851 rules = _readfile(ui, rules)
1852 1852 actions = parserules(rules, state)
1853 1853 ctxs = [repo[act.node] \
1854 1854 for act in state.actions if act.node]
1855 1855 warnverifyactions(ui, repo, actions, state, ctxs)
1856 1856 state.actions = actions
1857 1857 state.write()
1858 1858
1859 1859 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1860 1860 outg = opts.get('outgoing')
1861 1861 rules = opts.get('commands', '')
1862 1862 force = opts.get('force')
1863 1863
1864 1864 cmdutil.checkunfinished(repo)
1865 1865 cmdutil.bailifchanged(repo)
1866 1866
1867 1867 topmost, empty = repo.dirstate.parents()
1868 1868 if outg:
1869 1869 if freeargs:
1870 1870 remote = freeargs[0]
1871 1871 else:
1872 1872 remote = None
1873 1873 root = findoutgoing(ui, repo, remote, force, opts)
1874 1874 else:
1875 1875 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1876 1876 if len(rr) != 1:
1877 1877 raise error.Abort(_('The specified revisions must have '
1878 1878 'exactly one common root'))
1879 1879 root = rr[0].node()
1880 1880
1881 1881 revs = between(repo, root, topmost, state.keep)
1882 1882 if not revs:
1883 1883 raise error.Abort(_('%s is not an ancestor of working directory') %
1884 1884 node.short(root))
1885 1885
1886 1886 ctxs = [repo[r] for r in revs]
1887 1887 if not rules:
1888 1888 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1889 1889 actions = [pick(state, r) for r in revs]
1890 1890 rules = ruleeditor(repo, ui, actions, comment)
1891 1891 else:
1892 1892 rules = _readfile(ui, rules)
1893 1893 actions = parserules(rules, state)
1894 1894 warnverifyactions(ui, repo, actions, state, ctxs)
1895 1895
1896 1896 parentctxnode = repo[root].parents()[0].node()
1897 1897
1898 1898 state.parentctxnode = parentctxnode
1899 1899 state.actions = actions
1900 1900 state.topmost = topmost
1901 1901 state.replacements = []
1902 1902
1903 1903 ui.log("histedit", "%d actions to histedit\n", len(actions),
1904 1904 histedit_num_actions=len(actions))
1905 1905
1906 1906 # Create a backup so we can always abort completely.
1907 1907 backupfile = None
1908 1908 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1909 1909 backupfile = repair.backupbundle(repo, [parentctxnode],
1910 1910 [topmost], root, 'histedit')
1911 1911 state.backupfile = backupfile
1912 1912
1913 1913 def _getsummary(ctx):
1914 1914 # a common pattern is to extract the summary but default to the empty
1915 1915 # string
1916 1916 summary = ctx.description() or ''
1917 1917 if summary:
1918 1918 summary = summary.splitlines()[0]
1919 1919 return summary
1920 1920
1921 1921 def bootstrapcontinue(ui, state, opts):
1922 1922 repo = state.repo
1923 1923
1924 1924 ms = mergemod.mergestate.read(repo)
1925 1925 mergeutil.checkunresolved(ms)
1926 1926
1927 1927 if state.actions:
1928 1928 actobj = state.actions.pop(0)
1929 1929
1930 1930 if _isdirtywc(repo):
1931 1931 actobj.continuedirty()
1932 1932 if _isdirtywc(repo):
1933 1933 abortdirty()
1934 1934
1935 1935 parentctx, replacements = actobj.continueclean()
1936 1936
1937 1937 state.parentctxnode = parentctx.node()
1938 1938 state.replacements.extend(replacements)
1939 1939
1940 1940 return state
1941 1941
1942 1942 def between(repo, old, new, keep):
1943 1943 """select and validate the set of revision to edit
1944 1944
1945 1945 When keep is false, the specified set can't have children."""
1946 1946 revs = repo.revs('%n::%n', old, new)
1947 1947 if revs and not keep:
1948 1948 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1949 1949 repo.revs('(%ld::) - (%ld)', revs, revs)):
1950 1950 raise error.Abort(_('can only histedit a changeset together '
1951 1951 'with all its descendants'))
1952 1952 if repo.revs('(%ld) and merge()', revs):
1953 1953 raise error.Abort(_('cannot edit history that contains merges'))
1954 1954 root = repo[revs.first()] # list is already sorted by repo.revs()
1955 1955 if not root.mutable():
1956 1956 raise error.Abort(_('cannot edit public changeset: %s') % root,
1957 1957 hint=_("see 'hg help phases' for details"))
1958 1958 return pycompat.maplist(repo.changelog.node, revs)
1959 1959
1960 1960 def ruleeditor(repo, ui, actions, editcomment=""):
1961 1961 """open an editor to edit rules
1962 1962
1963 1963 rules are in the format [ [act, ctx], ...] like in state.rules
1964 1964 """
1965 1965 if repo.ui.configbool("experimental", "histedit.autoverb"):
1966 1966 newact = util.sortdict()
1967 1967 for act in actions:
1968 1968 ctx = repo[act.node]
1969 1969 summary = _getsummary(ctx)
1970 1970 fword = summary.split(' ', 1)[0].lower()
1971 1971 added = False
1972 1972
1973 1973 # if it doesn't end with the special character '!' just skip this
1974 1974 if fword.endswith('!'):
1975 1975 fword = fword[:-1]
1976 1976 if fword in primaryactions | secondaryactions | tertiaryactions:
1977 1977 act.verb = fword
1978 1978 # get the target summary
1979 1979 tsum = summary[len(fword) + 1:].lstrip()
1980 1980 # safe but slow: reverse iterate over the actions so we
1981 1981 # don't clash on two commits having the same summary
1982 1982 for na, l in reversed(list(newact.iteritems())):
1983 1983 actx = repo[na.node]
1984 1984 asum = _getsummary(actx)
1985 1985 if asum == tsum:
1986 1986 added = True
1987 1987 l.append(act)
1988 1988 break
1989 1989
1990 1990 if not added:
1991 1991 newact[act] = []
1992 1992
1993 1993 # copy over and flatten the new list
1994 1994 actions = []
1995 1995 for na, l in newact.iteritems():
1996 1996 actions.append(na)
1997 1997 actions += l
1998 1998
1999 1999 rules = '\n'.join([act.torule() for act in actions])
2000 2000 rules += '\n\n'
2001 2001 rules += editcomment
2002 2002 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2003 2003 repopath=repo.path, action='histedit')
2004 2004
2005 2005 # Save edit rules in .hg/histedit-last-edit.txt in case
2006 2006 # the user needs to ask for help after something
2007 2007 # surprising happens.
2008 2008 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2009 2009 f.write(rules)
2010 2010
2011 2011 return rules
2012 2012
2013 2013 def parserules(rules, state):
2014 2014 """Read the histedit rules string and return list of action objects """
2015 2015 rules = [l for l in (r.strip() for r in rules.splitlines())
2016 2016 if l and not l.startswith('#')]
2017 2017 actions = []
2018 2018 for r in rules:
2019 2019 if ' ' not in r:
2020 2020 raise error.ParseError(_('malformed line "%s"') % r)
2021 2021 verb, rest = r.split(' ', 1)
2022 2022
2023 2023 if verb not in actiontable:
2024 2024 raise error.ParseError(_('unknown action "%s"') % verb)
2025 2025
2026 2026 action = actiontable[verb].fromrule(state, rest)
2027 2027 actions.append(action)
2028 2028 return actions
2029 2029
2030 2030 def warnverifyactions(ui, repo, actions, state, ctxs):
2031 2031 try:
2032 2032 verifyactions(actions, state, ctxs)
2033 2033 except error.ParseError:
2034 2034 if repo.vfs.exists('histedit-last-edit.txt'):
2035 2035 ui.warn(_('warning: histedit rules saved '
2036 2036 'to: .hg/histedit-last-edit.txt\n'))
2037 2037 raise
2038 2038
2039 2039 def verifyactions(actions, state, ctxs):
2040 2040 """Verify that there exists exactly one action per given changeset and
2041 2041 other constraints.
2042 2042
2043 2043 Will abort if there are to many or too few rules, a malformed rule,
2044 2044 or a rule on a changeset outside of the user-given range.
2045 2045 """
2046 2046 expected = set(c.node() for c in ctxs)
2047 2047 seen = set()
2048 2048 prev = None
2049 2049
2050 2050 if actions and actions[0].verb in ['roll', 'fold']:
2051 2051 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2052 2052 actions[0].verb)
2053 2053
2054 2054 for action in actions:
2055 2055 action.verify(prev, expected, seen)
2056 2056 prev = action
2057 2057 if action.node is not None:
2058 2058 seen.add(action.node)
2059 2059 missing = sorted(expected - seen) # sort to stabilize output
2060 2060
2061 2061 if state.repo.ui.configbool('histedit', 'dropmissing'):
2062 2062 if len(actions) == 0:
2063 2063 raise error.ParseError(_('no rules provided'),
2064 2064 hint=_('use strip extension to remove commits'))
2065 2065
2066 2066 drops = [drop(state, n) for n in missing]
2067 2067 # put the in the beginning so they execute immediately and
2068 2068 # don't show in the edit-plan in the future
2069 2069 actions[:0] = drops
2070 2070 elif missing:
2071 2071 raise error.ParseError(_('missing rules for changeset %s') %
2072 2072 node.short(missing[0]),
2073 2073 hint=_('use "drop %s" to discard, see also: '
2074 2074 "'hg help -e histedit.config'")
2075 2075 % node.short(missing[0]))
2076 2076
2077 2077 def adjustreplacementsfrommarkers(repo, oldreplacements):
2078 2078 """Adjust replacements from obsolescence markers
2079 2079
2080 2080 Replacements structure is originally generated based on
2081 2081 histedit's state and does not account for changes that are
2082 2082 not recorded there. This function fixes that by adding
2083 2083 data read from obsolescence markers"""
2084 2084 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2085 2085 return oldreplacements
2086 2086
2087 2087 unfi = repo.unfiltered()
2088 2088 nm = unfi.changelog.nodemap
2089 2089 obsstore = repo.obsstore
2090 2090 newreplacements = list(oldreplacements)
2091 2091 oldsuccs = [r[1] for r in oldreplacements]
2092 2092 # successors that have already been added to succstocheck once
2093 2093 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2094 2094 succstocheck = list(seensuccs)
2095 2095 while succstocheck:
2096 2096 n = succstocheck.pop()
2097 2097 missing = nm.get(n) is None
2098 2098 markers = obsstore.successors.get(n, ())
2099 2099 if missing and not markers:
2100 2100 # dead end, mark it as such
2101 2101 newreplacements.append((n, ()))
2102 2102 for marker in markers:
2103 2103 nsuccs = marker[1]
2104 2104 newreplacements.append((n, nsuccs))
2105 2105 for nsucc in nsuccs:
2106 2106 if nsucc not in seensuccs:
2107 2107 seensuccs.add(nsucc)
2108 2108 succstocheck.append(nsucc)
2109 2109
2110 2110 return newreplacements
2111 2111
2112 2112 def processreplacement(state):
2113 2113 """process the list of replacements to return
2114 2114
2115 2115 1) the final mapping between original and created nodes
2116 2116 2) the list of temporary node created by histedit
2117 2117 3) the list of new commit created by histedit"""
2118 2118 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2119 2119 allsuccs = set()
2120 2120 replaced = set()
2121 2121 fullmapping = {}
2122 2122 # initialize basic set
2123 2123 # fullmapping records all operations recorded in replacement
2124 2124 for rep in replacements:
2125 2125 allsuccs.update(rep[1])
2126 2126 replaced.add(rep[0])
2127 2127 fullmapping.setdefault(rep[0], set()).update(rep[1])
2128 2128 new = allsuccs - replaced
2129 2129 tmpnodes = allsuccs & replaced
2130 2130 # Reduce content fullmapping into direct relation between original nodes
2131 2131 # and final node created during history edition
2132 2132 # Dropped changeset are replaced by an empty list
2133 2133 toproceed = set(fullmapping)
2134 2134 final = {}
2135 2135 while toproceed:
2136 2136 for x in list(toproceed):
2137 2137 succs = fullmapping[x]
2138 2138 for s in list(succs):
2139 2139 if s in toproceed:
2140 2140 # non final node with unknown closure
2141 2141 # We can't process this now
2142 2142 break
2143 2143 elif s in final:
2144 2144 # non final node, replace with closure
2145 2145 succs.remove(s)
2146 2146 succs.update(final[s])
2147 2147 else:
2148 2148 final[x] = succs
2149 2149 toproceed.remove(x)
2150 2150 # remove tmpnodes from final mapping
2151 2151 for n in tmpnodes:
2152 2152 del final[n]
2153 2153 # we expect all changes involved in final to exist in the repo
2154 2154 # turn `final` into list (topologically sorted)
2155 2155 nm = state.repo.changelog.nodemap
2156 2156 for prec, succs in final.items():
2157 2157 final[prec] = sorted(succs, key=nm.get)
2158 2158
2159 2159 # computed topmost element (necessary for bookmark)
2160 2160 if new:
2161 2161 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2162 2162 elif not final:
2163 2163 # Nothing rewritten at all. we won't need `newtopmost`
2164 2164 # It is the same as `oldtopmost` and `processreplacement` know it
2165 2165 newtopmost = None
2166 2166 else:
2167 2167 # every body died. The newtopmost is the parent of the root.
2168 2168 r = state.repo.changelog.rev
2169 2169 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2170 2170
2171 2171 return final, tmpnodes, new, newtopmost
2172 2172
2173 2173 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2174 2174 """Move bookmark from oldtopmost to newly created topmost
2175 2175
2176 2176 This is arguably a feature and we may only want that for the active
2177 2177 bookmark. But the behavior is kept compatible with the old version for now.
2178 2178 """
2179 2179 if not oldtopmost or not newtopmost:
2180 2180 return
2181 2181 oldbmarks = repo.nodebookmarks(oldtopmost)
2182 2182 if oldbmarks:
2183 2183 with repo.lock(), repo.transaction('histedit') as tr:
2184 2184 marks = repo._bookmarks
2185 2185 changes = []
2186 2186 for name in oldbmarks:
2187 2187 changes.append((name, newtopmost))
2188 2188 marks.applychanges(repo, tr, changes)
2189 2189
2190 2190 def cleanupnode(ui, repo, nodes, nobackup=False):
2191 2191 """strip a group of nodes from the repository
2192 2192
2193 2193 The set of node to strip may contains unknown nodes."""
2194 2194 with repo.lock():
2195 2195 # do not let filtering get in the way of the cleanse
2196 2196 # we should probably get rid of obsolescence marker created during the
2197 2197 # histedit, but we currently do not have such information.
2198 2198 repo = repo.unfiltered()
2199 2199 # Find all nodes that need to be stripped
2200 2200 # (we use %lr instead of %ln to silently ignore unknown items)
2201 2201 nm = repo.changelog.nodemap
2202 2202 nodes = sorted(n for n in nodes if n in nm)
2203 2203 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2204 2204 if roots:
2205 2205 backup = not nobackup
2206 2206 repair.strip(ui, repo, roots, backup=backup)
2207 2207
2208 2208 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2209 2209 if isinstance(nodelist, str):
2210 2210 nodelist = [nodelist]
2211 2211 state = histeditstate(repo)
2212 2212 if state.inprogress():
2213 2213 state.read()
2214 2214 histedit_nodes = {action.node for action
2215 2215 in state.actions if action.node}
2216 2216 common_nodes = histedit_nodes & set(nodelist)
2217 2217 if common_nodes:
2218 2218 raise error.Abort(_("histedit in progress, can't strip %s")
2219 2219 % ', '.join(node.short(x) for x in common_nodes))
2220 2220 return orig(ui, repo, nodelist, *args, **kwargs)
2221 2221
2222 2222 extensions.wrapfunction(repair, 'strip', stripwrapper)
2223 2223
2224 2224 def summaryhook(ui, repo):
2225 2225 state = histeditstate(repo)
2226 2226 if not state.inprogress():
2227 2227 return
2228 2228 state.read()
2229 2229 if state.actions:
2230 2230 # i18n: column positioning for "hg summary"
2231 2231 ui.write(_('hist: %s (histedit --continue)\n') %
2232 2232 (ui.label(_('%d remaining'), 'histedit.remaining') %
2233 2233 len(state.actions)))
2234 2234
2235 2235 def extsetup(ui):
2236 2236 cmdutil.summaryhooks.add('histedit', summaryhook)
2237 2237 cmdutil.unfinishedstates.append(
2238 2238 ['histedit-state', False, True, _('histedit in progress'),
2239 2239 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2240 2240 cmdutil.afterresolvedstates.append(
2241 2241 ['histedit-state', _('hg histedit --continue')])
@@ -1,504 +1,504 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > histedit=
6 6 > strip=
7 7 > EOF
8 8
9 9 $ initrepo ()
10 10 > {
11 11 > hg init r
12 12 > cd r
13 13 > for x in a b c d e f g; do
14 14 > echo $x > $x
15 15 > hg add $x
16 16 > hg ci -m $x
17 17 > done
18 18 > }
19 19
20 20 $ initrepo
21 21
22 22 log before edit
23 23 $ hg log --graph
24 24 @ changeset: 6:3c6a8ed2ebe8
25 25 | tag: tip
26 26 | user: test
27 27 | date: Thu Jan 01 00:00:00 1970 +0000
28 28 | summary: g
29 29 |
30 30 o changeset: 5:652413bf663e
31 31 | user: test
32 32 | date: Thu Jan 01 00:00:00 1970 +0000
33 33 | summary: f
34 34 |
35 35 o changeset: 4:e860deea161a
36 36 | user: test
37 37 | date: Thu Jan 01 00:00:00 1970 +0000
38 38 | summary: e
39 39 |
40 40 o changeset: 3:055a42cdd887
41 41 | user: test
42 42 | date: Thu Jan 01 00:00:00 1970 +0000
43 43 | summary: d
44 44 |
45 45 o changeset: 2:177f92b77385
46 46 | user: test
47 47 | date: Thu Jan 01 00:00:00 1970 +0000
48 48 | summary: c
49 49 |
50 50 o changeset: 1:d2ae7f538514
51 51 | user: test
52 52 | date: Thu Jan 01 00:00:00 1970 +0000
53 53 | summary: b
54 54 |
55 55 o changeset: 0:cb9a9f314b8b
56 56 user: test
57 57 date: Thu Jan 01 00:00:00 1970 +0000
58 58 summary: a
59 59
60 60 dirty a file
61 61 $ echo a > g
62 62 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF
63 63 > EOF
64 64 abort: uncommitted changes
65 65 [255]
66 66 $ echo g > g
67 67
68 68 edit the history
69 69 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
70 70 > pick 177f92b77385 c
71 71 > pick 055a42cdd887 d
72 72 > edit e860deea161a e
73 73 > pick 652413bf663e f
74 74 > pick 3c6a8ed2ebe8 g
75 75 > EOF
76 76 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
77 77 Editing (e860deea161a), you may commit or record as needed now.
78 78 (hg histedit --continue to resume)
79 79
80 80 try to update and get an error
81 81 $ hg update tip
82 82 abort: histedit in progress
83 83 (use 'hg histedit --continue' or 'hg histedit --abort')
84 84 [255]
85 85
86 86 edit the plan via the editor
87 87 $ cat >> $TESTTMP/editplan.sh <<EOF
88 88 > cat > \$1 <<EOF2
89 89 > drop e860deea161a e
90 90 > drop 652413bf663e f
91 91 > drop 3c6a8ed2ebe8 g
92 92 > EOF2
93 93 > EOF
94 94 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
95 95 $ cat .hg/histedit-state
96 96 v1
97 97 055a42cdd88768532f9cf79daa407fc8d138de9b
98 98 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
99 99 False
100 100 3
101 101 drop
102 102 e860deea161a2f77de56603b340ebbb4536308ae
103 103 drop
104 104 652413bf663ef2a641cab26574e46d5f5a64a55a
105 105 drop
106 106 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
107 107 0
108 108 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
109 109
110 110 edit the plan via --commands
111 111 $ hg histedit --edit-plan --commands - 2>&1 << EOF
112 112 > edit e860deea161a e
113 113 > pick 652413bf663e f
114 114 > drop 3c6a8ed2ebe8 g
115 115 > EOF
116 116 $ cat .hg/histedit-state
117 117 v1
118 118 055a42cdd88768532f9cf79daa407fc8d138de9b
119 119 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
120 120 False
121 121 3
122 122 edit
123 123 e860deea161a2f77de56603b340ebbb4536308ae
124 124 pick
125 125 652413bf663ef2a641cab26574e46d5f5a64a55a
126 126 drop
127 127 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
128 128 0
129 129 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
130 130
131 131 Go at a random point and try to continue
132 132
133 133 $ hg id -n
134 134 3+
135 135 $ hg up 0
136 136 abort: histedit in progress
137 137 (use 'hg histedit --continue' or 'hg histedit --abort')
138 138 [255]
139 139
140 140 Try to delete necessary commit
141 141 $ hg strip -r 652413b
142 142 abort: histedit in progress, can't strip 652413bf663e
143 143 [255]
144 144
145 145 commit, then edit the revision
146 146 $ hg ci -m 'wat'
147 147 created new head
148 148 $ echo a > e
149 149
150 150 qnew should fail while we're in the middle of the edit step
151 151
152 152 $ hg --config extensions.mq= qnew please-fail
153 153 abort: histedit in progress
154 154 (use 'hg histedit --continue' or 'hg histedit --abort')
155 155 [255]
156 156 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
157 157
158 158 $ hg log --graph
159 159 @ changeset: 6:b5f70786f9b0
160 160 | tag: tip
161 161 | user: test
162 162 | date: Thu Jan 01 00:00:00 1970 +0000
163 163 | summary: f
164 164 |
165 165 o changeset: 5:a5e1ba2f7afb
166 166 | user: test
167 167 | date: Thu Jan 01 00:00:00 1970 +0000
168 168 | summary: foobaz
169 169 |
170 170 o changeset: 4:1a60820cd1f6
171 171 | user: test
172 172 | date: Thu Jan 01 00:00:00 1970 +0000
173 173 | summary: wat
174 174 |
175 175 o changeset: 3:055a42cdd887
176 176 | user: test
177 177 | date: Thu Jan 01 00:00:00 1970 +0000
178 178 | summary: d
179 179 |
180 180 o changeset: 2:177f92b77385
181 181 | user: test
182 182 | date: Thu Jan 01 00:00:00 1970 +0000
183 183 | summary: c
184 184 |
185 185 o changeset: 1:d2ae7f538514
186 186 | user: test
187 187 | date: Thu Jan 01 00:00:00 1970 +0000
188 188 | summary: b
189 189 |
190 190 o changeset: 0:cb9a9f314b8b
191 191 user: test
192 192 date: Thu Jan 01 00:00:00 1970 +0000
193 193 summary: a
194 194
195 195
196 196 $ hg cat e
197 197 a
198 198
199 199 Stripping necessary commits should not break --abort
200 200
201 201 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
202 202 > edit 1a60820cd1f6 wat
203 203 > pick a5e1ba2f7afb foobaz
204 204 > pick b5f70786f9b0 g
205 205 > EOF
206 206 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
207 207 Editing (1a60820cd1f6), you may commit or record as needed now.
208 208 (hg histedit --continue to resume)
209 209
210 210 $ mv .hg/histedit-state .hg/histedit-state.bak
211 211 $ hg strip -q -r b5f70786f9b0
212 212 $ mv .hg/histedit-state.bak .hg/histedit-state
213 213 $ hg histedit --abort
214 214 adding changesets
215 215 adding manifests
216 216 adding file changes
217 217 added 1 changesets with 1 changes to 3 files
218 218 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
219 219 $ hg log -r .
220 220 changeset: 6:b5f70786f9b0
221 221 tag: tip
222 222 user: test
223 223 date: Thu Jan 01 00:00:00 1970 +0000
224 224 summary: f
225 225
226 226
227 227 check histedit_source
228 228
229 229 $ hg log --debug --rev 5
230 230 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
231 231 phase: draft
232 232 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
233 233 parent: -1:0000000000000000000000000000000000000000
234 234 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
235 235 user: test
236 236 date: Thu Jan 01 00:00:00 1970 +0000
237 237 files: e
238 238 extra: branch=default
239 239 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
240 240 description:
241 241 foobaz
242 242
243 243
244 244
245 245 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
246 246 > edit b5f70786f9b0 f
247 247 > EOF
248 248 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
249 249 Editing (b5f70786f9b0), you may commit or record as needed now.
250 250 (hg histedit --continue to resume)
251 251 $ hg status
252 252 A f
253 253
254 254 $ hg summary
255 255 parent: 5:a5e1ba2f7afb
256 256 foobaz
257 257 branch: default
258 258 commit: 1 added (new branch head)
259 259 update: 1 new changesets (update)
260 260 phases: 7 draft
261 261 hist: 1 remaining (histedit --continue)
262 262
263 263 (test also that editor is invoked if histedit is continued for
264 264 "edit" action)
265 265
266 266 $ HGEDITOR='cat' hg histedit --continue
267 267 f
268 268
269 269
270 270 HG: Enter commit message. Lines beginning with 'HG:' are removed.
271 271 HG: Leave message empty to abort commit.
272 272 HG: --
273 273 HG: user: test
274 274 HG: branch 'default'
275 275 HG: added f
276 276 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-histedit.hg
277 277
278 278 $ hg status
279 279
280 280 log after edit
281 281 $ hg log --limit 1
282 282 changeset: 6:a107ee126658
283 283 tag: tip
284 284 user: test
285 285 date: Thu Jan 01 00:00:00 1970 +0000
286 286 summary: f
287 287
288 288
289 289 say we'll change the message, but don't.
290 290 $ cat > ../edit.sh <<EOF
291 291 > cat "\$1" | sed s/pick/mess/ > tmp
292 292 > mv tmp "\$1"
293 293 > EOF
294 294 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
295 295 $ hg status
296 296 $ hg log --limit 1
297 297 changeset: 6:1fd3b2fe7754
298 298 tag: tip
299 299 user: test
300 300 date: Thu Jan 01 00:00:00 1970 +0000
301 301 summary: f
302 302
303 303
304 304 modify the message
305 305
306 306 check saving last-message.txt, at first
307 307
308 308 $ cat > $TESTTMP/commitfailure.py <<EOF
309 309 > from mercurial import error
310 310 > def reposetup(ui, repo):
311 311 > class commitfailure(repo.__class__):
312 312 > def commit(self, *args, **kwargs):
313 313 > raise error.Abort('emulating unexpected abort')
314 314 > repo.__class__ = commitfailure
315 315 > EOF
316 316 $ cat >> .hg/hgrc <<EOF
317 317 > [extensions]
318 318 > # this failure occurs before editor invocation
319 319 > commitfailure = $TESTTMP/commitfailure.py
320 320 > EOF
321 321
322 322 $ cat > $TESTTMP/editor.sh <<EOF
323 323 > echo "==== before editing"
324 324 > cat \$1
325 325 > echo "===="
326 326 > echo "check saving last-message.txt" >> \$1
327 327 > EOF
328 328
329 329 (test that editor is not invoked before transaction starting)
330 330
331 331 $ rm -f .hg/last-message.txt
332 332 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
333 333 > mess 1fd3b2fe7754 f
334 334 > EOF
335 335 abort: emulating unexpected abort
336 336 $ test -f .hg/last-message.txt
337 337 [1]
338 338
339 339 $ cat >> .hg/hgrc <<EOF
340 340 > [extensions]
341 341 > commitfailure = !
342 342 > EOF
343 343 $ hg histedit --abort -q
344 344
345 345 (test that editor is invoked and commit message is saved into
346 346 "last-message.txt")
347 347
348 348 $ cat >> .hg/hgrc <<EOF
349 349 > [hooks]
350 350 > # this failure occurs after editor invocation
351 351 > pretxncommit.unexpectedabort = false
352 352 > EOF
353 353
354 354 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
355 355 A f
356 356
357 357 $ rm -f .hg/last-message.txt
358 358 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
359 359 > mess 1fd3b2fe7754 f
360 360 > EOF
361 361 ==== before editing
362 362 f
363 363
364 364
365 365 HG: Enter commit message. Lines beginning with 'HG:' are removed.
366 366 HG: Leave message empty to abort commit.
367 367 HG: --
368 368 HG: user: test
369 369 HG: branch 'default'
370 370 HG: added f
371 371 ====
372 372 note: commit message saved in .hg/last-message.txt
373 373 transaction abort!
374 374 rollback completed
375 375 abort: pretxncommit.unexpectedabort hook exited with status 1
376 376 [255]
377 377 $ cat .hg/last-message.txt
378 378 f
379 379
380 380
381 381 check saving last-message.txt
382 382
383 383 (test also that editor is invoked if histedit is continued for "message"
384 384 action)
385 385
386 386 $ HGEDITOR=cat hg histedit --continue
387 387 f
388 388
389 389
390 390 HG: Enter commit message. Lines beginning with 'HG:' are removed.
391 391 HG: Leave message empty to abort commit.
392 392 HG: --
393 393 HG: user: test
394 394 HG: branch 'default'
395 395 HG: added f
396 396 note: commit message saved in .hg/last-message.txt
397 397 transaction abort!
398 398 rollback completed
399 399 abort: pretxncommit.unexpectedabort hook exited with status 1
400 400 [255]
401 401
402 402 $ cat >> .hg/hgrc <<EOF
403 403 > [hooks]
404 404 > pretxncommit.unexpectedabort =
405 405 > EOF
406 406 $ hg histedit --abort -q
407 407
408 408 then, check "modify the message" itself
409 409
410 410 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
411 411 > mess 1fd3b2fe7754 f
412 412 > EOF
413 413 $ hg status
414 414 $ hg log --limit 1
415 415 changeset: 6:62feedb1200e
416 416 tag: tip
417 417 user: test
418 418 date: Thu Jan 01 00:00:00 1970 +0000
419 419 summary: f
420 420
421 421
422 422 rollback should not work after a histedit
423 423 $ hg rollback
424 424 no rollback information available
425 425 [1]
426 426
427 427 $ cd ..
428 428 $ hg clone -qr0 r r0
429 429 $ cd r0
430 430 $ hg phase -fdr0
431 431 $ hg histedit --commands - 0 2>&1 << EOF
432 432 > edit cb9a9f314b8b a > $EDITED
433 433 > EOF
434 434 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
435 435 adding a
436 436 Editing (cb9a9f314b8b), you may commit or record as needed now.
437 437 (hg histedit --continue to resume)
438 438 [1]
439 439 $ HGEDITOR=true hg histedit --continue
440 440 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-histedit.hg
441 441
442 442 $ hg log -G
443 443 @ changeset: 0:0efcea34f18a
444 444 tag: tip
445 445 user: test
446 446 date: Thu Jan 01 00:00:00 1970 +0000
447 447 summary: a
448 448
449 449 $ echo foo >> b
450 450 $ hg addr
451 451 adding b
452 452 $ hg ci -m 'add b'
453 453 $ echo foo >> a
454 454 $ hg ci -m 'extend a'
455 455 $ hg phase --public 1
456 456 Attempting to fold a change into a public change should not work:
457 457 $ cat > ../edit.sh <<EOF
458 458 > cat "\$1" | sed s/pick/fold/ > tmp
459 459 > mv tmp "\$1"
460 460 > EOF
461 461 $ HGEDITOR="sh ../edit.sh" hg histedit 2
462 462 warning: histedit rules saved to: .hg/histedit-last-edit.txt
463 463 hg: parse error: first changeset cannot use verb "fold"
464 464 [255]
465 465 $ cat .hg/histedit-last-edit.txt
466 466 fold 0012be4a27ea 2 extend a
467 467
468 468 # Edit history between 0012be4a27ea and 0012be4a27ea
469 469 #
470 470 # Commits are listed from least to most recent
471 471 #
472 472 # You can reorder changesets by reordering the lines
473 473 #
474 474 # Commands:
475 475 #
476 476 # e, edit = use commit, but stop for amending
477 477 # m, mess = edit commit message without changing commit content
478 478 # p, fold = use commit
479 479 # b, base = checkout changeset and apply further changesets from there
480 480 # d, drop = remove commit from history
481 481 # f, fold = use commit, but combine it with the one above
482 482 # r, roll = like fold, but discard this commit's description and date
483 483 #
484 484
485 485 $ cd ..
486 486
487 487 warn the user on editing tagged commits
488 488
489 489 $ hg init issue4017
490 490 $ cd issue4017
491 491 $ echo > a
492 492 $ hg ci -Am 'add a'
493 493 adding a
494 494 $ hg tag a
495 495 $ hg tags
496 496 tip 1:bd7ee4f3939b
497 497 a 0:a8a82d372bb3
498 498 $ hg histedit
499 warning: tags associated with the given changeset will be lost after histedit.
499 warning: tags associated with the given changeset will be lost after histedit.
500 500 do you want to continue (yN)? n
501 501 abort: histedit cancelled
502 502
503 503 [255]
504 504 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now