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