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