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