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