sapling/edenscm/hgext/amend/metaedit.py
Jun Wu 16c156f11f edit: save editor text for 2 weeks
Summary:
There are multiple reports that the work in the commit message editor gets lost
for various reasons. We have `.hg/last-message.txt` for commit hook failure,
but that one does not take care of all code paths (ex. metaedit).

This diff changes `ui.edit` directly to try to save messages in `.hg/edit-tmp`
for 2 weeks.

Reviewed By: kulshrax

Differential Revision: D15347831

fbshipit-source-id: 9207adf4315d94a4892685a03f323e89d9c4a7f1
2019-05-15 17:20:13 -07:00

287 lines
10 KiB
Python

# metaedit.py - edit changeset metadata
#
# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
# Logilab SA <contact@logilab.fr>
# Pierre-Yves David <pierre-yves.david@ens-lyon.org>
# Patrick Mezard <patrick@mezard.eu>
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
from edenscm.mercurial import (
cmdutil,
commands,
error,
hg,
mutation,
node as nodemod,
phases,
registrar,
scmutil,
)
from edenscm.mercurial.i18n import _
from . import common, fold
cmdtable = {}
command = registrar.command(cmdtable)
def editmessages(repo, revs):
"""Invoke editor to edit messages in batch. Return {node: new message}"""
nodebanners = []
editortext = ""
for rev in revs:
ctx = repo[rev]
message = ctx.description()
short = nodemod.short(ctx.node())
bannerstart = cmdutil.hgprefix(_("Begin of commit %s") % short)
bannerend = cmdutil.hgprefix(_("End of commit %s") % short)
nodebanners.append((ctx.node(), bannerstart, bannerend))
if editortext:
editortext += cmdutil.hgprefix("-" * 77) + "\n"
else:
editortext += (
cmdutil.hgprefix(
_(
"Editing %s commits in batch. Do not change lines starting with 'HG:'."
)
% len(revs)
)
+ "\n"
)
editortext += "%s\n%s\n%s\n" % (bannerstart, message, bannerend)
result = {}
ui = repo.ui
newtext = ui.edit(editortext, ui.username(), action="metaedit", repopath=repo.path)
for node, bannerstart, bannerend in nodebanners:
if bannerstart in newtext and bannerend in newtext:
newmessage = newtext.split(bannerstart, 1)[1].split(bannerend, 1)[0]
result[node] = newmessage
return result
@command(
"^metaedit",
[
("r", "rev", [], _("revision to edit")),
("", "fold", False, _("fold specified revisions into one")),
(
"",
"batch",
False,
_("edit messages of multiple commits in one editor invocation"),
),
]
+ commands.commitopts
+ commands.commitopts2
+ cmdutil.formatteropts,
_("[OPTION]... [-r] [REV]"),
cmdtemplate=True,
)
def metaedit(ui, repo, templ, *revs, **opts):
"""edit commit message and other metadata
Edits the commit information for the specified revisions. By default, edits
commit information for the working directory parent.
With --fold, also folds multiple revisions into one if necessary. In this
case, the given revisions must form a linear unbroken chain.
.. container:: verbose
Some examples:
- Edit the commit message for the working directory parent::
hg metaedit
- Change the username for the working directory parent::
hg metaedit --user 'New User <new-email@example.com>'
- Combine all draft revisions that are ancestors of foo but not of @ into
one::
hg metaedit --fold 'draft() and only(foo,@)'
See :hg:`help phases` for more about draft revisions, and
:hg:`help revsets` for more about the `draft()` and `only()` keywords.
"""
revs = list(revs)
revs.extend(opts["rev"])
if not revs:
if opts["fold"]:
raise error.Abort(_("revisions must be specified with --fold"))
revs = ["."]
with repo.wlock(), repo.lock():
revs = scmutil.revrange(repo, revs)
msgmap = {} # {node: message}, predefined messages, currently used by --batch
if opts["fold"]:
root, head = fold._foldcheck(repo, revs)
else:
if repo.revs("%ld and public()", revs):
raise error.Abort(
_("cannot edit commit information for public " "revisions")
)
root = head = repo[revs.first()]
wctx = repo[None]
p1 = wctx.p1()
tr = repo.transaction("metaedit")
newp1 = None
try:
commitopts = opts.copy()
allctx = [repo[r] for r in revs]
if commitopts.get("message") or commitopts.get("logfile"):
commitopts["edit"] = False
else:
if opts["fold"]:
msgs = [_("HG: This is a fold of %d changesets.") % len(allctx)]
msgs += [
_("HG: Commit message of changeset %s.\n\n%s\n")
% (c.rev(), c.description())
for c in allctx
]
else:
if opts["batch"] and len(revs) > 1:
msgmap = editmessages(repo, revs)
msgs = [head.description()]
commitopts["message"] = "\n".join(msgs)
commitopts["edit"] = True
if root == head:
# fast path: use metarewrite
replacemap = {}
# adding commitopts to the revisions to metaedit
allctxopt = [{"ctx": ctx, "commitopts": commitopts} for ctx in allctx]
# all descendats that can be safely rewritten
newunstable = common.newunstable(repo, revs)
newunstableopt = [
{"ctx": ctx} for ctx in [repo[r] for r in newunstable]
]
# we need to edit descendants with the given revisions to not to
# corrupt the stacks
if _histediting(repo):
ui.note(
_(
"during histedit, the descendants of "
"the edited commit weren't auto-rebased\n"
)
)
else:
allctxopt += newunstableopt
# we need topological order for all
if mutation.enabled(repo):
allctxopt = mutation.toposort(
repo, allctxopt, nodefn=lambda copt: copt["ctx"].node()
)
else:
allctxopt = sorted(allctxopt, key=lambda copt: copt["ctx"].rev())
def _rewritesingle(c, _commitopts):
# Predefined message overrides other message editing choices.
msg = msgmap.get(c.node())
if msg is not None:
_commitopts["message"] = msg
_commitopts["edit"] = False
if _commitopts.get("edit", False):
_commitopts["message"] = (
"HG: Commit message of changeset %s\n%s"
% (str(c), c.description())
)
bases = [
replacemap.get(c.p1().node(), c.p1().node()),
replacemap.get(c.p2().node(), c.p2().node()),
]
if mutation.enabled(repo):
preds = [
replacemap[p]
for p in mutation.predecessorsset(
repo, c.node(), closest=True
)
if p in replacemap
]
else:
preds = []
newid, created = common.metarewrite(
repo, c, bases, commitopts=_commitopts, copypreds=preds
)
if created:
replacemap[c.node()] = newid
for copt in allctxopt:
_rewritesingle(
copt["ctx"],
copt.get(
"commitopts", {"date": commitopts.get("date") or None}
),
)
if p1.node() in replacemap:
repo.setparents(replacemap[p1.node()])
if len(replacemap) > 0:
mapping = dict(
map(
lambda oldnew: (oldnew[0], [oldnew[1]]),
replacemap.iteritems(),
)
)
templ.setprop("nodereplacements", mapping)
scmutil.cleanupnodes(repo, mapping, "metaedit")
# TODO: set poroper phase boundaries (affects secret
# phase only)
else:
ui.status(_("nothing changed\n"))
return 1
else:
# slow path: create a new commit
targetphase = max(c.phase() for c in allctx)
# TODO: if the author and message are the same, don't create a
# new hash. Right now we create a new hash because the date can
# be different.
newid, created = common.rewrite(
repo,
root,
allctx,
head,
[root.p1().node(), root.p2().node()],
commitopts=commitopts,
mutop="metaedit",
)
if created:
if p1.rev() in revs:
newp1 = newid
phases.retractboundary(repo, tr, targetphase, [newid])
mapping = dict([(repo[rev].node(), [newid]) for rev in revs])
templ.setprop("nodereplacements", mapping)
scmutil.cleanupnodes(repo, mapping, "metaedit")
else:
ui.status(_("nothing changed\n"))
return 1
tr.close()
finally:
tr.release()
if opts["fold"]:
ui.status(_("%i changesets folded\n") % len(revs))
if newp1 is not None:
hg.update(repo, newp1)
def _histediting(repo):
return repo.localvfs.exists("histedit-state")