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