histedit.py 84.2 KB
Newer Older
1
2
3
4
5
6
# histedit.py - interactive history editing for mercurial
#
# Copyright 2009 Augie Fackler <raf@durin42.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""interactive history editing

With this extension installed, Mercurial gains one new command: histedit. Usage
is as follows, assuming the following history::

 @  3[tip]   7c2fd3b9020c   2009-04-27 18:04 -0500   durin42
 |    Add delta
 |
 o  2   030b686bedc4   2009-04-27 18:04 -0500   durin42
 |    Add gamma
 |
 o  1   c561b4e977df   2009-04-27 18:04 -0500   durin42
 |    Add beta
 |
 o  0   d8d2fcd0e319   2009-04-27 18:04 -0500   durin42
      Add alpha

If you were to run ``hg histedit c561b4e977df``, you would see the following
file open in your editor::

 pick c561b4e977df Add beta
 pick 030b686bedc4 Add gamma
 pick 7c2fd3b9020c Add delta

31
 # Edit history between c561b4e977df and 7c2fd3b9020c
32
 #
33
34
 # Commits are listed from least to most recent
 #
35
36
 # Commands:
 #  p, pick = use commit
37
 #  e, edit = use commit, but allow edits before making new commit
38
 #  f, fold = use commit, but combine it with the one above
39
 #  r, roll = like fold, but discard this commit's description and date
40
 #  d, drop = remove commit from history
41
 #  m, mess = edit commit message without changing commit content
42
 #  b, base = checkout changeset and apply further changesets from there
43
44
45
46
47
48
49
50
51
52
53
 #

In this file, lines beginning with ``#`` are ignored. You must specify a rule
for each revision in your history. For example, if you had meant to add gamma
before beta, and then wanted to add delta in the same revision as beta, you
would reorganize the file to look like this::

 pick 030b686bedc4 Add gamma
 pick c561b4e977df Add beta
 fold 7c2fd3b9020c Add delta

54
 # Edit history between c561b4e977df and 7c2fd3b9020c
55
 #
56
57
 # Commits are listed from least to most recent
 #
58
59
 # Commands:
 #  p, pick = use commit
60
 #  e, edit = use commit, but allow edits before making new commit
61
 #  f, fold = use commit, but combine it with the one above
62
 #  r, roll = like fold, but discard this commit's description and date
63
 #  d, drop = remove commit from history
64
 #  m, mess = edit commit message without changing commit content
65
 #  b, base = checkout changeset and apply further changesets from there
66
67
68
69
70
71
72
73
74
 #

At which point you close the editor and ``histedit`` starts working. When you
specify a ``fold`` operation, ``histedit`` will open an editor when it folds
those revisions together, offering you a chance to clean up the commit message::

 Add beta
 ***
 Add delta
75

76
77
78
79
80
Edit the commit message to your liking, then close the editor. The date used
for the commit will be the later of the two commits' dates. For this example,
let's assume that the commit message was changed to ``Add beta and delta.``
After histedit has run and had a chance to remove any old or temporary
revisions it needed, the history looks like this::
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

 @  2[tip]   989b4d060121   2009-04-27 18:04 -0500   durin42
 |    Add beta and delta.
 |
 o  1   081603921c3f   2009-04-27 18:04 -0500   durin42
 |    Add gamma
 |
 o  0   d8d2fcd0e319   2009-04-27 18:04 -0500   durin42
      Add alpha

Note that ``histedit`` does *not* remove any revisions (even its own temporary
ones) until after it has completed all the editing operations, so it will
probably perform several strip operations when it's done. For the above example,
it had to run strip twice. Strip can be slow depending on a variety of factors,
so you might need to be a little patient. You can choose to keep the original
revisions by passing the ``--keep`` flag.

The ``edit`` operation will drop you back to a command prompt,
allowing you to edit files freely, or even use ``hg record`` to commit
some changes as a separate commit. When you're done, any remaining
uncommitted changes will be committed as well. When done, run ``hg
102
103
104
105
histedit --continue`` to finish this step. If there are uncommitted
changes, you'll be prompted for a new commit message, but the default
commit message will be the original message for the ``edit`` ed
revision, and the date of the original commit will be preserved.
106
107
108
109
110
111
112
113
114
115
116
117
118
119

