##// END OF EJS Templates
chistedit: change in-progress message...
Jordi Gutiérrez Hermoso -
r42189:1f567a9e default
parent child Browse files
Show More
@@ -1,2272 +1,2272 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 copysource=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
952 952 # Curses requires setting the locale or it will default to the C
953 953 # locale. This sets the locale to the user's default system
954 954 # locale.
955 955 import locale
956 956 locale.setlocale(locale.LC_ALL, r'')
957 957 except ImportError:
958 958 curses = None
959 959
960 960 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
961 961 ACTION_LABELS = {
962 962 'fold': '^fold',
963 963 'roll': '^roll',
964 964 }
965 965
966 966 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
967 967
968 968 E_QUIT, E_HISTEDIT = 1, 2
969 969 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
970 970 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
971 971
972 972 KEYTABLE = {
973 973 'global': {
974 974 'h': 'next-action',
975 975 'KEY_RIGHT': 'next-action',
976 976 'l': 'prev-action',
977 977 'KEY_LEFT': 'prev-action',
978 978 'q': 'quit',
979 979 'c': 'histedit',
980 980 'C': 'histedit',
981 981 'v': 'showpatch',
982 982 '?': 'help',
983 983 },
984 984 MODE_RULES: {
985 985 'd': 'action-drop',
986 986 'e': 'action-edit',
987 987 'f': 'action-fold',
988 988 'm': 'action-mess',
989 989 'p': 'action-pick',
990 990 'r': 'action-roll',
991 991 ' ': 'select',
992 992 'j': 'down',
993 993 'k': 'up',
994 994 'KEY_DOWN': 'down',
995 995 'KEY_UP': 'up',
996 996 'J': 'move-down',
997 997 'K': 'move-up',
998 998 'KEY_NPAGE': 'move-down',
999 999 'KEY_PPAGE': 'move-up',
1000 1000 '0': 'goto', # Used for 0..9
1001 1001 },
1002 1002 MODE_PATCH: {
1003 1003 ' ': 'page-down',
1004 1004 'KEY_NPAGE': 'page-down',
1005 1005 'KEY_PPAGE': 'page-up',
1006 1006 'j': 'line-down',
1007 1007 'k': 'line-up',
1008 1008 'KEY_DOWN': 'line-down',
1009 1009 'KEY_UP': 'line-up',
1010 1010 'J': 'down',
1011 1011 'K': 'up',
1012 1012 },
1013 1013 MODE_HELP: {
1014 1014 },
1015 1015 }
1016 1016
1017 1017 def screen_size():
1018 1018 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
1019 1019
1020 1020 class histeditrule(object):
1021 1021 def __init__(self, ctx, pos, action='pick'):
1022 1022 self.ctx = ctx
1023 1023 self.action = action
1024 1024 self.origpos = pos
1025 1025 self.pos = pos
1026 1026 self.conflicts = []
1027 1027
1028 1028 def __str__(self):
1029 1029 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1030 1030 # Add a marker showing which patch they apply to, and also omit the
1031 1031 # description for 'roll' (since it will get discarded). Example display:
1032 1032 #
1033 1033 # #10 pick 316392:06a16c25c053 add option to skip tests
1034 1034 # #11 ^roll 316393:71313c964cc5
1035 1035 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1036 1036 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1037 1037 #
1038 1038 # The carets point to the changeset being folded into ("roll this
1039 1039 # changeset into the changeset above").
1040 1040 action = ACTION_LABELS.get(self.action, self.action)
1041 1041 h = self.ctx.hex()[0:12]
1042 1042 r = self.ctx.rev()
1043 1043 desc = self.ctx.description().splitlines()[0].strip()
1044 1044 if self.action == 'roll':
1045 1045 desc = ''
1046 1046 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1047 1047 self.origpos, action, r, h, desc)
1048 1048
1049 1049 def checkconflicts(self, other):
1050 1050 if other.pos > self.pos and other.origpos <= self.origpos:
1051 1051 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1052 1052 self.conflicts.append(other)
1053 1053 return self.conflicts
1054 1054
1055 1055 if other in self.conflicts:
1056 1056 self.conflicts.remove(other)
1057 1057 return self.conflicts
1058 1058
1059 1059 # ============ EVENTS ===============
1060 1060 def movecursor(state, oldpos, newpos):
1061 1061 '''Change the rule/changeset that the cursor is pointing to, regardless of
1062 1062 current mode (you can switch between patches from the view patch window).'''
1063 1063 state['pos'] = newpos
1064 1064
1065 1065 mode, _ = state['mode']
1066 1066 if mode == MODE_RULES:
1067 1067 # Scroll through the list by updating the view for MODE_RULES, so that
1068 1068 # even if we are not currently viewing the rules, switching back will
1069 1069 # result in the cursor's rule being visible.
1070 1070 modestate = state['modes'][MODE_RULES]
1071 1071 if newpos < modestate['line_offset']:
1072 1072 modestate['line_offset'] = newpos
1073 1073 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1074 1074 modestate['line_offset'] = newpos - state['page_height'] + 1
1075 1075
1076 1076 # Reset the patch view region to the top of the new patch.
1077 1077 state['modes'][MODE_PATCH]['line_offset'] = 0
1078 1078
1079 1079 def changemode(state, mode):
1080 1080 curmode, _ = state['mode']
1081 1081 state['mode'] = (mode, curmode)
1082 1082
1083 1083 def makeselection(state, pos):
1084 1084 state['selected'] = pos
1085 1085
1086 1086 def swap(state, oldpos, newpos):
1087 1087 """Swap two positions and calculate necessary conflicts in
1088 1088 O(|newpos-oldpos|) time"""
1089 1089
1090 1090 rules = state['rules']
1091 1091 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1092 1092
1093 1093 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1094 1094
1095 1095 # TODO: swap should not know about histeditrule's internals
1096 1096 rules[newpos].pos = newpos
1097 1097 rules[oldpos].pos = oldpos
1098 1098
1099 1099 start = min(oldpos, newpos)
1100 1100 end = max(oldpos, newpos)
1101 1101 for r in pycompat.xrange(start, end + 1):
1102 1102 rules[newpos].checkconflicts(rules[r])
1103 1103 rules[oldpos].checkconflicts(rules[r])
1104 1104
1105 1105 if state['selected']:
1106 1106 makeselection(state, newpos)
1107 1107
1108 1108 def changeaction(state, pos, action):
1109 1109 """Change the action state on the given position to the new action"""
1110 1110 rules = state['rules']
1111 1111 assert 0 <= pos < len(rules)
1112 1112 rules[pos].action = action
1113 1113
1114 1114 def cycleaction(state, pos, next=False):
1115 1115 """Changes the action state the next or the previous action from
1116 1116 the action list"""
1117 1117 rules = state['rules']
1118 1118 assert 0 <= pos < len(rules)
1119 1119 current = rules[pos].action
1120 1120
1121 1121 assert current in KEY_LIST
1122 1122
1123 1123 index = KEY_LIST.index(current)
1124 1124 if next:
1125 1125 index += 1
1126 1126 else:
1127 1127 index -= 1
1128 1128 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1129 1129
1130 1130 def changeview(state, delta, unit):
1131 1131 '''Change the region of whatever is being viewed (a patch or the list of
1132 1132 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1133 1133 mode, _ = state['mode']
1134 1134 if mode != MODE_PATCH:
1135 1135 return
1136 1136 mode_state = state['modes'][mode]
1137 1137 num_lines = len(patchcontents(state))
1138 1138 page_height = state['page_height']
1139 1139 unit = page_height if unit == 'page' else 1
1140 1140 num_pages = 1 + (num_lines - 1) / page_height
1141 1141 max_offset = (num_pages - 1) * page_height
1142 1142 newline = mode_state['line_offset'] + delta * unit
1143 1143 mode_state['line_offset'] = max(0, min(max_offset, newline))
1144 1144
1145 1145 def event(state, ch):
1146 1146 """Change state based on the current character input
1147 1147
1148 1148 This takes the current state and based on the current character input from
1149 1149 the user we change the state.
1150 1150 """
1151 1151 selected = state['selected']
1152 1152 oldpos = state['pos']
1153 1153 rules = state['rules']
1154 1154
1155 1155 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1156 1156 return E_RESIZE
1157 1157
1158 1158 lookup_ch = ch
1159 1159 if '0' <= ch <= '9':
1160 1160 lookup_ch = '0'
1161 1161
1162 1162 curmode, prevmode = state['mode']
1163 1163 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1164 1164 if action is None:
1165 1165 return
1166 1166 if action in ('down', 'move-down'):
1167 1167 newpos = min(oldpos + 1, len(rules) - 1)
1168 1168 movecursor(state, oldpos, newpos)
1169 1169 if selected is not None or action == 'move-down':
1170 1170 swap(state, oldpos, newpos)
1171 1171 elif action in ('up', 'move-up'):
1172 1172 newpos = max(0, oldpos - 1)
1173 1173 movecursor(state, oldpos, newpos)
1174 1174 if selected is not None or action == 'move-up':
1175 1175 swap(state, oldpos, newpos)
1176 1176 elif action == 'next-action':
1177 1177 cycleaction(state, oldpos, next=True)
1178 1178 elif action == 'prev-action':
1179 1179 cycleaction(state, oldpos, next=False)
1180 1180 elif action == 'select':
1181 1181 selected = oldpos if selected is None else None
1182 1182 makeselection(state, selected)
1183 1183 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1184 1184 newrule = next((r for r in rules if r.origpos == int(ch)))
1185 1185 movecursor(state, oldpos, newrule.pos)
1186 1186 if selected is not None:
1187 1187 swap(state, oldpos, newrule.pos)
1188 1188 elif action.startswith('action-'):
1189 1189 changeaction(state, oldpos, action[7:])
1190 1190 elif action == 'showpatch':
1191 1191 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1192 1192 elif action == 'help':
1193 1193 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1194 1194 elif action == 'quit':
1195 1195 return E_QUIT
1196 1196 elif action == 'histedit':
1197 1197 return E_HISTEDIT
1198 1198 elif action == 'page-down':
1199 1199 return E_PAGEDOWN
1200 1200 elif action == 'page-up':
1201 1201 return E_PAGEUP
1202 1202 elif action == 'line-down':
1203 1203 return E_LINEDOWN
1204 1204 elif action == 'line-up':
1205 1205 return E_LINEUP
1206 1206
1207 1207 def makecommands(rules):
1208 1208 """Returns a list of commands consumable by histedit --commands based on
1209 1209 our list of rules"""
1210 1210 commands = []
1211 1211 for rules in rules:
1212 1212 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1213 1213 return commands
1214 1214
1215 1215 def addln(win, y, x, line, color=None):
1216 1216 """Add a line to the given window left padding but 100% filled with
1217 1217 whitespace characters, so that the color appears on the whole line"""
1218 1218 maxy, maxx = win.getmaxyx()
1219 1219 length = maxx - 1 - x
1220 1220 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1221 1221 if y < 0:
1222 1222 y = maxy + y
1223 1223 if x < 0:
1224 1224 x = maxx + x
1225 1225 if color:
1226 1226 win.addstr(y, x, line, color)
1227 1227 else:
1228 1228 win.addstr(y, x, line)
1229 1229
1230 1230 def patchcontents(state):
1231 1231 repo = state['repo']
1232 1232 rule = state['rules'][state['pos']]
1233 1233 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1234 1234 'patch': True, 'verbose': True
1235 1235 }, buffered=True)
1236 1236 displayer.show(rule.ctx)
1237 1237 displayer.close()
1238 1238 return displayer.hunk[rule.ctx.rev()].splitlines()
1239 1239
1240 1240 def _chisteditmain(repo, rules, stdscr):
1241 1241 # initialize color pattern
1242 1242 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1243 1243 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1244 1244 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1245 1245 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1246 1246 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1247 1247
1248 1248 # don't display the cursor
1249 1249 try:
1250 1250 curses.curs_set(0)
1251 1251 except curses.error:
1252 1252 pass
1253 1253
1254 1254 def rendercommit(win, state):
1255 1255 """Renders the commit window that shows the log of the current selected
1256 1256 commit"""
1257 1257 pos = state['pos']
1258 1258 rules = state['rules']
1259 1259 rule = rules[pos]
1260 1260
1261 1261 ctx = rule.ctx
1262 1262 win.box()
1263 1263
1264 1264 maxy, maxx = win.getmaxyx()
1265 1265 length = maxx - 3
1266 1266
1267 1267 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1268 1268 win.addstr(1, 1, line[:length])
1269 1269
1270 1270 line = "user: {0}".format(ctx.user())
1271 1271 win.addstr(2, 1, line[:length])
1272 1272
1273 1273 bms = repo.nodebookmarks(ctx.node())
1274 1274 line = "bookmark: {0}".format(' '.join(bms))
1275 1275 win.addstr(3, 1, line[:length])
1276 1276
1277 1277 line = "files: {0}".format(','.join(ctx.files()))
1278 1278 win.addstr(4, 1, line[:length])
1279 1279
1280 1280 line = "summary: {0}".format(ctx.description().splitlines()[0])
1281 1281 win.addstr(5, 1, line[:length])
1282 1282
1283 1283 conflicts = rule.conflicts
1284 1284 if len(conflicts) > 0:
1285 1285 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1286 1286 conflictstr = "changed files overlap with {0}".format(conflictstr)
1287 1287 else:
1288 1288 conflictstr = 'no overlap'
1289 1289
1290 1290 win.addstr(6, 1, conflictstr[:length])
1291 1291 win.noutrefresh()
1292 1292
1293 1293 def helplines(mode):
1294 1294 if mode == MODE_PATCH:
1295 1295 help = """\
1296 1296 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1297 1297 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1298 1298 """
1299 1299 else:
1300 1300 help = """\
1301 1301 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1302 1302 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1303 1303 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1304 1304 """
1305 1305 return help.splitlines()
1306 1306
1307 1307 def renderhelp(win, state):
1308 1308 maxy, maxx = win.getmaxyx()
1309 1309 mode, _ = state['mode']
1310 1310 for y, line in enumerate(helplines(mode)):
1311 1311 if y >= maxy:
1312 1312 break
1313 1313 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1314 1314 win.noutrefresh()
1315 1315
1316 1316 def renderrules(rulesscr, state):
1317 1317 rules = state['rules']
1318 1318 pos = state['pos']
1319 1319 selected = state['selected']
1320 1320 start = state['modes'][MODE_RULES]['line_offset']
1321 1321
1322 1322 conflicts = [r.ctx for r in rules if r.conflicts]
1323 1323 if len(conflicts) > 0:
1324 1324 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1325 1325 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1326 1326
1327 1327 for y, rule in enumerate(rules[start:]):
1328 1328 if y >= state['page_height']:
1329 1329 break
1330 1330 if len(rule.conflicts) > 0:
1331 1331 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1332 1332 else:
1333 1333 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1334 1334 if y + start == selected:
1335 1335 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1336 1336 elif y + start == pos:
1337 1337 addln(rulesscr, y, 2, rule,
1338 1338 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
1339 1339 else:
1340 1340 addln(rulesscr, y, 2, rule)
1341 1341 rulesscr.noutrefresh()
1342 1342
1343 1343 def renderstring(win, state, output):
1344 1344 maxy, maxx = win.getmaxyx()
1345 1345 length = min(maxy - 1, len(output))
1346 1346 for y in range(0, length):
1347 1347 win.addstr(y, 0, output[y])
1348 1348 win.noutrefresh()
1349 1349
1350 1350 def renderpatch(win, state):
1351 1351 start = state['modes'][MODE_PATCH]['line_offset']
1352 1352 renderstring(win, state, patchcontents(state)[start:])
1353 1353
1354 1354 def layout(mode):
1355 1355 maxy, maxx = stdscr.getmaxyx()
1356 1356 helplen = len(helplines(mode))
1357 1357 return {
1358 1358 'commit': (8, maxx),
1359 1359 'help': (helplen, maxx),
1360 1360 'main': (maxy - helplen - 8, maxx),
1361 1361 }
1362 1362
1363 1363 def drawvertwin(size, y, x):
1364 1364 win = curses.newwin(size[0], size[1], y, x)
1365 1365 y += size[0]
1366 1366 return win, y, x
1367 1367
1368 1368 state = {
1369 1369 'pos': 0,
1370 1370 'rules': rules,
1371 1371 'selected': None,
1372 1372 'mode': (MODE_INIT, MODE_INIT),
1373 1373 'page_height': None,
1374 1374 'modes': {
1375 1375 MODE_RULES: {
1376 1376 'line_offset': 0,
1377 1377 },
1378 1378 MODE_PATCH: {
1379 1379 'line_offset': 0,
1380 1380 }
1381 1381 },
1382 1382 'repo': repo,
1383 1383 }
1384 1384
1385 1385 # eventloop
1386 1386 ch = None
1387 1387 stdscr.clear()
1388 1388 stdscr.refresh()
1389 1389 while True:
1390 1390 try:
1391 1391 oldmode, _ = state['mode']
1392 1392 if oldmode == MODE_INIT:
1393 1393 changemode(state, MODE_RULES)
1394 1394 e = event(state, ch)
1395 1395
1396 1396 if e == E_QUIT:
1397 1397 return False
1398 1398 if e == E_HISTEDIT:
1399 1399 return state['rules']
1400 1400 else:
1401 1401 if e == E_RESIZE:
1402 1402 size = screen_size()
1403 1403 if size != stdscr.getmaxyx():
1404 1404 curses.resizeterm(*size)
1405 1405
1406 1406 curmode, _ = state['mode']
1407 1407 sizes = layout(curmode)
1408 1408 if curmode != oldmode:
1409 1409 state['page_height'] = sizes['main'][0]
1410 1410 # Adjust the view to fit the current screen size.
1411 1411 movecursor(state, state['pos'], state['pos'])
1412 1412
1413 1413 # Pack the windows against the top, each pane spread across the
1414 1414 # full width of the screen.
1415 1415 y, x = (0, 0)
1416 1416 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1417 1417 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1418 1418 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1419 1419
1420 1420 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1421 1421 if e == E_PAGEDOWN:
1422 1422 changeview(state, +1, 'page')
1423 1423 elif e == E_PAGEUP:
1424 1424 changeview(state, -1, 'page')
1425 1425 elif e == E_LINEDOWN:
1426 1426 changeview(state, +1, 'line')
1427 1427 elif e == E_LINEUP:
1428 1428 changeview(state, -1, 'line')
1429 1429
1430 1430 # start rendering
1431 1431 commitwin.erase()
1432 1432 helpwin.erase()
1433 1433 mainwin.erase()
1434 1434 if curmode == MODE_PATCH:
1435 1435 renderpatch(mainwin, state)
1436 1436 elif curmode == MODE_HELP:
1437 1437 renderstring(mainwin, state, __doc__.strip().splitlines())
1438 1438 else:
1439 1439 renderrules(mainwin, state)
1440 1440 rendercommit(commitwin, state)
1441 1441 renderhelp(helpwin, state)
1442 1442 curses.doupdate()
1443 1443 # done rendering
1444 1444 ch = stdscr.getkey()
1445 1445 except curses.error:
1446 1446 pass
1447 1447
1448 1448 def _chistedit(ui, repo, *freeargs, **opts):
1449 1449 """interactively edit changeset history via a curses interface
1450 1450
1451 1451 Provides a ncurses interface to histedit. Press ? in chistedit mode
1452 1452 to see an extensive help. Requires python-curses to be installed."""
1453 1453
1454 1454 if curses is None:
1455 1455 raise error.Abort(_("Python curses library required"))
1456 1456
1457 1457 # disable color
1458 1458 ui._colormode = None
1459 1459
1460 1460 try:
1461 1461 keep = opts.get('keep')
1462 1462 revs = opts.get('rev', [])[:]
1463 1463 cmdutil.checkunfinished(repo)
1464 1464 cmdutil.bailifchanged(repo)
1465 1465
1466 1466 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1467 1467 raise error.Abort(_('history edit already in progress, try '
1468 1468 '--continue or --abort'))
1469 1469 revs.extend(freeargs)
1470 1470 if not revs:
1471 1471 defaultrev = destutil.desthistedit(ui, repo)
1472 1472 if defaultrev is not None:
1473 1473 revs.append(defaultrev)
1474 1474 if len(revs) != 1:
1475 1475 raise error.Abort(
1476 1476 _('histedit requires exactly one ancestor revision'))
1477 1477
1478 1478 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1479 1479 if len(rr) != 1:
1480 1480 raise error.Abort(_('The specified revisions must have '
1481 1481 'exactly one common root'))
1482 1482 root = rr[0].node()
1483 1483
1484 1484 topmost = repo.dirstate.p1()
1485 1485 revs = between(repo, root, topmost, keep)
1486 1486 if not revs:
1487 1487 raise error.Abort(_('%s is not an ancestor of working directory') %
1488 1488 node.short(root))
1489 1489
1490 1490 ctxs = []
1491 1491 for i, r in enumerate(revs):
1492 1492 ctxs.append(histeditrule(repo[r], i))
1493 1493 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1494 1494 curses.echo()
1495 1495 curses.endwin()
1496 1496 if rc is False:
1497 1497 ui.write(_("histedit aborted\n"))
1498 1498 return 0
1499 1499 if type(rc) is list:
1500 ui.status(_("running histedit\n"))
1500 ui.status(_("performing changes\n"))
1501 1501 rules = makecommands(rc)
1502 1502 filename = repo.vfs.join('chistedit')
1503 1503 with open(filename, 'w+') as fp:
1504 1504 for r in rules:
1505 1505 fp.write(r)
1506 1506 opts['commands'] = filename
1507 1507 return _texthistedit(ui, repo, *freeargs, **opts)
1508 1508 except KeyboardInterrupt:
1509 1509 pass
1510 1510 return -1
1511 1511
1512 1512 @command('histedit',
1513 1513 [('', 'commands', '',
1514 1514 _('read history edits from the specified file'), _('FILE')),
1515 1515 ('c', 'continue', False, _('continue an edit already in progress')),
1516 1516 ('', 'edit-plan', False, _('edit remaining actions list')),
1517 1517 ('k', 'keep', False,
1518 1518 _("don't strip old nodes after edit is complete")),
1519 1519 ('', 'abort', False, _('abort an edit in progress')),
1520 1520 ('o', 'outgoing', False, _('changesets not found in destination')),
1521 1521 ('f', 'force', False,
1522 1522 _('force outgoing even for unrelated repositories')),
1523 1523 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1524 1524 cmdutil.formatteropts,
1525 1525 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1526 1526 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1527 1527 def histedit(ui, repo, *freeargs, **opts):
1528 1528 """interactively edit changeset history
1529 1529
1530 1530 This command lets you edit a linear series of changesets (up to
1531 1531 and including the working directory, which should be clean).
1532 1532 You can:
1533 1533
1534 1534 - `pick` to [re]order a changeset
1535 1535
1536 1536 - `drop` to omit changeset
1537 1537
1538 1538 - `mess` to reword the changeset commit message
1539 1539
1540 1540 - `fold` to combine it with the preceding changeset (using the later date)
1541 1541
1542 1542 - `roll` like fold, but discarding this commit's description and date
1543 1543
1544 1544 - `edit` to edit this changeset (preserving date)
1545 1545
1546 1546 - `base` to checkout changeset and apply further changesets from there
1547 1547
1548 1548 There are a number of ways to select the root changeset:
1549 1549
1550 1550 - Specify ANCESTOR directly
1551 1551
1552 1552 - Use --outgoing -- it will be the first linear changeset not
1553 1553 included in destination. (See :hg:`help config.paths.default-push`)
1554 1554
1555 1555 - Otherwise, the value from the "histedit.defaultrev" config option
1556 1556 is used as a revset to select the base revision when ANCESTOR is not
1557 1557 specified. The first revision returned by the revset is used. By
1558 1558 default, this selects the editable history that is unique to the
1559 1559 ancestry of the working directory.
1560 1560
1561 1561 .. container:: verbose
1562 1562
1563 1563 If you use --outgoing, this command will abort if there are ambiguous
1564 1564 outgoing revisions. For example, if there are multiple branches
1565 1565 containing outgoing revisions.
1566 1566
1567 1567 Use "min(outgoing() and ::.)" or similar revset specification
1568 1568 instead of --outgoing to specify edit target revision exactly in
1569 1569 such ambiguous situation. See :hg:`help revsets` for detail about
1570 1570 selecting revisions.
1571 1571
1572 1572 .. container:: verbose
1573 1573
1574 1574 Examples:
1575 1575
1576 1576 - A number of changes have been made.
1577 1577 Revision 3 is no longer needed.
1578 1578
1579 1579 Start history editing from revision 3::
1580 1580
1581 1581 hg histedit -r 3
1582 1582
1583 1583 An editor opens, containing the list of revisions,
1584 1584 with specific actions specified::
1585 1585
1586 1586 pick 5339bf82f0ca 3 Zworgle the foobar
1587 1587 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1588 1588 pick 0a9639fcda9d 5 Morgify the cromulancy
1589 1589
1590 1590 Additional information about the possible actions
1591 1591 to take appears below the list of revisions.
1592 1592
1593 1593 To remove revision 3 from the history,
1594 1594 its action (at the beginning of the relevant line)
1595 1595 is changed to 'drop'::
1596 1596
1597 1597 drop 5339bf82f0ca 3 Zworgle the foobar
1598 1598 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1599 1599 pick 0a9639fcda9d 5 Morgify the cromulancy
1600 1600
1601 1601 - A number of changes have been made.
1602 1602 Revision 2 and 4 need to be swapped.
1603 1603
1604 1604 Start history editing from revision 2::
1605 1605
1606 1606 hg histedit -r 2
1607 1607
1608 1608 An editor opens, containing the list of revisions,
1609 1609 with specific actions specified::
1610 1610
1611 1611 pick 252a1af424ad 2 Blorb a morgwazzle
1612 1612 pick 5339bf82f0ca 3 Zworgle the foobar
1613 1613 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1614 1614
1615 1615 To swap revision 2 and 4, its lines are swapped
1616 1616 in the editor::
1617 1617
1618 1618 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1619 1619 pick 5339bf82f0ca 3 Zworgle the foobar
1620 1620 pick 252a1af424ad 2 Blorb a morgwazzle
1621 1621
1622 1622 Returns 0 on success, 1 if user intervention is required (not only
1623 1623 for intentional "edit" command, but also for resolving unexpected
1624 1624 conflicts).
1625 1625 """
1626 1626 # kludge: _chistedit only works for starting an edit, not aborting
1627 1627 # or continuing, so fall back to regular _texthistedit for those
1628 1628 # operations.
1629 1629 if ui.interface('histedit') == 'curses' and _getgoal(
1630 1630 pycompat.byteskwargs(opts)) == goalnew:
1631 1631 return _chistedit(ui, repo, *freeargs, **opts)
1632 1632 return _texthistedit(ui, repo, *freeargs, **opts)
1633 1633
1634 1634 def _texthistedit(ui, repo, *freeargs, **opts):
1635 1635 state = histeditstate(repo)
1636 1636 with repo.wlock() as wlock, repo.lock() as lock:
1637 1637 state.wlock = wlock
1638 1638 state.lock = lock
1639 1639 _histedit(ui, repo, state, *freeargs, **opts)
1640 1640
1641 1641 goalcontinue = 'continue'
1642 1642 goalabort = 'abort'
1643 1643 goaleditplan = 'edit-plan'
1644 1644 goalnew = 'new'
1645 1645
1646 1646 def _getgoal(opts):
1647 1647 if opts.get(b'continue'):
1648 1648 return goalcontinue
1649 1649 if opts.get(b'abort'):
1650 1650 return goalabort
1651 1651 if opts.get(b'edit_plan'):
1652 1652 return goaleditplan
1653 1653 return goalnew
1654 1654
1655 1655 def _readfile(ui, path):
1656 1656 if path == '-':
1657 1657 with ui.timeblockedsection('histedit'):
1658 1658 return ui.fin.read()
1659 1659 else:
1660 1660 with open(path, 'rb') as f:
1661 1661 return f.read()
1662 1662
1663 1663 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1664 1664 # TODO only abort if we try to histedit mq patches, not just
1665 1665 # blanket if mq patches are applied somewhere
1666 1666 mq = getattr(repo, 'mq', None)
1667 1667 if mq and mq.applied:
1668 1668 raise error.Abort(_('source has mq patches applied'))
1669 1669
1670 1670 # basic argument incompatibility processing
1671 1671 outg = opts.get('outgoing')
1672 1672 editplan = opts.get('edit_plan')
1673 1673 abort = opts.get('abort')
1674 1674 force = opts.get('force')
1675 1675 if force and not outg:
1676 1676 raise error.Abort(_('--force only allowed with --outgoing'))
1677 1677 if goal == 'continue':
1678 1678 if any((outg, abort, revs, freeargs, rules, editplan)):
1679 1679 raise error.Abort(_('no arguments allowed with --continue'))
1680 1680 elif goal == 'abort':
1681 1681 if any((outg, revs, freeargs, rules, editplan)):
1682 1682 raise error.Abort(_('no arguments allowed with --abort'))
1683 1683 elif goal == 'edit-plan':
1684 1684 if any((outg, revs, freeargs)):
1685 1685 raise error.Abort(_('only --commands argument allowed with '
1686 1686 '--edit-plan'))
1687 1687 else:
1688 1688 if state.inprogress():
1689 1689 raise error.Abort(_('history edit already in progress, try '
1690 1690 '--continue or --abort'))
1691 1691 if outg:
1692 1692 if revs:
1693 1693 raise error.Abort(_('no revisions allowed with --outgoing'))
1694 1694 if len(freeargs) > 1:
1695 1695 raise error.Abort(
1696 1696 _('only one repo argument allowed with --outgoing'))
1697 1697 else:
1698 1698 revs.extend(freeargs)
1699 1699 if len(revs) == 0:
1700 1700 defaultrev = destutil.desthistedit(ui, repo)
1701 1701 if defaultrev is not None:
1702 1702 revs.append(defaultrev)
1703 1703
1704 1704 if len(revs) != 1:
1705 1705 raise error.Abort(
1706 1706 _('histedit requires exactly one ancestor revision'))
1707 1707
1708 1708 def _histedit(ui, repo, state, *freeargs, **opts):
1709 1709 opts = pycompat.byteskwargs(opts)
1710 1710 fm = ui.formatter('histedit', opts)
1711 1711 fm.startitem()
1712 1712 goal = _getgoal(opts)
1713 1713 revs = opts.get('rev', [])
1714 1714 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1715 1715 rules = opts.get('commands', '')
1716 1716 state.keep = opts.get('keep', False)
1717 1717
1718 1718 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1719 1719
1720 1720 hastags = False
1721 1721 if revs:
1722 1722 revs = scmutil.revrange(repo, revs)
1723 1723 ctxs = [repo[rev] for rev in revs]
1724 1724 for ctx in ctxs:
1725 1725 tags = [tag for tag in ctx.tags() if tag != 'tip']
1726 1726 if not hastags:
1727 1727 hastags = len(tags)
1728 1728 if hastags:
1729 1729 if ui.promptchoice(_('warning: tags associated with the given'
1730 1730 ' changeset will be lost after histedit.\n'
1731 1731 'do you want to continue (yN)? $$ &Yes $$ &No'),
1732 1732 default=1):
1733 1733 raise error.Abort(_('histedit cancelled\n'))
1734 1734 # rebuild state
1735 1735 if goal == goalcontinue:
1736 1736 state.read()
1737 1737 state = bootstrapcontinue(ui, state, opts)
1738 1738 elif goal == goaleditplan:
1739 1739 _edithisteditplan(ui, repo, state, rules)
1740 1740 return
1741 1741 elif goal == goalabort:
1742 1742 _aborthistedit(ui, repo, state, nobackup=nobackup)
1743 1743 return
1744 1744 else:
1745 1745 # goal == goalnew
1746 1746 _newhistedit(ui, repo, state, revs, freeargs, opts)
1747 1747
1748 1748 _continuehistedit(ui, repo, state)
1749 1749 _finishhistedit(ui, repo, state, fm)
1750 1750 fm.end()
1751 1751
1752 1752 def _continuehistedit(ui, repo, state):
1753 1753 """This function runs after either:
1754 1754 - bootstrapcontinue (if the goal is 'continue')
1755 1755 - _newhistedit (if the goal is 'new')
1756 1756 """
1757 1757 # preprocess rules so that we can hide inner folds from the user
1758 1758 # and only show one editor
1759 1759 actions = state.actions[:]
1760 1760 for idx, (action, nextact) in enumerate(
1761 1761 zip(actions, actions[1:] + [None])):
1762 1762 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1763 1763 state.actions[idx].__class__ = _multifold
1764 1764
1765 1765 # Force an initial state file write, so the user can run --abort/continue
1766 1766 # even if there's an exception before the first transaction serialize.
1767 1767 state.write()
1768 1768
1769 1769 tr = None
1770 1770 # Don't use singletransaction by default since it rolls the entire
1771 1771 # transaction back if an unexpected exception happens (like a
1772 1772 # pretxncommit hook throws, or the user aborts the commit msg editor).
1773 1773 if ui.configbool("histedit", "singletransaction"):
1774 1774 # Don't use a 'with' for the transaction, since actions may close
1775 1775 # and reopen a transaction. For example, if the action executes an
1776 1776 # external process it may choose to commit the transaction first.
1777 1777 tr = repo.transaction('histedit')
1778 1778 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1779 1779 total=len(state.actions))
1780 1780 with progress, util.acceptintervention(tr):
1781 1781 while state.actions:
1782 1782 state.write(tr=tr)
1783 1783 actobj = state.actions[0]
1784 1784 progress.increment(item=actobj.torule())
1785 1785 ui.debug('histedit: processing %s %s\n' % (actobj.verb,
1786 1786 actobj.torule()))
1787 1787 parentctx, replacement_ = actobj.run()
1788 1788 state.parentctxnode = parentctx.node()
1789 1789 state.replacements.extend(replacement_)
1790 1790 state.actions.pop(0)
1791 1791
1792 1792 state.write()
1793 1793
1794 1794 def _finishhistedit(ui, repo, state, fm):
1795 1795 """This action runs when histedit is finishing its session"""
1796 1796 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1797 1797
1798 1798 mapping, tmpnodes, created, ntm = processreplacement(state)
1799 1799 if mapping:
1800 1800 for prec, succs in mapping.iteritems():
1801 1801 if not succs:
1802 1802 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1803 1803 else:
1804 1804 ui.debug('histedit: %s is replaced by %s\n' % (
1805 1805 node.short(prec), node.short(succs[0])))
1806 1806 if len(succs) > 1:
1807 1807 m = 'histedit: %s'
1808 1808 for n in succs[1:]:
1809 1809 ui.debug(m % node.short(n))
1810 1810
1811 1811 if not state.keep:
1812 1812 if mapping:
1813 1813 movetopmostbookmarks(repo, state.topmost, ntm)
1814 1814 # TODO update mq state
1815 1815 else:
1816 1816 mapping = {}
1817 1817
1818 1818 for n in tmpnodes:
1819 1819 if n in repo:
1820 1820 mapping[n] = ()
1821 1821
1822 1822 # remove entries about unknown nodes
1823 1823 nodemap = repo.unfiltered().changelog.nodemap
1824 1824 mapping = {k: v for k, v in mapping.items()
1825 1825 if k in nodemap and all(n in nodemap for n in v)}
1826 1826 scmutil.cleanupnodes(repo, mapping, 'histedit')
1827 1827 hf = fm.hexfunc
1828 1828 fl = fm.formatlist
1829 1829 fd = fm.formatdict
1830 1830 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1831 1831 for oldn, newn in mapping.iteritems()},
1832 1832 key="oldnode", value="newnodes")
1833 1833 fm.data(nodechanges=nodechanges)
1834 1834
1835 1835 state.clear()
1836 1836 if os.path.exists(repo.sjoin('undo')):
1837 1837 os.unlink(repo.sjoin('undo'))
1838 1838 if repo.vfs.exists('histedit-last-edit.txt'):
1839 1839 repo.vfs.unlink('histedit-last-edit.txt')
1840 1840
1841 1841 def _aborthistedit(ui, repo, state, nobackup=False):
1842 1842 try:
1843 1843 state.read()
1844 1844 __, leafs, tmpnodes, __ = processreplacement(state)
1845 1845 ui.debug('restore wc to old parent %s\n'
1846 1846 % node.short(state.topmost))
1847 1847
1848 1848 # Recover our old commits if necessary
1849 1849 if not state.topmost in repo and state.backupfile:
1850 1850 backupfile = repo.vfs.join(state.backupfile)
1851 1851 f = hg.openpath(ui, backupfile)
1852 1852 gen = exchange.readbundle(ui, f, backupfile)
1853 1853 with repo.transaction('histedit.abort') as tr:
1854 1854 bundle2.applybundle(repo, gen, tr, source='histedit',
1855 1855 url='bundle:' + backupfile)
1856 1856
1857 1857 os.remove(backupfile)
1858 1858
1859 1859 # check whether we should update away
1860 1860 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1861 1861 state.parentctxnode, leafs | tmpnodes):
1862 1862 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1863 1863 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1864 1864 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1865 1865 except Exception:
1866 1866 if state.inprogress():
1867 1867 ui.warn(_('warning: encountered an exception during histedit '
1868 1868 '--abort; the repository may not have been completely '
1869 1869 'cleaned up\n'))
1870 1870 raise
1871 1871 finally:
1872 1872 state.clear()
1873 1873
1874 1874 def _edithisteditplan(ui, repo, state, rules):
1875 1875 state.read()
1876 1876 if not rules:
1877 1877 comment = geteditcomment(ui,
1878 1878 node.short(state.parentctxnode),
1879 1879 node.short(state.topmost))
1880 1880 rules = ruleeditor(repo, ui, state.actions, comment)
1881 1881 else:
1882 1882 rules = _readfile(ui, rules)
1883 1883 actions = parserules(rules, state)
1884 1884 ctxs = [repo[act.node]
1885 1885 for act in state.actions if act.node]
1886 1886 warnverifyactions(ui, repo, actions, state, ctxs)
1887 1887 state.actions = actions
1888 1888 state.write()
1889 1889
1890 1890 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1891 1891 outg = opts.get('outgoing')
1892 1892 rules = opts.get('commands', '')
1893 1893 force = opts.get('force')
1894 1894
1895 1895 cmdutil.checkunfinished(repo)
1896 1896 cmdutil.bailifchanged(repo)
1897 1897
1898 1898 topmost = repo.dirstate.p1()
1899 1899 if outg:
1900 1900 if freeargs:
1901 1901 remote = freeargs[0]
1902 1902 else:
1903 1903 remote = None
1904 1904 root = findoutgoing(ui, repo, remote, force, opts)
1905 1905 else:
1906 1906 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1907 1907 if len(rr) != 1:
1908 1908 raise error.Abort(_('The specified revisions must have '
1909 1909 'exactly one common root'))
1910 1910 root = rr[0].node()
1911 1911
1912 1912 revs = between(repo, root, topmost, state.keep)
1913 1913 if not revs:
1914 1914 raise error.Abort(_('%s is not an ancestor of working directory') %
1915 1915 node.short(root))
1916 1916
1917 1917 ctxs = [repo[r] for r in revs]
1918 1918 if not rules:
1919 1919 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1920 1920 actions = [pick(state, r) for r in revs]
1921 1921 rules = ruleeditor(repo, ui, actions, comment)
1922 1922 else:
1923 1923 rules = _readfile(ui, rules)
1924 1924 actions = parserules(rules, state)
1925 1925 warnverifyactions(ui, repo, actions, state, ctxs)
1926 1926
1927 1927 parentctxnode = repo[root].p1().node()
1928 1928
1929 1929 state.parentctxnode = parentctxnode
1930 1930 state.actions = actions
1931 1931 state.topmost = topmost
1932 1932 state.replacements = []
1933 1933
1934 1934 ui.log("histedit", "%d actions to histedit\n", len(actions),
1935 1935 histedit_num_actions=len(actions))
1936 1936
1937 1937 # Create a backup so we can always abort completely.
1938 1938 backupfile = None
1939 1939 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1940 1940 backupfile = repair.backupbundle(repo, [parentctxnode],
1941 1941 [topmost], root, 'histedit')
1942 1942 state.backupfile = backupfile
1943 1943
1944 1944 def _getsummary(ctx):
1945 1945 # a common pattern is to extract the summary but default to the empty
1946 1946 # string
1947 1947 summary = ctx.description() or ''
1948 1948 if summary:
1949 1949 summary = summary.splitlines()[0]
1950 1950 return summary
1951 1951
1952 1952 def bootstrapcontinue(ui, state, opts):
1953 1953 repo = state.repo
1954 1954
1955 1955 ms = mergemod.mergestate.read(repo)
1956 1956 mergeutil.checkunresolved(ms)
1957 1957
1958 1958 if state.actions:
1959 1959 actobj = state.actions.pop(0)
1960 1960
1961 1961 if _isdirtywc(repo):
1962 1962 actobj.continuedirty()
1963 1963 if _isdirtywc(repo):
1964 1964 abortdirty()
1965 1965
1966 1966 parentctx, replacements = actobj.continueclean()
1967 1967
1968 1968 state.parentctxnode = parentctx.node()
1969 1969 state.replacements.extend(replacements)
1970 1970
1971 1971 return state
1972 1972
1973 1973 def between(repo, old, new, keep):
1974 1974 """select and validate the set of revision to edit
1975 1975
1976 1976 When keep is false, the specified set can't have children."""
1977 1977 revs = repo.revs('%n::%n', old, new)
1978 1978 if revs and not keep:
1979 1979 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1980 1980 repo.revs('(%ld::) - (%ld)', revs, revs)):
1981 1981 raise error.Abort(_('can only histedit a changeset together '
1982 1982 'with all its descendants'))
1983 1983 if repo.revs('(%ld) and merge()', revs):
1984 1984 raise error.Abort(_('cannot edit history that contains merges'))
1985 1985 root = repo[revs.first()] # list is already sorted by repo.revs()
1986 1986 if not root.mutable():
1987 1987 raise error.Abort(_('cannot edit public changeset: %s') % root,
1988 1988 hint=_("see 'hg help phases' for details"))
1989 1989 return pycompat.maplist(repo.changelog.node, revs)
1990 1990
1991 1991 def ruleeditor(repo, ui, actions, editcomment=""):
1992 1992 """open an editor to edit rules
1993 1993
1994 1994 rules are in the format [ [act, ctx], ...] like in state.rules
1995 1995 """
1996 1996 if repo.ui.configbool("experimental", "histedit.autoverb"):
1997 1997 newact = util.sortdict()
1998 1998 for act in actions:
1999 1999 ctx = repo[act.node]
2000 2000 summary = _getsummary(ctx)
2001 2001 fword = summary.split(' ', 1)[0].lower()
2002 2002 added = False
2003 2003
2004 2004 # if it doesn't end with the special character '!' just skip this
2005 2005 if fword.endswith('!'):
2006 2006 fword = fword[:-1]
2007 2007 if fword in primaryactions | secondaryactions | tertiaryactions:
2008 2008 act.verb = fword
2009 2009 # get the target summary
2010 2010 tsum = summary[len(fword) + 1:].lstrip()
2011 2011 # safe but slow: reverse iterate over the actions so we
2012 2012 # don't clash on two commits having the same summary
2013 2013 for na, l in reversed(list(newact.iteritems())):
2014 2014 actx = repo[na.node]
2015 2015 asum = _getsummary(actx)
2016 2016 if asum == tsum:
2017 2017 added = True
2018 2018 l.append(act)
2019 2019 break
2020 2020
2021 2021 if not added:
2022 2022 newact[act] = []
2023 2023
2024 2024 # copy over and flatten the new list
2025 2025 actions = []
2026 2026 for na, l in newact.iteritems():
2027 2027 actions.append(na)
2028 2028 actions += l
2029 2029
2030 2030 rules = '\n'.join([act.torule() for act in actions])
2031 2031 rules += '\n\n'
2032 2032 rules += editcomment
2033 2033 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2034 2034 repopath=repo.path, action='histedit')
2035 2035
2036 2036 # Save edit rules in .hg/histedit-last-edit.txt in case
2037 2037 # the user needs to ask for help after something
2038 2038 # surprising happens.
2039 2039 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2040 2040 f.write(rules)
2041 2041
2042 2042 return rules
2043 2043
2044 2044 def parserules(rules, state):
2045 2045 """Read the histedit rules string and return list of action objects """
2046 2046 rules = [l for l in (r.strip() for r in rules.splitlines())
2047 2047 if l and not l.startswith('#')]
2048 2048 actions = []
2049 2049 for r in rules:
2050 2050 if ' ' not in r:
2051 2051 raise error.ParseError(_('malformed line "%s"') % r)
2052 2052 verb, rest = r.split(' ', 1)
2053 2053
2054 2054 if verb not in actiontable:
2055 2055 raise error.ParseError(_('unknown action "%s"') % verb)
2056 2056
2057 2057 action = actiontable[verb].fromrule(state, rest)
2058 2058 actions.append(action)
2059 2059 return actions
2060 2060
2061 2061 def warnverifyactions(ui, repo, actions, state, ctxs):
2062 2062 try:
2063 2063 verifyactions(actions, state, ctxs)
2064 2064 except error.ParseError:
2065 2065 if repo.vfs.exists('histedit-last-edit.txt'):
2066 2066 ui.warn(_('warning: histedit rules saved '
2067 2067 'to: .hg/histedit-last-edit.txt\n'))
2068 2068 raise
2069 2069
2070 2070 def verifyactions(actions, state, ctxs):
2071 2071 """Verify that there exists exactly one action per given changeset and
2072 2072 other constraints.
2073 2073
2074 2074 Will abort if there are to many or too few rules, a malformed rule,
2075 2075 or a rule on a changeset outside of the user-given range.
2076 2076 """
2077 2077 expected = set(c.node() for c in ctxs)
2078 2078 seen = set()
2079 2079 prev = None
2080 2080
2081 2081 if actions and actions[0].verb in ['roll', 'fold']:
2082 2082 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2083 2083 actions[0].verb)
2084 2084
2085 2085 for action in actions:
2086 2086 action.verify(prev, expected, seen)
2087 2087 prev = action
2088 2088 if action.node is not None:
2089 2089 seen.add(action.node)
2090 2090 missing = sorted(expected - seen) # sort to stabilize output
2091 2091
2092 2092 if state.repo.ui.configbool('histedit', 'dropmissing'):
2093 2093 if len(actions) == 0:
2094 2094 raise error.ParseError(_('no rules provided'),
2095 2095 hint=_('use strip extension to remove commits'))
2096 2096
2097 2097 drops = [drop(state, n) for n in missing]
2098 2098 # put the in the beginning so they execute immediately and
2099 2099 # don't show in the edit-plan in the future
2100 2100 actions[:0] = drops
2101 2101 elif missing:
2102 2102 raise error.ParseError(_('missing rules for changeset %s') %
2103 2103 node.short(missing[0]),
2104 2104 hint=_('use "drop %s" to discard, see also: '
2105 2105 "'hg help -e histedit.config'")
2106 2106 % node.short(missing[0]))
2107 2107
2108 2108 def adjustreplacementsfrommarkers(repo, oldreplacements):
2109 2109 """Adjust replacements from obsolescence markers
2110 2110
2111 2111 Replacements structure is originally generated based on
2112 2112 histedit's state and does not account for changes that are
2113 2113 not recorded there. This function fixes that by adding
2114 2114 data read from obsolescence markers"""
2115 2115 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2116 2116 return oldreplacements
2117 2117
2118 2118 unfi = repo.unfiltered()
2119 2119 nm = unfi.changelog.nodemap
2120 2120 obsstore = repo.obsstore
2121 2121 newreplacements = list(oldreplacements)
2122 2122 oldsuccs = [r[1] for r in oldreplacements]
2123 2123 # successors that have already been added to succstocheck once
2124 2124 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2125 2125 succstocheck = list(seensuccs)
2126 2126 while succstocheck:
2127 2127 n = succstocheck.pop()
2128 2128 missing = nm.get(n) is None
2129 2129 markers = obsstore.successors.get(n, ())
2130 2130 if missing and not markers:
2131 2131 # dead end, mark it as such
2132 2132 newreplacements.append((n, ()))
2133 2133 for marker in markers:
2134 2134 nsuccs = marker[1]
2135 2135 newreplacements.append((n, nsuccs))
2136 2136 for nsucc in nsuccs:
2137 2137 if nsucc not in seensuccs:
2138 2138 seensuccs.add(nsucc)
2139 2139 succstocheck.append(nsucc)
2140 2140
2141 2141 return newreplacements
2142 2142
2143 2143 def processreplacement(state):
2144 2144 """process the list of replacements to return
2145 2145
2146 2146 1) the final mapping between original and created nodes
2147 2147 2) the list of temporary node created by histedit
2148 2148 3) the list of new commit created by histedit"""
2149 2149 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2150 2150 allsuccs = set()
2151 2151 replaced = set()
2152 2152 fullmapping = {}
2153 2153 # initialize basic set
2154 2154 # fullmapping records all operations recorded in replacement
2155 2155 for rep in replacements:
2156 2156 allsuccs.update(rep[1])
2157 2157 replaced.add(rep[0])
2158 2158 fullmapping.setdefault(rep[0], set()).update(rep[1])
2159 2159 new = allsuccs - replaced
2160 2160 tmpnodes = allsuccs & replaced
2161 2161 # Reduce content fullmapping into direct relation between original nodes
2162 2162 # and final node created during history edition
2163 2163 # Dropped changeset are replaced by an empty list
2164 2164 toproceed = set(fullmapping)
2165 2165 final = {}
2166 2166 while toproceed:
2167 2167 for x in list(toproceed):
2168 2168 succs = fullmapping[x]
2169 2169 for s in list(succs):
2170 2170 if s in toproceed:
2171 2171 # non final node with unknown closure
2172 2172 # We can't process this now
2173 2173 break
2174 2174 elif s in final:
2175 2175 # non final node, replace with closure
2176 2176 succs.remove(s)
2177 2177 succs.update(final[s])
2178 2178 else:
2179 2179 final[x] = succs
2180 2180 toproceed.remove(x)
2181 2181 # remove tmpnodes from final mapping
2182 2182 for n in tmpnodes:
2183 2183 del final[n]
2184 2184 # we expect all changes involved in final to exist in the repo
2185 2185 # turn `final` into list (topologically sorted)
2186 2186 nm = state.repo.changelog.nodemap
2187 2187 for prec, succs in final.items():
2188 2188 final[prec] = sorted(succs, key=nm.get)
2189 2189
2190 2190 # computed topmost element (necessary for bookmark)
2191 2191 if new:
2192 2192 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2193 2193 elif not final:
2194 2194 # Nothing rewritten at all. we won't need `newtopmost`
2195 2195 # It is the same as `oldtopmost` and `processreplacement` know it
2196 2196 newtopmost = None
2197 2197 else:
2198 2198 # every body died. The newtopmost is the parent of the root.
2199 2199 r = state.repo.changelog.rev
2200 2200 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2201 2201
2202 2202 return final, tmpnodes, new, newtopmost
2203 2203
2204 2204 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2205 2205 """Move bookmark from oldtopmost to newly created topmost
2206 2206
2207 2207 This is arguably a feature and we may only want that for the active
2208 2208 bookmark. But the behavior is kept compatible with the old version for now.
2209 2209 """
2210 2210 if not oldtopmost or not newtopmost:
2211 2211 return
2212 2212 oldbmarks = repo.nodebookmarks(oldtopmost)
2213 2213 if oldbmarks:
2214 2214 with repo.lock(), repo.transaction('histedit') as tr:
2215 2215 marks = repo._bookmarks
2216 2216 changes = []
2217 2217 for name in oldbmarks:
2218 2218 changes.append((name, newtopmost))
2219 2219 marks.applychanges(repo, tr, changes)
2220 2220
2221 2221 def cleanupnode(ui, repo, nodes, nobackup=False):
2222 2222 """strip a group of nodes from the repository
2223 2223
2224 2224 The set of node to strip may contains unknown nodes."""
2225 2225 with repo.lock():
2226 2226 # do not let filtering get in the way of the cleanse
2227 2227 # we should probably get rid of obsolescence marker created during the
2228 2228 # histedit, but we currently do not have such information.
2229 2229 repo = repo.unfiltered()
2230 2230 # Find all nodes that need to be stripped
2231 2231 # (we use %lr instead of %ln to silently ignore unknown items)
2232 2232 nm = repo.changelog.nodemap
2233 2233 nodes = sorted(n for n in nodes if n in nm)
2234 2234 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2235 2235 if roots:
2236 2236 backup = not nobackup
2237 2237 repair.strip(ui, repo, roots, backup=backup)
2238 2238
2239 2239 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2240 2240 if isinstance(nodelist, str):
2241 2241 nodelist = [nodelist]
2242 2242 state = histeditstate(repo)
2243 2243 if state.inprogress():
2244 2244 state.read()
2245 2245 histedit_nodes = {action.node for action
2246 2246 in state.actions if action.node}
2247 2247 common_nodes = histedit_nodes & set(nodelist)
2248 2248 if common_nodes:
2249 2249 raise error.Abort(_("histedit in progress, can't strip %s")
2250 2250 % ', '.join(node.short(x) for x in common_nodes))
2251 2251 return orig(ui, repo, nodelist, *args, **kwargs)
2252 2252
2253 2253 extensions.wrapfunction(repair, 'strip', stripwrapper)
2254 2254
2255 2255 def summaryhook(ui, repo):
2256 2256 state = histeditstate(repo)
2257 2257 if not state.inprogress():
2258 2258 return
2259 2259 state.read()
2260 2260 if state.actions:
2261 2261 # i18n: column positioning for "hg summary"
2262 2262 ui.write(_('hist: %s (histedit --continue)\n') %
2263 2263 (ui.label(_('%d remaining'), 'histedit.remaining') %
2264 2264 len(state.actions)))
2265 2265
2266 2266 def extsetup(ui):
2267 2267 cmdutil.summaryhooks.add('histedit', summaryhook)
2268 2268 cmdutil.unfinishedstates.append(
2269 2269 ['histedit-state', False, True, _('histedit in progress'),
2270 2270 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2271 2271 cmdutil.afterresolvedstates.append(
2272 2272 ['histedit-state', _('hg histedit --continue')])
General Comments 0
You need to be logged in to leave comments. Login now