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