The ``message`` operation will give you a chance to revise a commit
message without changing the contents. It's a shortcut for doing
``edit`` immediately followed by `hg histedit --continue``.

If ``histedit`` encounters a conflict when moving a revision (while
handling ``pick`` or ``fold``), it'll stop in a similar manner to
``edit`` with the difference that it won't prompt you for a commit
message when done. If you decide at this point that you don't like how
much work it will be to rearrange history, or that you made a mistake,
you can use ``hg histedit --abort`` to abandon the new changes you
have made and return to the state before you attempted to edit your
history.

120
121
If we clone the histedit-ed example repository above and add four more
changes, such that we have the following history::
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

   @  6[tip]   038383181893   2009-04-27 18:04 -0500   stefan
   |    Add theta
   |
   o  5   140988835471   2009-04-27 18:04 -0500   stefan
   |    Add eta
   |
   o  4   122930637314   2009-04-27 18:04 -0500   stefan
   |    Add zeta
   |
   o  3   836302820282   2009-04-27 18:04 -0500   stefan
   |    Add epsilon
   |
   o  2   989b4d060121   2009-04-27 18:04 -0500   durin42
   |    Add beta and delta.
   |
   o  1   081603921c3f   2009-04-27 18:04 -0500   durin42
   |    Add gamma
   |
   o  0   d8d2fcd0e319   2009-04-27 18:04 -0500   durin42
        Add alpha

If you run ``hg histedit --outgoing`` on the clone then it is the same
as running ``hg histedit 836302820282``. If you need plan to push to a
repository that Mercurial does not detect to be related to the source
repo, you can add a ``--force`` option.
148

Mateusz Kwapich's avatar
Mateusz Kwapich committed
149
150
151
Config
------

152
Histedit rule lines are truncated to 80 characters by default. You
timeless's avatar
timeless committed
153
can customize this behavior by setting a different length in your
154
configuration file::
155

156
157
  [histedit]
  linelen = 120      # truncate rule lines at 120 characters
158

159
160
161
162
163
164
165
166
167
The summary of a change can be customized as well::

  [histedit]
  summary-template = '{rev} {bookmarks} {desc|firstline}'

The customized summary should be kept short enough that rule lines
will fit in the configured line length. See above if that requires
customization.

168
169
170
171
172
173
``hg histedit`` attempts to automatically choose an appropriate base
revision to use. To change which base revision is used, define a
revset in your configuration file::

  [histedit]
  defaultrev = only(.) & draft()
Mateusz Kwapich's avatar
Mateusz Kwapich committed
174
175
176

By default each edited revision needs to be present in histedit commands.
To remove revision you need to use ``drop`` operation. You can configure
177
the drop to be implicit for missing commits by adding::
Mateusz Kwapich's avatar
Mateusz Kwapich committed
178
179
180
181

  [histedit]
  dropmissing = True

182
183
184
185
186
187
188
189
190
By default, histedit will close the transaction after each action. For
performance purposes, you can configure histedit to use a single transaction
across the entire histedit. WARNING: This setting introduces a significant risk
of losing the work you've done in a histedit if the histedit aborts
unexpectedly::

  [histedit]
  singletransaction = True

191
"""
192

193
194
from __future__ import absolute_import

195
196
197
198
199
200
201
202
# chistedit dependencies that are not available everywhere
try:
    import fcntl
    import termios
except ImportError:
    fcntl = None
    termios = None

203
import functools
204
import os
205
import struct
206
207

from mercurial.i18n import _
208
209
210
211
from mercurial.pycompat import (
    getattr,
    open,
)
212
213
214
215
216
from mercurial.node import (
    bin,
    hex,
    short,
)
217
218
219
220
221
222
223
from mercurial import (
    bundle2,
    cmdutil,
    context,
    copies,
    destutil,
    discovery,
224
    encoding,
225
226
227
228
    error,
    exchange,
    extensions,
    hg,
229
    logcmdutil,
230
    merge as mergemod,
231
    mergestate as mergestatemod,
232
    mergeutil,
233
    obsolete,
234
    pycompat,
235
    registrar,
236
    repair,
237
    rewriteutil,
238
    scmutil,
239
    state as statemod,
240
241
    util,
)
242
from mercurial.utils import (
243
    dateutil,
244
    stringutil,
245
    urlutil,
246
)
247

248
pickle = util.pickle
249
cmdtable = {}
250
command = registrar.command(cmdtable)
251

252
253
configtable = {}
configitem = registrar.configitem(configtable)
254
configitem(
durin42's avatar
durin42 committed
255
256
257
    b'experimental',
    b'histedit.autoverb',
    default=False,
258
)
259
configitem(
durin42's avatar
durin42 committed
260
261
262
    b'histedit',
    b'defaultrev',
    default=None,
263
)
264
configitem(
durin42's avatar
durin42 committed
265
266
267
    b'histedit',
    b'dropmissing',
    default=False,
268
)
269
configitem(
durin42's avatar
durin42 committed
270
271
272
    b'histedit',
    b'linelen',
    default=80,
273
)
274
configitem(
durin42's avatar
durin42 committed
275
276
277
    b'histedit',
    b'singletransaction',
    default=False,
278
)
279
configitem(
durin42's avatar
durin42 committed
280
281
282
    b'ui',
    b'interface.histedit',
    default=None,
283
)
284
configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
285

286
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
287
288
289
# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
# be specifying the version(s) of Mercurial they are tested with, or
# leave the attribute unspecified.
290
testedwith = b'ships-with-hg-core'
291

292
293
294
295
296
297
actiontable = {}
primaryactions = set()
secondaryactions = set()
tertiaryactions = set()
internalactions = set()

298

299
def geteditcomment(ui, first, last):
durin42's avatar
durin42 committed
300
    """construct the editor comment
301
302
    The comment includes::
     - an intro
303
304
     - sorted primary commands
     - sorted short commands
305
     - sorted long commands
306
     - additional hints
307
308
309

    Commands are only included once.
    """
310
    intro = _(
311
        b"""Edit history between %s and %s
312
313
314

Commits are listed from least to most recent

315
316
You can reorder changesets by reordering the lines

317
Commands:
318
319
"""
    )
320
    actions = []
321

322
323
    def addverb(v):
        a = actiontable[v]
324
        lines = a.message.split(b"\n")
325
        if len(a.verbs):
326
327
            v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
        actions.append(b" %s = %s" % (v, lines[0]))
328
        actions.extend([b'  %s'] * (len(lines) - 1))
329
330

    for v in (
331
332
333
334
        sorted(primaryactions)
        + sorted(secondaryactions)
        + sorted(tertiaryactions)
    ):
335
        addverb(v)
336
    actions.append(b'')
337

338
    hints = []
339
    if ui.configbool(b'histedit', b'dropmissing'):
340
        hints.append(
341
342
            b"Deleting a changeset from the list "
            b"will DISCARD it from the edited history!"
343
        )
344

345
346
347
    lines = (intro % (first, last)).split(b'\n') + actions + hints

    return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
348

349

350
class histeditstate(object):
351
    def __init__(self, repo):
352
        self.repo = repo
353
354
355
356
357
358
        self.actions = None
        self.keep = None
        self.topmost = None
        self.parentctxnode = None
        self.lock = None
        self.wlock = None
359
        self.backupfile = None
360
        self.stateobj = statemod.cmdstate(repo, b'histedit-state')
361
        self.replacements = []
362

363
    def read(self):
364
        """Load histedit state from disk and set fields appropriately."""
365
        if not self.stateobj.exists():
366
            cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
367

368
        data = self._read()
369

370
371
        self.parentctxnode = data[b'parentctxnode']
        actions = parserules(data[b'rules'], self)
372
        self.actions = actions
373
374
375
376
        self.keep = data[b'keep']
        self.topmost = data[b'topmost']
        self.replacements = data[b'replacements']
        self.backupfile = data[b'backupfile']
377

378
    def _read(self):
379
380
        fp = self.repo.vfs.read(b'histedit-state')
        if fp.startswith(b'v1\n'):
381
382
383
            data = self._load()
            parentctxnode, rules, keep, topmost, replacements, backupfile = data
        else:
384
            data = pickle.loads(fp)
385
            parentctxnode, rules, keep, topmost, replacements = data
386
            backupfile = None
387
        rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
388

389
        return {
390
391
392
393
394
395
            b'parentctxnode': parentctxnode,
            b"rules": rules,
            b"keep": keep,
            b"topmost": topmost,
            b"replacements": replacements,
            b"backupfile": backupfile,
396
        }
397

398
399
    def write(self, tr=None):
        if tr:
400
            tr.addfilegenerator(
401
402
                b'histedit-state',
                (b'histedit-state',),
403
                self._write,
404
                location=b'plain',
405
            )
406
        else:
407
            with self.repo.vfs(b"histedit-state", b"w") as f:
408
409
410
                self._write(f)

    def _write(self, fp):
411
        fp.write(b'v1\n')
412
413
        fp.write(b'%s\n' % hex(self.parentctxnode))
        fp.write(b'%s\n' % hex(self.topmost))
414
415
        fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
        fp.write(b'%d\n' % len(self.actions))
416
        for action in self.actions:
417
418
            fp.write(b'%s\n' % action.tostate())
        fp.write(b'%d\n' % len(self.replacements))
419
        for replacement in self.replacements:
420
            fp.write(
421
                b'%s%s\n'
422
                % (
423
424
                    hex(replacement[0]),
                    b''.join(hex(r) for r in replacement[1]),
425
426
                )
            )
427
428
        backupfile = self.backupfile
        if not backupfile:
429
430
            backupfile = b''
        fp.write(b'%s\n' % backupfile)
431

432
    def _load(self):
433
        fp = self.repo.vfs(b'histedit-state', b'r')
434
435
436
        lines = [l[:-1] for l in fp.readlines()]

        index = 0
437
        lines[index]  # version number
438
439
        index += 1

440
        parentctxnode = bin(lines[index])
441
442
        index += 1

443
        topmost = bin(lines[index])
444
445
        index += 1

446
        keep = lines[index] == b'True'
447
448
449
450
451
452
        index += 1

        # Rules
        rules = []
        rulelen = int(lines[index])
        index += 1
Gregory Szorc's avatar
Gregory Szorc committed
453
        for i in pycompat.xrange(rulelen):
454
455
            ruleaction = lines[index]
            index += 1
456
457
            rule = lines[index]
            index += 1
458
            rules.append((ruleaction, rule))
459
460
461
462
463

        # Replacements
        replacements = []
        replacementlen = int(lines[index])
        index += 1
Gregory Szorc's avatar
Gregory Szorc committed
464
        for i in pycompat.xrange(replacementlen):
465
            replacement = lines[index]
466
            original = bin(replacement[:40])
467
            succ = [
468
                bin(replacement[i : i + 40])
469
470
                for i in range(40, len(replacement), 40)
            ]
471
472
473
            replacements.append((original, succ))
            index += 1

474
475
476
        backupfile = lines[index]
        index += 1

477
478
        fp.close()

479
        return parentctxnode, rules, keep, topmost, replacements, backupfile
480

481
    def clear(self):
482
        if self.inprogress():
483
            self.repo.vfs.unlink(b'histedit-state')
484

485
    def inprogress(self):
486
        return self.repo.vfs.exists(b'histedit-state')
487

488

489
490
491
492
493
494
495
496
class histeditaction(object):
    def __init__(self, state, node):
        self.state = state
        self.repo = state.repo
        self.node = node

    @classmethod
    def fromrule(cls, state, rule):
durin42's avatar
durin42 committed
497
        """Parses the given rule, returning an instance of the histeditaction."""
498
        ruleid = rule.strip().split(b' ', 1)[0]
499
500
        # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
        # Check for validation of rule ids and get the rulehash
501
        try:
502
            rev = bin(ruleid)
503
        except TypeError:
504
505
506
            try:
                _ctx = scmutil.revsingle(state.repo, ruleid)
                rulehash = _ctx.hex()
507
                rev = bin(rulehash)
508
            except error.RepoLookupError:
509
                raise error.ParseError(_(b"invalid changeset %s") % ruleid)
510
        return cls(state, rev)
511

512
    def verify(self, prev, expected, seen):
513
        """Verifies semantic correctness of the rule"""
514
        repo = self.repo
515
        ha = hex(self.node)
516
        self.node = scmutil.resolvehexnodeidprefix(repo, ha)
517
        if self.node is None:
518
            raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
519
        self._verifynodeconstraints(prev, expected, seen)
520

521
522
523
    def _verifynodeconstraints(self, prev, expected, seen):
        # by default command need a node in the edited list
        if self.node not in expected:
524
            raise error.ParseError(
525
                _(b'%s "%s" changeset was not a candidate')
526
                % (self.verb, short(self.node)),
527
                hint=_(b'only use listed changesets'),
528
            )
529
530
        # and only one command per node
        if self.node in seen:
531
            raise error.ParseError(
532
                _(b'duplicated command for changeset %s') % short(self.node)
533
            )
534

535
    def torule(self):
536
537
538
539
540
541
        """build a histedit rule line for an action

        by default lines are in the form:
        <hash> <rev> <summary>
        """
        ctx = self.repo[self.node]
542
        ui = self.repo.ui
543
544
545
546
547
        # We don't want color codes in the commit message template, so
        # disable the label() template function while we render it.
        with ui.configoverride(
            {(b'templatealias', b'label(l,x)'): b"x"}, b'histedit'
        ):
548
549
550
            summary = cmdutil.rendertemplate(
                ctx, ui.config(b'histedit', b'summary-template')
            )
551
552
        # Handle the fact that `''.splitlines() => []`
        summary = summary.splitlines()[0] if summary else b''
553
        line = b'%s %s %s' % (self.verb, ctx, summary)
554
555
        # trim to 75 columns by default so it's not stupidly wide in my editor
        # (the 5 more are left for verb)
556
        maxlen = self.repo.ui.configint(b'histedit', b'linelen')
557
        maxlen = max(maxlen, 22)  # avoid truncating hash
558
        return stringutil.ellipsis(line, maxlen)
559

560
561
    def tostate(self):
        """Print an action in format used by histedit state files
durin42's avatar
durin42 committed
562
        (the first line is a verb, the remainder is the second)
563
        """
564
        return b"%s\n%s" % (self.verb, hex(self.node))
565

566
567
568
569
570
571
572
573
574
575
576
577
    def run(self):
        """Runs the action. The default behavior is simply apply the action's
        rulectx onto the current parentctx."""
        self.applychange()
        self.continuedirty()
        return self.continueclean()

    def applychange(self):
        """Applies the changes from this action's rulectx onto the current
        parentctx, but does not commit them."""
        repo = self.repo
        rulectx = repo[self.node]
578
579
        with repo.ui.silent():
            hg.update(repo, self.state.parentctxnode, quietempty=True)
580
        stats = applychanges(repo.ui, repo, rulectx, {})
581
        repo.dirstate.setbranch(rulectx.branch())
582
        if stats.unresolvedcount:
583
            raise error.InterventionRequired(
584
                _(b'Fix up the change (%s %s)') % (self.verb, short(self.node)),
585
                hint=_(b'hg histedit --continue to resume'),
586
            )
587
588
589
590
591
592
593
594
595

    def continuedirty(self):
        """Continues the action when changes have been applied to the working
        copy. The default behavior is to commit the dirty changes."""
        repo = self.repo
        rulectx = repo[self.node]

        editor = self.commiteditor()
        commit = commitfuncfor(repo, rulectx)
596
        if repo.ui.configbool(b'rewrite', b'update-timestamp'):
597
598
599
            date = dateutil.makedate()
        else:
            date = rulectx.date()
600
601
602
603
604
605
606
        commit(
            text=rulectx.description(),
            user=rulectx.user(),
            date=date,
            extra=rulectx.extra(),
            editor=editor,
        )
607
608
609
610
611
612
613
614
615

    def commiteditor(self):
        """The editor to be used to edit the commit message."""
        return False

    def continueclean(self):
        """Continues the action when the working copy is clean. The default
        behavior is to accept the current commit as the new version of the
        rulectx."""
616
        ctx = self.repo[b'.']
617
        if ctx.node() == self.state.parentctxnode:
618
            self.repo.ui.warn(
619
                _(b'%s: skipping changeset (no changes)\n') % short(self.node)
620
            )
621
622
623
624
625
626
            return ctx, [(self.node, tuple())]
        if ctx.node() == self.node:
            # Nothing changed
            return ctx, []
        return ctx, [(self.node, (ctx.node(),))]

627

628
629
630
def commitfuncfor(repo, src):
    """Build a commit function for the replacement of <src>

631
    This function ensure we apply the same treatment to all changesets.
632

633
    - Add a 'histedit_source' entry in extra.
634

635
    Note that fold has its own separated logic because its handling is a bit
636
637
    different and not easily factored out of the fold method.
    """
638
    phasemin = src.phase()
639

640
    def commitfunc(**kwargs):
641
642
        overrides = {(b'phases', b'new-commit'): phasemin}
        with repo.ui.configoverride(overrides, b'histedit'):
643
            extra = kwargs.get('extra', {}).copy()
644
            extra[b'histedit_source'] = src.hex()
645
            kwargs['extra'] = extra
646
            return repo.commit(**kwargs)
647

648
649
    return commitfunc

650

651
652
def applychanges(ui, repo, ctx, opts):
    """Merge changeset from ctx (only) in the current working directory"""
653
    if ctx.p1().node() == repo.dirstate.p1():
timeless's avatar
timeless committed
654
        # edits are "in place" we do not need to make any merge,
655
        # just applies changes on parent for editing
656
657
658
        with ui.silent():
            cmdutil.revert(ui, repo, ctx, all=True)
            stats = mergemod.updateresult(0, 0, 0, 0)
659
660
661
    else:
        try:
            # ui.forcemerge is an internal variable, do not document
662
            repo.ui.setconfig(
663
                b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
664
            )
665
            stats = mergemod.graft(repo, ctx, labels=[b'local', b'histedit'])
666
        finally:
667
            repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
668
    return stats
669

670

671
def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
Pierre-Yves David's avatar
Pierre-Yves David committed
672
673
674
675
676
677
    """collapse the set of revisions from first to last as new one.

    Expected commit options are:
        - message
        - date
        - username
678
    Commit message is edited in all cases.
Pierre-Yves David's avatar
Pierre-Yves David committed
679
680

    This function works in memory."""
681
    ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
Pierre-Yves David's avatar
Pierre-Yves David committed
682
683
    if not ctxs:
        return None
684
685
    for c in ctxs:
        if not c.mutable():
686
            raise error.ParseError(
687
                _(b"cannot fold into public change %s") % short(c.node())
688
            )
689
    base = firstctx.p1()
Pierre-Yves David's avatar
Pierre-Yves David committed
690
691
692
693
694
695
696
697

    # commit a new version of the old changeset, including the update
    # collect all files which might be affected
    files = set()
    for ctx in ctxs:
        files.update(ctx.files())

    # Recompute copies (avoid recording a -> b -> a)
698
    copied = copies.pathcopies(base, lastctx)
Pierre-Yves David's avatar
Pierre-Yves David committed
699
700

    # prune files which were reverted by the updates
701
    files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
Pierre-Yves David's avatar
Pierre-Yves David committed
702
    # commit version of these files as defined by head
703
    headmf = lastctx.manifest()
704

Pierre-Yves David's avatar
Pierre-Yves David committed
705
706
    def filectxfn(repo, ctx, path):
        if path in headmf:
707
            fctx = lastctx[path]
Pierre-Yves David's avatar
Pierre-Yves David committed
708
            flags = fctx.flags()
709
710
711
712
713
            mctx = context.memfilectx(
                repo,
                ctx,
                fctx.path(),
                fctx.data(),
714
715
                islink=b'l' in flags,
                isexec=b'x' in flags,
716
717
                copysource=copied.get(path),
            )
Pierre-Yves David's avatar
Pierre-Yves David committed
718
            return mctx
719
        return None
Pierre-Yves David's avatar
Pierre-Yves David committed
720

721
722
    if commitopts.get(b'message'):
        message = commitopts[b'message']
Pierre-Yves David's avatar
Pierre-Yves David committed
723
    else:
724
        message = firstctx.description()
725
726
727
    user = commitopts.get(b'user')
    date = commitopts.get(b'date')
    extra = commitopts.get(b'extra')
Pierre-Yves David's avatar
Pierre-Yves David committed
728

729
    parents = (firstctx.p1().node(), firstctx.p2().node())
730
    editor = None
731
    if not skipprompt:
732
        editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
733
734
735
736
737
738
739
740
741
742
743
    new = context.memctx(
        repo,
        parents=parents,
        text=message,
        files=files,
        filectxfn=filectxfn,
        user=user,
        date=date,
        extra=extra,
        editor=editor,
    )
Pierre-Yves David's avatar
Pierre-Yves David committed
744
745
    return repo.commitctx(new)

746

747
748
749
def _isdirtywc(repo):
    return repo[None].dirty(missing=True)

750

751
def abortdirty():
752
    raise error.StateError(
753
        _(b'working copy has pending changes'),
754
        hint=_(
755
756
            b'amend, commit, or revert them and run histedit '
            b'--continue, or abort with histedit --abort'
757
758
759
        ),
    )

760

761
762
763
764
765
766
767
768
769
770
771
772
def action(verbs, message, priority=False, internal=False):
    def wrap(cls):
        assert not priority or not internal
        verb = verbs[0]
        if priority:
            primaryactions.add(verb)
        elif internal:
            internalactions.add(verb)
        elif len(verbs) > 1:
            secondaryactions.add(verb)
        else:
            tertiaryactions.add(verb)
773

774
775
776
        cls.verb = verb
        cls.verbs = verbs
        cls.message = message
777
778
779
        for verb in verbs:
            actiontable[verb] = cls
        return cls
780

781
782
    return wrap

783

784
@action([b'pick', b'p'], _(b'use commit'), priority=True)
785
786
787
class pick(histeditaction):
    def run(self):
        rulectx = self.repo[self.node]
788
        if rulectx.p1().node() == self.state.parentctxnode:
789
            self.repo.ui.debug(b'node %s unchanged\n' % short(self.node))
790
            return rulectx, []
791

792
        return super(pick, self).run()
793

794

795
796
797
798
799
@action(
    [b'edit', b'e'],
    _(b'use commit, but allow edits before making new commit'),
    priority=True,
)
800
801
802
803
class edit(histeditaction):
    def run(self):
        repo = self.repo
        rulectx = repo[self.node]
804
        hg.update(repo, self.state.parentctxnode, quietempty=True)
805
        applychanges(repo.ui, repo, rulectx, {})
806
        hint = _(b'to edit %s, `hg histedit --continue` after making changes')
807
        raise error.InterventionRequired(
808
            _(b'Editing (%s), commit as needed now to split the change')
809
810
            % short(self.node),
            hint=hint % short(self.node),
811
        )
812
813

    def commiteditor(self):
814
815
816
817
        return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')


@action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
818
class fold(histeditaction):
819
    def verify(self, prev, expected, seen):
820
        """Verifies semantic correctness of the fold rule"""
821
        super(fold, self).verify(prev, expected, seen)
822
823
        repo = self.repo
        if not prev:
824
            c = repo[self.node].p1()
825
        elif not prev.verb in (b'pick', b'base'):
826
827
828
829
            return
        else:
            c = repo[prev.node]
        if not c.mutable():
830
            raise error.ParseError(
831
                _(b"cannot fold into public change %s") % short(c.node())
832
            )
833

834
835
836
837
838
    def continuedirty(self):
        repo = self.repo
        rulectx = repo[self.node]

        commit = commitfuncfor(repo, rulectx)
839
        commit(
840
            text=b'fold-temp-revision %s' % short(self.node),
841
842
843
844
            user=rulectx.user(),
            date=rulectx.date(),
            extra=rulectx.extra(),
        )
845
846
847

    def continueclean(self):
        repo = self.repo
848
        ctx = repo[b'.']
849
850
851
        rulectx = repo[self.node]
        parentctxnode = self.state.parentctxnode
        if ctx.node() == parentctxnode:
852
            repo.ui.warn(_(b'%s: empty changeset\n') % short(self.node))
853
            return ctx, [(self.node, (parentctxnode,))]
854

855
        parentctx = repo[parentctxnode]
856
        newcommits = {
857
            c.node()
858
            for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
859
        }
860
        if not newcommits:
861
862
            repo.ui.warn(
                _(
863
864
                    b'%s: cannot fold - working copy is not a '
                    b'descendant of previous commit %s\n'
865
                )
866
                % (short(self.node), short(parentctxnode))
867
            )
868
869
870
871
872
            return ctx, [(self.node, (ctx.node(),))]

        middlecommits = newcommits.copy()
        middlecommits.discard(ctx.node())

873
874
875
        return self.finishfold(
            repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
        )
876

877
    def skipprompt(self):
878
879
880
881
882
        """Returns true if the rule should skip the message editor.

        For example, 'fold' wants to show an editor, but 'rollup'
        doesn't want to.
        """
883
        return False
884

885
886
887
888
889
890
891
892
    def mergedescs(self):
        """Returns true if the rule should merge messages of multiple changes.

        This exists mainly so that 'rollup' rules can be a subclass of
        'fold'.
        """
        return True

893
894
895
896
897
898
899
900
901
    def firstdate(self):
        """Returns true if the rule should preserve the date of the first
        change.

        This exists mainly so that 'rollup' rules can be a subclass of
        'fold'.
        """
        return False