##// END OF EJS Templates
histedit: fix formatting after D8150...
Martin von Zweigbergk -
r44925:24a3a9f7 stable
parent child Browse files
Show More
@@ -1,2638 +1,2635
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 locale
205 205 import os
206 206 import struct
207 207
208 208 from mercurial.i18n import _
209 209 from mercurial.pycompat import (
210 210 getattr,
211 211 open,
212 212 )
213 213 from mercurial import (
214 214 bundle2,
215 215 cmdutil,
216 216 context,
217 217 copies,
218 218 destutil,
219 219 discovery,
220 220 encoding,
221 221 error,
222 222 exchange,
223 223 extensions,
224 224 hg,
225 225 logcmdutil,
226 226 merge as mergemod,
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 """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 wcpar = repo.dirstate.p1()
639 639 if ctx.p1().node() == wcpar:
640 640 # edits are "in place" we do not need to make any merge,
641 641 # just applies changes on parent for editing
642 642 ui.pushbuffer()
643 643 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
644 644 stats = mergemod.updateresult(0, 0, 0, 0)
645 645 ui.popbuffer()
646 646 else:
647 647 try:
648 648 # ui.forcemerge is an internal variable, do not document
649 649 repo.ui.setconfig(
650 650 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
651 651 )
652 652 stats = mergemod.graft(repo, ctx, ctx.p1(), [b'local', b'histedit'])
653 653 finally:
654 654 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
655 655 return stats
656 656
657 657
658 658 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
659 659 """collapse the set of revisions from first to last as new one.
660 660
661 661 Expected commit options are:
662 662 - message
663 663 - date
664 664 - username
665 665 Commit message is edited in all cases.
666 666
667 667 This function works in memory."""
668 668 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
669 669 if not ctxs:
670 670 return None
671 671 for c in ctxs:
672 672 if not c.mutable():
673 673 raise error.ParseError(
674 674 _(b"cannot fold into public change %s") % node.short(c.node())
675 675 )
676 676 base = firstctx.p1()
677 677
678 678 # commit a new version of the old changeset, including the update
679 679 # collect all files which might be affected
680 680 files = set()
681 681 for ctx in ctxs:
682 682 files.update(ctx.files())
683 683
684 684 # Recompute copies (avoid recording a -> b -> a)
685 685 copied = copies.pathcopies(base, lastctx)
686 686
687 687 # prune files which were reverted by the updates
688 688 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
689 689 # commit version of these files as defined by head
690 690 headmf = lastctx.manifest()
691 691
692 692 def filectxfn(repo, ctx, path):
693 693 if path in headmf:
694 694 fctx = lastctx[path]
695 695 flags = fctx.flags()
696 696 mctx = context.memfilectx(
697 697 repo,
698 698 ctx,
699 699 fctx.path(),
700 700 fctx.data(),
701 701 islink=b'l' in flags,
702 702 isexec=b'x' in flags,
703 703 copysource=copied.get(path),
704 704 )
705 705 return mctx
706 706 return None
707 707
708 708 if commitopts.get(b'message'):
709 709 message = commitopts[b'message']
710 710 else:
711 711 message = firstctx.description()
712 712 user = commitopts.get(b'user')
713 713 date = commitopts.get(b'date')
714 714 extra = commitopts.get(b'extra')
715 715
716 716 parents = (firstctx.p1().node(), firstctx.p2().node())
717 717 editor = None
718 718 if not skipprompt:
719 719 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
720 720 new = context.memctx(
721 721 repo,
722 722 parents=parents,
723 723 text=message,
724 724 files=files,
725 725 filectxfn=filectxfn,
726 726 user=user,
727 727 date=date,
728 728 extra=extra,
729 729 editor=editor,
730 730 )
731 731 return repo.commitctx(new)
732 732
733 733
734 734 def _isdirtywc(repo):
735 735 return repo[None].dirty(missing=True)
736 736
737 737
738 738 def abortdirty():
739 739 raise error.Abort(
740 740 _(b'working copy has pending changes'),
741 741 hint=_(
742 742 b'amend, commit, or revert them and run histedit '
743 743 b'--continue, or abort with histedit --abort'
744 744 ),
745 745 )
746 746
747 747
748 748 def action(verbs, message, priority=False, internal=False):
749 749 def wrap(cls):
750 750 assert not priority or not internal
751 751 verb = verbs[0]
752 752 if priority:
753 753 primaryactions.add(verb)
754 754 elif internal:
755 755 internalactions.add(verb)
756 756 elif len(verbs) > 1:
757 757 secondaryactions.add(verb)
758 758 else:
759 759 tertiaryactions.add(verb)
760 760
761 761 cls.verb = verb
762 762 cls.verbs = verbs
763 763 cls.message = message
764 764 for verb in verbs:
765 765 actiontable[verb] = cls
766 766 return cls
767 767
768 768 return wrap
769 769
770 770
771 771 @action([b'pick', b'p'], _(b'use commit'), priority=True)
772 772 class pick(histeditaction):
773 773 def run(self):
774 774 rulectx = self.repo[self.node]
775 775 if rulectx.p1().node() == self.state.parentctxnode:
776 776 self.repo.ui.debug(b'node %s unchanged\n' % node.short(self.node))
777 777 return rulectx, []
778 778
779 779 return super(pick, self).run()
780 780
781 781
782 782 @action([b'edit', b'e'], _(b'use commit, but stop for amending'), priority=True)
783 783 class edit(histeditaction):
784 784 def run(self):
785 785 repo = self.repo
786 786 rulectx = repo[self.node]
787 787 hg.update(repo, self.state.parentctxnode, quietempty=True)
788 788 applychanges(repo.ui, repo, rulectx, {})
789 789 raise error.InterventionRequired(
790 790 _(b'Editing (%s), you may commit or record as needed now.')
791 791 % node.short(self.node),
792 792 hint=_(b'hg histedit --continue to resume'),
793 793 )
794 794
795 795 def commiteditor(self):
796 796 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
797 797
798 798
799 799 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
800 800 class fold(histeditaction):
801 801 def verify(self, prev, expected, seen):
802 802 """ Verifies semantic correctness of the fold rule"""
803 803 super(fold, self).verify(prev, expected, seen)
804 804 repo = self.repo
805 805 if not prev:
806 806 c = repo[self.node].p1()
807 807 elif not prev.verb in (b'pick', b'base'):
808 808 return
809 809 else:
810 810 c = repo[prev.node]
811 811 if not c.mutable():
812 812 raise error.ParseError(
813 813 _(b"cannot fold into public change %s") % node.short(c.node())
814 814 )
815 815
816 816 def continuedirty(self):
817 817 repo = self.repo
818 818 rulectx = repo[self.node]
819 819
820 820 commit = commitfuncfor(repo, rulectx)
821 821 commit(
822 822 text=b'fold-temp-revision %s' % node.short(self.node),
823 823 user=rulectx.user(),
824 824 date=rulectx.date(),
825 825 extra=rulectx.extra(),
826 826 )
827 827
828 828 def continueclean(self):
829 829 repo = self.repo
830 830 ctx = repo[b'.']
831 831 rulectx = repo[self.node]
832 832 parentctxnode = self.state.parentctxnode
833 833 if ctx.node() == parentctxnode:
834 834 repo.ui.warn(_(b'%s: empty changeset\n') % node.short(self.node))
835 835 return ctx, [(self.node, (parentctxnode,))]
836 836
837 837 parentctx = repo[parentctxnode]
838 838 newcommits = set(
839 839 c.node()
840 840 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
841 841 )
842 842 if not newcommits:
843 843 repo.ui.warn(
844 844 _(
845 845 b'%s: cannot fold - working copy is not a '
846 846 b'descendant of previous commit %s\n'
847 847 )
848 848 % (node.short(self.node), node.short(parentctxnode))
849 849 )
850 850 return ctx, [(self.node, (ctx.node(),))]
851 851
852 852 middlecommits = newcommits.copy()
853 853 middlecommits.discard(ctx.node())
854 854
855 855 return self.finishfold(
856 856 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
857 857 )
858 858
859 859 def skipprompt(self):
860 860 """Returns true if the rule should skip the message editor.
861 861
862 862 For example, 'fold' wants to show an editor, but 'rollup'
863 863 doesn't want to.
864 864 """
865 865 return False
866 866
867 867 def mergedescs(self):
868 868 """Returns true if the rule should merge messages of multiple changes.
869 869
870 870 This exists mainly so that 'rollup' rules can be a subclass of
871 871 'fold'.
872 872 """
873 873 return True
874 874
875 875 def firstdate(self):
876 876 """Returns true if the rule should preserve the date of the first
877 877 change.
878 878
879 879 This exists mainly so that 'rollup' rules can be a subclass of
880 880 'fold'.
881 881 """
882 882 return False
883 883
884 884 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
885 885 parent = ctx.p1().node()
886 886 hg.updaterepo(repo, parent, overwrite=False)
887 887 ### prepare new commit data
888 888 commitopts = {}
889 889 commitopts[b'user'] = ctx.user()
890 890 # commit message
891 891 if not self.mergedescs():
892 892 newmessage = ctx.description()
893 893 else:
894 894 newmessage = (
895 895 b'\n***\n'.join(
896 896 [ctx.description()]
897 897 + [repo[r].description() for r in internalchanges]
898 898 + [oldctx.description()]
899 899 )
900 900 + b'\n'
901 901 )
902 902 commitopts[b'message'] = newmessage
903 903 # date
904 904 if self.firstdate():
905 905 commitopts[b'date'] = ctx.date()
906 906 else:
907 907 commitopts[b'date'] = max(ctx.date(), oldctx.date())
908 908 # if date is to be updated to current
909 909 if ui.configbool(b'rewrite', b'update-timestamp'):
910 910 commitopts[b'date'] = dateutil.makedate()
911 911
912 912 extra = ctx.extra().copy()
913 913 # histedit_source
914 914 # note: ctx is likely a temporary commit but that the best we can do
915 915 # here. This is sufficient to solve issue3681 anyway.
916 916 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
917 917 commitopts[b'extra'] = extra
918 918 phasemin = max(ctx.phase(), oldctx.phase())
919 919 overrides = {(b'phases', b'new-commit'): phasemin}
920 920 with repo.ui.configoverride(overrides, b'histedit'):
921 921 n = collapse(
922 922 repo,
923 923 ctx,
924 924 repo[newnode],
925 925 commitopts,
926 926 skipprompt=self.skipprompt(),
927 927 )
928 928 if n is None:
929 929 return ctx, []
930 930 hg.updaterepo(repo, n, overwrite=False)
931 931 replacements = [
932 932 (oldctx.node(), (newnode,)),
933 933 (ctx.node(), (n,)),
934 934 (newnode, (n,)),
935 935 ]
936 936 for ich in internalchanges:
937 937 replacements.append((ich, (n,)))
938 938 return repo[n], replacements
939 939
940 940
941 941 @action(
942 942 [b'base', b'b'],
943 943 _(b'checkout changeset and apply further changesets from there'),
944 944 )
945 945 class base(histeditaction):
946 946 def run(self):
947 947 if self.repo[b'.'].node() != self.node:
948 948 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
949 949 return self.continueclean()
950 950
951 951 def continuedirty(self):
952 952 abortdirty()
953 953
954 954 def continueclean(self):
955 955 basectx = self.repo[b'.']
956 956 return basectx, []
957 957
958 958 def _verifynodeconstraints(self, prev, expected, seen):
959 959 # base can only be use with a node not in the edited set
960 960 if self.node in expected:
961 961 msg = _(b'%s "%s" changeset was an edited list candidate')
962 962 raise error.ParseError(
963 963 msg % (self.verb, node.short(self.node)),
964 964 hint=_(b'base must only use unlisted changesets'),
965 965 )
966 966
967 967
968 968 @action(
969 969 [b'_multifold'],
970 970 _(
971 971 """fold subclass used for when multiple folds happen in a row
972 972
973 973 We only want to fire the editor for the folded message once when
974 974 (say) four changes are folded down into a single change. This is
975 975 similar to rollup, but we should preserve both messages so that
976 976 when the last fold operation runs we can show the user all the
977 977 commit messages in their editor.
978 978 """
979 979 ),
980 980 internal=True,
981 981 )
982 982 class _multifold(fold):
983 983 def skipprompt(self):
984 984 return True
985 985
986 986
987 987 @action(
988 988 [b"roll", b"r"],
989 989 _(b"like fold, but discard this commit's description and date"),
990 990 )
991 991 class rollup(fold):
992 992 def mergedescs(self):
993 993 return False
994 994
995 995 def skipprompt(self):
996 996 return True
997 997
998 998 def firstdate(self):
999 999 return True
1000 1000
1001 1001
1002 1002 @action([b"drop", b"d"], _(b'remove commit from history'))
1003 1003 class drop(histeditaction):
1004 1004 def run(self):
1005 1005 parentctx = self.repo[self.state.parentctxnode]
1006 1006 return parentctx, [(self.node, tuple())]
1007 1007
1008 1008
1009 1009 @action(
1010 1010 [b"mess", b"m"],
1011 1011 _(b'edit commit message without changing commit content'),
1012 1012 priority=True,
1013 1013 )
1014 1014 class message(histeditaction):
1015 1015 def commiteditor(self):
1016 1016 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1017 1017
1018 1018
1019 1019 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1020 1020 """utility function to find the first outgoing changeset
1021 1021
1022 1022 Used by initialization code"""
1023 1023 if opts is None:
1024 1024 opts = {}
1025 1025 dest = ui.expandpath(remote or b'default-push', remote or b'default')
1026 1026 dest, branches = hg.parseurl(dest, None)[:2]
1027 1027 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1028 1028
1029 1029 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
1030 1030 other = hg.peer(repo, opts, dest)
1031 1031
1032 1032 if revs:
1033 1033 revs = [repo.lookup(rev) for rev in revs]
1034 1034
1035 1035 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1036 1036 if not outgoing.missing:
1037 1037 raise error.Abort(_(b'no outgoing ancestors'))
1038 1038 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1039 1039 if len(roots) > 1:
1040 1040 msg = _(b'there are ambiguous outgoing revisions')
1041 1041 hint = _(b"see 'hg help histedit' for more detail")
1042 1042 raise error.Abort(msg, hint=hint)
1043 1043 return repo[roots[0]].node()
1044 1044
1045 1045
1046 1046 # Curses Support
1047 1047 try:
1048 1048 import curses
1049 1049 except ImportError:
1050 1050 curses = None
1051 1051
1052 1052 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1053 1053 ACTION_LABELS = {
1054 1054 b'fold': b'^fold',
1055 1055 b'roll': b'^roll',
1056 1056 }
1057 1057
1058 1058 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1059 1059 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1060 1060 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1061 1061
1062 1062 E_QUIT, E_HISTEDIT = 1, 2
1063 1063 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1064 1064 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1065 1065
1066 1066 KEYTABLE = {
1067 1067 b'global': {
1068 1068 b'h': b'next-action',
1069 1069 b'KEY_RIGHT': b'next-action',
1070 1070 b'l': b'prev-action',
1071 1071 b'KEY_LEFT': b'prev-action',
1072 1072 b'q': b'quit',
1073 1073 b'c': b'histedit',
1074 1074 b'C': b'histedit',
1075 1075 b'v': b'showpatch',
1076 1076 b'?': b'help',
1077 1077 },
1078 1078 MODE_RULES: {
1079 1079 b'd': b'action-drop',
1080 1080 b'e': b'action-edit',
1081 1081 b'f': b'action-fold',
1082 1082 b'm': b'action-mess',
1083 1083 b'p': b'action-pick',
1084 1084 b'r': b'action-roll',
1085 1085 b' ': b'select',
1086 1086 b'j': b'down',
1087 1087 b'k': b'up',
1088 1088 b'KEY_DOWN': b'down',
1089 1089 b'KEY_UP': b'up',
1090 1090 b'J': b'move-down',
1091 1091 b'K': b'move-up',
1092 1092 b'KEY_NPAGE': b'move-down',
1093 1093 b'KEY_PPAGE': b'move-up',
1094 1094 b'0': b'goto', # Used for 0..9
1095 1095 },
1096 1096 MODE_PATCH: {
1097 1097 b' ': b'page-down',
1098 1098 b'KEY_NPAGE': b'page-down',
1099 1099 b'KEY_PPAGE': b'page-up',
1100 1100 b'j': b'line-down',
1101 1101 b'k': b'line-up',
1102 1102 b'KEY_DOWN': b'line-down',
1103 1103 b'KEY_UP': b'line-up',
1104 1104 b'J': b'down',
1105 1105 b'K': b'up',
1106 1106 },
1107 1107 MODE_HELP: {},
1108 1108 }
1109 1109
1110 1110
1111 1111 def screen_size():
1112 1112 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1113 1113
1114 1114
1115 1115 class histeditrule(object):
1116 1116 def __init__(self, ctx, pos, action=b'pick'):
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 1154 @property
1155 1155 def desc(self):
1156 1156 # This is split off from the prefix property so that we can
1157 1157 # separately make the description for 'roll' red (since it
1158 1158 # will get discarded).
1159 1159 return self.ctx.description().splitlines()[0].strip()
1160 1160
1161 1161 def checkconflicts(self, other):
1162 1162 if other.pos > self.pos and other.origpos <= self.origpos:
1163 1163 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1164 1164 self.conflicts.append(other)
1165 1165 return self.conflicts
1166 1166
1167 1167 if other in self.conflicts:
1168 1168 self.conflicts.remove(other)
1169 1169 return self.conflicts
1170 1170
1171 1171
1172 1172 # ============ EVENTS ===============
1173 1173 def movecursor(state, oldpos, newpos):
1174 1174 '''Change the rule/changeset that the cursor is pointing to, regardless of
1175 1175 current mode (you can switch between patches from the view patch window).'''
1176 1176 state[b'pos'] = newpos
1177 1177
1178 1178 mode, _ = state[b'mode']
1179 1179 if mode == MODE_RULES:
1180 1180 # Scroll through the list by updating the view for MODE_RULES, so that
1181 1181 # even if we are not currently viewing the rules, switching back will
1182 1182 # result in the cursor's rule being visible.
1183 1183 modestate = state[b'modes'][MODE_RULES]
1184 1184 if newpos < modestate[b'line_offset']:
1185 1185 modestate[b'line_offset'] = newpos
1186 1186 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1187 1187 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1188 1188
1189 1189 # Reset the patch view region to the top of the new patch.
1190 1190 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1191 1191
1192 1192
1193 1193 def changemode(state, mode):
1194 1194 curmode, _ = state[b'mode']
1195 1195 state[b'mode'] = (mode, curmode)
1196 1196 if mode == MODE_PATCH:
1197 1197 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1198 1198
1199 1199
1200 1200 def makeselection(state, pos):
1201 1201 state[b'selected'] = pos
1202 1202
1203 1203
1204 1204 def swap(state, oldpos, newpos):
1205 1205 """Swap two positions and calculate necessary conflicts in
1206 1206 O(|newpos-oldpos|) time"""
1207 1207
1208 1208 rules = state[b'rules']
1209 1209 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1210 1210
1211 1211 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1212 1212
1213 1213 # TODO: swap should not know about histeditrule's internals
1214 1214 rules[newpos].pos = newpos
1215 1215 rules[oldpos].pos = oldpos
1216 1216
1217 1217 start = min(oldpos, newpos)
1218 1218 end = max(oldpos, newpos)
1219 1219 for r in pycompat.xrange(start, end + 1):
1220 1220 rules[newpos].checkconflicts(rules[r])
1221 1221 rules[oldpos].checkconflicts(rules[r])
1222 1222
1223 1223 if state[b'selected']:
1224 1224 makeselection(state, newpos)
1225 1225
1226 1226
1227 1227 def changeaction(state, pos, action):
1228 1228 """Change the action state on the given position to the new action"""
1229 1229 rules = state[b'rules']
1230 1230 assert 0 <= pos < len(rules)
1231 1231 rules[pos].action = action
1232 1232
1233 1233
1234 1234 def cycleaction(state, pos, next=False):
1235 1235 """Changes the action state the next or the previous action from
1236 1236 the action list"""
1237 1237 rules = state[b'rules']
1238 1238 assert 0 <= pos < len(rules)
1239 1239 current = rules[pos].action
1240 1240
1241 1241 assert current in KEY_LIST
1242 1242
1243 1243 index = KEY_LIST.index(current)
1244 1244 if next:
1245 1245 index += 1
1246 1246 else:
1247 1247 index -= 1
1248 1248 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1249 1249
1250 1250
1251 1251 def changeview(state, delta, unit):
1252 1252 '''Change the region of whatever is being viewed (a patch or the list of
1253 1253 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1254 1254 mode, _ = state[b'mode']
1255 1255 if mode != MODE_PATCH:
1256 1256 return
1257 1257 mode_state = state[b'modes'][mode]
1258 1258 num_lines = len(mode_state[b'patchcontents'])
1259 1259 page_height = state[b'page_height']
1260 1260 unit = page_height if unit == b'page' else 1
1261 1261 num_pages = 1 + (num_lines - 1) / page_height
1262 1262 max_offset = (num_pages - 1) * page_height
1263 1263 newline = mode_state[b'line_offset'] + delta * unit
1264 1264 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1265 1265
1266 1266
1267 1267 def event(state, ch):
1268 1268 """Change state based on the current character input
1269 1269
1270 1270 This takes the current state and based on the current character input from
1271 1271 the user we change the state.
1272 1272 """
1273 1273 selected = state[b'selected']
1274 1274 oldpos = state[b'pos']
1275 1275 rules = state[b'rules']
1276 1276
1277 1277 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1278 1278 return E_RESIZE
1279 1279
1280 1280 lookup_ch = ch
1281 1281 if ch is not None and b'0' <= ch <= b'9':
1282 1282 lookup_ch = b'0'
1283 1283
1284 1284 curmode, prevmode = state[b'mode']
1285 1285 action = KEYTABLE[curmode].get(
1286 1286 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1287 1287 )
1288 1288 if action is None:
1289 1289 return
1290 1290 if action in (b'down', b'move-down'):
1291 1291 newpos = min(oldpos + 1, len(rules) - 1)
1292 1292 movecursor(state, oldpos, newpos)
1293 1293 if selected is not None or action == b'move-down':
1294 1294 swap(state, oldpos, newpos)
1295 1295 elif action in (b'up', b'move-up'):
1296 1296 newpos = max(0, oldpos - 1)
1297 1297 movecursor(state, oldpos, newpos)
1298 1298 if selected is not None or action == b'move-up':
1299 1299 swap(state, oldpos, newpos)
1300 1300 elif action == b'next-action':
1301 1301 cycleaction(state, oldpos, next=True)
1302 1302 elif action == b'prev-action':
1303 1303 cycleaction(state, oldpos, next=False)
1304 1304 elif action == b'select':
1305 1305 selected = oldpos if selected is None else None
1306 1306 makeselection(state, selected)
1307 1307 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1308 1308 newrule = next((r for r in rules if r.origpos == int(ch)))
1309 1309 movecursor(state, oldpos, newrule.pos)
1310 1310 if selected is not None:
1311 1311 swap(state, oldpos, newrule.pos)
1312 1312 elif action.startswith(b'action-'):
1313 1313 changeaction(state, oldpos, action[7:])
1314 1314 elif action == b'showpatch':
1315 1315 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1316 1316 elif action == b'help':
1317 1317 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1318 1318 elif action == b'quit':
1319 1319 return E_QUIT
1320 1320 elif action == b'histedit':
1321 1321 return E_HISTEDIT
1322 1322 elif action == b'page-down':
1323 1323 return E_PAGEDOWN
1324 1324 elif action == b'page-up':
1325 1325 return E_PAGEUP
1326 1326 elif action == b'line-down':
1327 1327 return E_LINEDOWN
1328 1328 elif action == b'line-up':
1329 1329 return E_LINEUP
1330 1330
1331 1331
1332 1332 def makecommands(rules):
1333 1333 """Returns a list of commands consumable by histedit --commands based on
1334 1334 our list of rules"""
1335 1335 commands = []
1336 1336 for rules in rules:
1337 1337 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1338 1338 return commands
1339 1339
1340 1340
1341 1341 def addln(win, y, x, line, color=None):
1342 1342 """Add a line to the given window left padding but 100% filled with
1343 1343 whitespace characters, so that the color appears on the whole line"""
1344 1344 maxy, maxx = win.getmaxyx()
1345 1345 length = maxx - 1 - x
1346 1346 line = bytes(line).ljust(length)[:length]
1347 1347 if y < 0:
1348 1348 y = maxy + y
1349 1349 if x < 0:
1350 1350 x = maxx + x
1351 1351 if color:
1352 1352 win.addstr(y, x, line, color)
1353 1353 else:
1354 1354 win.addstr(y, x, line)
1355 1355
1356 1356
1357 1357 def _trunc_head(line, n):
1358 1358 if len(line) <= n:
1359 1359 return line
1360 1360 return b'> ' + line[-(n - 2) :]
1361 1361
1362 1362
1363 1363 def _trunc_tail(line, n):
1364 1364 if len(line) <= n:
1365 1365 return line
1366 1366 return line[: n - 2] + b' >'
1367 1367
1368 1368
1369 1369 def patchcontents(state):
1370 1370 repo = state[b'repo']
1371 1371 rule = state[b'rules'][state[b'pos']]
1372 1372 displayer = logcmdutil.changesetdisplayer(
1373 1373 repo.ui, repo, {b"patch": True, b"template": b"status"}, buffered=True
1374 1374 )
1375 1375 overrides = {(b'ui', b'verbose'): True}
1376 1376 with repo.ui.configoverride(overrides, source=b'histedit'):
1377 1377 displayer.show(rule.ctx)
1378 1378 displayer.close()
1379 1379 return displayer.hunk[rule.ctx.rev()].splitlines()
1380 1380
1381 1381
1382 1382 def _chisteditmain(repo, rules, stdscr):
1383 1383 try:
1384 1384 curses.use_default_colors()
1385 1385 except curses.error:
1386 1386 pass
1387 1387
1388 1388 # initialize color pattern
1389 1389 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1390 1390 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1391 1391 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1392 1392 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1393 1393 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1394 1394 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1395 1395 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1396 1396 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1397 1397 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1398 1398 curses.init_pair(
1399 1399 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1400 1400 )
1401 1401 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1402 1402
1403 1403 # don't display the cursor
1404 1404 try:
1405 1405 curses.curs_set(0)
1406 1406 except curses.error:
1407 1407 pass
1408 1408
1409 1409 def rendercommit(win, state):
1410 1410 """Renders the commit window that shows the log of the current selected
1411 1411 commit"""
1412 1412 pos = state[b'pos']
1413 1413 rules = state[b'rules']
1414 1414 rule = rules[pos]
1415 1415
1416 1416 ctx = rule.ctx
1417 1417 win.box()
1418 1418
1419 1419 maxy, maxx = win.getmaxyx()
1420 1420 length = maxx - 3
1421 1421
1422 1422 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1423 1423 win.addstr(1, 1, line[:length])
1424 1424
1425 1425 line = b"user: %s" % ctx.user()
1426 1426 win.addstr(2, 1, line[:length])
1427 1427
1428 1428 bms = repo.nodebookmarks(ctx.node())
1429 1429 line = b"bookmark: %s" % b' '.join(bms)
1430 1430 win.addstr(3, 1, line[:length])
1431 1431
1432 1432 line = b"summary: %s" % (ctx.description().splitlines()[0])
1433 1433 win.addstr(4, 1, line[:length])
1434 1434
1435 1435 line = b"files: "
1436 1436 win.addstr(5, 1, line)
1437 1437 fnx = 1 + len(line)
1438 1438 fnmaxx = length - fnx + 1
1439 1439 y = 5
1440 1440 fnmaxn = maxy - (1 + y) - 1
1441 1441 files = ctx.files()
1442 1442 for i, line1 in enumerate(files):
1443 1443 if len(files) > fnmaxn and i == fnmaxn - 1:
1444 1444 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1445 1445 y = y + 1
1446 1446 break
1447 1447 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1448 1448 y = y + 1
1449 1449
1450 1450 conflicts = rule.conflicts
1451 1451 if len(conflicts) > 0:
1452 1452 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1453 1453 conflictstr = b"changed files overlap with %s" % conflictstr
1454 1454 else:
1455 1455 conflictstr = b'no overlap'
1456 1456
1457 1457 win.addstr(y, 1, conflictstr[:length])
1458 1458 win.noutrefresh()
1459 1459
1460 1460 def helplines(mode):
1461 1461 if mode == MODE_PATCH:
1462 1462 help = b"""\
1463 1463 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1464 1464 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1465 1465 """
1466 1466 else:
1467 1467 help = b"""\
1468 1468 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1469 1469 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1470 1470 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1471 1471 """
1472 1472 return help.splitlines()
1473 1473
1474 1474 def renderhelp(win, state):
1475 1475 maxy, maxx = win.getmaxyx()
1476 1476 mode, _ = state[b'mode']
1477 1477 for y, line in enumerate(helplines(mode)):
1478 1478 if y >= maxy:
1479 1479 break
1480 1480 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1481 1481 win.noutrefresh()
1482 1482
1483 1483 def renderrules(rulesscr, state):
1484 1484 rules = state[b'rules']
1485 1485 pos = state[b'pos']
1486 1486 selected = state[b'selected']
1487 1487 start = state[b'modes'][MODE_RULES][b'line_offset']
1488 1488
1489 1489 conflicts = [r.ctx for r in rules if r.conflicts]
1490 1490 if len(conflicts) > 0:
1491 1491 line = b"potential conflict in %s" % b','.join(
1492 1492 map(pycompat.bytestr, conflicts)
1493 1493 )
1494 1494 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1495 1495
1496 1496 for y, rule in enumerate(rules[start:]):
1497 1497 if y >= state[b'page_height']:
1498 1498 break
1499 1499 if len(rule.conflicts) > 0:
1500 1500 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1501 1501 else:
1502 1502 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1503 1503
1504 1504 if y + start == selected:
1505 1505 rollcolor = COLOR_ROLL_SELECTED
1506 1506 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1507 1507 elif y + start == pos:
1508 1508 rollcolor = COLOR_ROLL_CURRENT
1509 1509 addln(
1510 1510 rulesscr,
1511 1511 y,
1512 1512 2,
1513 1513 rule,
1514 1514 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1515 1515 )
1516 1516 else:
1517 1517 rollcolor = COLOR_ROLL
1518 1518 addln(rulesscr, y, 2, rule)
1519 1519
1520 1520 if rule.action == b'roll':
1521 1521 rulesscr.addstr(
1522 1522 y,
1523 1523 2 + len(rule.prefix),
1524 1524 rule.desc,
1525 1525 curses.color_pair(rollcolor),
1526 1526 )
1527 1527
1528 1528 rulesscr.noutrefresh()
1529 1529
1530 1530 def renderstring(win, state, output, diffcolors=False):
1531 1531 maxy, maxx = win.getmaxyx()
1532 1532 length = min(maxy - 1, len(output))
1533 1533 for y in range(0, length):
1534 1534 line = output[y]
1535 1535 if diffcolors:
1536 1536 if line and line[0] == b'+':
1537 1537 win.addstr(
1538 1538 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1539 1539 )
1540 1540 elif line and line[0] == b'-':
1541 1541 win.addstr(
1542 1542 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1543 1543 )
1544 1544 elif line.startswith(b'@@ '):
1545 1545 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1546 1546 else:
1547 1547 win.addstr(y, 0, line)
1548 1548 else:
1549 1549 win.addstr(y, 0, line)
1550 1550 win.noutrefresh()
1551 1551
1552 1552 def renderpatch(win, state):
1553 1553 start = state[b'modes'][MODE_PATCH][b'line_offset']
1554 1554 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1555 1555 renderstring(win, state, content[start:], diffcolors=True)
1556 1556
1557 1557 def layout(mode):
1558 1558 maxy, maxx = stdscr.getmaxyx()
1559 1559 helplen = len(helplines(mode))
1560 1560 return {
1561 1561 b'commit': (12, maxx),
1562 1562 b'help': (helplen, maxx),
1563 1563 b'main': (maxy - helplen - 12, maxx),
1564 1564 }
1565 1565
1566 1566 def drawvertwin(size, y, x):
1567 1567 win = curses.newwin(size[0], size[1], y, x)
1568 1568 y += size[0]
1569 1569 return win, y, x
1570 1570
1571 1571 state = {
1572 1572 b'pos': 0,
1573 1573 b'rules': rules,
1574 1574 b'selected': None,
1575 1575 b'mode': (MODE_INIT, MODE_INIT),
1576 1576 b'page_height': None,
1577 1577 b'modes': {
1578 1578 MODE_RULES: {b'line_offset': 0,},
1579 1579 MODE_PATCH: {b'line_offset': 0,},
1580 1580 },
1581 1581 b'repo': repo,
1582 1582 }
1583 1583
1584 1584 # eventloop
1585 1585 ch = None
1586 1586 stdscr.clear()
1587 1587 stdscr.refresh()
1588 1588 while True:
1589 1589 try:
1590 1590 oldmode, _ = state[b'mode']
1591 1591 if oldmode == MODE_INIT:
1592 1592 changemode(state, MODE_RULES)
1593 1593 e = event(state, ch)
1594 1594
1595 1595 if e == E_QUIT:
1596 1596 return False
1597 1597 if e == E_HISTEDIT:
1598 1598 return state[b'rules']
1599 1599 else:
1600 1600 if e == E_RESIZE:
1601 1601 size = screen_size()
1602 1602 if size != stdscr.getmaxyx():
1603 1603 curses.resizeterm(*size)
1604 1604
1605 1605 curmode, _ = state[b'mode']
1606 1606 sizes = layout(curmode)
1607 1607 if curmode != oldmode:
1608 1608 state[b'page_height'] = sizes[b'main'][0]
1609 1609 # Adjust the view to fit the current screen size.
1610 1610 movecursor(state, state[b'pos'], state[b'pos'])
1611 1611
1612 1612 # Pack the windows against the top, each pane spread across the
1613 1613 # full width of the screen.
1614 1614 y, x = (0, 0)
1615 1615 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1616 1616 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1617 1617 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1618 1618
1619 1619 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1620 1620 if e == E_PAGEDOWN:
1621 1621 changeview(state, +1, b'page')
1622 1622 elif e == E_PAGEUP:
1623 1623 changeview(state, -1, b'page')
1624 1624 elif e == E_LINEDOWN:
1625 1625 changeview(state, +1, b'line')
1626 1626 elif e == E_LINEUP:
1627 1627 changeview(state, -1, b'line')
1628 1628
1629 1629 # start rendering
1630 1630 commitwin.erase()
1631 1631 helpwin.erase()
1632 1632 mainwin.erase()
1633 1633 if curmode == MODE_PATCH:
1634 1634 renderpatch(mainwin, state)
1635 1635 elif curmode == MODE_HELP:
1636 1636 renderstring(mainwin, state, __doc__.strip().splitlines())
1637 1637 else:
1638 1638 renderrules(mainwin, state)
1639 1639 rendercommit(commitwin, state)
1640 1640 renderhelp(helpwin, state)
1641 1641 curses.doupdate()
1642 1642 # done rendering
1643 1643 ch = encoding.strtolocal(stdscr.getkey())
1644 1644 except curses.error:
1645 1645 pass
1646 1646
1647 1647
1648 1648 def _chistedit(ui, repo, freeargs, opts):
1649 1649 """interactively edit changeset history via a curses interface
1650 1650
1651 1651 Provides a ncurses interface to histedit. Press ? in chistedit mode
1652 1652 to see an extensive help. Requires python-curses to be installed."""
1653 1653
1654 1654 if curses is None:
1655 1655 raise error.Abort(_(b"Python curses library required"))
1656 1656
1657 1657 # disable color
1658 1658 ui._colormode = None
1659 1659
1660 1660 try:
1661 1661 keep = opts.get(b'keep')
1662 1662 revs = opts.get(b'rev', [])[:]
1663 1663 cmdutil.checkunfinished(repo)
1664 1664 cmdutil.bailifchanged(repo)
1665 1665
1666 1666 if os.path.exists(os.path.join(repo.path, b'histedit-state')):
1667 1667 raise error.Abort(
1668 1668 _(
1669 1669 b'history edit already in progress, try '
1670 1670 b'--continue or --abort'
1671 1671 )
1672 1672 )
1673 1673 revs.extend(freeargs)
1674 1674 if not revs:
1675 1675 defaultrev = destutil.desthistedit(ui, repo)
1676 1676 if defaultrev is not None:
1677 1677 revs.append(defaultrev)
1678 1678 if len(revs) != 1:
1679 1679 raise error.Abort(
1680 1680 _(b'histedit requires exactly one ancestor revision')
1681 1681 )
1682 1682
1683 1683 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1684 1684 if len(rr) != 1:
1685 1685 raise error.Abort(
1686 1686 _(
1687 1687 b'The specified revisions must have '
1688 1688 b'exactly one common root'
1689 1689 )
1690 1690 )
1691 1691 root = rr[0].node()
1692 1692
1693 1693 topmost = repo.dirstate.p1()
1694 1694 revs = between(repo, root, topmost, keep)
1695 1695 if not revs:
1696 1696 raise error.Abort(
1697 1697 _(b'%s is not an ancestor of working directory')
1698 1698 % node.short(root)
1699 1699 )
1700 1700
1701 1701 ctxs = []
1702 1702 for i, r in enumerate(revs):
1703 1703 ctxs.append(histeditrule(repo[r], i))
1704 1704 # Curses requires setting the locale or it will default to the C
1705 1705 # locale. This sets the locale to the user's default system
1706 1706 # locale.
1707 1707 locale.setlocale(locale.LC_ALL, '')
1708 1708 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1709 1709 curses.echo()
1710 1710 curses.endwin()
1711 1711 if rc is False:
1712 1712 ui.write(_(b"histedit aborted\n"))
1713 1713 return 0
1714 1714 if type(rc) is list:
1715 1715 ui.status(_(b"performing changes\n"))
1716 1716 rules = makecommands(rc)
1717 1717 with repo.vfs(b'chistedit', b'w+') as fp:
1718 1718 for r in rules:
1719 1719 fp.write(r)
1720 1720 opts[b'commands'] = fp.name
1721 1721 return _texthistedit(ui, repo, freeargs, opts)
1722 1722 except KeyboardInterrupt:
1723 1723 pass
1724 1724 return -1
1725 1725
1726 1726
1727 1727 @command(
1728 1728 b'histedit',
1729 1729 [
1730 1730 (
1731 1731 b'',
1732 1732 b'commands',
1733 1733 b'',
1734 1734 _(b'read history edits from the specified file'),
1735 1735 _(b'FILE'),
1736 1736 ),
1737 1737 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1738 1738 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1739 1739 (
1740 1740 b'k',
1741 1741 b'keep',
1742 1742 False,
1743 1743 _(b"don't strip old nodes after edit is complete"),
1744 1744 ),
1745 1745 (b'', b'abort', False, _(b'abort an edit in progress')),
1746 1746 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1747 1747 (
1748 1748 b'f',
1749 1749 b'force',
1750 1750 False,
1751 1751 _(b'force outgoing even for unrelated repositories'),
1752 1752 ),
1753 1753 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1754 1754 ]
1755 1755 + cmdutil.formatteropts,
1756 1756 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1757 1757 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1758 1758 )
1759 1759 def histedit(ui, repo, *freeargs, **opts):
1760 1760 """interactively edit changeset history
1761 1761
1762 1762 This command lets you edit a linear series of changesets (up to
1763 1763 and including the working directory, which should be clean).
1764 1764 You can:
1765 1765
1766 1766 - `pick` to [re]order a changeset
1767 1767
1768 1768 - `drop` to omit changeset
1769 1769
1770 1770 - `mess` to reword the changeset commit message
1771 1771
1772 1772 - `fold` to combine it with the preceding changeset (using the later date)
1773 1773
1774 1774 - `roll` like fold, but discarding this commit's description and date
1775 1775
1776 1776 - `edit` to edit this changeset (preserving date)
1777 1777
1778 1778 - `base` to checkout changeset and apply further changesets from there
1779 1779
1780 1780 There are a number of ways to select the root changeset:
1781 1781
1782 1782 - Specify ANCESTOR directly
1783 1783
1784 1784 - Use --outgoing -- it will be the first linear changeset not
1785 1785 included in destination. (See :hg:`help config.paths.default-push`)
1786 1786
1787 1787 - Otherwise, the value from the "histedit.defaultrev" config option
1788 1788 is used as a revset to select the base revision when ANCESTOR is not
1789 1789 specified. The first revision returned by the revset is used. By
1790 1790 default, this selects the editable history that is unique to the
1791 1791 ancestry of the working directory.
1792 1792
1793 1793 .. container:: verbose
1794 1794
1795 1795 If you use --outgoing, this command will abort if there are ambiguous
1796 1796 outgoing revisions. For example, if there are multiple branches
1797 1797 containing outgoing revisions.
1798 1798
1799 1799 Use "min(outgoing() and ::.)" or similar revset specification
1800 1800 instead of --outgoing to specify edit target revision exactly in
1801 1801 such ambiguous situation. See :hg:`help revsets` for detail about
1802 1802 selecting revisions.
1803 1803
1804 1804 .. container:: verbose
1805 1805
1806 1806 Examples:
1807 1807
1808 1808 - A number of changes have been made.
1809 1809 Revision 3 is no longer needed.
1810 1810
1811 1811 Start history editing from revision 3::
1812 1812
1813 1813 hg histedit -r 3
1814 1814
1815 1815 An editor opens, containing the list of revisions,
1816 1816 with specific actions specified::
1817 1817
1818 1818 pick 5339bf82f0ca 3 Zworgle the foobar
1819 1819 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1820 1820 pick 0a9639fcda9d 5 Morgify the cromulancy
1821 1821
1822 1822 Additional information about the possible actions
1823 1823 to take appears below the list of revisions.
1824 1824
1825 1825 To remove revision 3 from the history,
1826 1826 its action (at the beginning of the relevant line)
1827 1827 is changed to 'drop'::
1828 1828
1829 1829 drop 5339bf82f0ca 3 Zworgle the foobar
1830 1830 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1831 1831 pick 0a9639fcda9d 5 Morgify the cromulancy
1832 1832
1833 1833 - A number of changes have been made.
1834 1834 Revision 2 and 4 need to be swapped.
1835 1835
1836 1836 Start history editing from revision 2::
1837 1837
1838 1838 hg histedit -r 2
1839 1839
1840 1840 An editor opens, containing the list of revisions,
1841 1841 with specific actions specified::
1842 1842
1843 1843 pick 252a1af424ad 2 Blorb a morgwazzle
1844 1844 pick 5339bf82f0ca 3 Zworgle the foobar
1845 1845 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1846 1846
1847 1847 To swap revision 2 and 4, its lines are swapped
1848 1848 in the editor::
1849 1849
1850 1850 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1851 1851 pick 5339bf82f0ca 3 Zworgle the foobar
1852 1852 pick 252a1af424ad 2 Blorb a morgwazzle
1853 1853
1854 1854 Returns 0 on success, 1 if user intervention is required (not only
1855 1855 for intentional "edit" command, but also for resolving unexpected
1856 1856 conflicts).
1857 1857 """
1858 1858 opts = pycompat.byteskwargs(opts)
1859 1859
1860 1860 # kludge: _chistedit only works for starting an edit, not aborting
1861 1861 # or continuing, so fall back to regular _texthistedit for those
1862 1862 # operations.
1863 if (
1864 ui.interface(b'histedit') == b'curses'
1865 and _getgoal(opts) == goalnew
1866 ):
1863 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1867 1864 return _chistedit(ui, repo, freeargs, opts)
1868 1865 return _texthistedit(ui, repo, freeargs, opts)
1869 1866
1870 1867
1871 1868 def _texthistedit(ui, repo, freeargs, opts):
1872 1869 state = histeditstate(repo)
1873 1870 with repo.wlock() as wlock, repo.lock() as lock:
1874 1871 state.wlock = wlock
1875 1872 state.lock = lock
1876 1873 _histedit(ui, repo, state, freeargs, opts)
1877 1874
1878 1875
1879 1876 goalcontinue = b'continue'
1880 1877 goalabort = b'abort'
1881 1878 goaleditplan = b'edit-plan'
1882 1879 goalnew = b'new'
1883 1880
1884 1881
1885 1882 def _getgoal(opts):
1886 1883 if opts.get(b'continue'):
1887 1884 return goalcontinue
1888 1885 if opts.get(b'abort'):
1889 1886 return goalabort
1890 1887 if opts.get(b'edit_plan'):
1891 1888 return goaleditplan
1892 1889 return goalnew
1893 1890
1894 1891
1895 1892 def _readfile(ui, path):
1896 1893 if path == b'-':
1897 1894 with ui.timeblockedsection(b'histedit'):
1898 1895 return ui.fin.read()
1899 1896 else:
1900 1897 with open(path, b'rb') as f:
1901 1898 return f.read()
1902 1899
1903 1900
1904 1901 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1905 1902 # TODO only abort if we try to histedit mq patches, not just
1906 1903 # blanket if mq patches are applied somewhere
1907 1904 mq = getattr(repo, 'mq', None)
1908 1905 if mq and mq.applied:
1909 1906 raise error.Abort(_(b'source has mq patches applied'))
1910 1907
1911 1908 # basic argument incompatibility processing
1912 1909 outg = opts.get(b'outgoing')
1913 1910 editplan = opts.get(b'edit_plan')
1914 1911 abort = opts.get(b'abort')
1915 1912 force = opts.get(b'force')
1916 1913 if force and not outg:
1917 1914 raise error.Abort(_(b'--force only allowed with --outgoing'))
1918 1915 if goal == b'continue':
1919 1916 if any((outg, abort, revs, freeargs, rules, editplan)):
1920 1917 raise error.Abort(_(b'no arguments allowed with --continue'))
1921 1918 elif goal == b'abort':
1922 1919 if any((outg, revs, freeargs, rules, editplan)):
1923 1920 raise error.Abort(_(b'no arguments allowed with --abort'))
1924 1921 elif goal == b'edit-plan':
1925 1922 if any((outg, revs, freeargs)):
1926 1923 raise error.Abort(
1927 1924 _(b'only --commands argument allowed with --edit-plan')
1928 1925 )
1929 1926 else:
1930 1927 if state.inprogress():
1931 1928 raise error.Abort(
1932 1929 _(
1933 1930 b'history edit already in progress, try '
1934 1931 b'--continue or --abort'
1935 1932 )
1936 1933 )
1937 1934 if outg:
1938 1935 if revs:
1939 1936 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1940 1937 if len(freeargs) > 1:
1941 1938 raise error.Abort(
1942 1939 _(b'only one repo argument allowed with --outgoing')
1943 1940 )
1944 1941 else:
1945 1942 revs.extend(freeargs)
1946 1943 if len(revs) == 0:
1947 1944 defaultrev = destutil.desthistedit(ui, repo)
1948 1945 if defaultrev is not None:
1949 1946 revs.append(defaultrev)
1950 1947
1951 1948 if len(revs) != 1:
1952 1949 raise error.Abort(
1953 1950 _(b'histedit requires exactly one ancestor revision')
1954 1951 )
1955 1952
1956 1953
1957 1954 def _histedit(ui, repo, state, freeargs, opts):
1958 1955 fm = ui.formatter(b'histedit', opts)
1959 1956 fm.startitem()
1960 1957 goal = _getgoal(opts)
1961 1958 revs = opts.get(b'rev', [])
1962 1959 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1963 1960 rules = opts.get(b'commands', b'')
1964 1961 state.keep = opts.get(b'keep', False)
1965 1962
1966 1963 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1967 1964
1968 1965 hastags = False
1969 1966 if revs:
1970 1967 revs = scmutil.revrange(repo, revs)
1971 1968 ctxs = [repo[rev] for rev in revs]
1972 1969 for ctx in ctxs:
1973 1970 tags = [tag for tag in ctx.tags() if tag != b'tip']
1974 1971 if not hastags:
1975 1972 hastags = len(tags)
1976 1973 if hastags:
1977 1974 if ui.promptchoice(
1978 1975 _(
1979 1976 b'warning: tags associated with the given'
1980 1977 b' changeset will be lost after histedit.\n'
1981 1978 b'do you want to continue (yN)? $$ &Yes $$ &No'
1982 1979 ),
1983 1980 default=1,
1984 1981 ):
1985 1982 raise error.Abort(_(b'histedit cancelled\n'))
1986 1983 # rebuild state
1987 1984 if goal == goalcontinue:
1988 1985 state.read()
1989 1986 state = bootstrapcontinue(ui, state, opts)
1990 1987 elif goal == goaleditplan:
1991 1988 _edithisteditplan(ui, repo, state, rules)
1992 1989 return
1993 1990 elif goal == goalabort:
1994 1991 _aborthistedit(ui, repo, state, nobackup=nobackup)
1995 1992 return
1996 1993 else:
1997 1994 # goal == goalnew
1998 1995 _newhistedit(ui, repo, state, revs, freeargs, opts)
1999 1996
2000 1997 _continuehistedit(ui, repo, state)
2001 1998 _finishhistedit(ui, repo, state, fm)
2002 1999 fm.end()
2003 2000
2004 2001
2005 2002 def _continuehistedit(ui, repo, state):
2006 2003 """This function runs after either:
2007 2004 - bootstrapcontinue (if the goal is 'continue')
2008 2005 - _newhistedit (if the goal is 'new')
2009 2006 """
2010 2007 # preprocess rules so that we can hide inner folds from the user
2011 2008 # and only show one editor
2012 2009 actions = state.actions[:]
2013 2010 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2014 2011 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2015 2012 state.actions[idx].__class__ = _multifold
2016 2013
2017 2014 # Force an initial state file write, so the user can run --abort/continue
2018 2015 # even if there's an exception before the first transaction serialize.
2019 2016 state.write()
2020 2017
2021 2018 tr = None
2022 2019 # Don't use singletransaction by default since it rolls the entire
2023 2020 # transaction back if an unexpected exception happens (like a
2024 2021 # pretxncommit hook throws, or the user aborts the commit msg editor).
2025 2022 if ui.configbool(b"histedit", b"singletransaction"):
2026 2023 # Don't use a 'with' for the transaction, since actions may close
2027 2024 # and reopen a transaction. For example, if the action executes an
2028 2025 # external process it may choose to commit the transaction first.
2029 2026 tr = repo.transaction(b'histedit')
2030 2027 progress = ui.makeprogress(
2031 2028 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2032 2029 )
2033 2030 with progress, util.acceptintervention(tr):
2034 2031 while state.actions:
2035 2032 state.write(tr=tr)
2036 2033 actobj = state.actions[0]
2037 2034 progress.increment(item=actobj.torule())
2038 2035 ui.debug(
2039 2036 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2040 2037 )
2041 2038 parentctx, replacement_ = actobj.run()
2042 2039 state.parentctxnode = parentctx.node()
2043 2040 state.replacements.extend(replacement_)
2044 2041 state.actions.pop(0)
2045 2042
2046 2043 state.write()
2047 2044
2048 2045
2049 2046 def _finishhistedit(ui, repo, state, fm):
2050 2047 """This action runs when histedit is finishing its session"""
2051 2048 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
2052 2049
2053 2050 mapping, tmpnodes, created, ntm = processreplacement(state)
2054 2051 if mapping:
2055 2052 for prec, succs in pycompat.iteritems(mapping):
2056 2053 if not succs:
2057 2054 ui.debug(b'histedit: %s is dropped\n' % node.short(prec))
2058 2055 else:
2059 2056 ui.debug(
2060 2057 b'histedit: %s is replaced by %s\n'
2061 2058 % (node.short(prec), node.short(succs[0]))
2062 2059 )
2063 2060 if len(succs) > 1:
2064 2061 m = b'histedit: %s'
2065 2062 for n in succs[1:]:
2066 2063 ui.debug(m % node.short(n))
2067 2064
2068 2065 if not state.keep:
2069 2066 if mapping:
2070 2067 movetopmostbookmarks(repo, state.topmost, ntm)
2071 2068 # TODO update mq state
2072 2069 else:
2073 2070 mapping = {}
2074 2071
2075 2072 for n in tmpnodes:
2076 2073 if n in repo:
2077 2074 mapping[n] = ()
2078 2075
2079 2076 # remove entries about unknown nodes
2080 2077 has_node = repo.unfiltered().changelog.index.has_node
2081 2078 mapping = {
2082 2079 k: v
2083 2080 for k, v in mapping.items()
2084 2081 if has_node(k) and all(has_node(n) for n in v)
2085 2082 }
2086 2083 scmutil.cleanupnodes(repo, mapping, b'histedit')
2087 2084 hf = fm.hexfunc
2088 2085 fl = fm.formatlist
2089 2086 fd = fm.formatdict
2090 2087 nodechanges = fd(
2091 2088 {
2092 2089 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2093 2090 for oldn, newn in pycompat.iteritems(mapping)
2094 2091 },
2095 2092 key=b"oldnode",
2096 2093 value=b"newnodes",
2097 2094 )
2098 2095 fm.data(nodechanges=nodechanges)
2099 2096
2100 2097 state.clear()
2101 2098 if os.path.exists(repo.sjoin(b'undo')):
2102 2099 os.unlink(repo.sjoin(b'undo'))
2103 2100 if repo.vfs.exists(b'histedit-last-edit.txt'):
2104 2101 repo.vfs.unlink(b'histedit-last-edit.txt')
2105 2102
2106 2103
2107 2104 def _aborthistedit(ui, repo, state, nobackup=False):
2108 2105 try:
2109 2106 state.read()
2110 2107 __, leafs, tmpnodes, __ = processreplacement(state)
2111 2108 ui.debug(b'restore wc to old parent %s\n' % node.short(state.topmost))
2112 2109
2113 2110 # Recover our old commits if necessary
2114 2111 if not state.topmost in repo and state.backupfile:
2115 2112 backupfile = repo.vfs.join(state.backupfile)
2116 2113 f = hg.openpath(ui, backupfile)
2117 2114 gen = exchange.readbundle(ui, f, backupfile)
2118 2115 with repo.transaction(b'histedit.abort') as tr:
2119 2116 bundle2.applybundle(
2120 2117 repo,
2121 2118 gen,
2122 2119 tr,
2123 2120 source=b'histedit',
2124 2121 url=b'bundle:' + backupfile,
2125 2122 )
2126 2123
2127 2124 os.remove(backupfile)
2128 2125
2129 2126 # check whether we should update away
2130 2127 if repo.unfiltered().revs(
2131 2128 b'parents() and (%n or %ln::)',
2132 2129 state.parentctxnode,
2133 2130 leafs | tmpnodes,
2134 2131 ):
2135 2132 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2136 2133 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2137 2134 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2138 2135 except Exception:
2139 2136 if state.inprogress():
2140 2137 ui.warn(
2141 2138 _(
2142 2139 b'warning: encountered an exception during histedit '
2143 2140 b'--abort; the repository may not have been completely '
2144 2141 b'cleaned up\n'
2145 2142 )
2146 2143 )
2147 2144 raise
2148 2145 finally:
2149 2146 state.clear()
2150 2147
2151 2148
2152 2149 def hgaborthistedit(ui, repo):
2153 2150 state = histeditstate(repo)
2154 2151 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2155 2152 with repo.wlock() as wlock, repo.lock() as lock:
2156 2153 state.wlock = wlock
2157 2154 state.lock = lock
2158 2155 _aborthistedit(ui, repo, state, nobackup=nobackup)
2159 2156
2160 2157
2161 2158 def _edithisteditplan(ui, repo, state, rules):
2162 2159 state.read()
2163 2160 if not rules:
2164 2161 comment = geteditcomment(
2165 2162 ui, node.short(state.parentctxnode), node.short(state.topmost)
2166 2163 )
2167 2164 rules = ruleeditor(repo, ui, state.actions, comment)
2168 2165 else:
2169 2166 rules = _readfile(ui, rules)
2170 2167 actions = parserules(rules, state)
2171 2168 ctxs = [repo[act.node] for act in state.actions if act.node]
2172 2169 warnverifyactions(ui, repo, actions, state, ctxs)
2173 2170 state.actions = actions
2174 2171 state.write()
2175 2172
2176 2173
2177 2174 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2178 2175 outg = opts.get(b'outgoing')
2179 2176 rules = opts.get(b'commands', b'')
2180 2177 force = opts.get(b'force')
2181 2178
2182 2179 cmdutil.checkunfinished(repo)
2183 2180 cmdutil.bailifchanged(repo)
2184 2181
2185 2182 topmost = repo.dirstate.p1()
2186 2183 if outg:
2187 2184 if freeargs:
2188 2185 remote = freeargs[0]
2189 2186 else:
2190 2187 remote = None
2191 2188 root = findoutgoing(ui, repo, remote, force, opts)
2192 2189 else:
2193 2190 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2194 2191 if len(rr) != 1:
2195 2192 raise error.Abort(
2196 2193 _(
2197 2194 b'The specified revisions must have '
2198 2195 b'exactly one common root'
2199 2196 )
2200 2197 )
2201 2198 root = rr[0].node()
2202 2199
2203 2200 revs = between(repo, root, topmost, state.keep)
2204 2201 if not revs:
2205 2202 raise error.Abort(
2206 2203 _(b'%s is not an ancestor of working directory') % node.short(root)
2207 2204 )
2208 2205
2209 2206 ctxs = [repo[r] for r in revs]
2210 2207
2211 2208 wctx = repo[None]
2212 2209 # Please don't ask me why `ancestors` is this value. I figured it
2213 2210 # out with print-debugging, not by actually understanding what the
2214 2211 # merge code is doing. :(
2215 2212 ancs = [repo[b'.']]
2216 2213 # Sniff-test to make sure we won't collide with untracked files in
2217 2214 # the working directory. If we don't do this, we can get a
2218 2215 # collision after we've started histedit and backing out gets ugly
2219 2216 # for everyone, especially the user.
2220 2217 for c in [ctxs[0].p1()] + ctxs:
2221 2218 try:
2222 2219 mergemod.calculateupdates(
2223 2220 repo,
2224 2221 wctx,
2225 2222 c,
2226 2223 ancs,
2227 2224 # These parameters were determined by print-debugging
2228 2225 # what happens later on inside histedit.
2229 2226 branchmerge=False,
2230 2227 force=False,
2231 2228 acceptremote=False,
2232 2229 followcopies=False,
2233 2230 )
2234 2231 except error.Abort:
2235 2232 raise error.Abort(
2236 2233 _(
2237 2234 b"untracked files in working directory conflict with files in %s"
2238 2235 )
2239 2236 % c
2240 2237 )
2241 2238
2242 2239 if not rules:
2243 2240 comment = geteditcomment(ui, node.short(root), node.short(topmost))
2244 2241 actions = [pick(state, r) for r in revs]
2245 2242 rules = ruleeditor(repo, ui, actions, comment)
2246 2243 else:
2247 2244 rules = _readfile(ui, rules)
2248 2245 actions = parserules(rules, state)
2249 2246 warnverifyactions(ui, repo, actions, state, ctxs)
2250 2247
2251 2248 parentctxnode = repo[root].p1().node()
2252 2249
2253 2250 state.parentctxnode = parentctxnode
2254 2251 state.actions = actions
2255 2252 state.topmost = topmost
2256 2253 state.replacements = []
2257 2254
2258 2255 ui.log(
2259 2256 b"histedit",
2260 2257 b"%d actions to histedit\n",
2261 2258 len(actions),
2262 2259 histedit_num_actions=len(actions),
2263 2260 )
2264 2261
2265 2262 # Create a backup so we can always abort completely.
2266 2263 backupfile = None
2267 2264 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2268 2265 backupfile = repair.backupbundle(
2269 2266 repo, [parentctxnode], [topmost], root, b'histedit'
2270 2267 )
2271 2268 state.backupfile = backupfile
2272 2269
2273 2270
2274 2271 def _getsummary(ctx):
2275 2272 # a common pattern is to extract the summary but default to the empty
2276 2273 # string
2277 2274 summary = ctx.description() or b''
2278 2275 if summary:
2279 2276 summary = summary.splitlines()[0]
2280 2277 return summary
2281 2278
2282 2279
2283 2280 def bootstrapcontinue(ui, state, opts):
2284 2281 repo = state.repo
2285 2282
2286 2283 ms = mergemod.mergestate.read(repo)
2287 2284 mergeutil.checkunresolved(ms)
2288 2285
2289 2286 if state.actions:
2290 2287 actobj = state.actions.pop(0)
2291 2288
2292 2289 if _isdirtywc(repo):
2293 2290 actobj.continuedirty()
2294 2291 if _isdirtywc(repo):
2295 2292 abortdirty()
2296 2293
2297 2294 parentctx, replacements = actobj.continueclean()
2298 2295
2299 2296 state.parentctxnode = parentctx.node()
2300 2297 state.replacements.extend(replacements)
2301 2298
2302 2299 return state
2303 2300
2304 2301
2305 2302 def between(repo, old, new, keep):
2306 2303 """select and validate the set of revision to edit
2307 2304
2308 2305 When keep is false, the specified set can't have children."""
2309 2306 revs = repo.revs(b'%n::%n', old, new)
2310 2307 if revs and not keep:
2311 2308 rewriteutil.precheck(repo, revs, b'edit')
2312 2309 if repo.revs(b'(%ld) and merge()', revs):
2313 2310 raise error.Abort(_(b'cannot edit history that contains merges'))
2314 2311 return pycompat.maplist(repo.changelog.node, revs)
2315 2312
2316 2313
2317 2314 def ruleeditor(repo, ui, actions, editcomment=b""):
2318 2315 """open an editor to edit rules
2319 2316
2320 2317 rules are in the format [ [act, ctx], ...] like in state.rules
2321 2318 """
2322 2319 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2323 2320 newact = util.sortdict()
2324 2321 for act in actions:
2325 2322 ctx = repo[act.node]
2326 2323 summary = _getsummary(ctx)
2327 2324 fword = summary.split(b' ', 1)[0].lower()
2328 2325 added = False
2329 2326
2330 2327 # if it doesn't end with the special character '!' just skip this
2331 2328 if fword.endswith(b'!'):
2332 2329 fword = fword[:-1]
2333 2330 if fword in primaryactions | secondaryactions | tertiaryactions:
2334 2331 act.verb = fword
2335 2332 # get the target summary
2336 2333 tsum = summary[len(fword) + 1 :].lstrip()
2337 2334 # safe but slow: reverse iterate over the actions so we
2338 2335 # don't clash on two commits having the same summary
2339 2336 for na, l in reversed(list(pycompat.iteritems(newact))):
2340 2337 actx = repo[na.node]
2341 2338 asum = _getsummary(actx)
2342 2339 if asum == tsum:
2343 2340 added = True
2344 2341 l.append(act)
2345 2342 break
2346 2343
2347 2344 if not added:
2348 2345 newact[act] = []
2349 2346
2350 2347 # copy over and flatten the new list
2351 2348 actions = []
2352 2349 for na, l in pycompat.iteritems(newact):
2353 2350 actions.append(na)
2354 2351 actions += l
2355 2352
2356 2353 rules = b'\n'.join([act.torule() for act in actions])
2357 2354 rules += b'\n\n'
2358 2355 rules += editcomment
2359 2356 rules = ui.edit(
2360 2357 rules,
2361 2358 ui.username(),
2362 2359 {b'prefix': b'histedit'},
2363 2360 repopath=repo.path,
2364 2361 action=b'histedit',
2365 2362 )
2366 2363
2367 2364 # Save edit rules in .hg/histedit-last-edit.txt in case
2368 2365 # the user needs to ask for help after something
2369 2366 # surprising happens.
2370 2367 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2371 2368 f.write(rules)
2372 2369
2373 2370 return rules
2374 2371
2375 2372
2376 2373 def parserules(rules, state):
2377 2374 """Read the histedit rules string and return list of action objects """
2378 2375 rules = [
2379 2376 l
2380 2377 for l in (r.strip() for r in rules.splitlines())
2381 2378 if l and not l.startswith(b'#')
2382 2379 ]
2383 2380 actions = []
2384 2381 for r in rules:
2385 2382 if b' ' not in r:
2386 2383 raise error.ParseError(_(b'malformed line "%s"') % r)
2387 2384 verb, rest = r.split(b' ', 1)
2388 2385
2389 2386 if verb not in actiontable:
2390 2387 raise error.ParseError(_(b'unknown action "%s"') % verb)
2391 2388
2392 2389 action = actiontable[verb].fromrule(state, rest)
2393 2390 actions.append(action)
2394 2391 return actions
2395 2392
2396 2393
2397 2394 def warnverifyactions(ui, repo, actions, state, ctxs):
2398 2395 try:
2399 2396 verifyactions(actions, state, ctxs)
2400 2397 except error.ParseError:
2401 2398 if repo.vfs.exists(b'histedit-last-edit.txt'):
2402 2399 ui.warn(
2403 2400 _(
2404 2401 b'warning: histedit rules saved '
2405 2402 b'to: .hg/histedit-last-edit.txt\n'
2406 2403 )
2407 2404 )
2408 2405 raise
2409 2406
2410 2407
2411 2408 def verifyactions(actions, state, ctxs):
2412 2409 """Verify that there exists exactly one action per given changeset and
2413 2410 other constraints.
2414 2411
2415 2412 Will abort if there are to many or too few rules, a malformed rule,
2416 2413 or a rule on a changeset outside of the user-given range.
2417 2414 """
2418 2415 expected = set(c.node() for c in ctxs)
2419 2416 seen = set()
2420 2417 prev = None
2421 2418
2422 2419 if actions and actions[0].verb in [b'roll', b'fold']:
2423 2420 raise error.ParseError(
2424 2421 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2425 2422 )
2426 2423
2427 2424 for action in actions:
2428 2425 action.verify(prev, expected, seen)
2429 2426 prev = action
2430 2427 if action.node is not None:
2431 2428 seen.add(action.node)
2432 2429 missing = sorted(expected - seen) # sort to stabilize output
2433 2430
2434 2431 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2435 2432 if len(actions) == 0:
2436 2433 raise error.ParseError(
2437 2434 _(b'no rules provided'),
2438 2435 hint=_(b'use strip extension to remove commits'),
2439 2436 )
2440 2437
2441 2438 drops = [drop(state, n) for n in missing]
2442 2439 # put the in the beginning so they execute immediately and
2443 2440 # don't show in the edit-plan in the future
2444 2441 actions[:0] = drops
2445 2442 elif missing:
2446 2443 raise error.ParseError(
2447 2444 _(b'missing rules for changeset %s') % node.short(missing[0]),
2448 2445 hint=_(
2449 2446 b'use "drop %s" to discard, see also: '
2450 2447 b"'hg help -e histedit.config'"
2451 2448 )
2452 2449 % node.short(missing[0]),
2453 2450 )
2454 2451
2455 2452
2456 2453 def adjustreplacementsfrommarkers(repo, oldreplacements):
2457 2454 """Adjust replacements from obsolescence markers
2458 2455
2459 2456 Replacements structure is originally generated based on
2460 2457 histedit's state and does not account for changes that are
2461 2458 not recorded there. This function fixes that by adding
2462 2459 data read from obsolescence markers"""
2463 2460 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2464 2461 return oldreplacements
2465 2462
2466 2463 unfi = repo.unfiltered()
2467 2464 get_rev = unfi.changelog.index.get_rev
2468 2465 obsstore = repo.obsstore
2469 2466 newreplacements = list(oldreplacements)
2470 2467 oldsuccs = [r[1] for r in oldreplacements]
2471 2468 # successors that have already been added to succstocheck once
2472 2469 seensuccs = set().union(
2473 2470 *oldsuccs
2474 2471 ) # create a set from an iterable of tuples
2475 2472 succstocheck = list(seensuccs)
2476 2473 while succstocheck:
2477 2474 n = succstocheck.pop()
2478 2475 missing = get_rev(n) is None
2479 2476 markers = obsstore.successors.get(n, ())
2480 2477 if missing and not markers:
2481 2478 # dead end, mark it as such
2482 2479 newreplacements.append((n, ()))
2483 2480 for marker in markers:
2484 2481 nsuccs = marker[1]
2485 2482 newreplacements.append((n, nsuccs))
2486 2483 for nsucc in nsuccs:
2487 2484 if nsucc not in seensuccs:
2488 2485 seensuccs.add(nsucc)
2489 2486 succstocheck.append(nsucc)
2490 2487
2491 2488 return newreplacements
2492 2489
2493 2490
2494 2491 def processreplacement(state):
2495 2492 """process the list of replacements to return
2496 2493
2497 2494 1) the final mapping between original and created nodes
2498 2495 2) the list of temporary node created by histedit
2499 2496 3) the list of new commit created by histedit"""
2500 2497 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2501 2498 allsuccs = set()
2502 2499 replaced = set()
2503 2500 fullmapping = {}
2504 2501 # initialize basic set
2505 2502 # fullmapping records all operations recorded in replacement
2506 2503 for rep in replacements:
2507 2504 allsuccs.update(rep[1])
2508 2505 replaced.add(rep[0])
2509 2506 fullmapping.setdefault(rep[0], set()).update(rep[1])
2510 2507 new = allsuccs - replaced
2511 2508 tmpnodes = allsuccs & replaced
2512 2509 # Reduce content fullmapping into direct relation between original nodes
2513 2510 # and final node created during history edition
2514 2511 # Dropped changeset are replaced by an empty list
2515 2512 toproceed = set(fullmapping)
2516 2513 final = {}
2517 2514 while toproceed:
2518 2515 for x in list(toproceed):
2519 2516 succs = fullmapping[x]
2520 2517 for s in list(succs):
2521 2518 if s in toproceed:
2522 2519 # non final node with unknown closure
2523 2520 # We can't process this now
2524 2521 break
2525 2522 elif s in final:
2526 2523 # non final node, replace with closure
2527 2524 succs.remove(s)
2528 2525 succs.update(final[s])
2529 2526 else:
2530 2527 final[x] = succs
2531 2528 toproceed.remove(x)
2532 2529 # remove tmpnodes from final mapping
2533 2530 for n in tmpnodes:
2534 2531 del final[n]
2535 2532 # we expect all changes involved in final to exist in the repo
2536 2533 # turn `final` into list (topologically sorted)
2537 2534 get_rev = state.repo.changelog.index.get_rev
2538 2535 for prec, succs in final.items():
2539 2536 final[prec] = sorted(succs, key=get_rev)
2540 2537
2541 2538 # computed topmost element (necessary for bookmark)
2542 2539 if new:
2543 2540 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2544 2541 elif not final:
2545 2542 # Nothing rewritten at all. we won't need `newtopmost`
2546 2543 # It is the same as `oldtopmost` and `processreplacement` know it
2547 2544 newtopmost = None
2548 2545 else:
2549 2546 # every body died. The newtopmost is the parent of the root.
2550 2547 r = state.repo.changelog.rev
2551 2548 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2552 2549
2553 2550 return final, tmpnodes, new, newtopmost
2554 2551
2555 2552
2556 2553 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2557 2554 """Move bookmark from oldtopmost to newly created topmost
2558 2555
2559 2556 This is arguably a feature and we may only want that for the active
2560 2557 bookmark. But the behavior is kept compatible with the old version for now.
2561 2558 """
2562 2559 if not oldtopmost or not newtopmost:
2563 2560 return
2564 2561 oldbmarks = repo.nodebookmarks(oldtopmost)
2565 2562 if oldbmarks:
2566 2563 with repo.lock(), repo.transaction(b'histedit') as tr:
2567 2564 marks = repo._bookmarks
2568 2565 changes = []
2569 2566 for name in oldbmarks:
2570 2567 changes.append((name, newtopmost))
2571 2568 marks.applychanges(repo, tr, changes)
2572 2569
2573 2570
2574 2571 def cleanupnode(ui, repo, nodes, nobackup=False):
2575 2572 """strip a group of nodes from the repository
2576 2573
2577 2574 The set of node to strip may contains unknown nodes."""
2578 2575 with repo.lock():
2579 2576 # do not let filtering get in the way of the cleanse
2580 2577 # we should probably get rid of obsolescence marker created during the
2581 2578 # histedit, but we currently do not have such information.
2582 2579 repo = repo.unfiltered()
2583 2580 # Find all nodes that need to be stripped
2584 2581 # (we use %lr instead of %ln to silently ignore unknown items)
2585 2582 has_node = repo.changelog.index.has_node
2586 2583 nodes = sorted(n for n in nodes if has_node(n))
2587 2584 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2588 2585 if roots:
2589 2586 backup = not nobackup
2590 2587 repair.strip(ui, repo, roots, backup=backup)
2591 2588
2592 2589
2593 2590 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2594 2591 if isinstance(nodelist, bytes):
2595 2592 nodelist = [nodelist]
2596 2593 state = histeditstate(repo)
2597 2594 if state.inprogress():
2598 2595 state.read()
2599 2596 histedit_nodes = {
2600 2597 action.node for action in state.actions if action.node
2601 2598 }
2602 2599 common_nodes = histedit_nodes & set(nodelist)
2603 2600 if common_nodes:
2604 2601 raise error.Abort(
2605 2602 _(b"histedit in progress, can't strip %s")
2606 2603 % b', '.join(node.short(x) for x in common_nodes)
2607 2604 )
2608 2605 return orig(ui, repo, nodelist, *args, **kwargs)
2609 2606
2610 2607
2611 2608 extensions.wrapfunction(repair, b'strip', stripwrapper)
2612 2609
2613 2610
2614 2611 def summaryhook(ui, repo):
2615 2612 state = histeditstate(repo)
2616 2613 if not state.inprogress():
2617 2614 return
2618 2615 state.read()
2619 2616 if state.actions:
2620 2617 # i18n: column positioning for "hg summary"
2621 2618 ui.write(
2622 2619 _(b'hist: %s (histedit --continue)\n')
2623 2620 % (
2624 2621 ui.label(_(b'%d remaining'), b'histedit.remaining')
2625 2622 % len(state.actions)
2626 2623 )
2627 2624 )
2628 2625
2629 2626
2630 2627 def extsetup(ui):
2631 2628 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2632 2629 statemod.addunfinished(
2633 2630 b'histedit',
2634 2631 fname=b'histedit-state',
2635 2632 allowcommit=True,
2636 2633 continueflag=True,
2637 2634 abortfunc=hgaborthistedit,
2638 2635 )
General Comments 0
You need to be logged in to leave comments. Login now