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