##// END OF EJS Templates
histedit: use more specific exceptions for more detailed exit codes...
Martin von Zweigbergk -
r48868:f27a8339 default
parent child Browse files
Show More
@@ -1,2651 +1,2655
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 allow edits before making new commit
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 allow edits before making new commit
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 The summary of a change can be customized as well::
160 160
161 161 [histedit]
162 162 summary-template = '{rev} {bookmarks} {desc|firstline}'
163 163
164 164 The customized summary should be kept short enough that rule lines
165 165 will fit in the configured line length. See above if that requires
166 166 customization.
167 167
168 168 ``hg histedit`` attempts to automatically choose an appropriate base
169 169 revision to use. To change which base revision is used, define a
170 170 revset in your configuration file::
171 171
172 172 [histedit]
173 173 defaultrev = only(.) & draft()
174 174
175 175 By default each edited revision needs to be present in histedit commands.
176 176 To remove revision you need to use ``drop`` operation. You can configure
177 177 the drop to be implicit for missing commits by adding::
178 178
179 179 [histedit]
180 180 dropmissing = True
181 181
182 182 By default, histedit will close the transaction after each action. For
183 183 performance purposes, you can configure histedit to use a single transaction
184 184 across the entire histedit. WARNING: This setting introduces a significant risk
185 185 of losing the work you've done in a histedit if the histedit aborts
186 186 unexpectedly::
187 187
188 188 [histedit]
189 189 singletransaction = True
190 190
191 191 """
192 192
193 193 from __future__ import absolute_import
194 194
195 195 # chistedit dependencies that are not available everywhere
196 196 try:
197 197 import fcntl
198 198 import termios
199 199 except ImportError:
200 200 fcntl = None
201 201 termios = None
202 202
203 203 import functools
204 204 import os
205 205 import struct
206 206
207 207 from mercurial.i18n import _
208 208 from mercurial.pycompat import (
209 209 getattr,
210 210 open,
211 211 )
212 212 from mercurial.node import (
213 213 bin,
214 214 hex,
215 215 short,
216 216 )
217 217 from mercurial import (
218 218 bundle2,
219 219 cmdutil,
220 220 context,
221 221 copies,
222 222 destutil,
223 223 discovery,
224 224 encoding,
225 225 error,
226 226 exchange,
227 227 extensions,
228 228 hg,
229 229 logcmdutil,
230 230 merge as mergemod,
231 231 mergestate as mergestatemod,
232 232 mergeutil,
233 233 obsolete,
234 234 pycompat,
235 235 registrar,
236 236 repair,
237 237 rewriteutil,
238 238 scmutil,
239 239 state as statemod,
240 240 util,
241 241 )
242 242 from mercurial.utils import (
243 243 dateutil,
244 244 stringutil,
245 245 urlutil,
246 246 )
247 247
248 248 pickle = util.pickle
249 249 cmdtable = {}
250 250 command = registrar.command(cmdtable)
251 251
252 252 configtable = {}
253 253 configitem = registrar.configitem(configtable)
254 254 configitem(
255 255 b'experimental',
256 256 b'histedit.autoverb',
257 257 default=False,
258 258 )
259 259 configitem(
260 260 b'histedit',
261 261 b'defaultrev',
262 262 default=None,
263 263 )
264 264 configitem(
265 265 b'histedit',
266 266 b'dropmissing',
267 267 default=False,
268 268 )
269 269 configitem(
270 270 b'histedit',
271 271 b'linelen',
272 272 default=80,
273 273 )
274 274 configitem(
275 275 b'histedit',
276 276 b'singletransaction',
277 277 default=False,
278 278 )
279 279 configitem(
280 280 b'ui',
281 281 b'interface.histedit',
282 282 default=None,
283 283 )
284 284 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
285 285
286 286 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
287 287 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
288 288 # be specifying the version(s) of Mercurial they are tested with, or
289 289 # leave the attribute unspecified.
290 290 testedwith = b'ships-with-hg-core'
291 291
292 292 actiontable = {}
293 293 primaryactions = set()
294 294 secondaryactions = set()
295 295 tertiaryactions = set()
296 296 internalactions = set()
297 297
298 298
299 299 def geteditcomment(ui, first, last):
300 300 """construct the editor comment
301 301 The comment includes::
302 302 - an intro
303 303 - sorted primary commands
304 304 - sorted short commands
305 305 - sorted long commands
306 306 - additional hints
307 307
308 308 Commands are only included once.
309 309 """
310 310 intro = _(
311 311 b"""Edit history between %s and %s
312 312
313 313 Commits are listed from least to most recent
314 314
315 315 You can reorder changesets by reordering the lines
316 316
317 317 Commands:
318 318 """
319 319 )
320 320 actions = []
321 321
322 322 def addverb(v):
323 323 a = actiontable[v]
324 324 lines = a.message.split(b"\n")
325 325 if len(a.verbs):
326 326 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
327 327 actions.append(b" %s = %s" % (v, lines[0]))
328 328 actions.extend([b' %s'] * (len(lines) - 1))
329 329
330 330 for v in (
331 331 sorted(primaryactions)
332 332 + sorted(secondaryactions)
333 333 + sorted(tertiaryactions)
334 334 ):
335 335 addverb(v)
336 336 actions.append(b'')
337 337
338 338 hints = []
339 339 if ui.configbool(b'histedit', b'dropmissing'):
340 340 hints.append(
341 341 b"Deleting a changeset from the list "
342 342 b"will DISCARD it from the edited history!"
343 343 )
344 344
345 345 lines = (intro % (first, last)).split(b'\n') + actions + hints
346 346
347 347 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
348 348
349 349
350 350 class histeditstate(object):
351 351 def __init__(self, repo):
352 352 self.repo = repo
353 353 self.actions = None
354 354 self.keep = None
355 355 self.topmost = None
356 356 self.parentctxnode = None
357 357 self.lock = None
358 358 self.wlock = None
359 359 self.backupfile = None
360 360 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
361 361 self.replacements = []
362 362
363 363 def read(self):
364 364 """Load histedit state from disk and set fields appropriately."""
365 365 if not self.stateobj.exists():
366 366 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
367 367
368 368 data = self._read()
369 369
370 370 self.parentctxnode = data[b'parentctxnode']
371 371 actions = parserules(data[b'rules'], self)
372 372 self.actions = actions
373 373 self.keep = data[b'keep']
374 374 self.topmost = data[b'topmost']
375 375 self.replacements = data[b'replacements']
376 376 self.backupfile = data[b'backupfile']
377 377
378 378 def _read(self):
379 379 fp = self.repo.vfs.read(b'histedit-state')
380 380 if fp.startswith(b'v1\n'):
381 381 data = self._load()
382 382 parentctxnode, rules, keep, topmost, replacements, backupfile = data
383 383 else:
384 384 data = pickle.loads(fp)
385 385 parentctxnode, rules, keep, topmost, replacements = data
386 386 backupfile = None
387 387 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
388 388
389 389 return {
390 390 b'parentctxnode': parentctxnode,
391 391 b"rules": rules,
392 392 b"keep": keep,
393 393 b"topmost": topmost,
394 394 b"replacements": replacements,
395 395 b"backupfile": backupfile,
396 396 }
397 397
398 398 def write(self, tr=None):
399 399 if tr:
400 400 tr.addfilegenerator(
401 401 b'histedit-state',
402 402 (b'histedit-state',),
403 403 self._write,
404 404 location=b'plain',
405 405 )
406 406 else:
407 407 with self.repo.vfs(b"histedit-state", b"w") as f:
408 408 self._write(f)
409 409
410 410 def _write(self, fp):
411 411 fp.write(b'v1\n')
412 412 fp.write(b'%s\n' % hex(self.parentctxnode))
413 413 fp.write(b'%s\n' % hex(self.topmost))
414 414 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
415 415 fp.write(b'%d\n' % len(self.actions))
416 416 for action in self.actions:
417 417 fp.write(b'%s\n' % action.tostate())
418 418 fp.write(b'%d\n' % len(self.replacements))
419 419 for replacement in self.replacements:
420 420 fp.write(
421 421 b'%s%s\n'
422 422 % (
423 423 hex(replacement[0]),
424 424 b''.join(hex(r) for r in replacement[1]),
425 425 )
426 426 )
427 427 backupfile = self.backupfile
428 428 if not backupfile:
429 429 backupfile = b''
430 430 fp.write(b'%s\n' % backupfile)
431 431
432 432 def _load(self):
433 433 fp = self.repo.vfs(b'histedit-state', b'r')
434 434 lines = [l[:-1] for l in fp.readlines()]
435 435
436 436 index = 0
437 437 lines[index] # version number
438 438 index += 1
439 439
440 440 parentctxnode = bin(lines[index])
441 441 index += 1
442 442
443 443 topmost = bin(lines[index])
444 444 index += 1
445 445
446 446 keep = lines[index] == b'True'
447 447 index += 1
448 448
449 449 # Rules
450 450 rules = []
451 451 rulelen = int(lines[index])
452 452 index += 1
453 453 for i in pycompat.xrange(rulelen):
454 454 ruleaction = lines[index]
455 455 index += 1
456 456 rule = lines[index]
457 457 index += 1
458 458 rules.append((ruleaction, rule))
459 459
460 460 # Replacements
461 461 replacements = []
462 462 replacementlen = int(lines[index])
463 463 index += 1
464 464 for i in pycompat.xrange(replacementlen):
465 465 replacement = lines[index]
466 466 original = bin(replacement[:40])
467 467 succ = [
468 468 bin(replacement[i : i + 40])
469 469 for i in range(40, len(replacement), 40)
470 470 ]
471 471 replacements.append((original, succ))
472 472 index += 1
473 473
474 474 backupfile = lines[index]
475 475 index += 1
476 476
477 477 fp.close()
478 478
479 479 return parentctxnode, rules, keep, topmost, replacements, backupfile
480 480
481 481 def clear(self):
482 482 if self.inprogress():
483 483 self.repo.vfs.unlink(b'histedit-state')
484 484
485 485 def inprogress(self):
486 486 return self.repo.vfs.exists(b'histedit-state')
487 487
488 488
489 489 class histeditaction(object):
490 490 def __init__(self, state, node):
491 491 self.state = state
492 492 self.repo = state.repo
493 493 self.node = node
494 494
495 495 @classmethod
496 496 def fromrule(cls, state, rule):
497 497 """Parses the given rule, returning an instance of the histeditaction."""
498 498 ruleid = rule.strip().split(b' ', 1)[0]
499 499 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
500 500 # Check for validation of rule ids and get the rulehash
501 501 try:
502 502 rev = bin(ruleid)
503 503 except TypeError:
504 504 try:
505 505 _ctx = scmutil.revsingle(state.repo, ruleid)
506 506 rulehash = _ctx.hex()
507 507 rev = bin(rulehash)
508 508 except error.RepoLookupError:
509 509 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
510 510 return cls(state, rev)
511 511
512 512 def verify(self, prev, expected, seen):
513 513 """Verifies semantic correctness of the rule"""
514 514 repo = self.repo
515 515 ha = hex(self.node)
516 516 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
517 517 if self.node is None:
518 518 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
519 519 self._verifynodeconstraints(prev, expected, seen)
520 520
521 521 def _verifynodeconstraints(self, prev, expected, seen):
522 522 # by default command need a node in the edited list
523 523 if self.node not in expected:
524 524 raise error.ParseError(
525 525 _(b'%s "%s" changeset was not a candidate')
526 526 % (self.verb, short(self.node)),
527 527 hint=_(b'only use listed changesets'),
528 528 )
529 529 # and only one command per node
530 530 if self.node in seen:
531 531 raise error.ParseError(
532 532 _(b'duplicated command for changeset %s') % short(self.node)
533 533 )
534 534
535 535 def torule(self):
536 536 """build a histedit rule line for an action
537 537
538 538 by default lines are in the form:
539 539 <hash> <rev> <summary>
540 540 """
541 541 ctx = self.repo[self.node]
542 542 ui = self.repo.ui
543 543 # We don't want color codes in the commit message template, so
544 544 # disable the label() template function while we render it.
545 545 with ui.configoverride(
546 546 {(b'templatealias', b'label(l,x)'): b"x"}, b'histedit'
547 547 ):
548 548 summary = cmdutil.rendertemplate(
549 549 ctx, ui.config(b'histedit', b'summary-template')
550 550 )
551 551 # Handle the fact that `''.splitlines() => []`
552 552 summary = summary.splitlines()[0] if summary else b''
553 553 line = b'%s %s %s' % (self.verb, ctx, summary)
554 554 # trim to 75 columns by default so it's not stupidly wide in my editor
555 555 # (the 5 more are left for verb)
556 556 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
557 557 maxlen = max(maxlen, 22) # avoid truncating hash
558 558 return stringutil.ellipsis(line, maxlen)
559 559
560 560 def tostate(self):
561 561 """Print an action in format used by histedit state files
562 562 (the first line is a verb, the remainder is the second)
563 563 """
564 564 return b"%s\n%s" % (self.verb, hex(self.node))
565 565
566 566 def run(self):
567 567 """Runs the action. The default behavior is simply apply the action's
568 568 rulectx onto the current parentctx."""
569 569 self.applychange()
570 570 self.continuedirty()
571 571 return self.continueclean()
572 572
573 573 def applychange(self):
574 574 """Applies the changes from this action's rulectx onto the current
575 575 parentctx, but does not commit them."""
576 576 repo = self.repo
577 577 rulectx = repo[self.node]
578 578 with repo.ui.silent():
579 579 hg.update(repo, self.state.parentctxnode, quietempty=True)
580 580 stats = applychanges(repo.ui, repo, rulectx, {})
581 581 repo.dirstate.setbranch(rulectx.branch())
582 582 if stats.unresolvedcount:
583 583 raise error.InterventionRequired(
584 584 _(b'Fix up the change (%s %s)') % (self.verb, short(self.node)),
585 585 hint=_(b'hg histedit --continue to resume'),
586 586 )
587 587
588 588 def continuedirty(self):
589 589 """Continues the action when changes have been applied to the working
590 590 copy. The default behavior is to commit the dirty changes."""
591 591 repo = self.repo
592 592 rulectx = repo[self.node]
593 593
594 594 editor = self.commiteditor()
595 595 commit = commitfuncfor(repo, rulectx)
596 596 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
597 597 date = dateutil.makedate()
598 598 else:
599 599 date = rulectx.date()
600 600 commit(
601 601 text=rulectx.description(),
602 602 user=rulectx.user(),
603 603 date=date,
604 604 extra=rulectx.extra(),
605 605 editor=editor,
606 606 )
607 607
608 608 def commiteditor(self):
609 609 """The editor to be used to edit the commit message."""
610 610 return False
611 611
612 612 def continueclean(self):
613 613 """Continues the action when the working copy is clean. The default
614 614 behavior is to accept the current commit as the new version of the
615 615 rulectx."""
616 616 ctx = self.repo[b'.']
617 617 if ctx.node() == self.state.parentctxnode:
618 618 self.repo.ui.warn(
619 619 _(b'%s: skipping changeset (no changes)\n') % short(self.node)
620 620 )
621 621 return ctx, [(self.node, tuple())]
622 622 if ctx.node() == self.node:
623 623 # Nothing changed
624 624 return ctx, []
625 625 return ctx, [(self.node, (ctx.node(),))]
626 626
627 627
628 628 def commitfuncfor(repo, src):
629 629 """Build a commit function for the replacement of <src>
630 630
631 631 This function ensure we apply the same treatment to all changesets.
632 632
633 633 - Add a 'histedit_source' entry in extra.
634 634
635 635 Note that fold has its own separated logic because its handling is a bit
636 636 different and not easily factored out of the fold method.
637 637 """
638 638 phasemin = src.phase()
639 639
640 640 def commitfunc(**kwargs):
641 641 overrides = {(b'phases', b'new-commit'): phasemin}
642 642 with repo.ui.configoverride(overrides, b'histedit'):
643 643 extra = kwargs.get('extra', {}).copy()
644 644 extra[b'histedit_source'] = src.hex()
645 645 kwargs['extra'] = extra
646 646 return repo.commit(**kwargs)
647 647
648 648 return commitfunc
649 649
650 650
651 651 def applychanges(ui, repo, ctx, opts):
652 652 """Merge changeset from ctx (only) in the current working directory"""
653 653 if ctx.p1().node() == repo.dirstate.p1():
654 654 # edits are "in place" we do not need to make any merge,
655 655 # just applies changes on parent for editing
656 656 with ui.silent():
657 657 cmdutil.revert(ui, repo, ctx, all=True)
658 658 stats = mergemod.updateresult(0, 0, 0, 0)
659 659 else:
660 660 try:
661 661 # ui.forcemerge is an internal variable, do not document
662 662 repo.ui.setconfig(
663 663 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
664 664 )
665 665 stats = mergemod.graft(repo, ctx, labels=[b'local', b'histedit'])
666 666 finally:
667 667 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
668 668 return stats
669 669
670 670
671 671 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
672 672 """collapse the set of revisions from first to last as new one.
673 673
674 674 Expected commit options are:
675 675 - message
676 676 - date
677 677 - username
678 678 Commit message is edited in all cases.
679 679
680 680 This function works in memory."""
681 681 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
682 682 if not ctxs:
683 683 return None
684 684 for c in ctxs:
685 685 if not c.mutable():
686 686 raise error.ParseError(
687 687 _(b"cannot fold into public change %s") % short(c.node())
688 688 )
689 689 base = firstctx.p1()
690 690
691 691 # commit a new version of the old changeset, including the update
692 692 # collect all files which might be affected
693 693 files = set()
694 694 for ctx in ctxs:
695 695 files.update(ctx.files())
696 696
697 697 # Recompute copies (avoid recording a -> b -> a)
698 698 copied = copies.pathcopies(base, lastctx)
699 699
700 700 # prune files which were reverted by the updates
701 701 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
702 702 # commit version of these files as defined by head
703 703 headmf = lastctx.manifest()
704 704
705 705 def filectxfn(repo, ctx, path):
706 706 if path in headmf:
707 707 fctx = lastctx[path]
708 708 flags = fctx.flags()
709 709 mctx = context.memfilectx(
710 710 repo,
711 711 ctx,
712 712 fctx.path(),
713 713 fctx.data(),
714 714 islink=b'l' in flags,
715 715 isexec=b'x' in flags,
716 716 copysource=copied.get(path),
717 717 )
718 718 return mctx
719 719 return None
720 720
721 721 if commitopts.get(b'message'):
722 722 message = commitopts[b'message']
723 723 else:
724 724 message = firstctx.description()
725 725 user = commitopts.get(b'user')
726 726 date = commitopts.get(b'date')
727 727 extra = commitopts.get(b'extra')
728 728
729 729 parents = (firstctx.p1().node(), firstctx.p2().node())
730 730 editor = None
731 731 if not skipprompt:
732 732 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
733 733 new = context.memctx(
734 734 repo,
735 735 parents=parents,
736 736 text=message,
737 737 files=files,
738 738 filectxfn=filectxfn,
739 739 user=user,
740 740 date=date,
741 741 extra=extra,
742 742 editor=editor,
743 743 )
744 744 return repo.commitctx(new)
745 745
746 746
747 747 def _isdirtywc(repo):
748 748 return repo[None].dirty(missing=True)
749 749
750 750
751 751 def abortdirty():
752 raise error.Abort(
752 raise error.StateError(
753 753 _(b'working copy has pending changes'),
754 754 hint=_(
755 755 b'amend, commit, or revert them and run histedit '
756 756 b'--continue, or abort with histedit --abort'
757 757 ),
758 758 )
759 759
760 760
761 761 def action(verbs, message, priority=False, internal=False):
762 762 def wrap(cls):
763 763 assert not priority or not internal
764 764 verb = verbs[0]
765 765 if priority:
766 766 primaryactions.add(verb)
767 767 elif internal:
768 768 internalactions.add(verb)
769 769 elif len(verbs) > 1:
770 770 secondaryactions.add(verb)
771 771 else:
772 772 tertiaryactions.add(verb)
773 773
774 774 cls.verb = verb
775 775 cls.verbs = verbs
776 776 cls.message = message
777 777 for verb in verbs:
778 778 actiontable[verb] = cls
779 779 return cls
780 780
781 781 return wrap
782 782
783 783
784 784 @action([b'pick', b'p'], _(b'use commit'), priority=True)
785 785 class pick(histeditaction):
786 786 def run(self):
787 787 rulectx = self.repo[self.node]
788 788 if rulectx.p1().node() == self.state.parentctxnode:
789 789 self.repo.ui.debug(b'node %s unchanged\n' % short(self.node))
790 790 return rulectx, []
791 791
792 792 return super(pick, self).run()
793 793
794 794
795 795 @action(
796 796 [b'edit', b'e'],
797 797 _(b'use commit, but allow edits before making new commit'),
798 798 priority=True,
799 799 )
800 800 class edit(histeditaction):
801 801 def run(self):
802 802 repo = self.repo
803 803 rulectx = repo[self.node]
804 804 hg.update(repo, self.state.parentctxnode, quietempty=True)
805 805 applychanges(repo.ui, repo, rulectx, {})
806 806 hint = _(b'to edit %s, `hg histedit --continue` after making changes')
807 807 raise error.InterventionRequired(
808 808 _(b'Editing (%s), commit as needed now to split the change')
809 809 % short(self.node),
810 810 hint=hint % short(self.node),
811 811 )
812 812
813 813 def commiteditor(self):
814 814 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
815 815
816 816
817 817 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
818 818 class fold(histeditaction):
819 819 def verify(self, prev, expected, seen):
820 820 """Verifies semantic correctness of the fold rule"""
821 821 super(fold, self).verify(prev, expected, seen)
822 822 repo = self.repo
823 823 if not prev:
824 824 c = repo[self.node].p1()
825 825 elif not prev.verb in (b'pick', b'base'):
826 826 return
827 827 else:
828 828 c = repo[prev.node]
829 829 if not c.mutable():
830 830 raise error.ParseError(
831 831 _(b"cannot fold into public change %s") % short(c.node())
832 832 )
833 833
834 834 def continuedirty(self):
835 835 repo = self.repo
836 836 rulectx = repo[self.node]
837 837
838 838 commit = commitfuncfor(repo, rulectx)
839 839 commit(
840 840 text=b'fold-temp-revision %s' % short(self.node),
841 841 user=rulectx.user(),
842 842 date=rulectx.date(),
843 843 extra=rulectx.extra(),
844 844 )
845 845
846 846 def continueclean(self):
847 847 repo = self.repo
848 848 ctx = repo[b'.']
849 849 rulectx = repo[self.node]
850 850 parentctxnode = self.state.parentctxnode
851 851 if ctx.node() == parentctxnode:
852 852 repo.ui.warn(_(b'%s: empty changeset\n') % short(self.node))
853 853 return ctx, [(self.node, (parentctxnode,))]
854 854
855 855 parentctx = repo[parentctxnode]
856 856 newcommits = {
857 857 c.node()
858 858 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
859 859 }
860 860 if not newcommits:
861 861 repo.ui.warn(
862 862 _(
863 863 b'%s: cannot fold - working copy is not a '
864 864 b'descendant of previous commit %s\n'
865 865 )
866 866 % (short(self.node), short(parentctxnode))
867 867 )
868 868 return ctx, [(self.node, (ctx.node(),))]
869 869
870 870 middlecommits = newcommits.copy()
871 871 middlecommits.discard(ctx.node())
872 872
873 873 return self.finishfold(
874 874 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
875 875 )
876 876
877 877 def skipprompt(self):
878 878 """Returns true if the rule should skip the message editor.
879 879
880 880 For example, 'fold' wants to show an editor, but 'rollup'
881 881 doesn't want to.
882 882 """
883 883 return False
884 884
885 885 def mergedescs(self):
886 886 """Returns true if the rule should merge messages of multiple changes.
887 887
888 888 This exists mainly so that 'rollup' rules can be a subclass of
889 889 'fold'.
890 890 """
891 891 return True
892 892
893 893 def firstdate(self):
894 894 """Returns true if the rule should preserve the date of the first
895 895 change.
896 896
897 897 This exists mainly so that 'rollup' rules can be a subclass of
898 898 'fold'.
899 899 """
900 900 return False
901 901
902 902 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
903 903 mergemod.update(ctx.p1())
904 904 ### prepare new commit data
905 905 commitopts = {}
906 906 commitopts[b'user'] = ctx.user()
907 907 # commit message
908 908 if not self.mergedescs():
909 909 newmessage = ctx.description()
910 910 else:
911 911 newmessage = (
912 912 b'\n***\n'.join(
913 913 [ctx.description()]
914 914 + [repo[r].description() for r in internalchanges]
915 915 + [oldctx.description()]
916 916 )
917 917 + b'\n'
918 918 )
919 919 commitopts[b'message'] = newmessage
920 920 # date
921 921 if self.firstdate():
922 922 commitopts[b'date'] = ctx.date()
923 923 else:
924 924 commitopts[b'date'] = max(ctx.date(), oldctx.date())
925 925 # if date is to be updated to current
926 926 if ui.configbool(b'rewrite', b'update-timestamp'):
927 927 commitopts[b'date'] = dateutil.makedate()
928 928
929 929 extra = ctx.extra().copy()
930 930 # histedit_source
931 931 # note: ctx is likely a temporary commit but that the best we can do
932 932 # here. This is sufficient to solve issue3681 anyway.
933 933 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
934 934 commitopts[b'extra'] = extra
935 935 phasemin = max(ctx.phase(), oldctx.phase())
936 936 overrides = {(b'phases', b'new-commit'): phasemin}
937 937 with repo.ui.configoverride(overrides, b'histedit'):
938 938 n = collapse(
939 939 repo,
940 940 ctx,
941 941 repo[newnode],
942 942 commitopts,
943 943 skipprompt=self.skipprompt(),
944 944 )
945 945 if n is None:
946 946 return ctx, []
947 947 mergemod.update(repo[n])
948 948 replacements = [
949 949 (oldctx.node(), (newnode,)),
950 950 (ctx.node(), (n,)),
951 951 (newnode, (n,)),
952 952 ]
953 953 for ich in internalchanges:
954 954 replacements.append((ich, (n,)))
955 955 return repo[n], replacements
956 956
957 957
958 958 @action(
959 959 [b'base', b'b'],
960 960 _(b'checkout changeset and apply further changesets from there'),
961 961 )
962 962 class base(histeditaction):
963 963 def run(self):
964 964 if self.repo[b'.'].node() != self.node:
965 965 mergemod.clean_update(self.repo[self.node])
966 966 return self.continueclean()
967 967
968 968 def continuedirty(self):
969 969 abortdirty()
970 970
971 971 def continueclean(self):
972 972 basectx = self.repo[b'.']
973 973 return basectx, []
974 974
975 975 def _verifynodeconstraints(self, prev, expected, seen):
976 976 # base can only be use with a node not in the edited set
977 977 if self.node in expected:
978 978 msg = _(b'%s "%s" changeset was an edited list candidate')
979 979 raise error.ParseError(
980 980 msg % (self.verb, short(self.node)),
981 981 hint=_(b'base must only use unlisted changesets'),
982 982 )
983 983
984 984
985 985 @action(
986 986 [b'_multifold'],
987 987 _(
988 988 """fold subclass used for when multiple folds happen in a row
989 989
990 990 We only want to fire the editor for the folded message once when
991 991 (say) four changes are folded down into a single change. This is
992 992 similar to rollup, but we should preserve both messages so that
993 993 when the last fold operation runs we can show the user all the
994 994 commit messages in their editor.
995 995 """
996 996 ),
997 997 internal=True,
998 998 )
999 999 class _multifold(fold):
1000 1000 def skipprompt(self):
1001 1001 return True
1002 1002
1003 1003
1004 1004 @action(
1005 1005 [b"roll", b"r"],
1006 1006 _(b"like fold, but discard this commit's description and date"),
1007 1007 )
1008 1008 class rollup(fold):
1009 1009 def mergedescs(self):
1010 1010 return False
1011 1011
1012 1012 def skipprompt(self):
1013 1013 return True
1014 1014
1015 1015 def firstdate(self):
1016 1016 return True
1017 1017
1018 1018
1019 1019 @action([b"drop", b"d"], _(b'remove commit from history'))
1020 1020 class drop(histeditaction):
1021 1021 def run(self):
1022 1022 parentctx = self.repo[self.state.parentctxnode]
1023 1023 return parentctx, [(self.node, tuple())]
1024 1024
1025 1025
1026 1026 @action(
1027 1027 [b"mess", b"m"],
1028 1028 _(b'edit commit message without changing commit content'),
1029 1029 priority=True,
1030 1030 )
1031 1031 class message(histeditaction):
1032 1032 def commiteditor(self):
1033 1033 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1034 1034
1035 1035
1036 1036 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1037 1037 """utility function to find the first outgoing changeset
1038 1038
1039 1039 Used by initialization code"""
1040 1040 if opts is None:
1041 1041 opts = {}
1042 1042 path = urlutil.get_unique_push_path(b'histedit', repo, ui, remote)
1043 1043 dest = path.pushloc or path.loc
1044 1044
1045 1045 ui.status(_(b'comparing with %s\n') % urlutil.hidepassword(dest))
1046 1046
1047 1047 revs, checkout = hg.addbranchrevs(repo, repo, (path.branch, []), None)
1048 1048 other = hg.peer(repo, opts, dest)
1049 1049
1050 1050 if revs:
1051 1051 revs = [repo.lookup(rev) for rev in revs]
1052 1052
1053 1053 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1054 1054 if not outgoing.missing:
1055 raise error.Abort(_(b'no outgoing ancestors'))
1055 raise error.StateError(_(b'no outgoing ancestors'))
1056 1056 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1057 1057 if len(roots) > 1:
1058 1058 msg = _(b'there are ambiguous outgoing revisions')
1059 1059 hint = _(b"see 'hg help histedit' for more detail")
1060 raise error.Abort(msg, hint=hint)
1060 raise error.StateError(msg, hint=hint)
1061 1061 return repo[roots[0]].node()
1062 1062
1063 1063
1064 1064 # Curses Support
1065 1065 try:
1066 1066 import curses
1067 1067 except ImportError:
1068 1068 curses = None
1069 1069
1070 1070 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1071 1071 ACTION_LABELS = {
1072 1072 b'fold': b'^fold',
1073 1073 b'roll': b'^roll',
1074 1074 }
1075 1075
1076 1076 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1077 1077 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1078 1078 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1079 1079
1080 1080 E_QUIT, E_HISTEDIT = 1, 2
1081 1081 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1082 1082 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1083 1083
1084 1084 KEYTABLE = {
1085 1085 b'global': {
1086 1086 b'h': b'next-action',
1087 1087 b'KEY_RIGHT': b'next-action',
1088 1088 b'l': b'prev-action',
1089 1089 b'KEY_LEFT': b'prev-action',
1090 1090 b'q': b'quit',
1091 1091 b'c': b'histedit',
1092 1092 b'C': b'histedit',
1093 1093 b'v': b'showpatch',
1094 1094 b'?': b'help',
1095 1095 },
1096 1096 MODE_RULES: {
1097 1097 b'd': b'action-drop',
1098 1098 b'e': b'action-edit',
1099 1099 b'f': b'action-fold',
1100 1100 b'm': b'action-mess',
1101 1101 b'p': b'action-pick',
1102 1102 b'r': b'action-roll',
1103 1103 b' ': b'select',
1104 1104 b'j': b'down',
1105 1105 b'k': b'up',
1106 1106 b'KEY_DOWN': b'down',
1107 1107 b'KEY_UP': b'up',
1108 1108 b'J': b'move-down',
1109 1109 b'K': b'move-up',
1110 1110 b'KEY_NPAGE': b'move-down',
1111 1111 b'KEY_PPAGE': b'move-up',
1112 1112 b'0': b'goto', # Used for 0..9
1113 1113 },
1114 1114 MODE_PATCH: {
1115 1115 b' ': b'page-down',
1116 1116 b'KEY_NPAGE': b'page-down',
1117 1117 b'KEY_PPAGE': b'page-up',
1118 1118 b'j': b'line-down',
1119 1119 b'k': b'line-up',
1120 1120 b'KEY_DOWN': b'line-down',
1121 1121 b'KEY_UP': b'line-up',
1122 1122 b'J': b'down',
1123 1123 b'K': b'up',
1124 1124 },
1125 1125 MODE_HELP: {},
1126 1126 }
1127 1127
1128 1128
1129 1129 def screen_size():
1130 1130 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1131 1131
1132 1132
1133 1133 class histeditrule(object):
1134 1134 def __init__(self, ui, ctx, pos, action=b'pick'):
1135 1135 self.ui = ui
1136 1136 self.ctx = ctx
1137 1137 self.action = action
1138 1138 self.origpos = pos
1139 1139 self.pos = pos
1140 1140 self.conflicts = []
1141 1141
1142 1142 def __bytes__(self):
1143 1143 # Example display of several histeditrules:
1144 1144 #
1145 1145 # #10 pick 316392:06a16c25c053 add option to skip tests
1146 1146 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1147 1147 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1148 1148 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1149 1149 #
1150 1150 # The carets point to the changeset being folded into ("roll this
1151 1151 # changeset into the changeset above").
1152 1152 return b'%s%s' % (self.prefix, self.desc)
1153 1153
1154 1154 __str__ = encoding.strmethod(__bytes__)
1155 1155
1156 1156 @property
1157 1157 def prefix(self):
1158 1158 # Some actions ('fold' and 'roll') combine a patch with a
1159 1159 # previous one. Add a marker showing which patch they apply
1160 1160 # to.
1161 1161 action = ACTION_LABELS.get(self.action, self.action)
1162 1162
1163 1163 h = self.ctx.hex()[0:12]
1164 1164 r = self.ctx.rev()
1165 1165
1166 1166 return b"#%s %s %d:%s " % (
1167 1167 (b'%d' % self.origpos).ljust(2),
1168 1168 action.ljust(6),
1169 1169 r,
1170 1170 h,
1171 1171 )
1172 1172
1173 1173 @util.propertycache
1174 1174 def desc(self):
1175 1175 summary = cmdutil.rendertemplate(
1176 1176 self.ctx, self.ui.config(b'histedit', b'summary-template')
1177 1177 )
1178 1178 if summary:
1179 1179 return summary
1180 1180 # This is split off from the prefix property so that we can
1181 1181 # separately make the description for 'roll' red (since it
1182 1182 # will get discarded).
1183 1183 return self.ctx.description().splitlines()[0].strip()
1184 1184
1185 1185 def checkconflicts(self, other):
1186 1186 if other.pos > self.pos and other.origpos <= self.origpos:
1187 1187 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1188 1188 self.conflicts.append(other)
1189 1189 return self.conflicts
1190 1190
1191 1191 if other in self.conflicts:
1192 1192 self.conflicts.remove(other)
1193 1193 return self.conflicts
1194 1194
1195 1195
1196 1196 # ============ EVENTS ===============
1197 1197 def movecursor(state, oldpos, newpos):
1198 1198 """Change the rule/changeset that the cursor is pointing to, regardless of
1199 1199 current mode (you can switch between patches from the view patch window)."""
1200 1200 state[b'pos'] = newpos
1201 1201
1202 1202 mode, _ = state[b'mode']
1203 1203 if mode == MODE_RULES:
1204 1204 # Scroll through the list by updating the view for MODE_RULES, so that
1205 1205 # even if we are not currently viewing the rules, switching back will
1206 1206 # result in the cursor's rule being visible.
1207 1207 modestate = state[b'modes'][MODE_RULES]
1208 1208 if newpos < modestate[b'line_offset']:
1209 1209 modestate[b'line_offset'] = newpos
1210 1210 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1211 1211 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1212 1212
1213 1213 # Reset the patch view region to the top of the new patch.
1214 1214 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1215 1215
1216 1216
1217 1217 def changemode(state, mode):
1218 1218 curmode, _ = state[b'mode']
1219 1219 state[b'mode'] = (mode, curmode)
1220 1220 if mode == MODE_PATCH:
1221 1221 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1222 1222
1223 1223
1224 1224 def makeselection(state, pos):
1225 1225 state[b'selected'] = pos
1226 1226
1227 1227
1228 1228 def swap(state, oldpos, newpos):
1229 1229 """Swap two positions and calculate necessary conflicts in
1230 1230 O(|newpos-oldpos|) time"""
1231 1231
1232 1232 rules = state[b'rules']
1233 1233 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1234 1234
1235 1235 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1236 1236
1237 1237 # TODO: swap should not know about histeditrule's internals
1238 1238 rules[newpos].pos = newpos
1239 1239 rules[oldpos].pos = oldpos
1240 1240
1241 1241 start = min(oldpos, newpos)
1242 1242 end = max(oldpos, newpos)
1243 1243 for r in pycompat.xrange(start, end + 1):
1244 1244 rules[newpos].checkconflicts(rules[r])
1245 1245 rules[oldpos].checkconflicts(rules[r])
1246 1246
1247 1247 if state[b'selected']:
1248 1248 makeselection(state, newpos)
1249 1249
1250 1250
1251 1251 def changeaction(state, pos, action):
1252 1252 """Change the action state on the given position to the new action"""
1253 1253 rules = state[b'rules']
1254 1254 assert 0 <= pos < len(rules)
1255 1255 rules[pos].action = action
1256 1256
1257 1257
1258 1258 def cycleaction(state, pos, next=False):
1259 1259 """Changes the action state the next or the previous action from
1260 1260 the action list"""
1261 1261 rules = state[b'rules']
1262 1262 assert 0 <= pos < len(rules)
1263 1263 current = rules[pos].action
1264 1264
1265 1265 assert current in KEY_LIST
1266 1266
1267 1267 index = KEY_LIST.index(current)
1268 1268 if next:
1269 1269 index += 1
1270 1270 else:
1271 1271 index -= 1
1272 1272 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1273 1273
1274 1274
1275 1275 def changeview(state, delta, unit):
1276 1276 """Change the region of whatever is being viewed (a patch or the list of
1277 1277 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'."""
1278 1278 mode, _ = state[b'mode']
1279 1279 if mode != MODE_PATCH:
1280 1280 return
1281 1281 mode_state = state[b'modes'][mode]
1282 1282 num_lines = len(mode_state[b'patchcontents'])
1283 1283 page_height = state[b'page_height']
1284 1284 unit = page_height if unit == b'page' else 1
1285 1285 num_pages = 1 + (num_lines - 1) // page_height
1286 1286 max_offset = (num_pages - 1) * page_height
1287 1287 newline = mode_state[b'line_offset'] + delta * unit
1288 1288 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1289 1289
1290 1290
1291 1291 def event(state, ch):
1292 1292 """Change state based on the current character input
1293 1293
1294 1294 This takes the current state and based on the current character input from
1295 1295 the user we change the state.
1296 1296 """
1297 1297 selected = state[b'selected']
1298 1298 oldpos = state[b'pos']
1299 1299 rules = state[b'rules']
1300 1300
1301 1301 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1302 1302 return E_RESIZE
1303 1303
1304 1304 lookup_ch = ch
1305 1305 if ch is not None and b'0' <= ch <= b'9':
1306 1306 lookup_ch = b'0'
1307 1307
1308 1308 curmode, prevmode = state[b'mode']
1309 1309 action = KEYTABLE[curmode].get(
1310 1310 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1311 1311 )
1312 1312 if action is None:
1313 1313 return
1314 1314 if action in (b'down', b'move-down'):
1315 1315 newpos = min(oldpos + 1, len(rules) - 1)
1316 1316 movecursor(state, oldpos, newpos)
1317 1317 if selected is not None or action == b'move-down':
1318 1318 swap(state, oldpos, newpos)
1319 1319 elif action in (b'up', b'move-up'):
1320 1320 newpos = max(0, oldpos - 1)
1321 1321 movecursor(state, oldpos, newpos)
1322 1322 if selected is not None or action == b'move-up':
1323 1323 swap(state, oldpos, newpos)
1324 1324 elif action == b'next-action':
1325 1325 cycleaction(state, oldpos, next=True)
1326 1326 elif action == b'prev-action':
1327 1327 cycleaction(state, oldpos, next=False)
1328 1328 elif action == b'select':
1329 1329 selected = oldpos if selected is None else None
1330 1330 makeselection(state, selected)
1331 1331 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1332 1332 newrule = next((r for r in rules if r.origpos == int(ch)))
1333 1333 movecursor(state, oldpos, newrule.pos)
1334 1334 if selected is not None:
1335 1335 swap(state, oldpos, newrule.pos)
1336 1336 elif action.startswith(b'action-'):
1337 1337 changeaction(state, oldpos, action[7:])
1338 1338 elif action == b'showpatch':
1339 1339 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1340 1340 elif action == b'help':
1341 1341 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1342 1342 elif action == b'quit':
1343 1343 return E_QUIT
1344 1344 elif action == b'histedit':
1345 1345 return E_HISTEDIT
1346 1346 elif action == b'page-down':
1347 1347 return E_PAGEDOWN
1348 1348 elif action == b'page-up':
1349 1349 return E_PAGEUP
1350 1350 elif action == b'line-down':
1351 1351 return E_LINEDOWN
1352 1352 elif action == b'line-up':
1353 1353 return E_LINEUP
1354 1354
1355 1355
1356 1356 def makecommands(rules):
1357 1357 """Returns a list of commands consumable by histedit --commands based on
1358 1358 our list of rules"""
1359 1359 commands = []
1360 1360 for rules in rules:
1361 1361 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1362 1362 return commands
1363 1363
1364 1364
1365 1365 def addln(win, y, x, line, color=None):
1366 1366 """Add a line to the given window left padding but 100% filled with
1367 1367 whitespace characters, so that the color appears on the whole line"""
1368 1368 maxy, maxx = win.getmaxyx()
1369 1369 length = maxx - 1 - x
1370 1370 line = bytes(line).ljust(length)[:length]
1371 1371 if y < 0:
1372 1372 y = maxy + y
1373 1373 if x < 0:
1374 1374 x = maxx + x
1375 1375 if color:
1376 1376 win.addstr(y, x, line, color)
1377 1377 else:
1378 1378 win.addstr(y, x, line)
1379 1379
1380 1380
1381 1381 def _trunc_head(line, n):
1382 1382 if len(line) <= n:
1383 1383 return line
1384 1384 return b'> ' + line[-(n - 2) :]
1385 1385
1386 1386
1387 1387 def _trunc_tail(line, n):
1388 1388 if len(line) <= n:
1389 1389 return line
1390 1390 return line[: n - 2] + b' >'
1391 1391
1392 1392
1393 1393 def patchcontents(state):
1394 1394 repo = state[b'repo']
1395 1395 rule = state[b'rules'][state[b'pos']]
1396 1396 displayer = logcmdutil.changesetdisplayer(
1397 1397 repo.ui, repo, {b"patch": True, b"template": b"status"}, buffered=True
1398 1398 )
1399 1399 overrides = {(b'ui', b'verbose'): True}
1400 1400 with repo.ui.configoverride(overrides, source=b'histedit'):
1401 1401 displayer.show(rule.ctx)
1402 1402 displayer.close()
1403 1403 return displayer.hunk[rule.ctx.rev()].splitlines()
1404 1404
1405 1405
1406 1406 def _chisteditmain(repo, rules, stdscr):
1407 1407 try:
1408 1408 curses.use_default_colors()
1409 1409 except curses.error:
1410 1410 pass
1411 1411
1412 1412 # initialize color pattern
1413 1413 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1414 1414 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1415 1415 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1416 1416 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1417 1417 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1418 1418 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1419 1419 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1420 1420 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1421 1421 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1422 1422 curses.init_pair(
1423 1423 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1424 1424 )
1425 1425 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1426 1426
1427 1427 # don't display the cursor
1428 1428 try:
1429 1429 curses.curs_set(0)
1430 1430 except curses.error:
1431 1431 pass
1432 1432
1433 1433 def rendercommit(win, state):
1434 1434 """Renders the commit window that shows the log of the current selected
1435 1435 commit"""
1436 1436 pos = state[b'pos']
1437 1437 rules = state[b'rules']
1438 1438 rule = rules[pos]
1439 1439
1440 1440 ctx = rule.ctx
1441 1441 win.box()
1442 1442
1443 1443 maxy, maxx = win.getmaxyx()
1444 1444 length = maxx - 3
1445 1445
1446 1446 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1447 1447 win.addstr(1, 1, line[:length])
1448 1448
1449 1449 line = b"user: %s" % ctx.user()
1450 1450 win.addstr(2, 1, line[:length])
1451 1451
1452 1452 bms = repo.nodebookmarks(ctx.node())
1453 1453 line = b"bookmark: %s" % b' '.join(bms)
1454 1454 win.addstr(3, 1, line[:length])
1455 1455
1456 1456 line = b"summary: %s" % (ctx.description().splitlines()[0])
1457 1457 win.addstr(4, 1, line[:length])
1458 1458
1459 1459 line = b"files: "
1460 1460 win.addstr(5, 1, line)
1461 1461 fnx = 1 + len(line)
1462 1462 fnmaxx = length - fnx + 1
1463 1463 y = 5
1464 1464 fnmaxn = maxy - (1 + y) - 1
1465 1465 files = ctx.files()
1466 1466 for i, line1 in enumerate(files):
1467 1467 if len(files) > fnmaxn and i == fnmaxn - 1:
1468 1468 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1469 1469 y = y + 1
1470 1470 break
1471 1471 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1472 1472 y = y + 1
1473 1473
1474 1474 conflicts = rule.conflicts
1475 1475 if len(conflicts) > 0:
1476 1476 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1477 1477 conflictstr = b"changed files overlap with %s" % conflictstr
1478 1478 else:
1479 1479 conflictstr = b'no overlap'
1480 1480
1481 1481 win.addstr(y, 1, conflictstr[:length])
1482 1482 win.noutrefresh()
1483 1483
1484 1484 def helplines(mode):
1485 1485 if mode == MODE_PATCH:
1486 1486 help = b"""\
1487 1487 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1488 1488 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1489 1489 """
1490 1490 else:
1491 1491 help = b"""\
1492 1492 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1493 1493 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1494 1494 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1495 1495 """
1496 1496 return help.splitlines()
1497 1497
1498 1498 def renderhelp(win, state):
1499 1499 maxy, maxx = win.getmaxyx()
1500 1500 mode, _ = state[b'mode']
1501 1501 for y, line in enumerate(helplines(mode)):
1502 1502 if y >= maxy:
1503 1503 break
1504 1504 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1505 1505 win.noutrefresh()
1506 1506
1507 1507 def renderrules(rulesscr, state):
1508 1508 rules = state[b'rules']
1509 1509 pos = state[b'pos']
1510 1510 selected = state[b'selected']
1511 1511 start = state[b'modes'][MODE_RULES][b'line_offset']
1512 1512
1513 1513 conflicts = [r.ctx for r in rules if r.conflicts]
1514 1514 if len(conflicts) > 0:
1515 1515 line = b"potential conflict in %s" % b','.join(
1516 1516 map(pycompat.bytestr, conflicts)
1517 1517 )
1518 1518 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1519 1519
1520 1520 for y, rule in enumerate(rules[start:]):
1521 1521 if y >= state[b'page_height']:
1522 1522 break
1523 1523 if len(rule.conflicts) > 0:
1524 1524 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1525 1525 else:
1526 1526 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1527 1527
1528 1528 if y + start == selected:
1529 1529 rollcolor = COLOR_ROLL_SELECTED
1530 1530 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1531 1531 elif y + start == pos:
1532 1532 rollcolor = COLOR_ROLL_CURRENT
1533 1533 addln(
1534 1534 rulesscr,
1535 1535 y,
1536 1536 2,
1537 1537 rule,
1538 1538 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1539 1539 )
1540 1540 else:
1541 1541 rollcolor = COLOR_ROLL
1542 1542 addln(rulesscr, y, 2, rule)
1543 1543
1544 1544 if rule.action == b'roll':
1545 1545 rulesscr.addstr(
1546 1546 y,
1547 1547 2 + len(rule.prefix),
1548 1548 rule.desc,
1549 1549 curses.color_pair(rollcolor),
1550 1550 )
1551 1551
1552 1552 rulesscr.noutrefresh()
1553 1553
1554 1554 def renderstring(win, state, output, diffcolors=False):
1555 1555 maxy, maxx = win.getmaxyx()
1556 1556 length = min(maxy - 1, len(output))
1557 1557 for y in range(0, length):
1558 1558 line = output[y]
1559 1559 if diffcolors:
1560 1560 if line and line[0] == b'+':
1561 1561 win.addstr(
1562 1562 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1563 1563 )
1564 1564 elif line and line[0] == b'-':
1565 1565 win.addstr(
1566 1566 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1567 1567 )
1568 1568 elif line.startswith(b'@@ '):
1569 1569 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1570 1570 else:
1571 1571 win.addstr(y, 0, line)
1572 1572 else:
1573 1573 win.addstr(y, 0, line)
1574 1574 win.noutrefresh()
1575 1575
1576 1576 def renderpatch(win, state):
1577 1577 start = state[b'modes'][MODE_PATCH][b'line_offset']
1578 1578 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1579 1579 renderstring(win, state, content[start:], diffcolors=True)
1580 1580
1581 1581 def layout(mode):
1582 1582 maxy, maxx = stdscr.getmaxyx()
1583 1583 helplen = len(helplines(mode))
1584 1584 mainlen = maxy - helplen - 12
1585 1585 if mainlen < 1:
1586 1586 raise error.Abort(
1587 1587 _(b"terminal dimensions %d by %d too small for curses histedit")
1588 1588 % (maxy, maxx),
1589 1589 hint=_(
1590 1590 b"enlarge your terminal or use --config ui.interface=text"
1591 1591 ),
1592 1592 )
1593 1593 return {
1594 1594 b'commit': (12, maxx),
1595 1595 b'help': (helplen, maxx),
1596 1596 b'main': (mainlen, maxx),
1597 1597 }
1598 1598
1599 1599 def drawvertwin(size, y, x):
1600 1600 win = curses.newwin(size[0], size[1], y, x)
1601 1601 y += size[0]
1602 1602 return win, y, x
1603 1603
1604 1604 state = {
1605 1605 b'pos': 0,
1606 1606 b'rules': rules,
1607 1607 b'selected': None,
1608 1608 b'mode': (MODE_INIT, MODE_INIT),
1609 1609 b'page_height': None,
1610 1610 b'modes': {
1611 1611 MODE_RULES: {
1612 1612 b'line_offset': 0,
1613 1613 },
1614 1614 MODE_PATCH: {
1615 1615 b'line_offset': 0,
1616 1616 },
1617 1617 },
1618 1618 b'repo': repo,
1619 1619 }
1620 1620
1621 1621 # eventloop
1622 1622 ch = None
1623 1623 stdscr.clear()
1624 1624 stdscr.refresh()
1625 1625 while True:
1626 1626 oldmode, unused = state[b'mode']
1627 1627 if oldmode == MODE_INIT:
1628 1628 changemode(state, MODE_RULES)
1629 1629 e = event(state, ch)
1630 1630
1631 1631 if e == E_QUIT:
1632 1632 return False
1633 1633 if e == E_HISTEDIT:
1634 1634 return state[b'rules']
1635 1635 else:
1636 1636 if e == E_RESIZE:
1637 1637 size = screen_size()
1638 1638 if size != stdscr.getmaxyx():
1639 1639 curses.resizeterm(*size)
1640 1640
1641 1641 curmode, unused = state[b'mode']
1642 1642 sizes = layout(curmode)
1643 1643 if curmode != oldmode:
1644 1644 state[b'page_height'] = sizes[b'main'][0]
1645 1645 # Adjust the view to fit the current screen size.
1646 1646 movecursor(state, state[b'pos'], state[b'pos'])
1647 1647
1648 1648 # Pack the windows against the top, each pane spread across the
1649 1649 # full width of the screen.
1650 1650 y, x = (0, 0)
1651 1651 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1652 1652 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1653 1653 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1654 1654
1655 1655 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1656 1656 if e == E_PAGEDOWN:
1657 1657 changeview(state, +1, b'page')
1658 1658 elif e == E_PAGEUP:
1659 1659 changeview(state, -1, b'page')
1660 1660 elif e == E_LINEDOWN:
1661 1661 changeview(state, +1, b'line')
1662 1662 elif e == E_LINEUP:
1663 1663 changeview(state, -1, b'line')
1664 1664
1665 1665 # start rendering
1666 1666 commitwin.erase()
1667 1667 helpwin.erase()
1668 1668 mainwin.erase()
1669 1669 if curmode == MODE_PATCH:
1670 1670 renderpatch(mainwin, state)
1671 1671 elif curmode == MODE_HELP:
1672 1672 renderstring(mainwin, state, __doc__.strip().splitlines())
1673 1673 else:
1674 1674 renderrules(mainwin, state)
1675 1675 rendercommit(commitwin, state)
1676 1676 renderhelp(helpwin, state)
1677 1677 curses.doupdate()
1678 1678 # done rendering
1679 1679 ch = encoding.strtolocal(stdscr.getkey())
1680 1680
1681 1681
1682 1682 def _chistedit(ui, repo, freeargs, opts):
1683 1683 """interactively edit changeset history via a curses interface
1684 1684
1685 1685 Provides a ncurses interface to histedit. Press ? in chistedit mode
1686 1686 to see an extensive help. Requires python-curses to be installed."""
1687 1687
1688 1688 if curses is None:
1689 1689 raise error.Abort(_(b"Python curses library required"))
1690 1690
1691 1691 # disable color
1692 1692 ui._colormode = None
1693 1693
1694 1694 try:
1695 1695 keep = opts.get(b'keep')
1696 1696 revs = opts.get(b'rev', [])[:]
1697 1697 cmdutil.checkunfinished(repo)
1698 1698 cmdutil.bailifchanged(repo)
1699 1699
1700 1700 revs.extend(freeargs)
1701 1701 if not revs:
1702 1702 defaultrev = destutil.desthistedit(ui, repo)
1703 1703 if defaultrev is not None:
1704 1704 revs.append(defaultrev)
1705 1705 if len(revs) != 1:
1706 raise error.Abort(
1706 raise error.InputError(
1707 1707 _(b'histedit requires exactly one ancestor revision')
1708 1708 )
1709 1709
1710 1710 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1711 1711 if len(rr) != 1:
1712 raise error.Abort(
1712 raise error.InputError(
1713 1713 _(
1714 1714 b'The specified revisions must have '
1715 1715 b'exactly one common root'
1716 1716 )
1717 1717 )
1718 1718 root = rr[0].node()
1719 1719
1720 1720 topmost = repo.dirstate.p1()
1721 1721 revs = between(repo, root, topmost, keep)
1722 1722 if not revs:
1723 raise error.Abort(
1723 raise error.InputError(
1724 1724 _(b'%s is not an ancestor of working directory') % short(root)
1725 1725 )
1726 1726
1727 1727 ctxs = []
1728 1728 for i, r in enumerate(revs):
1729 1729 ctxs.append(histeditrule(ui, repo[r], i))
1730 1730 with util.with_lc_ctype():
1731 1731 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1732 1732 curses.echo()
1733 1733 curses.endwin()
1734 1734 if rc is False:
1735 1735 ui.write(_(b"histedit aborted\n"))
1736 1736 return 0
1737 1737 if type(rc) is list:
1738 1738 ui.status(_(b"performing changes\n"))
1739 1739 rules = makecommands(rc)
1740 1740 with repo.vfs(b'chistedit', b'w+') as fp:
1741 1741 for r in rules:
1742 1742 fp.write(r)
1743 1743 opts[b'commands'] = fp.name
1744 1744 return _texthistedit(ui, repo, freeargs, opts)
1745 1745 except KeyboardInterrupt:
1746 1746 pass
1747 1747 return -1
1748 1748
1749 1749
1750 1750 @command(
1751 1751 b'histedit',
1752 1752 [
1753 1753 (
1754 1754 b'',
1755 1755 b'commands',
1756 1756 b'',
1757 1757 _(b'read history edits from the specified file'),
1758 1758 _(b'FILE'),
1759 1759 ),
1760 1760 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1761 1761 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1762 1762 (
1763 1763 b'k',
1764 1764 b'keep',
1765 1765 False,
1766 1766 _(b"don't strip old nodes after edit is complete"),
1767 1767 ),
1768 1768 (b'', b'abort', False, _(b'abort an edit in progress')),
1769 1769 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1770 1770 (
1771 1771 b'f',
1772 1772 b'force',
1773 1773 False,
1774 1774 _(b'force outgoing even for unrelated repositories'),
1775 1775 ),
1776 1776 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1777 1777 ]
1778 1778 + cmdutil.formatteropts,
1779 1779 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1780 1780 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1781 1781 )
1782 1782 def histedit(ui, repo, *freeargs, **opts):
1783 1783 """interactively edit changeset history
1784 1784
1785 1785 This command lets you edit a linear series of changesets (up to
1786 1786 and including the working directory, which should be clean).
1787 1787 You can:
1788 1788
1789 1789 - `pick` to [re]order a changeset
1790 1790
1791 1791 - `drop` to omit changeset
1792 1792
1793 1793 - `mess` to reword the changeset commit message
1794 1794
1795 1795 - `fold` to combine it with the preceding changeset (using the later date)
1796 1796
1797 1797 - `roll` like fold, but discarding this commit's description and date
1798 1798
1799 1799 - `edit` to edit this changeset (preserving date)
1800 1800
1801 1801 - `base` to checkout changeset and apply further changesets from there
1802 1802
1803 1803 There are a number of ways to select the root changeset:
1804 1804
1805 1805 - Specify ANCESTOR directly
1806 1806
1807 1807 - Use --outgoing -- it will be the first linear changeset not
1808 1808 included in destination. (See :hg:`help config.paths.default-push`)
1809 1809
1810 1810 - Otherwise, the value from the "histedit.defaultrev" config option
1811 1811 is used as a revset to select the base revision when ANCESTOR is not
1812 1812 specified. The first revision returned by the revset is used. By
1813 1813 default, this selects the editable history that is unique to the
1814 1814 ancestry of the working directory.
1815 1815
1816 1816 .. container:: verbose
1817 1817
1818 1818 If you use --outgoing, this command will abort if there are ambiguous
1819 1819 outgoing revisions. For example, if there are multiple branches
1820 1820 containing outgoing revisions.
1821 1821
1822 1822 Use "min(outgoing() and ::.)" or similar revset specification
1823 1823 instead of --outgoing to specify edit target revision exactly in
1824 1824 such ambiguous situation. See :hg:`help revsets` for detail about
1825 1825 selecting revisions.
1826 1826
1827 1827 .. container:: verbose
1828 1828
1829 1829 Examples:
1830 1830
1831 1831 - A number of changes have been made.
1832 1832 Revision 3 is no longer needed.
1833 1833
1834 1834 Start history editing from revision 3::
1835 1835
1836 1836 hg histedit -r 3
1837 1837
1838 1838 An editor opens, containing the list of revisions,
1839 1839 with specific actions specified::
1840 1840
1841 1841 pick 5339bf82f0ca 3 Zworgle the foobar
1842 1842 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1843 1843 pick 0a9639fcda9d 5 Morgify the cromulancy
1844 1844
1845 1845 Additional information about the possible actions
1846 1846 to take appears below the list of revisions.
1847 1847
1848 1848 To remove revision 3 from the history,
1849 1849 its action (at the beginning of the relevant line)
1850 1850 is changed to 'drop'::
1851 1851
1852 1852 drop 5339bf82f0ca 3 Zworgle the foobar
1853 1853 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1854 1854 pick 0a9639fcda9d 5 Morgify the cromulancy
1855 1855
1856 1856 - A number of changes have been made.
1857 1857 Revision 2 and 4 need to be swapped.
1858 1858
1859 1859 Start history editing from revision 2::
1860 1860
1861 1861 hg histedit -r 2
1862 1862
1863 1863 An editor opens, containing the list of revisions,
1864 1864 with specific actions specified::
1865 1865
1866 1866 pick 252a1af424ad 2 Blorb a morgwazzle
1867 1867 pick 5339bf82f0ca 3 Zworgle the foobar
1868 1868 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1869 1869
1870 1870 To swap revision 2 and 4, its lines are swapped
1871 1871 in the editor::
1872 1872
1873 1873 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1874 1874 pick 5339bf82f0ca 3 Zworgle the foobar
1875 1875 pick 252a1af424ad 2 Blorb a morgwazzle
1876 1876
1877 1877 Returns 0 on success, 1 if user intervention is required (not only
1878 1878 for intentional "edit" command, but also for resolving unexpected
1879 1879 conflicts).
1880 1880 """
1881 1881 opts = pycompat.byteskwargs(opts)
1882 1882
1883 1883 # kludge: _chistedit only works for starting an edit, not aborting
1884 1884 # or continuing, so fall back to regular _texthistedit for those
1885 1885 # operations.
1886 1886 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1887 1887 return _chistedit(ui, repo, freeargs, opts)
1888 1888 return _texthistedit(ui, repo, freeargs, opts)
1889 1889
1890 1890
1891 1891 def _texthistedit(ui, repo, freeargs, opts):
1892 1892 state = histeditstate(repo)
1893 1893 with repo.wlock() as wlock, repo.lock() as lock:
1894 1894 state.wlock = wlock
1895 1895 state.lock = lock
1896 1896 _histedit(ui, repo, state, freeargs, opts)
1897 1897
1898 1898
1899 1899 goalcontinue = b'continue'
1900 1900 goalabort = b'abort'
1901 1901 goaleditplan = b'edit-plan'
1902 1902 goalnew = b'new'
1903 1903
1904 1904
1905 1905 def _getgoal(opts):
1906 1906 if opts.get(b'continue'):
1907 1907 return goalcontinue
1908 1908 if opts.get(b'abort'):
1909 1909 return goalabort
1910 1910 if opts.get(b'edit_plan'):
1911 1911 return goaleditplan
1912 1912 return goalnew
1913 1913
1914 1914
1915 1915 def _readfile(ui, path):
1916 1916 if path == b'-':
1917 1917 with ui.timeblockedsection(b'histedit'):
1918 1918 return ui.fin.read()
1919 1919 else:
1920 1920 with open(path, b'rb') as f:
1921 1921 return f.read()
1922 1922
1923 1923
1924 1924 def _validateargs(ui, repo, freeargs, opts, goal, rules, revs):
1925 1925 # TODO only abort if we try to histedit mq patches, not just
1926 1926 # blanket if mq patches are applied somewhere
1927 1927 mq = getattr(repo, 'mq', None)
1928 1928 if mq and mq.applied:
1929 raise error.Abort(_(b'source has mq patches applied'))
1929 raise error.StateError(_(b'source has mq patches applied'))
1930 1930
1931 1931 # basic argument incompatibility processing
1932 1932 outg = opts.get(b'outgoing')
1933 1933 editplan = opts.get(b'edit_plan')
1934 1934 abort = opts.get(b'abort')
1935 1935 force = opts.get(b'force')
1936 1936 if force and not outg:
1937 raise error.Abort(_(b'--force only allowed with --outgoing'))
1937 raise error.InputError(_(b'--force only allowed with --outgoing'))
1938 1938 if goal == b'continue':
1939 1939 if any((outg, abort, revs, freeargs, rules, editplan)):
1940 raise error.Abort(_(b'no arguments allowed with --continue'))
1940 raise error.InputError(_(b'no arguments allowed with --continue'))
1941 1941 elif goal == b'abort':
1942 1942 if any((outg, revs, freeargs, rules, editplan)):
1943 raise error.Abort(_(b'no arguments allowed with --abort'))
1943 raise error.InputError(_(b'no arguments allowed with --abort'))
1944 1944 elif goal == b'edit-plan':
1945 1945 if any((outg, revs, freeargs)):
1946 raise error.Abort(
1946 raise error.InputError(
1947 1947 _(b'only --commands argument allowed with --edit-plan')
1948 1948 )
1949 1949 else:
1950 1950 if outg:
1951 1951 if revs:
1952 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1952 raise error.InputError(
1953 _(b'no revisions allowed with --outgoing')
1954 )
1953 1955 if len(freeargs) > 1:
1954 raise error.Abort(
1956 raise error.InputError(
1955 1957 _(b'only one repo argument allowed with --outgoing')
1956 1958 )
1957 1959 else:
1958 1960 revs.extend(freeargs)
1959 1961 if len(revs) == 0:
1960 1962 defaultrev = destutil.desthistedit(ui, repo)
1961 1963 if defaultrev is not None:
1962 1964 revs.append(defaultrev)
1963 1965
1964 1966 if len(revs) != 1:
1965 raise error.Abort(
1967 raise error.InputError(
1966 1968 _(b'histedit requires exactly one ancestor revision')
1967 1969 )
1968 1970
1969 1971
1970 1972 def _histedit(ui, repo, state, freeargs, opts):
1971 1973 fm = ui.formatter(b'histedit', opts)
1972 1974 fm.startitem()
1973 1975 goal = _getgoal(opts)
1974 1976 revs = opts.get(b'rev', [])
1975 1977 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1976 1978 rules = opts.get(b'commands', b'')
1977 1979 state.keep = opts.get(b'keep', False)
1978 1980
1979 1981 _validateargs(ui, repo, freeargs, opts, goal, rules, revs)
1980 1982
1981 1983 hastags = False
1982 1984 if revs:
1983 1985 revs = scmutil.revrange(repo, revs)
1984 1986 ctxs = [repo[rev] for rev in revs]
1985 1987 for ctx in ctxs:
1986 1988 tags = [tag for tag in ctx.tags() if tag != b'tip']
1987 1989 if not hastags:
1988 1990 hastags = len(tags)
1989 1991 if hastags:
1990 1992 if ui.promptchoice(
1991 1993 _(
1992 1994 b'warning: tags associated with the given'
1993 1995 b' changeset will be lost after histedit.\n'
1994 1996 b'do you want to continue (yN)? $$ &Yes $$ &No'
1995 1997 ),
1996 1998 default=1,
1997 1999 ):
1998 raise error.Abort(_(b'histedit cancelled\n'))
2000 raise error.CanceledError(_(b'histedit cancelled\n'))
1999 2001 # rebuild state
2000 2002 if goal == goalcontinue:
2001 2003 state.read()
2002 2004 state = bootstrapcontinue(ui, state, opts)
2003 2005 elif goal == goaleditplan:
2004 2006 _edithisteditplan(ui, repo, state, rules)
2005 2007 return
2006 2008 elif goal == goalabort:
2007 2009 _aborthistedit(ui, repo, state, nobackup=nobackup)
2008 2010 return
2009 2011 else:
2010 2012 # goal == goalnew
2011 2013 _newhistedit(ui, repo, state, revs, freeargs, opts)
2012 2014
2013 2015 _continuehistedit(ui, repo, state)
2014 2016 _finishhistedit(ui, repo, state, fm)
2015 2017 fm.end()
2016 2018
2017 2019
2018 2020 def _continuehistedit(ui, repo, state):
2019 2021 """This function runs after either:
2020 2022 - bootstrapcontinue (if the goal is 'continue')
2021 2023 - _newhistedit (if the goal is 'new')
2022 2024 """
2023 2025 # preprocess rules so that we can hide inner folds from the user
2024 2026 # and only show one editor
2025 2027 actions = state.actions[:]
2026 2028 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2027 2029 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2028 2030 state.actions[idx].__class__ = _multifold
2029 2031
2030 2032 # Force an initial state file write, so the user can run --abort/continue
2031 2033 # even if there's an exception before the first transaction serialize.
2032 2034 state.write()
2033 2035
2034 2036 tr = None
2035 2037 # Don't use singletransaction by default since it rolls the entire
2036 2038 # transaction back if an unexpected exception happens (like a
2037 2039 # pretxncommit hook throws, or the user aborts the commit msg editor).
2038 2040 if ui.configbool(b"histedit", b"singletransaction"):
2039 2041 # Don't use a 'with' for the transaction, since actions may close
2040 2042 # and reopen a transaction. For example, if the action executes an
2041 2043 # external process it may choose to commit the transaction first.
2042 2044 tr = repo.transaction(b'histedit')
2043 2045 progress = ui.makeprogress(
2044 2046 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2045 2047 )
2046 2048 with progress, util.acceptintervention(tr):
2047 2049 while state.actions:
2048 2050 state.write(tr=tr)
2049 2051 actobj = state.actions[0]
2050 2052 progress.increment(item=actobj.torule())
2051 2053 ui.debug(
2052 2054 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2053 2055 )
2054 2056 parentctx, replacement_ = actobj.run()
2055 2057 state.parentctxnode = parentctx.node()
2056 2058 state.replacements.extend(replacement_)
2057 2059 state.actions.pop(0)
2058 2060
2059 2061 state.write()
2060 2062
2061 2063
2062 2064 def _finishhistedit(ui, repo, state, fm):
2063 2065 """This action runs when histedit is finishing its session"""
2064 2066 mergemod.update(repo[state.parentctxnode])
2065 2067
2066 2068 mapping, tmpnodes, created, ntm = processreplacement(state)
2067 2069 if mapping:
2068 2070 for prec, succs in pycompat.iteritems(mapping):
2069 2071 if not succs:
2070 2072 ui.debug(b'histedit: %s is dropped\n' % short(prec))
2071 2073 else:
2072 2074 ui.debug(
2073 2075 b'histedit: %s is replaced by %s\n'
2074 2076 % (short(prec), short(succs[0]))
2075 2077 )
2076 2078 if len(succs) > 1:
2077 2079 m = b'histedit: %s'
2078 2080 for n in succs[1:]:
2079 2081 ui.debug(m % short(n))
2080 2082
2081 2083 if not state.keep:
2082 2084 if mapping:
2083 2085 movetopmostbookmarks(repo, state.topmost, ntm)
2084 2086 # TODO update mq state
2085 2087 else:
2086 2088 mapping = {}
2087 2089
2088 2090 for n in tmpnodes:
2089 2091 if n in repo:
2090 2092 mapping[n] = ()
2091 2093
2092 2094 # remove entries about unknown nodes
2093 2095 has_node = repo.unfiltered().changelog.index.has_node
2094 2096 mapping = {
2095 2097 k: v
2096 2098 for k, v in mapping.items()
2097 2099 if has_node(k) and all(has_node(n) for n in v)
2098 2100 }
2099 2101 scmutil.cleanupnodes(repo, mapping, b'histedit')
2100 2102 hf = fm.hexfunc
2101 2103 fl = fm.formatlist
2102 2104 fd = fm.formatdict
2103 2105 nodechanges = fd(
2104 2106 {
2105 2107 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2106 2108 for oldn, newn in pycompat.iteritems(mapping)
2107 2109 },
2108 2110 key=b"oldnode",
2109 2111 value=b"newnodes",
2110 2112 )
2111 2113 fm.data(nodechanges=nodechanges)
2112 2114
2113 2115 state.clear()
2114 2116 if os.path.exists(repo.sjoin(b'undo')):
2115 2117 os.unlink(repo.sjoin(b'undo'))
2116 2118 if repo.vfs.exists(b'histedit-last-edit.txt'):
2117 2119 repo.vfs.unlink(b'histedit-last-edit.txt')
2118 2120
2119 2121
2120 2122 def _aborthistedit(ui, repo, state, nobackup=False):
2121 2123 try:
2122 2124 state.read()
2123 2125 __, leafs, tmpnodes, __ = processreplacement(state)
2124 2126 ui.debug(b'restore wc to old parent %s\n' % short(state.topmost))
2125 2127
2126 2128 # Recover our old commits if necessary
2127 2129 if not state.topmost in repo and state.backupfile:
2128 2130 backupfile = repo.vfs.join(state.backupfile)
2129 2131 f = hg.openpath(ui, backupfile)
2130 2132 gen = exchange.readbundle(ui, f, backupfile)
2131 2133 with repo.transaction(b'histedit.abort') as tr:
2132 2134 bundle2.applybundle(
2133 2135 repo,
2134 2136 gen,
2135 2137 tr,
2136 2138 source=b'histedit',
2137 2139 url=b'bundle:' + backupfile,
2138 2140 )
2139 2141
2140 2142 os.remove(backupfile)
2141 2143
2142 2144 # check whether we should update away
2143 2145 if repo.unfiltered().revs(
2144 2146 b'parents() and (%n or %ln::)',
2145 2147 state.parentctxnode,
2146 2148 leafs | tmpnodes,
2147 2149 ):
2148 2150 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2149 2151 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2150 2152 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2151 2153 except Exception:
2152 2154 if state.inprogress():
2153 2155 ui.warn(
2154 2156 _(
2155 2157 b'warning: encountered an exception during histedit '
2156 2158 b'--abort; the repository may not have been completely '
2157 2159 b'cleaned up\n'
2158 2160 )
2159 2161 )
2160 2162 raise
2161 2163 finally:
2162 2164 state.clear()
2163 2165
2164 2166
2165 2167 def hgaborthistedit(ui, repo):
2166 2168 state = histeditstate(repo)
2167 2169 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2168 2170 with repo.wlock() as wlock, repo.lock() as lock:
2169 2171 state.wlock = wlock
2170 2172 state.lock = lock
2171 2173 _aborthistedit(ui, repo, state, nobackup=nobackup)
2172 2174
2173 2175
2174 2176 def _edithisteditplan(ui, repo, state, rules):
2175 2177 state.read()
2176 2178 if not rules:
2177 2179 comment = geteditcomment(
2178 2180 ui, short(state.parentctxnode), short(state.topmost)
2179 2181 )
2180 2182 rules = ruleeditor(repo, ui, state.actions, comment)
2181 2183 else:
2182 2184 rules = _readfile(ui, rules)
2183 2185 actions = parserules(rules, state)
2184 2186 ctxs = [repo[act.node] for act in state.actions if act.node]
2185 2187 warnverifyactions(ui, repo, actions, state, ctxs)
2186 2188 state.actions = actions
2187 2189 state.write()
2188 2190
2189 2191
2190 2192 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2191 2193 outg = opts.get(b'outgoing')
2192 2194 rules = opts.get(b'commands', b'')
2193 2195 force = opts.get(b'force')
2194 2196
2195 2197 cmdutil.checkunfinished(repo)
2196 2198 cmdutil.bailifchanged(repo)
2197 2199
2198 2200 topmost = repo.dirstate.p1()
2199 2201 if outg:
2200 2202 if freeargs:
2201 2203 remote = freeargs[0]
2202 2204 else:
2203 2205 remote = None
2204 2206 root = findoutgoing(ui, repo, remote, force, opts)
2205 2207 else:
2206 2208 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2207 2209 if len(rr) != 1:
2208 raise error.Abort(
2210 raise error.InputError(
2209 2211 _(
2210 2212 b'The specified revisions must have '
2211 2213 b'exactly one common root'
2212 2214 )
2213 2215 )
2214 2216 root = rr[0].node()
2215 2217
2216 2218 revs = between(repo, root, topmost, state.keep)
2217 2219 if not revs:
2218 raise error.Abort(
2220 raise error.InputError(
2219 2221 _(b'%s is not an ancestor of working directory') % short(root)
2220 2222 )
2221 2223
2222 2224 ctxs = [repo[r] for r in revs]
2223 2225
2224 2226 wctx = repo[None]
2225 2227 # Please don't ask me why `ancestors` is this value. I figured it
2226 2228 # out with print-debugging, not by actually understanding what the
2227 2229 # merge code is doing. :(
2228 2230 ancs = [repo[b'.']]
2229 2231 # Sniff-test to make sure we won't collide with untracked files in
2230 2232 # the working directory. If we don't do this, we can get a
2231 2233 # collision after we've started histedit and backing out gets ugly
2232 2234 # for everyone, especially the user.
2233 2235 for c in [ctxs[0].p1()] + ctxs:
2234 2236 try:
2235 2237 mergemod.calculateupdates(
2236 2238 repo,
2237 2239 wctx,
2238 2240 c,
2239 2241 ancs,
2240 2242 # These parameters were determined by print-debugging
2241 2243 # what happens later on inside histedit.
2242 2244 branchmerge=False,
2243 2245 force=False,
2244 2246 acceptremote=False,
2245 2247 followcopies=False,
2246 2248 )
2247 2249 except error.Abort:
2248 raise error.Abort(
2250 raise error.StateError(
2249 2251 _(
2250 2252 b"untracked files in working directory conflict with files in %s"
2251 2253 )
2252 2254 % c
2253 2255 )
2254 2256
2255 2257 if not rules:
2256 2258 comment = geteditcomment(ui, short(root), short(topmost))
2257 2259 actions = [pick(state, r) for r in revs]
2258 2260 rules = ruleeditor(repo, ui, actions, comment)
2259 2261 else:
2260 2262 rules = _readfile(ui, rules)
2261 2263 actions = parserules(rules, state)
2262 2264 warnverifyactions(ui, repo, actions, state, ctxs)
2263 2265
2264 2266 parentctxnode = repo[root].p1().node()
2265 2267
2266 2268 state.parentctxnode = parentctxnode
2267 2269 state.actions = actions
2268 2270 state.topmost = topmost
2269 2271 state.replacements = []
2270 2272
2271 2273 ui.log(
2272 2274 b"histedit",
2273 2275 b"%d actions to histedit\n",
2274 2276 len(actions),
2275 2277 histedit_num_actions=len(actions),
2276 2278 )
2277 2279
2278 2280 # Create a backup so we can always abort completely.
2279 2281 backupfile = None
2280 2282 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2281 2283 backupfile = repair.backupbundle(
2282 2284 repo, [parentctxnode], [topmost], root, b'histedit'
2283 2285 )
2284 2286 state.backupfile = backupfile
2285 2287
2286 2288
2287 2289 def _getsummary(ctx):
2288 2290 # a common pattern is to extract the summary but default to the empty
2289 2291 # string
2290 2292 summary = ctx.description() or b''
2291 2293 if summary:
2292 2294 summary = summary.splitlines()[0]
2293 2295 return summary
2294 2296
2295 2297
2296 2298 def bootstrapcontinue(ui, state, opts):
2297 2299 repo = state.repo
2298 2300
2299 2301 ms = mergestatemod.mergestate.read(repo)
2300 2302 mergeutil.checkunresolved(ms)
2301 2303
2302 2304 if state.actions:
2303 2305 actobj = state.actions.pop(0)
2304 2306
2305 2307 if _isdirtywc(repo):
2306 2308 actobj.continuedirty()
2307 2309 if _isdirtywc(repo):
2308 2310 abortdirty()
2309 2311
2310 2312 parentctx, replacements = actobj.continueclean()
2311 2313
2312 2314 state.parentctxnode = parentctx.node()
2313 2315 state.replacements.extend(replacements)
2314 2316
2315 2317 return state
2316 2318
2317 2319
2318 2320 def between(repo, old, new, keep):
2319 2321 """select and validate the set of revision to edit
2320 2322
2321 2323 When keep is false, the specified set can't have children."""
2322 2324 revs = repo.revs(b'%n::%n', old, new)
2323 2325 if revs and not keep:
2324 2326 rewriteutil.precheck(repo, revs, b'edit')
2325 2327 if repo.revs(b'(%ld) and merge()', revs):
2326 raise error.Abort(_(b'cannot edit history that contains merges'))
2328 raise error.StateError(
2329 _(b'cannot edit history that contains merges')
2330 )
2327 2331 return pycompat.maplist(repo.changelog.node, revs)
2328 2332
2329 2333
2330 2334 def ruleeditor(repo, ui, actions, editcomment=b""):
2331 2335 """open an editor to edit rules
2332 2336
2333 2337 rules are in the format [ [act, ctx], ...] like in state.rules
2334 2338 """
2335 2339 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2336 2340 newact = util.sortdict()
2337 2341 for act in actions:
2338 2342 ctx = repo[act.node]
2339 2343 summary = _getsummary(ctx)
2340 2344 fword = summary.split(b' ', 1)[0].lower()
2341 2345 added = False
2342 2346
2343 2347 # if it doesn't end with the special character '!' just skip this
2344 2348 if fword.endswith(b'!'):
2345 2349 fword = fword[:-1]
2346 2350 if fword in primaryactions | secondaryactions | tertiaryactions:
2347 2351 act.verb = fword
2348 2352 # get the target summary
2349 2353 tsum = summary[len(fword) + 1 :].lstrip()
2350 2354 # safe but slow: reverse iterate over the actions so we
2351 2355 # don't clash on two commits having the same summary
2352 2356 for na, l in reversed(list(pycompat.iteritems(newact))):
2353 2357 actx = repo[na.node]
2354 2358 asum = _getsummary(actx)
2355 2359 if asum == tsum:
2356 2360 added = True
2357 2361 l.append(act)
2358 2362 break
2359 2363
2360 2364 if not added:
2361 2365 newact[act] = []
2362 2366
2363 2367 # copy over and flatten the new list
2364 2368 actions = []
2365 2369 for na, l in pycompat.iteritems(newact):
2366 2370 actions.append(na)
2367 2371 actions += l
2368 2372
2369 2373 rules = b'\n'.join([act.torule() for act in actions])
2370 2374 rules += b'\n\n'
2371 2375 rules += editcomment
2372 2376 rules = ui.edit(
2373 2377 rules,
2374 2378 ui.username(),
2375 2379 {b'prefix': b'histedit'},
2376 2380 repopath=repo.path,
2377 2381 action=b'histedit',
2378 2382 )
2379 2383
2380 2384 # Save edit rules in .hg/histedit-last-edit.txt in case
2381 2385 # the user needs to ask for help after something
2382 2386 # surprising happens.
2383 2387 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2384 2388 f.write(rules)
2385 2389
2386 2390 return rules
2387 2391
2388 2392
2389 2393 def parserules(rules, state):
2390 2394 """Read the histedit rules string and return list of action objects"""
2391 2395 rules = [
2392 2396 l
2393 2397 for l in (r.strip() for r in rules.splitlines())
2394 2398 if l and not l.startswith(b'#')
2395 2399 ]
2396 2400 actions = []
2397 2401 for r in rules:
2398 2402 if b' ' not in r:
2399 2403 raise error.ParseError(_(b'malformed line "%s"') % r)
2400 2404 verb, rest = r.split(b' ', 1)
2401 2405
2402 2406 if verb not in actiontable:
2403 2407 raise error.ParseError(_(b'unknown action "%s"') % verb)
2404 2408
2405 2409 action = actiontable[verb].fromrule(state, rest)
2406 2410 actions.append(action)
2407 2411 return actions
2408 2412
2409 2413
2410 2414 def warnverifyactions(ui, repo, actions, state, ctxs):
2411 2415 try:
2412 2416 verifyactions(actions, state, ctxs)
2413 2417 except error.ParseError:
2414 2418 if repo.vfs.exists(b'histedit-last-edit.txt'):
2415 2419 ui.warn(
2416 2420 _(
2417 2421 b'warning: histedit rules saved '
2418 2422 b'to: .hg/histedit-last-edit.txt\n'
2419 2423 )
2420 2424 )
2421 2425 raise
2422 2426
2423 2427
2424 2428 def verifyactions(actions, state, ctxs):
2425 2429 """Verify that there exists exactly one action per given changeset and
2426 2430 other constraints.
2427 2431
2428 2432 Will abort if there are to many or too few rules, a malformed rule,
2429 2433 or a rule on a changeset outside of the user-given range.
2430 2434 """
2431 2435 expected = {c.node() for c in ctxs}
2432 2436 seen = set()
2433 2437 prev = None
2434 2438
2435 2439 if actions and actions[0].verb in [b'roll', b'fold']:
2436 2440 raise error.ParseError(
2437 2441 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2438 2442 )
2439 2443
2440 2444 for action in actions:
2441 2445 action.verify(prev, expected, seen)
2442 2446 prev = action
2443 2447 if action.node is not None:
2444 2448 seen.add(action.node)
2445 2449 missing = sorted(expected - seen) # sort to stabilize output
2446 2450
2447 2451 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2448 2452 if len(actions) == 0:
2449 2453 raise error.ParseError(
2450 2454 _(b'no rules provided'),
2451 2455 hint=_(b'use strip extension to remove commits'),
2452 2456 )
2453 2457
2454 2458 drops = [drop(state, n) for n in missing]
2455 2459 # put the in the beginning so they execute immediately and
2456 2460 # don't show in the edit-plan in the future
2457 2461 actions[:0] = drops
2458 2462 elif missing:
2459 2463 raise error.ParseError(
2460 2464 _(b'missing rules for changeset %s') % short(missing[0]),
2461 2465 hint=_(
2462 2466 b'use "drop %s" to discard, see also: '
2463 2467 b"'hg help -e histedit.config'"
2464 2468 )
2465 2469 % short(missing[0]),
2466 2470 )
2467 2471
2468 2472
2469 2473 def adjustreplacementsfrommarkers(repo, oldreplacements):
2470 2474 """Adjust replacements from obsolescence markers
2471 2475
2472 2476 Replacements structure is originally generated based on
2473 2477 histedit's state and does not account for changes that are
2474 2478 not recorded there. This function fixes that by adding
2475 2479 data read from obsolescence markers"""
2476 2480 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2477 2481 return oldreplacements
2478 2482
2479 2483 unfi = repo.unfiltered()
2480 2484 get_rev = unfi.changelog.index.get_rev
2481 2485 obsstore = repo.obsstore
2482 2486 newreplacements = list(oldreplacements)
2483 2487 oldsuccs = [r[1] for r in oldreplacements]
2484 2488 # successors that have already been added to succstocheck once
2485 2489 seensuccs = set().union(
2486 2490 *oldsuccs
2487 2491 ) # create a set from an iterable of tuples
2488 2492 succstocheck = list(seensuccs)
2489 2493 while succstocheck:
2490 2494 n = succstocheck.pop()
2491 2495 missing = get_rev(n) is None
2492 2496 markers = obsstore.successors.get(n, ())
2493 2497 if missing and not markers:
2494 2498 # dead end, mark it as such
2495 2499 newreplacements.append((n, ()))
2496 2500 for marker in markers:
2497 2501 nsuccs = marker[1]
2498 2502 newreplacements.append((n, nsuccs))
2499 2503 for nsucc in nsuccs:
2500 2504 if nsucc not in seensuccs:
2501 2505 seensuccs.add(nsucc)
2502 2506 succstocheck.append(nsucc)
2503 2507
2504 2508 return newreplacements
2505 2509
2506 2510
2507 2511 def processreplacement(state):
2508 2512 """process the list of replacements to return
2509 2513
2510 2514 1) the final mapping between original and created nodes
2511 2515 2) the list of temporary node created by histedit
2512 2516 3) the list of new commit created by histedit"""
2513 2517 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2514 2518 allsuccs = set()
2515 2519 replaced = set()
2516 2520 fullmapping = {}
2517 2521 # initialize basic set
2518 2522 # fullmapping records all operations recorded in replacement
2519 2523 for rep in replacements:
2520 2524 allsuccs.update(rep[1])
2521 2525 replaced.add(rep[0])
2522 2526 fullmapping.setdefault(rep[0], set()).update(rep[1])
2523 2527 new = allsuccs - replaced
2524 2528 tmpnodes = allsuccs & replaced
2525 2529 # Reduce content fullmapping into direct relation between original nodes
2526 2530 # and final node created during history edition
2527 2531 # Dropped changeset are replaced by an empty list
2528 2532 toproceed = set(fullmapping)
2529 2533 final = {}
2530 2534 while toproceed:
2531 2535 for x in list(toproceed):
2532 2536 succs = fullmapping[x]
2533 2537 for s in list(succs):
2534 2538 if s in toproceed:
2535 2539 # non final node with unknown closure
2536 2540 # We can't process this now
2537 2541 break
2538 2542 elif s in final:
2539 2543 # non final node, replace with closure
2540 2544 succs.remove(s)
2541 2545 succs.update(final[s])
2542 2546 else:
2543 2547 final[x] = succs
2544 2548 toproceed.remove(x)
2545 2549 # remove tmpnodes from final mapping
2546 2550 for n in tmpnodes:
2547 2551 del final[n]
2548 2552 # we expect all changes involved in final to exist in the repo
2549 2553 # turn `final` into list (topologically sorted)
2550 2554 get_rev = state.repo.changelog.index.get_rev
2551 2555 for prec, succs in final.items():
2552 2556 final[prec] = sorted(succs, key=get_rev)
2553 2557
2554 2558 # computed topmost element (necessary for bookmark)
2555 2559 if new:
2556 2560 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2557 2561 elif not final:
2558 2562 # Nothing rewritten at all. we won't need `newtopmost`
2559 2563 # It is the same as `oldtopmost` and `processreplacement` know it
2560 2564 newtopmost = None
2561 2565 else:
2562 2566 # every body died. The newtopmost is the parent of the root.
2563 2567 r = state.repo.changelog.rev
2564 2568 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2565 2569
2566 2570 return final, tmpnodes, new, newtopmost
2567 2571
2568 2572
2569 2573 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2570 2574 """Move bookmark from oldtopmost to newly created topmost
2571 2575
2572 2576 This is arguably a feature and we may only want that for the active
2573 2577 bookmark. But the behavior is kept compatible with the old version for now.
2574 2578 """
2575 2579 if not oldtopmost or not newtopmost:
2576 2580 return
2577 2581 oldbmarks = repo.nodebookmarks(oldtopmost)
2578 2582 if oldbmarks:
2579 2583 with repo.lock(), repo.transaction(b'histedit') as tr:
2580 2584 marks = repo._bookmarks
2581 2585 changes = []
2582 2586 for name in oldbmarks:
2583 2587 changes.append((name, newtopmost))
2584 2588 marks.applychanges(repo, tr, changes)
2585 2589
2586 2590
2587 2591 def cleanupnode(ui, repo, nodes, nobackup=False):
2588 2592 """strip a group of nodes from the repository
2589 2593
2590 2594 The set of node to strip may contains unknown nodes."""
2591 2595 with repo.lock():
2592 2596 # do not let filtering get in the way of the cleanse
2593 2597 # we should probably get rid of obsolescence marker created during the
2594 2598 # histedit, but we currently do not have such information.
2595 2599 repo = repo.unfiltered()
2596 2600 # Find all nodes that need to be stripped
2597 2601 # (we use %lr instead of %ln to silently ignore unknown items)
2598 2602 has_node = repo.changelog.index.has_node
2599 2603 nodes = sorted(n for n in nodes if has_node(n))
2600 2604 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2601 2605 if roots:
2602 2606 backup = not nobackup
2603 2607 repair.strip(ui, repo, roots, backup=backup)
2604 2608
2605 2609
2606 2610 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2607 2611 if isinstance(nodelist, bytes):
2608 2612 nodelist = [nodelist]
2609 2613 state = histeditstate(repo)
2610 2614 if state.inprogress():
2611 2615 state.read()
2612 2616 histedit_nodes = {
2613 2617 action.node for action in state.actions if action.node
2614 2618 }
2615 2619 common_nodes = histedit_nodes & set(nodelist)
2616 2620 if common_nodes:
2617 2621 raise error.Abort(
2618 2622 _(b"histedit in progress, can't strip %s")
2619 2623 % b', '.join(short(x) for x in common_nodes)
2620 2624 )
2621 2625 return orig(ui, repo, nodelist, *args, **kwargs)
2622 2626
2623 2627
2624 2628 extensions.wrapfunction(repair, b'strip', stripwrapper)
2625 2629
2626 2630
2627 2631 def summaryhook(ui, repo):
2628 2632 state = histeditstate(repo)
2629 2633 if not state.inprogress():
2630 2634 return
2631 2635 state.read()
2632 2636 if state.actions:
2633 2637 # i18n: column positioning for "hg summary"
2634 2638 ui.write(
2635 2639 _(b'hist: %s (histedit --continue)\n')
2636 2640 % (
2637 2641 ui.label(_(b'%d remaining'), b'histedit.remaining')
2638 2642 % len(state.actions)
2639 2643 )
2640 2644 )
2641 2645
2642 2646
2643 2647 def extsetup(ui):
2644 2648 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2645 2649 statemod.addunfinished(
2646 2650 b'histedit',
2647 2651 fname=b'histedit-state',
2648 2652 allowcommit=True,
2649 2653 continueflag=True,
2650 2654 abortfunc=hgaborthistedit,
2651 2655 )
@@ -1,605 +1,605
1 1 #testcases abortcommand abortflag
2 2
3 3 #if abortflag
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [alias]
6 6 > abort = histedit --abort
7 7 > EOF
8 8 #endif
9 9
10 10 Test argument handling and various data parsing
11 11 ==================================================
12 12
13 13
14 14 Enable extensions used by this test.
15 15 $ cat >>$HGRCPATH <<EOF
16 16 > [extensions]
17 17 > histedit=
18 18 > EOF
19 19
20 20 Repo setup.
21 21 $ hg init foo
22 22 $ cd foo
23 23 $ echo alpha >> alpha
24 24 $ hg addr
25 25 adding alpha
26 26 $ hg ci -m one
27 27 $ echo alpha >> alpha
28 28 $ hg ci -m two
29 29 $ echo alpha >> alpha
30 30 $ hg ci -m three
31 31 $ echo alpha >> alpha
32 32 $ hg ci -m four
33 33 $ echo alpha >> alpha
34 34 $ hg ci -m five
35 35
36 36 $ hg log --style compact --graph
37 37 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
38 38 | five
39 39 |
40 40 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
41 41 | four
42 42 |
43 43 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
44 44 | three
45 45 |
46 46 o 1 579e40513370 1970-01-01 00:00 +0000 test
47 47 | two
48 48 |
49 49 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
50 50 one
51 51
52 52
53 53 histedit --continue/--abort with no existing state
54 54 --------------------------------------------------
55 55
56 56 $ hg histedit --continue
57 57 abort: no histedit in progress
58 58 [20]
59 59 $ hg abort
60 60 abort: no histedit in progress (abortflag !)
61 61 abort: no operation in progress (abortcommand !)
62 62 [20]
63 63
64 64 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
65 65 --------------------------------------------------------------------
66 66
67 67 $ HGEDITOR=cat hg histedit "tip^^"
68 68 pick eb57da33312f 2 three
69 69 pick c8e68270e35a 3 four
70 70 pick 08d98a8350f3 4 five
71 71
72 72 # Edit history between eb57da33312f and 08d98a8350f3
73 73 #
74 74 # Commits are listed from least to most recent
75 75 #
76 76 # You can reorder changesets by reordering the lines
77 77 #
78 78 # Commands:
79 79 #
80 80 # e, edit = use commit, but allow edits before making new commit
81 81 # m, mess = edit commit message without changing commit content
82 82 # p, pick = use commit
83 83 # b, base = checkout changeset and apply further changesets from there
84 84 # d, drop = remove commit from history
85 85 # f, fold = use commit, but combine it with the one above
86 86 # r, roll = like fold, but discard this commit's description and date
87 87 #
88 88
89 89 Run on a revision not ancestors of the current working directory.
90 90 --------------------------------------------------------------------
91 91
92 92 $ hg up 2
93 93 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
94 94 $ hg histedit -r 4
95 95 abort: 08d98a8350f3 is not an ancestor of working directory
96 [255]
96 [10]
97 97 $ hg up --quiet
98 98
99 99
100 100 Test that we pick the minimum of a revrange
101 101 ---------------------------------------
102 102
103 103 $ HGEDITOR=cat hg histedit '2::' --commands - << EOF
104 104 > pick eb57da33312f 2 three
105 105 > pick c8e68270e35a 3 four
106 106 > pick 08d98a8350f3 4 five
107 107 > EOF
108 108 $ hg up --quiet
109 109
110 110 $ HGEDITOR=cat hg histedit 'tip:2' --commands - << EOF
111 111 > pick eb57da33312f 2 three
112 112 > pick c8e68270e35a 3 four
113 113 > pick 08d98a8350f3 4 five
114 114 > EOF
115 115 $ hg up --quiet
116 116
117 117 Test config specified default
118 118 -----------------------------
119 119
120 120 $ HGEDITOR=cat hg histedit --config "histedit.defaultrev=only(.) - ::eb57da33312f" --commands - << EOF
121 121 > pick c8e68270e35a 3 four
122 122 > pick 08d98a8350f3 4 five
123 123 > EOF
124 124
125 125 Test invalid config default
126 126 ---------------------------
127 127
128 128 $ hg histedit --config "histedit.defaultrev="
129 129 config error: config option histedit.defaultrev can't be empty
130 130 [30]
131 131
132 132 Run on a revision not descendants of the initial parent
133 133 --------------------------------------------------------------------
134 134
135 135 Test the message shown for inconsistent histedit state, which may be
136 136 created (and forgotten) by Mercurial earlier than 2.7. This emulates
137 137 Mercurial earlier than 2.7 by renaming ".hg/histedit-state"
138 138 temporarily.
139 139
140 140 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
141 141 @ 4 08d9 five
142 142 |
143 143 o 3 c8e6 four
144 144 |
145 145 o 2 eb57 three
146 146 |
147 147 ~
148 148 $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF
149 149 > edit 08d98a8350f3 4 five
150 150 > EOF
151 151 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 152 Editing (08d98a8350f3), commit as needed now to split the change
153 153 (to edit 08d98a8350f3, `hg histedit --continue` after making changes)
154 154 [240]
155 155
156 156 $ hg graft --continue
157 157 abort: no graft in progress
158 158 (continue: hg histedit --continue)
159 159 [20]
160 160
161 161 $ mv .hg/histedit-state .hg/histedit-state.back
162 162 $ hg update --quiet --clean 2
163 163 $ echo alpha >> alpha
164 164 $ mv .hg/histedit-state.back .hg/histedit-state
165 165
166 166 $ hg histedit --continue
167 167 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
168 168 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
169 169 @ 4 f5ed five
170 170 |
171 171 | o 3 c8e6 four
172 172 |/
173 173 o 2 eb57 three
174 174 |
175 175 ~
176 176
177 177 $ hg unbundle -q $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
178 178 $ hg strip -q -r f5ed --config extensions.strip=
179 179 $ hg up -q 08d98a8350f3
180 180
181 181 Test that missing revisions are detected
182 182 ---------------------------------------
183 183
184 184 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
185 185 > pick eb57da33312f 2 three
186 186 > pick 08d98a8350f3 4 five
187 187 > EOF
188 188 hg: parse error: missing rules for changeset c8e68270e35a
189 189 (use "drop c8e68270e35a" to discard, see also: 'hg help -e histedit.config')
190 190 [10]
191 191
192 192 Test that extra revisions are detected
193 193 ---------------------------------------
194 194
195 195 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
196 196 > pick 6058cbb6cfd7 0 one
197 197 > pick c8e68270e35a 3 four
198 198 > pick 08d98a8350f3 4 five
199 199 > EOF
200 200 hg: parse error: pick "6058cbb6cfd7" changeset was not a candidate
201 201 (only use listed changesets)
202 202 [10]
203 203
204 204 Test malformed line
205 205 ---------------------------------------
206 206
207 207 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
208 208 > pickeb57da33312f2three
209 209 > pick c8e68270e35a 3 four
210 210 > pick 08d98a8350f3 4 five
211 211 > EOF
212 212 hg: parse error: malformed line "pickeb57da33312f2three"
213 213 [10]
214 214
215 215 Test unknown changeset
216 216 ---------------------------------------
217 217
218 218 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
219 219 > pick 0123456789ab 2 three
220 220 > pick c8e68270e35a 3 four
221 221 > pick 08d98a8350f3 4 five
222 222 > EOF
223 223 hg: parse error: unknown changeset 0123456789ab listed
224 224 [10]
225 225
226 226 Test unknown command
227 227 ---------------------------------------
228 228
229 229 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
230 230 > coin eb57da33312f 2 three
231 231 > pick c8e68270e35a 3 four
232 232 > pick 08d98a8350f3 4 five
233 233 > EOF
234 234 hg: parse error: unknown action "coin"
235 235 [10]
236 236
237 237 Test duplicated changeset
238 238 ---------------------------------------
239 239
240 240 So one is missing and one appear twice.
241 241
242 242 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
243 243 > pick eb57da33312f 2 three
244 244 > pick eb57da33312f 2 three
245 245 > pick 08d98a8350f3 4 five
246 246 > EOF
247 247 hg: parse error: duplicated command for changeset eb57da33312f
248 248 [10]
249 249
250 250 Test bogus rev
251 251 ---------------------------------------
252 252
253 253 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
254 254 > pick eb57da33312f 2 three
255 255 > pick 0u98
256 256 > pick 08d98a8350f3 4 five
257 257 > EOF
258 258 hg: parse error: invalid changeset 0u98
259 259 [10]
260 260
261 261 Test short version of command
262 262 ---------------------------------------
263 263
264 264 Note: we use varying amounts of white space between command name and changeset
265 265 short hash. This tests issue3893.
266 266
267 267 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
268 268 > pick eb57da33312f 2 three
269 269 > p c8e68270e35a 3 four
270 270 > f 08d98a8350f3 4 five
271 271 > EOF
272 272 four
273 273 ***
274 274 five
275 275
276 276
277 277
278 278 HG: Enter commit message. Lines beginning with 'HG:' are removed.
279 279 HG: Leave message empty to abort commit.
280 280 HG: --
281 281 HG: user: test
282 282 HG: branch 'default'
283 283 HG: changed alpha
284 284 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/c8e68270e35a-63d8b8d8-histedit.hg
285 285
286 286 $ hg update -q 2
287 287 $ echo x > x
288 288 $ hg add x
289 289 $ hg commit -m'x' x
290 290 created new head
291 291 $ hg histedit -r 'heads(all())'
292 292 abort: The specified revisions must have exactly one common root
293 [255]
293 [10]
294 294
295 295 Test that trimming description using multi-byte characters
296 296 --------------------------------------------------------------------
297 297
298 298 $ "$PYTHON" <<EOF
299 299 > fp = open('logfile', 'wb')
300 300 > fp.write(b'12345678901234567890123456789012345678901234567890' +
301 301 > b'12345') # there are 5 more columns for 80 columns
302 302 >
303 303 > # 2 x 4 = 8 columns, but 3 x 4 = 12 bytes
304 304 > fp.write(u'\u3042\u3044\u3046\u3048'.encode('utf-8'))
305 305 >
306 306 > fp.close()
307 307 > EOF
308 308 $ echo xx >> x
309 309 $ hg --encoding utf-8 commit --logfile logfile
310 310
311 311 $ HGEDITOR=cat hg --encoding utf-8 histedit tip
312 312 pick 3d3ea1f3a10b 5 1234567890123456789012345678901234567890123456789012345\xe3\x81\x82... (esc)
313 313
314 314 # Edit history between 3d3ea1f3a10b and 3d3ea1f3a10b
315 315 #
316 316 # Commits are listed from least to most recent
317 317 #
318 318 # You can reorder changesets by reordering the lines
319 319 #
320 320 # Commands:
321 321 #
322 322 # e, edit = use commit, but allow edits before making new commit
323 323 # m, mess = edit commit message without changing commit content
324 324 # p, pick = use commit
325 325 # b, base = checkout changeset and apply further changesets from there
326 326 # d, drop = remove commit from history
327 327 # f, fold = use commit, but combine it with the one above
328 328 # r, roll = like fold, but discard this commit's description and date
329 329 #
330 330
331 331 Test --continue with --keep
332 332
333 333 $ hg strip -q -r . --config extensions.strip=
334 334 $ hg histedit '.^' -q --keep --commands - << EOF
335 335 > edit eb57da33312f 2 three
336 336 > pick f3cfcca30c44 4 x
337 337 > EOF
338 338 Editing (eb57da33312f), commit as needed now to split the change
339 339 (to edit eb57da33312f, `hg histedit --continue` after making changes)
340 340 [240]
341 341 $ echo edit >> alpha
342 342 $ hg histedit -q --continue
343 343 $ hg log -G -T '{rev}:{node|short} {desc}'
344 344 @ 6:8fda0c726bf2 x
345 345 |
346 346 o 5:63379946892c three
347 347 |
348 348 | o 4:f3cfcca30c44 x
349 349 | |
350 350 | | o 3:2a30f3cfee78 four
351 351 | |/ ***
352 352 | | five
353 353 | o 2:eb57da33312f three
354 354 |/
355 355 o 1:579e40513370 two
356 356 |
357 357 o 0:6058cbb6cfd7 one
358 358
359 359
360 360 Test that abort fails gracefully on exception
361 361 ----------------------------------------------
362 362 $ hg histedit . -q --commands - << EOF
363 363 > edit 8fda0c726bf2 6 x
364 364 > EOF
365 365 Editing (8fda0c726bf2), commit as needed now to split the change
366 366 (to edit 8fda0c726bf2, `hg histedit --continue` after making changes)
367 367 [240]
368 368 Corrupt histedit state file
369 369 $ sed 's/8fda0c726bf2/123456789012/' .hg/histedit-state > ../corrupt-histedit
370 370 $ mv ../corrupt-histedit .hg/histedit-state
371 371 $ hg abort
372 372 warning: encountered an exception during histedit --abort; the repository may not have been completely cleaned up
373 373 abort: $TESTTMP/foo/.hg/strip-backup/*-histedit.hg: $ENOENT$ (glob) (windows !)
374 374 abort: $ENOENT$: '$TESTTMP/foo/.hg/strip-backup/*-histedit.hg' (glob) (no-windows !)
375 375 [255]
376 376 Histedit state has been exited
377 377 $ hg summary -q
378 378 parent: 5:63379946892c
379 379 commit: 1 added, 1 unknown (new branch head)
380 380 update: 4 new changesets (update)
381 381
382 382 $ cd ..
383 383
384 384 Set up default base revision tests
385 385
386 386 $ hg init defaultbase
387 387 $ cd defaultbase
388 388 $ touch foo
389 389 $ hg -q commit -A -m root
390 390 $ echo 1 > foo
391 391 $ hg commit -m 'public 1'
392 392 $ hg phase --force --public -r .
393 393 $ echo 2 > foo
394 394 $ hg commit -m 'draft after public'
395 395 $ hg -q up -r 1
396 396 $ echo 3 > foo
397 397 $ hg commit -m 'head 1 public'
398 398 created new head
399 399 $ hg phase --force --public -r .
400 400 $ echo 4 > foo
401 401 $ hg commit -m 'head 1 draft 1'
402 402 $ echo 5 > foo
403 403 $ hg commit -m 'head 1 draft 2'
404 404 $ hg -q up -r 2
405 405 $ echo 6 > foo
406 406 $ hg commit -m 'head 2 commit 1'
407 407 $ echo 7 > foo
408 408 $ hg commit -m 'head 2 commit 2'
409 409 $ hg -q up -r 2
410 410 $ echo 8 > foo
411 411 $ hg commit -m 'head 3'
412 412 created new head
413 413 $ hg -q up -r 2
414 414 $ echo 9 > foo
415 415 $ hg commit -m 'head 4'
416 416 created new head
417 417 $ hg merge --tool :local -r 8
418 418 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
419 419 (branch merge, don't forget to commit)
420 420 $ hg commit -m 'merge head 3 into head 4'
421 421 $ echo 11 > foo
422 422 $ hg commit -m 'commit 1 after merge'
423 423 $ echo 12 > foo
424 424 $ hg commit -m 'commit 2 after merge'
425 425
426 426 $ hg log -G -T '{rev}:{node|short} {phase} {desc}\n'
427 427 @ 12:8cde254db839 draft commit 2 after merge
428 428 |
429 429 o 11:6f2f0241f119 draft commit 1 after merge
430 430 |
431 431 o 10:90506cc76b00 draft merge head 3 into head 4
432 432 |\
433 433 | o 9:f8607a373a97 draft head 4
434 434 | |
435 435 o | 8:0da92be05148 draft head 3
436 436 |/
437 437 | o 7:4c35cdf97d5e draft head 2 commit 2
438 438 | |
439 439 | o 6:931820154288 draft head 2 commit 1
440 440 |/
441 441 | o 5:8cdc02b9bc63 draft head 1 draft 2
442 442 | |
443 443 | o 4:463b8c0d2973 draft head 1 draft 1
444 444 | |
445 445 | o 3:23a0c4eefcbf public head 1 public
446 446 | |
447 447 o | 2:4117331c3abb draft draft after public
448 448 |/
449 449 o 1:4426d359ea59 public public 1
450 450 |
451 451 o 0:54136a8ddf32 public root
452 452
453 453
454 454 Default base revision should stop at public changesets
455 455
456 456 $ hg -q up 8cdc02b9bc63
457 457 $ hg histedit --commands - <<EOF
458 458 > pick 463b8c0d2973
459 459 > pick 8cdc02b9bc63
460 460 > EOF
461 461
462 462 Default base revision should stop at branchpoint
463 463
464 464 $ hg -q up 4c35cdf97d5e
465 465 $ hg histedit --commands - <<EOF
466 466 > pick 931820154288
467 467 > pick 4c35cdf97d5e
468 468 > EOF
469 469
470 470 Default base revision should stop at merge commit
471 471
472 472 $ hg -q up 8cde254db839
473 473 $ hg histedit --commands - <<EOF
474 474 > pick 6f2f0241f119
475 475 > pick 8cde254db839
476 476 > EOF
477 477
478 478 commit --amend should abort if histedit is in progress
479 479 (issue4800) and markers are not being created.
480 480 Eventually, histedit could perhaps look at `source` extra,
481 481 in which case this test should be revisited.
482 482
483 483 $ hg -q up 8cde254db839
484 484 $ hg histedit 6f2f0241f119 --commands - <<EOF
485 485 > pick 8cde254db839
486 486 > edit 6f2f0241f119
487 487 > EOF
488 488 merging foo
489 489 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
490 490 Fix up the change (pick 8cde254db839)
491 491 (hg histedit --continue to resume)
492 492 [240]
493 493 $ hg resolve -m --all
494 494 (no more unresolved files)
495 495 continue: hg histedit --continue
496 496 $ hg histedit --cont
497 497 merging foo
498 498 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
499 499 Editing (6f2f0241f119), commit as needed now to split the change
500 500 (to edit 6f2f0241f119, `hg histedit --continue` after making changes)
501 501 [240]
502 502 $ hg resolve -m --all
503 503 (no more unresolved files)
504 504 continue: hg histedit --continue
505 505 $ hg commit --amend -m 'reject this fold'
506 506 abort: histedit in progress
507 507 (use 'hg histedit --continue' or 'hg histedit --abort')
508 508 [20]
509 509
510 510 With markers enabled, histedit does not get confused, and
511 511 amend should not be blocked by the ongoing histedit.
512 512
513 513 $ cat >>$HGRCPATH <<EOF
514 514 > [experimental]
515 515 > evolution.createmarkers=True
516 516 > evolution.allowunstable=True
517 517 > EOF
518 518 $ hg commit --amend -m 'allow this fold'
519 519 $ hg histedit --continue
520 520
521 521 $ cd ..
522 522
523 523 Test autoverb feature
524 524
525 525 $ hg init autoverb
526 526 $ cd autoverb
527 527 $ echo alpha >> alpha
528 528 $ hg ci -qAm one
529 529 $ echo alpha >> alpha
530 530 $ hg ci -qm two
531 531 $ echo beta >> beta
532 532 $ hg ci -qAm "roll! one"
533 533
534 534 $ hg log --style compact --graph
535 535 @ 2[tip] 4f34d0f8b5fa 1970-01-01 00:00 +0000 test
536 536 | roll! one
537 537 |
538 538 o 1 579e40513370 1970-01-01 00:00 +0000 test
539 539 | two
540 540 |
541 541 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
542 542 one
543 543
544 544
545 545 Check that 'roll' is selected by default
546 546
547 547 $ HGEDITOR=cat hg histedit 0 --config experimental.histedit.autoverb=True
548 548 pick 6058cbb6cfd7 0 one
549 549 roll 4f34d0f8b5fa 2 roll! one
550 550 pick 579e40513370 1 two
551 551
552 552 # Edit history between 6058cbb6cfd7 and 4f34d0f8b5fa
553 553 #
554 554 # Commits are listed from least to most recent
555 555 #
556 556 # You can reorder changesets by reordering the lines
557 557 #
558 558 # Commands:
559 559 #
560 560 # e, edit = use commit, but allow edits before making new commit
561 561 # m, mess = edit commit message without changing commit content
562 562 # p, pick = use commit
563 563 # b, base = checkout changeset and apply further changesets from there
564 564 # d, drop = remove commit from history
565 565 # f, fold = use commit, but combine it with the one above
566 566 # r, roll = like fold, but discard this commit's description and date
567 567 #
568 568
569 569 $ cd ..
570 570
571 571 Check that histedit's commands accept revsets
572 572 $ hg init bar
573 573 $ cd bar
574 574 $ echo w >> a
575 575 $ hg ci -qAm "adds a"
576 576 $ echo x >> b
577 577 $ hg ci -qAm "adds b"
578 578 $ echo y >> c
579 579 $ hg ci -qAm "adds c"
580 580 $ echo z >> d
581 581 $ hg ci -qAm "adds d"
582 582 $ hg log -G -T '{rev} {desc}\n'
583 583 @ 3 adds d
584 584 |
585 585 o 2 adds c
586 586 |
587 587 o 1 adds b
588 588 |
589 589 o 0 adds a
590 590
591 591 $ HGEDITOR=cat hg histedit "2" --commands - << EOF
592 592 > base -4 adds c
593 593 > pick 2 adds c
594 594 > pick tip adds d
595 595 > EOF
596 596 $ hg log -G -T '{rev} {desc}\n'
597 597 @ 5 adds d
598 598 |
599 599 o 4 adds c
600 600 |
601 601 | o 1 adds b
602 602 |/
603 603 o 0 adds a
604 604
605 605
@@ -1,556 +1,556
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > histedit=
6 6 > strip=
7 7 > mockmakedate = $TESTDIR/mockmakedate.py
8 8 > EOF
9 9
10 10 $ initrepo ()
11 11 > {
12 12 > hg init r
13 13 > cd r
14 14 > for x in a b c d e f g; do
15 15 > echo $x > $x
16 16 > hg add $x
17 17 > hg ci -m $x
18 18 > done
19 19 > }
20 20
21 21 $ initrepo
22 22
23 23 log before edit
24 24 $ hg log --graph
25 25 @ changeset: 6:3c6a8ed2ebe8
26 26 | tag: tip
27 27 | user: test
28 28 | date: Thu Jan 01 00:00:00 1970 +0000
29 29 | summary: g
30 30 |
31 31 o changeset: 5:652413bf663e
32 32 | user: test
33 33 | date: Thu Jan 01 00:00:00 1970 +0000
34 34 | summary: f
35 35 |
36 36 o changeset: 4:e860deea161a
37 37 | user: test
38 38 | date: Thu Jan 01 00:00:00 1970 +0000
39 39 | summary: e
40 40 |
41 41 o changeset: 3:055a42cdd887
42 42 | user: test
43 43 | date: Thu Jan 01 00:00:00 1970 +0000
44 44 | summary: d
45 45 |
46 46 o changeset: 2:177f92b77385
47 47 | user: test
48 48 | date: Thu Jan 01 00:00:00 1970 +0000
49 49 | summary: c
50 50 |
51 51 o changeset: 1:d2ae7f538514
52 52 | user: test
53 53 | date: Thu Jan 01 00:00:00 1970 +0000
54 54 | summary: b
55 55 |
56 56 o changeset: 0:cb9a9f314b8b
57 57 user: test
58 58 date: Thu Jan 01 00:00:00 1970 +0000
59 59 summary: a
60 60
61 61 dirty a file
62 62 $ echo a > g
63 63 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF
64 64 > EOF
65 65 abort: uncommitted changes
66 66 [20]
67 67 $ echo g > g
68 68
69 69 edit the history
70 70 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
71 71 > pick 177f92b77385 c
72 72 > pick 055a42cdd887 d
73 73 > edit e860deea161a e
74 74 > pick 652413bf663e f
75 75 > pick 3c6a8ed2ebe8 g
76 76 > EOF
77 77 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
78 78 Editing (e860deea161a), commit as needed now to split the change
79 79 (to edit e860deea161a, `hg histedit --continue` after making changes)
80 80
81 81 try to update and get an error
82 82 $ hg update tip
83 83 abort: histedit in progress
84 84 (use 'hg histedit --continue' or 'hg histedit --abort')
85 85 [20]
86 86
87 87 edit the plan via the editor
88 88 $ cat >> $TESTTMP/editplan.sh <<EOF
89 89 > cat > \$1 <<EOF2
90 90 > drop e860deea161a e
91 91 > drop 652413bf663e f
92 92 > drop 3c6a8ed2ebe8 g
93 93 > EOF2
94 94 > EOF
95 95 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
96 96 $ cat .hg/histedit-state
97 97 v1
98 98 055a42cdd88768532f9cf79daa407fc8d138de9b
99 99 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
100 100 False
101 101 3
102 102 drop
103 103 e860deea161a2f77de56603b340ebbb4536308ae
104 104 drop
105 105 652413bf663ef2a641cab26574e46d5f5a64a55a
106 106 drop
107 107 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
108 108 0
109 109 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
110 110
111 111 edit the plan via --commands
112 112 $ hg histedit --edit-plan --commands - 2>&1 << EOF
113 113 > edit e860deea161a e
114 114 > pick 652413bf663e f
115 115 > drop 3c6a8ed2ebe8 g
116 116 > EOF
117 117 $ cat .hg/histedit-state
118 118 v1
119 119 055a42cdd88768532f9cf79daa407fc8d138de9b
120 120 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
121 121 False
122 122 3
123 123 edit
124 124 e860deea161a2f77de56603b340ebbb4536308ae
125 125 pick
126 126 652413bf663ef2a641cab26574e46d5f5a64a55a
127 127 drop
128 128 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
129 129 0
130 130 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
131 131
132 132 Go at a random point and try to continue
133 133
134 134 $ hg id -n
135 135 3+
136 136 $ hg up 0
137 137 abort: histedit in progress
138 138 (use 'hg histedit --continue' or 'hg histedit --abort')
139 139 [20]
140 140
141 141 Try to delete necessary commit
142 142 $ hg strip -r 652413b
143 143 abort: histedit in progress, can't strip 652413bf663e
144 144 [255]
145 145
146 146 commit, then edit the revision
147 147 $ hg ci -m 'wat'
148 148 created new head
149 149 $ echo a > e
150 150
151 151 qnew should fail while we're in the middle of the edit step
152 152
153 153 $ hg --config extensions.mq= qnew please-fail
154 154 abort: histedit in progress
155 155 (use 'hg histedit --continue' or 'hg histedit --abort')
156 156 [20]
157 157 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
158 158
159 159 $ hg log --graph
160 160 @ changeset: 6:b5f70786f9b0
161 161 | tag: tip
162 162 | user: test
163 163 | date: Thu Jan 01 00:00:00 1970 +0000
164 164 | summary: f
165 165 |
166 166 o changeset: 5:a5e1ba2f7afb
167 167 | user: test
168 168 | date: Thu Jan 01 00:00:00 1970 +0000
169 169 | summary: foobaz
170 170 |
171 171 o changeset: 4:1a60820cd1f6
172 172 | user: test
173 173 | date: Thu Jan 01 00:00:00 1970 +0000
174 174 | summary: wat
175 175 |
176 176 o changeset: 3:055a42cdd887
177 177 | user: test
178 178 | date: Thu Jan 01 00:00:00 1970 +0000
179 179 | summary: d
180 180 |
181 181 o changeset: 2:177f92b77385
182 182 | user: test
183 183 | date: Thu Jan 01 00:00:00 1970 +0000
184 184 | summary: c
185 185 |
186 186 o changeset: 1:d2ae7f538514
187 187 | user: test
188 188 | date: Thu Jan 01 00:00:00 1970 +0000
189 189 | summary: b
190 190 |
191 191 o changeset: 0:cb9a9f314b8b
192 192 user: test
193 193 date: Thu Jan 01 00:00:00 1970 +0000
194 194 summary: a
195 195
196 196
197 197 $ hg cat e
198 198 a
199 199
200 200 Stripping necessary commits should not break --abort
201 201
202 202 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
203 203 > edit 1a60820cd1f6 wat
204 204 > pick a5e1ba2f7afb foobaz
205 205 > pick b5f70786f9b0 g
206 206 > EOF
207 207 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
208 208 Editing (1a60820cd1f6), commit as needed now to split the change
209 209 (to edit 1a60820cd1f6, `hg histedit --continue` after making changes)
210 210
211 211 $ mv .hg/histedit-state .hg/histedit-state.bak
212 212 $ hg strip -q -r b5f70786f9b0
213 213 $ mv .hg/histedit-state.bak .hg/histedit-state
214 214 $ hg histedit --abort
215 215 adding changesets
216 216 adding manifests
217 217 adding file changes
218 218 added 1 changesets with 1 changes to 3 files
219 219 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
220 220 $ hg log -r .
221 221 changeset: 6:b5f70786f9b0
222 222 tag: tip
223 223 user: test
224 224 date: Thu Jan 01 00:00:00 1970 +0000
225 225 summary: f
226 226
227 227
228 228 check histedit_source
229 229
230 230 $ hg log --debug --rev 5
231 231 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
232 232 phase: draft
233 233 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
234 234 parent: -1:0000000000000000000000000000000000000000
235 235 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
236 236 user: test
237 237 date: Thu Jan 01 00:00:00 1970 +0000
238 238 files: e
239 239 extra: branch=default
240 240 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
241 241 description:
242 242 foobaz
243 243
244 244
245 245
246 246 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
247 247 > edit b5f70786f9b0 f
248 248 > EOF
249 249 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
250 250 Editing (b5f70786f9b0), commit as needed now to split the change
251 251 (to edit b5f70786f9b0, `hg histedit --continue` after making changes)
252 252 $ hg status
253 253 A f
254 254
255 255 $ hg summary
256 256 parent: 5:a5e1ba2f7afb
257 257 foobaz
258 258 branch: default
259 259 commit: 1 added (new branch head)
260 260 update: 1 new changesets (update)
261 261 phases: 7 draft
262 262 hist: 1 remaining (histedit --continue)
263 263
264 264 (test also that editor is invoked if histedit is continued for
265 265 "edit" action)
266 266
267 267 $ HGEDITOR='cat' hg histedit --continue
268 268 f
269 269
270 270
271 271 HG: Enter commit message. Lines beginning with 'HG:' are removed.
272 272 HG: Leave message empty to abort commit.
273 273 HG: --
274 274 HG: user: test
275 275 HG: branch 'default'
276 276 HG: added f
277 277 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-histedit.hg
278 278
279 279 $ hg status
280 280
281 281 log after edit
282 282 $ hg log --limit 1
283 283 changeset: 6:a107ee126658
284 284 tag: tip
285 285 user: test
286 286 date: Thu Jan 01 00:00:00 1970 +0000
287 287 summary: f
288 288
289 289
290 290 say we'll change the message, but don't.
291 291 $ cat > ../edit.sh <<EOF
292 292 > cat "\$1" | sed s/pick/mess/ > tmp
293 293 > mv tmp "\$1"
294 294 > EOF
295 295 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
296 296 $ hg status
297 297 $ hg log --limit 1
298 298 changeset: 6:1fd3b2fe7754
299 299 tag: tip
300 300 user: test
301 301 date: Thu Jan 01 00:00:00 1970 +0000
302 302 summary: f
303 303
304 304
305 305 modify the message
306 306
307 307 check saving last-message.txt, at first
308 308
309 309 $ cat > $TESTTMP/commitfailure.py <<EOF
310 310 > from mercurial import error
311 311 > def reposetup(ui, repo):
312 312 > class commitfailure(repo.__class__):
313 313 > def commit(self, *args, **kwargs):
314 314 > raise error.Abort(b'emulating unexpected abort')
315 315 > repo.__class__ = commitfailure
316 316 > EOF
317 317 $ cat >> .hg/hgrc <<EOF
318 318 > [extensions]
319 319 > # this failure occurs before editor invocation
320 320 > commitfailure = $TESTTMP/commitfailure.py
321 321 > EOF
322 322
323 323 $ cat > $TESTTMP/editor.sh <<EOF
324 324 > echo "==== before editing"
325 325 > cat \$1
326 326 > echo "===="
327 327 > echo "check saving last-message.txt" >> \$1
328 328 > EOF
329 329
330 330 (test that editor is not invoked before transaction starting)
331 331
332 332 $ rm -f .hg/last-message.txt
333 333 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
334 334 > mess 1fd3b2fe7754 f
335 335 > EOF
336 336 abort: emulating unexpected abort
337 337 $ test -f .hg/last-message.txt
338 338 [1]
339 339
340 340 $ cat >> .hg/hgrc <<EOF
341 341 > [extensions]
342 342 > commitfailure = !
343 343 > EOF
344 344 $ hg histedit --abort -q
345 345
346 346 (test that editor is invoked and commit message is saved into
347 347 "last-message.txt")
348 348
349 349 $ cat >> .hg/hgrc <<EOF
350 350 > [hooks]
351 351 > # this failure occurs after editor invocation
352 352 > pretxncommit.unexpectedabort = false
353 353 > EOF
354 354
355 355 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
356 356 A f
357 357
358 358 $ rm -f .hg/last-message.txt
359 359 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
360 360 > mess 1fd3b2fe7754 f
361 361 > EOF
362 362 ==== before editing
363 363 f
364 364
365 365
366 366 HG: Enter commit message. Lines beginning with 'HG:' are removed.
367 367 HG: Leave message empty to abort commit.
368 368 HG: --
369 369 HG: user: test
370 370 HG: branch 'default'
371 371 HG: added f
372 372 ====
373 373 transaction abort!
374 374 rollback completed
375 375 note: commit message saved in .hg/last-message.txt
376 376 note: use 'hg commit --logfile .hg/last-message.txt --edit' to reuse it
377 377 abort: pretxncommit.unexpectedabort hook exited with status 1
378 378 [40]
379 379 $ cat .hg/last-message.txt
380 380 f
381 381
382 382
383 383 check saving last-message.txt
384 384
385 385 (test also that editor is invoked if histedit is continued for "message"
386 386 action)
387 387
388 388 $ HGEDITOR=cat hg histedit --continue
389 389 f
390 390
391 391
392 392 HG: Enter commit message. Lines beginning with 'HG:' are removed.
393 393 HG: Leave message empty to abort commit.
394 394 HG: --
395 395 HG: user: test
396 396 HG: branch 'default'
397 397 HG: added f
398 398 transaction abort!
399 399 rollback completed
400 400 note: commit message saved in .hg/last-message.txt
401 401 note: use 'hg commit --logfile .hg/last-message.txt --edit' to reuse it
402 402 abort: pretxncommit.unexpectedabort hook exited with status 1
403 403 [40]
404 404
405 405 $ cat >> .hg/hgrc <<EOF
406 406 > [hooks]
407 407 > pretxncommit.unexpectedabort =
408 408 > EOF
409 409 $ hg histedit --abort -q
410 410
411 411 then, check "modify the message" itself
412 412
413 413 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
414 414 > mess 1fd3b2fe7754 f
415 415 > EOF
416 416 $ hg status
417 417 $ hg log --limit 1
418 418 changeset: 6:62feedb1200e
419 419 tag: tip
420 420 user: test
421 421 date: Thu Jan 01 00:00:00 1970 +0000
422 422 summary: f
423 423
424 424
425 425 rollback should not work after a histedit
426 426 $ hg rollback
427 427 no rollback information available
428 428 [1]
429 429
430 430 $ cd ..
431 431 $ hg clone -qr0 r r0
432 432 $ cd r0
433 433 $ hg phase -fdr0
434 434 $ hg histedit --commands - 0 2>&1 << EOF
435 435 > edit cb9a9f314b8b a > $EDITED
436 436 > EOF
437 437 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
438 438 Editing (cb9a9f314b8b), commit as needed now to split the change
439 439 (to edit cb9a9f314b8b, `hg histedit --continue` after making changes)
440 440 [240]
441 441 $ HGEDITOR=true hg histedit --continue
442 442 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-histedit.hg
443 443
444 444 $ hg log -G
445 445 @ changeset: 0:0efcea34f18a
446 446 tag: tip
447 447 user: test
448 448 date: Thu Jan 01 00:00:00 1970 +0000
449 449 summary: a
450 450
451 451 $ echo foo >> b
452 452 $ hg addr
453 453 adding b
454 454 $ hg ci -m 'add b'
455 455 $ echo foo >> a
456 456 $ hg ci -m 'extend a'
457 457 $ hg phase --public 1
458 458 Attempting to fold a change into a public change should not work:
459 459 $ cat > ../edit.sh <<EOF
460 460 > cat "\$1" | sed s/pick/fold/ > tmp
461 461 > mv tmp "\$1"
462 462 > EOF
463 463 $ HGEDITOR="sh ../edit.sh" hg histedit 2
464 464 warning: histedit rules saved to: .hg/histedit-last-edit.txt
465 465 hg: parse error: first changeset cannot use verb "fold"
466 466 [10]
467 467 $ cat .hg/histedit-last-edit.txt
468 468 fold 0012be4a27ea 2 extend a
469 469
470 470 # Edit history between 0012be4a27ea and 0012be4a27ea
471 471 #
472 472 # Commits are listed from least to most recent
473 473 #
474 474 # You can reorder changesets by reordering the lines
475 475 #
476 476 # Commands:
477 477 #
478 478 # e, edit = use commit, but allow edits before making new commit
479 479 # m, mess = edit commit message without changing commit content
480 480 # p, fold = use commit
481 481 # b, base = checkout changeset and apply further changesets from there
482 482 # d, drop = remove commit from history
483 483 # f, fold = use commit, but combine it with the one above
484 484 # r, roll = like fold, but discard this commit's description and date
485 485 #
486 486
487 487 $ cd ..
488 488
489 489 ============================================
490 490 Test update-timestamp config option in mess|
491 491 ============================================
492 492
493 493 $ addwithdate ()
494 494 > {
495 495 > echo $1 > $1
496 496 > hg add $1
497 497 > hg ci -m $1 -d "$2 0"
498 498 > }
499 499
500 500 $ initrepo ()
501 501 > {
502 502 > hg init r2
503 503 > cd r2
504 504 > addwithdate a 1
505 505 > addwithdate b 2
506 506 > addwithdate c 3
507 507 > addwithdate d 4
508 508 > addwithdate e 5
509 509 > addwithdate f 6
510 510 > }
511 511
512 512 $ initrepo
513 513
514 514 log before edit
515 515
516 516 $ hg log --limit 1
517 517 changeset: 5:178e35e0ce73
518 518 tag: tip
519 519 user: test
520 520 date: Thu Jan 01 00:00:06 1970 +0000
521 521 summary: f
522 522
523 523 $ hg histedit tip --commands - 2>&1 --config rewrite.update-timestamp=True << EOF | fixbundle
524 524 > mess 178e35e0ce73 f
525 525 > EOF
526 526
527 527 log after edit
528 528
529 529 $ hg log --limit 1
530 530 changeset: 5:98bf456d476b
531 531 tag: tip
532 532 user: test
533 533 date: Thu Jan 01 00:00:00 1970 +0000
534 534 summary: f
535 535
536 536
537 537 $ cd ..
538 538
539 539 warn the user on editing tagged commits
540 540
541 541 $ hg init issue4017
542 542 $ cd issue4017
543 543 $ echo > a
544 544 $ hg ci -Am 'add a'
545 545 adding a
546 546 $ hg tag a
547 547 $ hg tags
548 548 tip 1:bd7ee4f3939b
549 549 a 0:a8a82d372bb3
550 550 $ hg histedit
551 551 warning: tags associated with the given changeset will be lost after histedit.
552 552 do you want to continue (yN)? n
553 553 abort: histedit cancelled
554 554
555 [255]
555 [250]
556 556 $ cd ..
@@ -1,174 +1,174
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > histedit=
6 6 > EOF
7 7
8 8 $ initrepo ()
9 9 > {
10 10 > hg init r
11 11 > cd r
12 12 > for x in a b c d e f ; do
13 13 > echo $x > $x
14 14 > hg add $x
15 15 > hg ci -m $x
16 16 > done
17 17 > echo a >> e
18 18 > hg ci -m 'does not commute with e'
19 19 > cd ..
20 20 > }
21 21
22 22 $ initrepo
23 23 $ cd r
24 24
25 25 log before edit
26 26 $ hg log --graph
27 27 @ changeset: 6:bfa474341cc9
28 28 | tag: tip
29 29 | user: test
30 30 | date: Thu Jan 01 00:00:00 1970 +0000
31 31 | summary: does not commute with e
32 32 |
33 33 o changeset: 5:652413bf663e
34 34 | user: test
35 35 | date: Thu Jan 01 00:00:00 1970 +0000
36 36 | summary: f
37 37 |
38 38 o changeset: 4:e860deea161a
39 39 | user: test
40 40 | date: Thu Jan 01 00:00:00 1970 +0000
41 41 | summary: e
42 42 |
43 43 o changeset: 3:055a42cdd887
44 44 | user: test
45 45 | date: Thu Jan 01 00:00:00 1970 +0000
46 46 | summary: d
47 47 |
48 48 o changeset: 2:177f92b77385
49 49 | user: test
50 50 | date: Thu Jan 01 00:00:00 1970 +0000
51 51 | summary: c
52 52 |
53 53 o changeset: 1:d2ae7f538514
54 54 | user: test
55 55 | date: Thu Jan 01 00:00:00 1970 +0000
56 56 | summary: b
57 57 |
58 58 o changeset: 0:cb9a9f314b8b
59 59 user: test
60 60 date: Thu Jan 01 00:00:00 1970 +0000
61 61 summary: a
62 62
63 63
64 64 edit the history
65 65 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
66 66 > pick 177f92b77385 c
67 67 > pick 055a42cdd887 d
68 68 > pick bfa474341cc9 does not commute with e
69 69 > pick e860deea161a e
70 70 > pick 652413bf663e f
71 71 > EOF
72 72 merging e
73 73 warning: conflicts while merging e! (edit, then use 'hg resolve --mark')
74 74 Fix up the change (pick e860deea161a)
75 75 (hg histedit --continue to resume)
76 76
77 77 insert unsupported advisory merge record
78 78 $ hg --config extensions.fakemergerecord=$TESTDIR/fakemergerecord.py fakemergerecord -x
79 79 $ hg debugmergestate
80 80 local (local): 8f7551c7e4a2f2efe0bc8c741baf7f227d65d758
81 81 other (histedit): e860deea161a2f77de56603b340ebbb4536308ae
82 82 file: e (state "u")
83 83 local path: e (hash 58e6b3a414a1e090dfc6029add0f3555ccba127f, flags "")
84 84 ancestor path: e (node 0000000000000000000000000000000000000000)
85 85 other path: e (node 6b67ccefd5ce6de77e7ead4f5292843a0255329f)
86 86 extra: ancestorlinknode = 0000000000000000000000000000000000000000
87 87 extra: merged = yes
88 88 $ hg resolve -l
89 89 U e
90 90
91 91 insert unsupported mandatory merge record
92 92 $ hg --config extensions.fakemergerecord=$TESTDIR/fakemergerecord.py fakemergerecord -X
93 93 $ hg debugmergestate
94 94 abort: unsupported merge state records: X
95 95 (see https://mercurial-scm.org/wiki/MergeStateRecords for more information)
96 96 [255]
97 97 $ hg resolve -l
98 98 abort: unsupported merge state records: X
99 99 (see https://mercurial-scm.org/wiki/MergeStateRecords for more information)
100 100 [255]
101 101 $ hg resolve -ma
102 102 abort: unsupported merge state records: X
103 103 (see https://mercurial-scm.org/wiki/MergeStateRecords for more information)
104 104 [255]
105 105
106 106 abort the edit (should clear out merge state)
107 107 $ hg histedit --abort 2>&1 | fixbundle
108 108 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
109 109 $ hg debugmergestate
110 110 no merge state found
111 111
112 112 log after abort
113 113 $ hg resolve -l
114 114 $ hg log --graph
115 115 @ changeset: 6:bfa474341cc9
116 116 | tag: tip
117 117 | user: test
118 118 | date: Thu Jan 01 00:00:00 1970 +0000
119 119 | summary: does not commute with e
120 120 |
121 121 o changeset: 5:652413bf663e
122 122 | user: test
123 123 | date: Thu Jan 01 00:00:00 1970 +0000
124 124 | summary: f
125 125 |
126 126 o changeset: 4:e860deea161a
127 127 | user: test
128 128 | date: Thu Jan 01 00:00:00 1970 +0000
129 129 | summary: e
130 130 |
131 131 o changeset: 3:055a42cdd887
132 132 | user: test
133 133 | date: Thu Jan 01 00:00:00 1970 +0000
134 134 | summary: d
135 135 |
136 136 o changeset: 2:177f92b77385
137 137 | user: test
138 138 | date: Thu Jan 01 00:00:00 1970 +0000
139 139 | summary: c
140 140 |
141 141 o changeset: 1:d2ae7f538514
142 142 | user: test
143 143 | date: Thu Jan 01 00:00:00 1970 +0000
144 144 | summary: b
145 145 |
146 146 o changeset: 0:cb9a9f314b8b
147 147 user: test
148 148 date: Thu Jan 01 00:00:00 1970 +0000
149 149 summary: a
150 150
151 151
152 152 Early tree conflict doesn't leave histedit in a wedged state. Note
153 153 that we don't specify --commands here: we catch the problem before we
154 154 even prompt the user for rules, sidestepping any dataloss issues.
155 155
156 156 $ hg rm c
157 157 $ hg ci -m 'remove c'
158 158 $ echo collision > c
159 159
160 160 $ hg histedit e860deea161a
161 161 c: untracked file differs
162 162 abort: untracked files in working directory conflict with files in 055a42cdd887
163 [255]
163 [20]
164 164
165 165 We should have detected the collision early enough we're not in a
166 166 histedit state, and p1 is unchanged.
167 167
168 168 $ hg log -r 'p1()' -T'{node}\n'
169 169 1b0954ff00fccb15a37b679e4a35e9b01dfe685e
170 170 $ hg status --config ui.tweakdefaults=yes
171 171 ? c
172 172 ? e.orig
173 173
174 174 $ cd ..
@@ -1,591 +1,591
1 1 #testcases abortcommand abortflag
2 2
3 3 #if abortflag
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [alias]
6 6 > abort = histedit --abort
7 7 > EOF
8 8 #endif
9 9
10 10 $ . "$TESTDIR/histedit-helpers.sh"
11 11
12 12 Enable obsolete
13 13
14 14 $ cat >> $HGRCPATH << EOF
15 15 > [ui]
16 16 > logtemplate= {rev}:{node|short} {desc|firstline}
17 17 > [phases]
18 18 > publish=False
19 19 > [experimental]
20 20 > evolution.createmarkers=True
21 21 > evolution.allowunstable=True
22 22 > [extensions]
23 23 > histedit=
24 24 > rebase=
25 25 > EOF
26 26
27 27 Test that histedit learns about obsolescence not stored in histedit state
28 28 $ hg init boo
29 29 $ cd boo
30 30 $ echo a > a
31 31 $ hg ci -Am a
32 32 adding a
33 33 $ echo a > b
34 34 $ echo a > c
35 35 $ echo a > c
36 36 $ hg ci -Am b
37 37 adding b
38 38 adding c
39 39 $ echo a > d
40 40 $ hg ci -Am c
41 41 adding d
42 42 $ echo "pick `hg log -r 0 -T '{node|short}'`" > plan
43 43 $ echo "pick `hg log -r 2 -T '{node|short}'`" >> plan
44 44 $ echo "edit `hg log -r 1 -T '{node|short}'`" >> plan
45 45 $ hg histedit -r 'all()' --commands plan
46 46 Editing (1b2d564fad96), commit as needed now to split the change
47 47 (to edit 1b2d564fad96, `hg histedit --continue` after making changes)
48 48 [240]
49 49 $ hg st
50 50 A b
51 51 A c
52 52 ? plan
53 53 $ hg commit --amend b
54 54 $ hg histedit --continue
55 55 $ hg log -G
56 56 @ 5:46abc7c4d873 b
57 57 |
58 58 o 4:49d44ab2be1b c
59 59 |
60 60 o 0:cb9a9f314b8b a
61 61
62 62 $ hg debugobsolete
63 63 e72d22b19f8ecf4150ab4f91d0973fd9955d3ddf 49d44ab2be1b67a79127568a67c9c99430633b48 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
64 64 1b2d564fad96311b45362f17c2aa855150efb35f 46abc7c4d8738e8563e577f7889e1b6db3da4199 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '12', 'operation': 'histedit', 'user': 'test'}
65 65 114f4176969ef342759a8a57e6bccefc4234829b 49d44ab2be1b67a79127568a67c9c99430633b48 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '12', 'operation': 'histedit', 'user': 'test'}
66 66
67 67 With some node gone missing during the edit.
68 68
69 69 $ echo "pick `hg log -r 0 -T '{node|short}'`" > plan
70 70 $ echo "pick `hg log -r 5 -T '{node|short}'`" >> plan
71 71 $ echo "edit `hg log -r 4 -T '{node|short}'`" >> plan
72 72 $ hg histedit -r 'all()' --commands plan
73 73 Editing (49d44ab2be1b), commit as needed now to split the change
74 74 (to edit 49d44ab2be1b, `hg histedit --continue` after making changes)
75 75 [240]
76 76 $ hg st
77 77 A b
78 78 A d
79 79 ? plan
80 80 $ hg commit --amend -X . -m XXXXXX
81 81 $ hg commit --amend -X . -m b2
82 82 $ hg --hidden --config extensions.strip= strip 'desc(XXXXXX)' --no-backup
83 83 $ hg histedit --continue
84 84 $ hg log -G
85 85 @ 8:273c1f3b8626 c
86 86 |
87 87 o 7:aba7da937030 b2
88 88 |
89 89 o 0:cb9a9f314b8b a
90 90
91 91 $ hg debugobsolete
92 92 e72d22b19f8ecf4150ab4f91d0973fd9955d3ddf 49d44ab2be1b67a79127568a67c9c99430633b48 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '8', 'operation': 'amend', 'user': 'test'}
93 93 1b2d564fad96311b45362f17c2aa855150efb35f 46abc7c4d8738e8563e577f7889e1b6db3da4199 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '12', 'operation': 'histedit', 'user': 'test'}
94 94 114f4176969ef342759a8a57e6bccefc4234829b 49d44ab2be1b67a79127568a67c9c99430633b48 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '12', 'operation': 'histedit', 'user': 'test'}
95 95 76f72745eac0643d16530e56e2f86e36e40631f1 2ca853e48edbd6453a0674dc0fe28a0974c51b9c 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '1', 'operation': 'amend', 'user': 'test'}
96 96 2ca853e48edbd6453a0674dc0fe28a0974c51b9c aba7da93703075eec9fb1dbaf143ff2bc1c49d46 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '1', 'operation': 'amend', 'user': 'test'}
97 97 49d44ab2be1b67a79127568a67c9c99430633b48 273c1f3b86267ed3ec684bb13af1fa4d6ba56e02 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '4', 'operation': 'histedit', 'user': 'test'}
98 98 46abc7c4d8738e8563e577f7889e1b6db3da4199 aba7da93703075eec9fb1dbaf143ff2bc1c49d46 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '5', 'operation': 'histedit', 'user': 'test'}
99 99 $ cd ..
100 100
101 101 Base setup for the rest of the testing
102 102 ======================================
103 103
104 104 $ hg init base
105 105 $ cd base
106 106
107 107 $ for x in a b c d e f ; do
108 108 > echo $x > $x
109 109 > hg add $x
110 110 > hg ci -m $x
111 111 > done
112 112
113 113 $ hg log --graph
114 114 @ 5:652413bf663e f
115 115 |
116 116 o 4:e860deea161a e
117 117 |
118 118 o 3:055a42cdd887 d
119 119 |
120 120 o 2:177f92b77385 c
121 121 |
122 122 o 1:d2ae7f538514 b
123 123 |
124 124 o 0:cb9a9f314b8b a
125 125
126 126
127 127 $ HGEDITOR=cat hg histedit 1
128 128 pick d2ae7f538514 1 b
129 129 pick 177f92b77385 2 c
130 130 pick 055a42cdd887 3 d
131 131 pick e860deea161a 4 e
132 132 pick 652413bf663e 5 f
133 133
134 134 # Edit history between d2ae7f538514 and 652413bf663e
135 135 #
136 136 # Commits are listed from least to most recent
137 137 #
138 138 # You can reorder changesets by reordering the lines
139 139 #
140 140 # Commands:
141 141 #
142 142 # e, edit = use commit, but allow edits before making new commit
143 143 # m, mess = edit commit message without changing commit content
144 144 # p, pick = use commit
145 145 # b, base = checkout changeset and apply further changesets from there
146 146 # d, drop = remove commit from history
147 147 # f, fold = use commit, but combine it with the one above
148 148 # r, roll = like fold, but discard this commit's description and date
149 149 #
150 150 $ hg histedit 1 --commands - --verbose <<EOF | grep histedit
151 151 > pick 177f92b77385 2 c
152 152 > drop d2ae7f538514 1 b
153 153 > pick 055a42cdd887 3 d
154 154 > fold e860deea161a 4 e
155 155 > pick 652413bf663e 5 f
156 156 > EOF
157 157 [1]
158 158 $ hg log --graph --hidden
159 159 @ 10:cacdfd884a93 f
160 160 |
161 161 o 9:59d9f330561f d
162 162 |
163 163 | x 8:b558abc46d09 fold-temp-revision e860deea161a
164 164 | |
165 165 | x 7:96e494a2d553 d
166 166 |/
167 167 o 6:b346ab9a313d c
168 168 |
169 169 | x 5:652413bf663e f
170 170 | |
171 171 | x 4:e860deea161a e
172 172 | |
173 173 | x 3:055a42cdd887 d
174 174 | |
175 175 | x 2:177f92b77385 c
176 176 | |
177 177 | x 1:d2ae7f538514 b
178 178 |/
179 179 o 0:cb9a9f314b8b a
180 180
181 181 $ hg debugobsolete
182 182 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b} (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '0', 'operation': 'histedit', 'user': 'test'}
183 183 177f92b773850b59254aa5e923436f921b55483b b346ab9a313db8537ecf96fca3ca3ca984ef3bd7 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '4', 'operation': 'histedit', 'user': 'test'}
184 184 055a42cdd88768532f9cf79daa407fc8d138de9b 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '13', 'operation': 'histedit', 'user': 'test'}
185 185 e860deea161a2f77de56603b340ebbb4536308ae 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '13', 'operation': 'histedit', 'user': 'test'}
186 186 652413bf663ef2a641cab26574e46d5f5a64a55a cacdfd884a9321ec4e1de275ef3949fa953a1f83 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '4', 'operation': 'histedit', 'user': 'test'}
187 187 96e494a2d553dd05902ba1cee1d94d4cb7b8faed 0 {b346ab9a313db8537ecf96fca3ca3ca984ef3bd7} (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '0', 'operation': 'histedit', 'user': 'test'}
188 188 b558abc46d09c30f57ac31e85a8a3d64d2e906e4 0 {96e494a2d553dd05902ba1cee1d94d4cb7b8faed} (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '0', 'operation': 'histedit', 'user': 'test'}
189 189
190 190
191 191 Ensure hidden revision does not prevent histedit
192 192 -------------------------------------------------
193 193
194 194 create an hidden revision
195 195
196 196 $ hg histedit 6 --commands - << EOF
197 197 > pick b346ab9a313d 6 c
198 198 > drop 59d9f330561f 7 d
199 199 > pick cacdfd884a93 8 f
200 200 > EOF
201 201 $ hg log --graph
202 202 @ 11:c13eb81022ca f
203 203 |
204 204 o 6:b346ab9a313d c
205 205 |
206 206 o 0:cb9a9f314b8b a
207 207
208 208 check hidden revision are ignored (6 have hidden children 7 and 8)
209 209
210 210 $ hg histedit 6 --commands - << EOF
211 211 > pick b346ab9a313d 6 c
212 212 > pick c13eb81022ca 8 f
213 213 > EOF
214 214
215 215
216 216
217 217 Test that rewriting leaving instability behind is allowed
218 218 ---------------------------------------------------------------------
219 219
220 220 $ hg up '.^'
221 221 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
222 222 $ hg log -r 'children(.)'
223 223 11:c13eb81022ca f (no-eol)
224 224 $ hg histedit -r '.' --commands - <<EOF
225 225 > edit b346ab9a313d 6 c
226 226 > EOF
227 227 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
228 228 Editing (b346ab9a313d), commit as needed now to split the change
229 229 (to edit b346ab9a313d, `hg histedit --continue` after making changes)
230 230 [240]
231 231 $ echo c >> c
232 232 $ hg histedit --continue
233 233 1 new orphan changesets
234 234
235 235 $ hg log -r 'orphan()'
236 236 11:c13eb81022ca f (no-eol)
237 237
238 238 stabilise
239 239
240 240 $ hg rebase -r 'orphan()' -d .
241 241 rebasing 11:c13eb81022ca "f"
242 242 $ hg up tip -q
243 243
244 244 Test dropping of changeset on the top of the stack
245 245 -------------------------------------------------------
246 246
247 247 Nothing is rewritten below, the working directory parent must be change for the
248 248 dropped changeset to be hidden.
249 249
250 250 $ cd ..
251 251 $ hg clone base droplast
252 252 updating to branch default
253 253 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 $ cd droplast
255 255 $ hg histedit -r '40db8afa467b' --commands - << EOF
256 256 > pick 40db8afa467b 10 c
257 257 > drop b449568bf7fc 11 f
258 258 > EOF
259 259 $ hg log -G
260 260 @ 12:40db8afa467b c
261 261 |
262 262 o 0:cb9a9f314b8b a
263 263
264 264
265 265 With rewritten ancestors
266 266
267 267 $ echo e > e
268 268 $ hg add e
269 269 $ hg commit -m g
270 270 $ echo f > f
271 271 $ hg add f
272 272 $ hg commit -m h
273 273 $ hg histedit -r '40db8afa467b' --commands - << EOF
274 274 > pick 47a8561c0449 12 g
275 275 > pick 40db8afa467b 10 c
276 276 > drop 1b3b05f35ff0 13 h
277 277 > EOF
278 278 $ hg log -G
279 279 @ 17:ee6544123ab8 c
280 280 |
281 281 o 16:269e713e9eae g
282 282 |
283 283 o 0:cb9a9f314b8b a
284 284
285 285 $ cd ../base
286 286
287 287
288 288
289 289 Test phases support
290 290 ===========================================
291 291
292 292 Check that histedit respect immutability
293 293 -------------------------------------------
294 294
295 295 $ cat >> $HGRCPATH << EOF
296 296 > [command-templates]
297 297 > log = {rev}:{node|short} ({phase}) {desc|firstline}\n
298 298 > EOF
299 299
300 300 $ hg ph -pv '.^'
301 301 phase changed for 2 changesets
302 302 $ hg log -G
303 303 @ 13:b449568bf7fc (draft) f
304 304 |
305 305 o 12:40db8afa467b (public) c
306 306 |
307 307 o 0:cb9a9f314b8b (public) a
308 308
309 309 $ hg histedit -r '.~2'
310 310 abort: cannot edit public changesets: cb9a9f314b8b, 40db8afa467b
311 311 (see 'hg help phases' for details)
312 312 [10]
313 313
314 314
315 315 Prepare further testing
316 316 -------------------------------------------
317 317
318 318 $ for x in g h i j k ; do
319 319 > echo $x > $x
320 320 > hg add $x
321 321 > hg ci -m $x
322 322 > done
323 323 $ hg phase --force --secret .~2
324 324 $ hg log -G
325 325 @ 18:ee118ab9fa44 (secret) k
326 326 |
327 327 o 17:3a6c53ee7f3d (secret) j
328 328 |
329 329 o 16:b605fb7503f2 (secret) i
330 330 |
331 331 o 15:7395e1ff83bd (draft) h
332 332 |
333 333 o 14:6b70183d2492 (draft) g
334 334 |
335 335 o 13:b449568bf7fc (draft) f
336 336 |
337 337 o 12:40db8afa467b (public) c
338 338 |
339 339 o 0:cb9a9f314b8b (public) a
340 340
341 341 $ cd ..
342 342
343 343 simple phase conservation
344 344 -------------------------------------------
345 345
346 346 Resulting changeset should conserve the phase of the original one whatever the
347 347 phases.new-commit option is.
348 348
349 349 New-commit as draft (default)
350 350
351 351 $ cp -R base simple-draft
352 352 $ cd simple-draft
353 353 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
354 354 > edit b449568bf7fc 11 f
355 355 > pick 6b70183d2492 12 g
356 356 > pick 7395e1ff83bd 13 h
357 357 > pick b605fb7503f2 14 i
358 358 > pick 3a6c53ee7f3d 15 j
359 359 > pick ee118ab9fa44 16 k
360 360 > EOF
361 361 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
362 362 Editing (b449568bf7fc), commit as needed now to split the change
363 363 (to edit b449568bf7fc, `hg histedit --continue` after making changes)
364 364 [240]
365 365 $ echo f >> f
366 366 $ hg histedit --continue
367 367 $ hg log -G
368 368 @ 24:12e89af74238 (secret) k
369 369 |
370 370 o 23:636a8687b22e (secret) j
371 371 |
372 372 o 22:ccaf0a38653f (secret) i
373 373 |
374 374 o 21:11a89d1c2613 (draft) h
375 375 |
376 376 o 20:c1dec7ca82ea (draft) g
377 377 |
378 378 o 19:087281e68428 (draft) f
379 379 |
380 380 o 12:40db8afa467b (public) c
381 381 |
382 382 o 0:cb9a9f314b8b (public) a
383 383
384 384 $ cd ..
385 385
386 386
387 387 New-commit as secret (config)
388 388
389 389 $ cp -R base simple-secret
390 390 $ cd simple-secret
391 391 $ cat >> .hg/hgrc << EOF
392 392 > [phases]
393 393 > new-commit=secret
394 394 > EOF
395 395 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
396 396 > edit b449568bf7fc 11 f
397 397 > pick 6b70183d2492 12 g
398 398 > pick 7395e1ff83bd 13 h
399 399 > pick b605fb7503f2 14 i
400 400 > pick 3a6c53ee7f3d 15 j
401 401 > pick ee118ab9fa44 16 k
402 402 > EOF
403 403 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
404 404 Editing (b449568bf7fc), commit as needed now to split the change
405 405 (to edit b449568bf7fc, `hg histedit --continue` after making changes)
406 406 [240]
407 407 $ echo f >> f
408 408 $ hg histedit --continue
409 409 $ hg log -G
410 410 @ 24:12e89af74238 (secret) k
411 411 |
412 412 o 23:636a8687b22e (secret) j
413 413 |
414 414 o 22:ccaf0a38653f (secret) i
415 415 |
416 416 o 21:11a89d1c2613 (draft) h
417 417 |
418 418 o 20:c1dec7ca82ea (draft) g
419 419 |
420 420 o 19:087281e68428 (draft) f
421 421 |
422 422 o 12:40db8afa467b (public) c
423 423 |
424 424 o 0:cb9a9f314b8b (public) a
425 425
426 426 $ cd ..
427 427
428 428
429 429 Changeset reordering
430 430 -------------------------------------------
431 431
432 432 If a secret changeset is put before a draft one, all descendant should be secret.
433 433 It seems more important to present the secret phase.
434 434
435 435 $ cp -R base reorder
436 436 $ cd reorder
437 437 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
438 438 > pick b449568bf7fc 11 f
439 439 > pick 3a6c53ee7f3d 15 j
440 440 > pick 6b70183d2492 12 g
441 441 > pick b605fb7503f2 14 i
442 442 > pick 7395e1ff83bd 13 h
443 443 > pick ee118ab9fa44 16 k
444 444 > EOF
445 445 $ hg log -G
446 446 @ 23:558246857888 (secret) k
447 447 |
448 448 o 22:28bd44768535 (secret) h
449 449 |
450 450 o 21:d5395202aeb9 (secret) i
451 451 |
452 452 o 20:21edda8e341b (secret) g
453 453 |
454 454 o 19:5ab64f3a4832 (secret) j
455 455 |
456 456 o 13:b449568bf7fc (draft) f
457 457 |
458 458 o 12:40db8afa467b (public) c
459 459 |
460 460 o 0:cb9a9f314b8b (public) a
461 461
462 462 $ cd ..
463 463
464 464 Changeset folding
465 465 -------------------------------------------
466 466
467 467 Folding a secret changeset with a draft one turn the result secret (again,
468 468 better safe than sorry). Folding between same phase changeset still works
469 469
470 470 Note that there is a few reordering in this series for more extensive test
471 471
472 472 $ cp -R base folding
473 473 $ cd folding
474 474 $ cat >> .hg/hgrc << EOF
475 475 > [phases]
476 476 > new-commit=secret
477 477 > EOF
478 478 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
479 479 > pick 7395e1ff83bd 13 h
480 480 > fold b449568bf7fc 11 f
481 481 > pick 6b70183d2492 12 g
482 482 > fold 3a6c53ee7f3d 15 j
483 483 > pick b605fb7503f2 14 i
484 484 > fold ee118ab9fa44 16 k
485 485 > EOF
486 486 $ hg log -G
487 487 @ 27:f9daec13fb98 (secret) i
488 488 |
489 489 o 24:49807617f46a (secret) g
490 490 |
491 491 o 21:050280826e04 (draft) h
492 492 |
493 493 o 12:40db8afa467b (public) c
494 494 |
495 495 o 0:cb9a9f314b8b (public) a
496 496
497 497 $ hg co 49807617f46a
498 498 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
499 499 $ echo wat >> wat
500 500 $ hg add wat
501 501 $ hg ci -m 'add wat'
502 502 created new head
503 503 $ hg merge f9daec13fb98
504 504 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
505 505 (branch merge, don't forget to commit)
506 506 $ hg ci -m 'merge'
507 507 $ echo not wat > wat
508 508 $ hg ci -m 'modify wat'
509 509 $ hg histedit 050280826e04
510 510 abort: cannot edit history that contains merges
511 [255]
511 [20]
512 512 $ cd ..
513 513
514 514 Check abort behavior
515 515 -------------------------------------------
516 516
517 517 We checks that abort properly clean the repository so the same histedit can be
518 518 attempted later.
519 519
520 520 $ cp -R base abort
521 521 $ cd abort
522 522 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
523 523 > pick b449568bf7fc 13 f
524 524 > pick 7395e1ff83bd 15 h
525 525 > pick 6b70183d2492 14 g
526 526 > pick b605fb7503f2 16 i
527 527 > roll 3a6c53ee7f3d 17 j
528 528 > edit ee118ab9fa44 18 k
529 529 > EOF
530 530 Editing (ee118ab9fa44), commit as needed now to split the change
531 531 (to edit ee118ab9fa44, `hg histedit --continue` after making changes)
532 532 [240]
533 533
534 534 #if abortcommand
535 535 when in dry-run mode
536 536 $ hg abort --dry-run
537 537 histedit in progress, will be aborted
538 538 #endif
539 539
540 540 $ hg abort
541 541 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
542 542 saved backup bundle to $TESTTMP/abort/.hg/strip-backup/4dc06258baa6-dff4ef05-backup.hg
543 543
544 544 $ hg log -G
545 545 @ 18:ee118ab9fa44 (secret) k
546 546 |
547 547 o 17:3a6c53ee7f3d (secret) j
548 548 |
549 549 o 16:b605fb7503f2 (secret) i
550 550 |
551 551 o 15:7395e1ff83bd (draft) h
552 552 |
553 553 o 14:6b70183d2492 (draft) g
554 554 |
555 555 o 13:b449568bf7fc (draft) f
556 556 |
557 557 o 12:40db8afa467b (public) c
558 558 |
559 559 o 0:cb9a9f314b8b (public) a
560 560
561 561 $ hg histedit -r 'b449568bf7fc' --commands - << EOF --config experimental.evolution.track-operation=1
562 562 > pick b449568bf7fc 13 f
563 563 > pick 7395e1ff83bd 15 h
564 564 > pick 6b70183d2492 14 g
565 565 > pick b605fb7503f2 16 i
566 566 > pick 3a6c53ee7f3d 17 j
567 567 > edit ee118ab9fa44 18 k
568 568 > EOF
569 569 Editing (ee118ab9fa44), commit as needed now to split the change
570 570 (to edit ee118ab9fa44, `hg histedit --continue` after making changes)
571 571 [240]
572 572 $ hg histedit --continue --config experimental.evolution.track-operation=1
573 573 $ hg log -G
574 574 @ 23:175d6b286a22 (secret) k
575 575 |
576 576 o 22:44ca09d59ae4 (secret) j
577 577 |
578 578 o 21:31747692a644 (secret) i
579 579 |
580 580 o 20:9985cd4f21fa (draft) g
581 581 |
582 582 o 19:4dc06258baa6 (draft) h
583 583 |
584 584 o 13:b449568bf7fc (draft) f
585 585 |
586 586 o 12:40db8afa467b (public) c
587 587 |
588 588 o 0:cb9a9f314b8b (public) a
589 589
590 590 $ hg debugobsolete --rev .
591 591 ee118ab9fa44ebb86be85996548b5517a39e5093 175d6b286a224c23f192e79a581ce83131a53fa2 0 (Thu Jan 01 00:00:00 1970 +0000) {'ef1': '4', 'operation': 'histedit', 'user': 'test'}
@@ -1,156 +1,156
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > histedit=
4 4 > EOF
5 5
6 6 $ initrepos ()
7 7 > {
8 8 > hg init r
9 9 > cd r
10 10 > for x in a b c ; do
11 11 > echo $x > $x
12 12 > hg add $x
13 13 > hg ci -m $x
14 14 > done
15 15 > cd ..
16 16 > hg clone r r2 | grep -v updating
17 17 > cd r2
18 18 > for x in d e f ; do
19 19 > echo $x > $x
20 20 > hg add $x
21 21 > hg ci -m $x
22 22 > done
23 23 > cd ..
24 24 > hg init r3
25 25 > cd r3
26 26 > for x in g h i ; do
27 27 > echo $x > $x
28 28 > hg add $x
29 29 > hg ci -m $x
30 30 > done
31 31 > cd ..
32 32 > }
33 33
34 34 $ initrepos
35 35 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 36
37 37 show the edit commands offered by outgoing
38 38 $ cd r2
39 39 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
40 40 pick 055a42cdd887 3 d
41 41 pick e860deea161a 4 e
42 42 pick 652413bf663e 5 f
43 43
44 44 # Edit history between 055a42cdd887 and 652413bf663e
45 45 #
46 46 # Commits are listed from least to most recent
47 47 #
48 48 # You can reorder changesets by reordering the lines
49 49 #
50 50 # Commands:
51 51 #
52 52 # e, edit = use commit, but allow edits before making new commit
53 53 # m, mess = edit commit message without changing commit content
54 54 # p, pick = use commit
55 55 # b, base = checkout changeset and apply further changesets from there
56 56 # d, drop = remove commit from history
57 57 # f, fold = use commit, but combine it with the one above
58 58 # r, roll = like fold, but discard this commit's description and date
59 59 #
60 60 $ cd ..
61 61
62 62 show the error from unrelated repos
63 63 $ cd r3
64 64 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
65 65 abort: repository is unrelated
66 66 [1]
67 67 $ cd ..
68 68
69 69 show the error from unrelated repos
70 70 $ cd r3
71 71 $ HGEDITOR=cat hg histedit --force --outgoing ../r
72 72 comparing with ../r
73 73 searching for changes
74 74 warning: repository is unrelated
75 75 pick 2a4042b45417 0 g
76 76 pick 68c46b4927ce 1 h
77 77 pick 51281e65ba79 2 i
78 78
79 79 # Edit history between 2a4042b45417 and 51281e65ba79
80 80 #
81 81 # Commits are listed from least to most recent
82 82 #
83 83 # You can reorder changesets by reordering the lines
84 84 #
85 85 # Commands:
86 86 #
87 87 # e, edit = use commit, but allow edits before making new commit
88 88 # m, mess = edit commit message without changing commit content
89 89 # p, pick = use commit
90 90 # b, base = checkout changeset and apply further changesets from there
91 91 # d, drop = remove commit from history
92 92 # f, fold = use commit, but combine it with the one above
93 93 # r, roll = like fold, but discard this commit's description and date
94 94 #
95 95 $ cd ..
96 96
97 97 test sensitivity to branch in URL:
98 98
99 99 $ cd r2
100 100 $ hg -q update 2
101 101 $ hg -q branch foo
102 102 $ hg commit -m 'create foo branch'
103 103 $ HGEDITOR=cat hg histedit --outgoing '../r#foo' | grep -v comparing | grep -v searching
104 104 pick f26599ee3441 6 create foo branch
105 105
106 106 # Edit history between f26599ee3441 and f26599ee3441
107 107 #
108 108 # Commits are listed from least to most recent
109 109 #
110 110 # You can reorder changesets by reordering the lines
111 111 #
112 112 # Commands:
113 113 #
114 114 # e, edit = use commit, but allow edits before making new commit
115 115 # m, mess = edit commit message without changing commit content
116 116 # p, pick = use commit
117 117 # b, base = checkout changeset and apply further changesets from there
118 118 # d, drop = remove commit from history
119 119 # f, fold = use commit, but combine it with the one above
120 120 # r, roll = like fold, but discard this commit's description and date
121 121 #
122 122
123 123 test to check number of roots in outgoing revisions
124 124
125 125 $ hg -q outgoing -G --template '{node|short}({branch})' '../r'
126 126 @ f26599ee3441(foo)
127 127
128 128 o 652413bf663e(default)
129 129 |
130 130 o e860deea161a(default)
131 131 |
132 132 o 055a42cdd887(default)
133 133
134 134 $ HGEDITOR=cat hg -q histedit --outgoing '../r'
135 135 abort: there are ambiguous outgoing revisions
136 136 (see 'hg help histedit' for more detail)
137 [255]
137 [20]
138 138
139 139 $ hg -q update -C 2
140 140 $ echo aa >> a
141 141 $ hg -q commit -m 'another head on default'
142 142 $ hg -q outgoing -G --template '{node|short}({branch})' '../r#default'
143 143 @ 3879dc049647(default)
144 144
145 145 o 652413bf663e(default)
146 146 |
147 147 o e860deea161a(default)
148 148 |
149 149 o 055a42cdd887(default)
150 150
151 151 $ HGEDITOR=cat hg -q histedit --outgoing '../r#default'
152 152 abort: there are ambiguous outgoing revisions
153 153 (see 'hg help histedit' for more detail)
154 [255]
154 [20]
155 155
156 156 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now