sapling/fbamend.py
Ryan McElroy 5cdf597fd6 fbamend: histedit awareness
Summary:
Previously, fbamend allowed all sorts of atrocities during histedits, literally
destroying the world. Literally. Make fbamend histedit-aware -- it will now
give correct advice and refuse to delete old commits during a histedit.

Test Plan: Updated tests

Reviewers: sid0, daviser, davidsp, ericsumner, pyd, mitrandir, durham

Reviewed By: mitrandir, durham

Differential Revision: https://phabricator.fb.com/D1844984

Signature: t1:1844984:1423768068:edc85af8b203aefdcdac9d7fee0425fc1f1c5281
2015-02-12 11:02:34 -08:00

217 lines
6.8 KiB
Python

# fbamend.py - improved amend functionality
#
# Copyright 2013 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.
"""extends the existing commit amend functionality
Adds an hg amend command that amends the current parent commit with the
changes in the working copy. Similiar to the existing hg commit --amend
except it doesn't prompt for the commit message unless --edit is provided.
Allows amending commits that have children and can automatically rebase
the children onto the new version of the commit
This extension is incompatible with changeset evolution. The command will
automatically disable itself if changeset evolution is enabled.
"""
from hgext import rebase
from mercurial import util, cmdutil, phases, commands, bookmarks, repair
from mercurial import merge, extensions
from mercurial.node import hex
from mercurial import obsolete
from mercurial.i18n import _
import errno, os, re
cmdtable = {}
command = cmdutil.command(cmdtable)
testedwith = 'internal'
amendopts = [
('', 'rebase', None, _('rebases children commits after the amend')),
('', 'fixup', None, _('rebase children commits from a previous amend')),
]
def uisetup(ui):
if obsolete._enabled:
msg = ('fbamend and evolve extension are imcompatible, '
'fbamend deactivated.\n'
'You can either disable it globally:\n'
'- type `hg config --edit`\n'
'- drop the `fbamend=` line from the `[extensions]` section\n'
'or disable it for a specific repo:\n'
'- type `hg config --local --edit`\n'
'- add a `fbamend=!%s` line in the `[extensions]` section\n')
msg %= ui.config('extensions', 'fbamend')
ui.write_err(msg)
return
entry = extensions.wrapcommand(commands.table, 'commit', commit)
for opt in amendopts:
opt = (opt[0], opt[1], opt[2], "(with --amend) " + opt[3])
entry[1].append(opt)
# manual call of the decorator
command('^amend', [
('e', 'edit', None, _('prompt to edit the commit message')),
] + amendopts + commands.walkopts + commands.commitopts,
_('hg amend [OPTION]...'))(amend)
def commit(orig, ui, repo, *pats, **opts):
if opts.get("amend"):
# commit --amend default behavior is to prompt for edit
opts['ignoremessage'] = True
return amend(ui, repo, *pats, **opts)
else:
return orig(ui, repo, *pats, **opts)
def amend(ui, repo, *pats, **opts):
'''amend the current commit with more changes
'''
rebase = opts.get('rebase')
if rebase and _histediting(repo):
# if a histedit is in flight, it's dangerous to remove old commits
hint = _('during histedit, use amend without --rebase')
raise util.Abort('histedit in progress', hint=hint)
fixup = opts.get('fixup')
if fixup:
fixupamend(ui, repo)
return
old = repo['.']
if old.phase() == phases.public:
raise util.Abort(_('cannot amend public changesets'))
if len(repo[None].parents()) > 1:
raise util.Abort(_('cannot amend while merging'))
haschildren = len(old.children()) > 0
ignoremessage = opts.get('ignoremessage')
if not ignoremessage:
opts['message'] = old.description()
tempnode = []
def commitfunc(ui, repo, message, match, opts):
e = cmdutil.commiteditor
noderesult = repo.commit(message,
old.user(),
old.date(),
match,
editor=e,
extra={})
# the temporary commit is the very first commit
if not tempnode:
tempnode.append(noderesult)
return noderesult
current = repo._bookmarkcurrent
oldbookmarks = old.bookmarks()
if haschildren:
def fakestrip(orig, ui, repo, *args, **kwargs):
if tempnode:
if tempnode[0]:
# don't strip everything, just the temp node
# this is very hacky
orig(ui, repo, tempnode[0], backup='none')
tempnode.pop()
else:
orig(ui, repo, *args, **kwargs)
extensions.wrapfunction(repair, 'strip', fakestrip)
node = cmdutil.amend(ui, repo, commitfunc, old, {}, pats, opts)
if node == old.node():
ui.status(_("nothing changed\n"))
return 1
if haschildren and not rebase:
msg = _("warning: the commit's children were left behind\n")
if _histediting(repo):
ui.warn(msg)
ui.status(_('(this is okay since a histedit is in progress)\n'))
else:
_usereducation(ui)
ui.warn(msg)
ui.status("(use 'hg amend --fixup' to rebase them)\n")
newbookmarks = repo._bookmarks
# move old bookmarks to new node
for bm in oldbookmarks:
newbookmarks[bm] = node
if not _histediting(repo):
preamendname = _preamendname(repo, node)
if haschildren:
newbookmarks[preamendname] = old.node()
elif not repo._bookmarkcurrent:
# update bookmark if it isn't based on the current bookmark name
oldname = _preamendname(repo, old.node())
if oldname in repo._bookmarks:
newbookmarks[preamendname] = repo._bookmarks[oldname]
del newbookmarks[oldname]
newbookmarks.write()
if rebase and haschildren:
fixupamend(ui, repo)
def fixupamend(ui, repo):
"""rebases any children found on the preamend commit and strips the
preamend commit
"""
current = repo['.']
preamendname = _preamendname(repo, current.node())
if not preamendname in repo._bookmarks:
raise util.Abort(_('no bookmark %s' % preamendname),
hint=_('check if your bookmark is active'))
ui.status("rebasing the children of %s\n" % (preamendname))
old = repo[preamendname]
oldbookmarks = old.bookmarks()
active = repo._bookmarkcurrent
opts = {
'rev' : [str(c.rev()) for c in old.descendants()],
'dest' : active
}
if opts['rev'] and opts['rev'][0]:
rebase.rebase(ui, repo, **opts)
repair.strip(ui, repo, old.node(), topic='preamend-backup')
for bookmark in oldbookmarks:
repo._bookmarks.pop(bookmark)
repo._bookmarks.write()
merge.update(repo, current.node(), False, True, False)
if active:
bookmarks.setcurrent(repo, active)
def _preamendname(repo, node):
suffix = '.preamend'
name = repo._bookmarkcurrent
if not name:
name = hex(node)[:12]
return name + suffix
def _histediting(repo):
return repo.vfs.exists('histedit-state')
def _usereducation(ui):
"""
You can print out a message to the user here
"""
pass