mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 00:14:35 +03:00
af8ecd5f80
Summary: Rewriting a set of commits where there are replacement relationship among the commits do not have an optimal UX today. For example, `rebase -s A -d Z` or `metaedit A` in the below graph. B1, B2, C will all be replaced. But the new B1 and B2 replacement won't have the B1 -> B2 relationship, and the "new B1" appears to be revived. ``` o C | x B1 (amended as B2) | | o B2 |/ o A o Z ``` One solution is to avoid rebasing `obsolete()::`, as implemented in D7067121 for metaedit. That would result in ``` o C | x B1 (amended as B2) o new B2 | | x A o new A ``` The stack of A, B1, C is forced to break into two parts. This is fine for power users. But n00b users would wonder why C is left behind. Per discussion with simpkins at an internal post about the metaedit case, we think a more linear history is more user-friendly. That is: ``` o new C | x new B1 (amended as *new* B2) | | o new B2 |/ o new A ``` The stack stays in a same shape. This diff implements the "copying obsmarkers" behavior at the "createmarkers" level so everything using that API would get the feature for free, including metaedit and rebase. D7067121 is reverted since the new UX is preferred. The test added is for `metaedit` command, changes to rebase will be added in a later patch. Differential Revision: D7121487 fbshipit-source-id: fd3c8a96ab434b131fb86d9882ccbdff8f63f05e
238 lines
8.5 KiB
Python
238 lines
8.5 KiB
Python
# common.py - common utilities for building commands
|
|
#
|
|
# Copyright 2016 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 collections import defaultdict
|
|
|
|
from hgext import rebase
|
|
from mercurial import (
|
|
cmdutil,
|
|
context,
|
|
copies,
|
|
error,
|
|
extensions,
|
|
lock as lockmod,
|
|
)
|
|
from mercurial.i18n import _
|
|
from mercurial.node import nullrev
|
|
|
|
def getchildrelationships(repo, revs):
|
|
"""Build a defaultdict of child relationships between all descendants of
|
|
revs. This information will prevent us from having to repeatedly
|
|
perform children that reconstruct these relationships each time.
|
|
"""
|
|
cl = repo.changelog
|
|
children = defaultdict(set)
|
|
for rev in repo.revs('(%ld)::', revs):
|
|
for parent in cl.parentrevs(rev):
|
|
if parent != nullrev:
|
|
children[parent].add(rev)
|
|
return children
|
|
|
|
def restackonce(ui, repo, rev, rebaseopts=None, childrenonly=False):
|
|
"""Rebase all descendants of precursors of rev onto rev, thereby
|
|
stabilzing any non-obsolete descendants of those precursors.
|
|
Takes in an optional dict of options for the rebase command.
|
|
If childrenonly is True, only rebases direct children of precursors
|
|
of rev rather than all descendants of those precursors.
|
|
"""
|
|
# Get visible descendants of precusors of rev.
|
|
# Excluding obsoleted changesets avoids divergence issues.
|
|
allpredecessors = repo.revs('allpredecessors(%d)', rev)
|
|
fmt = ('%s(%%ld) - %%ld - obsolete()'
|
|
% ('children' if childrenonly else 'descendants'))
|
|
descendants = repo.revs(fmt, allpredecessors, allpredecessors)
|
|
|
|
# Nothing to do if there are no descendants.
|
|
if not descendants:
|
|
return
|
|
|
|
# Overwrite source and destination, leave all other options.
|
|
if rebaseopts is None:
|
|
rebaseopts = {}
|
|
rebaseopts['rev'] = descendants
|
|
rebaseopts['dest'] = rev
|
|
|
|
# We need to ensure that the 'operation' field in the obsmarker metadata
|
|
# is always set to 'rebase', regardless of the current command so that
|
|
# the restacked commits will appear as 'rebased' in smartlog.
|
|
overrides = {}
|
|
try:
|
|
tweakdefaults = extensions.find('tweakdefaults')
|
|
except KeyError:
|
|
# No tweakdefaults extension -- skip this since there is no wrapper
|
|
# to set the metadata.
|
|
pass
|
|
else:
|
|
overrides[(tweakdefaults.globaldata,
|
|
tweakdefaults.createmarkersoperation)] = 'rebase'
|
|
|
|
# Perform rebase.
|
|
with repo.ui.configoverride(overrides, 'restack'):
|
|
rebase.rebase(ui, repo, **rebaseopts)
|
|
|
|
def latest(repo, rev):
|
|
"""Find the "latest version" of the given revision -- either the
|
|
latest visible successor, or the revision itself if it has no
|
|
visible successors.
|
|
"""
|
|
latest = repo.revs('allsuccessors(%d)', rev).last()
|
|
return latest if latest is not None else rev
|
|
|
|
def bookmarksupdater(repo, oldids, tr):
|
|
"""Return a callable update(newid) updating the current bookmark
|
|
and bookmarks bound to oldid to newid.
|
|
"""
|
|
if type(oldids) is bytes:
|
|
oldids = [oldids]
|
|
def updatebookmarks(newid):
|
|
dirty = False
|
|
for oldid in oldids:
|
|
changes = []
|
|
oldbookmarks = repo.nodebookmarks(oldid)
|
|
if oldbookmarks:
|
|
for b in oldbookmarks:
|
|
changes.append((b, newid))
|
|
dirty = True
|
|
if dirty:
|
|
repo._bookmarks.applychanges(repo, tr, changes)
|
|
return updatebookmarks
|
|
|
|
def rewrite(repo, old, updates, head, newbases, commitopts):
|
|
"""Return (nodeid, created) where nodeid is the identifier of the
|
|
changeset generated by the rewrite process, and created is True if
|
|
nodeid was actually created. If created is False, nodeid
|
|
references a changeset existing before the rewrite call.
|
|
"""
|
|
wlock = lock = tr = None
|
|
try:
|
|
wlock = repo.wlock()
|
|
lock = repo.lock()
|
|
tr = repo.transaction('rewrite')
|
|
if len(old.parents()) > 1: # XXX remove this unnecessary limitation.
|
|
raise error.Abort(_('cannot amend merge changesets'))
|
|
base = old.p1()
|
|
updatebookmarks = bookmarksupdater(
|
|
repo, [old.node()] + [u.node() for u in updates], tr)
|
|
|
|
# commit a new version of the old changeset, including the update
|
|
# collect all files which might be affected
|
|
files = set(old.files())
|
|
for u in updates:
|
|
files.update(u.files())
|
|
|
|
# Recompute copies (avoid recording a -> b -> a)
|
|
copied = copies.pathcopies(base, head)
|
|
|
|
# prune files which were reverted by the updates
|
|
def samefile(f):
|
|
if f in head.manifest():
|
|
a = head.filectx(f)
|
|
if f in base.manifest():
|
|
b = base.filectx(f)
|
|
return (a.data() == b.data()
|
|
and a.flags() == b.flags())
|
|
else:
|
|
return False
|
|
else:
|
|
return f not in base.manifest()
|
|
files = [f for f in files if not samefile(f)]
|
|
# commit version of these files as defined by head
|
|
headmf = head.manifest()
|
|
|
|
def filectxfn(repo, ctx, path):
|
|
if path in headmf:
|
|
fctx = head[path]
|
|
flags = fctx.flags()
|
|
mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(),
|
|
islink='l' in flags,
|
|
isexec='x' in flags,
|
|
copied=copied.get(path))
|
|
return mctx
|
|
return None
|
|
|
|
message = cmdutil.logmessage(repo.ui, commitopts)
|
|
if not message:
|
|
message = old.description()
|
|
|
|
user = commitopts.get('user') or old.user()
|
|
# TODO: In case not date is given, we should take the old commit date
|
|
# if we are working one one changeset or mimic the fold behavior about
|
|
# date
|
|
date = commitopts.get('date') or None
|
|
extra = dict(commitopts.get('extra', old.extra()))
|
|
extra['branch'] = head.branch()
|
|
|
|
new = context.memctx(repo,
|
|
parents=newbases,
|
|
text=message,
|
|
files=files,
|
|
filectxfn=filectxfn,
|
|
user=user,
|
|
date=date,
|
|
extra=extra)
|
|
|
|
if commitopts.get('edit'):
|
|
new._text = cmdutil.commitforceeditor(repo, new, [])
|
|
revcount = len(repo)
|
|
newid = repo.commitctx(new)
|
|
new = repo[newid]
|
|
created = len(repo) != revcount
|
|
updatebookmarks(newid)
|
|
|
|
tr.close()
|
|
return newid, created
|
|
finally:
|
|
lockmod.release(tr, lock, wlock)
|
|
|
|
def metarewrite(repo, old, newbases, commitopts):
|
|
"""Return (nodeid, created) where nodeid is the identifier of the
|
|
changeset generated by the rewrite process, and created is True if
|
|
nodeid was actually created. If created is False, nodeid
|
|
references a changeset existing before the rewrite call.
|
|
"""
|
|
wlock = lock = tr = None
|
|
try:
|
|
wlock = repo.wlock()
|
|
lock = repo.lock()
|
|
tr = repo.transaction('rewrite')
|
|
updatebookmarks = bookmarksupdater(repo, old.node(), tr)
|
|
|
|
message = cmdutil.logmessage(repo.ui, commitopts)
|
|
if not message:
|
|
message = old.description()
|
|
|
|
user = commitopts.get('user') or old.user()
|
|
date = commitopts.get('date') or None # old.date()
|
|
extra = dict(commitopts.get('extra', old.extra()))
|
|
extra['branch'] = old.branch()
|
|
|
|
new = context.metadataonlyctx(repo,
|
|
old,
|
|
parents=newbases,
|
|
text=message,
|
|
user=user,
|
|
date=date,
|
|
extra=extra)
|
|
|
|
if commitopts.get('edit'):
|
|
new._text = cmdutil.commitforceeditor(repo, new, [])
|
|
revcount = len(repo)
|
|
newid = repo.commitctx(new)
|
|
new = repo[newid]
|
|
created = len(repo) != revcount
|
|
updatebookmarks(newid)
|
|
|
|
tr.close()
|
|
return newid, created
|
|
finally:
|
|
lockmod.release(tr, lock, wlock)
|
|
|
|
def newunstable(repo, revs):
|
|
return repo.revs("(%ld::) - %ld", revs, revs)
|