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