2018-09-26 02:04:23 +03:00
|
|
|
# stackpush - specialized pushrebase
|
|
|
|
#
|
|
|
|
# Copyright 2018 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.
|
|
|
|
"""
|
|
|
|
push a stack of linear commits to the destination.
|
|
|
|
|
|
|
|
Typically a push looks like this:
|
|
|
|
|
|
|
|
F onto bookmark (in critical section)
|
|
|
|
.
|
|
|
|
.
|
|
|
|
E onto bookmark (outside critical section)
|
|
|
|
.
|
|
|
|
. D stack top
|
|
|
|
| .
|
|
|
|
| .
|
|
|
|
| C
|
|
|
|
| |
|
|
|
|
| B stack bottom
|
|
|
|
|/
|
|
|
|
A stack parent
|
|
|
|
|
|
|
|
Pushrebase would need to check files changed in B::D are not touched in A::F.
|
|
|
|
|
|
|
|
stackpush tries to minimize steps inside the critical section:
|
|
|
|
|
|
|
|
1. Avoid constructing a bundle repo in the critical section.
|
|
|
|
Instead, collect all the data needed for *checking* and pushing B::D
|
|
|
|
beforehand. That is, a {path: old_filenode} map for checking, and
|
|
|
|
[(commit_metadata, {path: new_file})] for pushing.
|
|
|
|
2. Only check F's manifest for the final decision for conflicts.
|
|
|
|
Do not read E::F in the critical section.
|
|
|
|
"""
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
2019-01-30 03:25:33 +03:00
|
|
|
from edenscm.mercurial import context, error, mutation
|
2019-02-26 22:12:38 +03:00
|
|
|
from edenscm.mercurial.i18n import _
|
2019-02-01 20:39:54 +03:00
|
|
|
from edenscm.mercurial.node import hex, nullid, nullrev
|
2018-09-26 02:04:23 +03:00
|
|
|
|
|
|
|
from .errors import ConflictsError, StackPushUnsupportedError
|
|
|
|
|
|
|
|
|
|
|
|
class pushcommit(object):
|
2019-02-21 20:55:42 +03:00
|
|
|
def __init__(
|
|
|
|
self, user, date, desc, extra, filechanges, examinepaths, orignode=None
|
|
|
|
):
|
2019-02-26 22:12:38 +03:00
|
|
|
"""constructor for pushcommit
|
|
|
|
|
|
|
|
This class is designed to only include simple types (list, dict,
|
|
|
|
strings), without coupling with Mercurial internals, for maximum
|
|
|
|
portability.
|
|
|
|
|
|
|
|
Do not add states that are not simple types (ex. repo, ui, or bundle).
|
|
|
|
"""
|
2018-09-26 02:04:23 +03:00
|
|
|
self.user = user
|
|
|
|
self.date = date
|
|
|
|
self.desc = desc
|
|
|
|
self.extra = extra
|
|
|
|
self.filechanges = filechanges # {path: (mode, content, copysource) | None}
|
|
|
|
self.examinepaths = examinepaths # {path}
|
2019-02-21 20:55:42 +03:00
|
|
|
self.orignode = orignode
|
2018-09-26 02:04:23 +03:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def fromctx(cls, ctx):
|
|
|
|
filechanges = {}
|
|
|
|
examinepaths = set(ctx.files())
|
|
|
|
for path in ctx.files():
|
|
|
|
try:
|
|
|
|
fctx = ctx[path]
|
|
|
|
except error.ManifestLookupError:
|
|
|
|
filechanges[path] = None
|
|
|
|
else:
|
|
|
|
if fctx.rawflags():
|
|
|
|
raise StackPushUnsupportedError("stackpush does not support LFS")
|
|
|
|
renamed = fctx.renamed()
|
|
|
|
if renamed:
|
|
|
|
copysource = renamed[0]
|
|
|
|
examinepaths.add(copysource)
|
|
|
|
else:
|
|
|
|
copysource = None
|
|
|
|
filechanges[path] = (fctx.flags(), fctx.data(), copysource)
|
|
|
|
return cls(
|
|
|
|
ctx.user(),
|
|
|
|
ctx.date(),
|
|
|
|
ctx.description(),
|
|
|
|
ctx.extra(),
|
|
|
|
filechanges,
|
|
|
|
examinepaths,
|
2019-02-21 20:55:42 +03:00
|
|
|
orignode=ctx.node(),
|
2018-09-26 02:04:23 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class pushrequest(object):
|
2019-02-26 22:12:38 +03:00
|
|
|
def __init__(self, stackparentnode, pushcommits, fileconditions):
|
|
|
|
"""constructor for pushrequest
|
|
|
|
|
|
|
|
This class is designed to only include simple types (list, dict,
|
|
|
|
strings), without coupling with Mercurial internals, for maximum
|
|
|
|
portability.
|
|
|
|
|
|
|
|
Do not add states that are not simple types (ex. repo, ui, or bundle).
|
|
|
|
"""
|
|
|
|
|
2018-09-26 02:04:23 +03:00
|
|
|
self.stackparentnode = stackparentnode
|
|
|
|
self.pushcommits = pushcommits
|
|
|
|
self.fileconditions = fileconditions # {path: None | filenode}
|
|
|
|
|
|
|
|
@classmethod
|
2019-02-26 22:12:38 +03:00
|
|
|
def fromrevset(cls, repo, spec):
|
2018-09-26 02:04:23 +03:00
|
|
|
"""Construct a pushrequest from revset"""
|
|
|
|
# No merge commits allowed.
|
|
|
|
revs = list(repo.revs(spec))
|
|
|
|
if repo.revs("%ld and merge()", revs):
|
|
|
|
raise StackPushUnsupportedError("stackpush does not support merges")
|
|
|
|
parentrevs = list(repo.revs("parents(%ld)-%ld", revs, revs))
|
|
|
|
if len(parentrevs) > 1:
|
|
|
|
raise StackPushUnsupportedError(
|
|
|
|
"stackpush only supports single linear stack"
|
|
|
|
)
|
|
|
|
|
|
|
|
examinepaths = set()
|
|
|
|
|
|
|
|
# calculate "pushcommit"s, and paths to examine
|
|
|
|
pushcommits = []
|
|
|
|
for rev in revs:
|
|
|
|
ctx = repo[rev]
|
|
|
|
commit = pushcommit.fromctx(ctx)
|
|
|
|
examinepaths.update(commit.examinepaths)
|
|
|
|
pushcommits.append(commit)
|
|
|
|
|
|
|
|
parentctx = repo[(parentrevs + [nullrev])[0]]
|
2019-02-26 22:12:38 +03:00
|
|
|
return cls(
|
|
|
|
parentctx.node(),
|
|
|
|
pushcommits,
|
|
|
|
cls._calculatefileconditions(parentctx, examinepaths),
|
|
|
|
)
|
|
|
|
|
2019-02-26 22:12:38 +03:00
|
|
|
@classmethod
|
|
|
|
def frommemcommit(cls, repo, commitparams):
|
|
|
|
changelist = commitparams.changelist
|
|
|
|
metadata = commitparams.metadata
|
|
|
|
|
|
|
|
files = changelist.files
|
|
|
|
filechanges = {}
|
|
|
|
examinepaths = set(files.keys())
|
|
|
|
|
|
|
|
for path, info in files.iteritems():
|
|
|
|
if info.deleted:
|
|
|
|
filechanges[path] = None
|
|
|
|
else:
|
|
|
|
copysource = info.copysource
|
|
|
|
if copysource:
|
|
|
|
examinepaths.add(copysource)
|
|
|
|
filechanges[path] = (info.flags, info.content, copysource)
|
|
|
|
|
|
|
|
commit = pushcommit(
|
|
|
|
metadata.author,
|
|
|
|
None,
|
|
|
|
metadata.description,
|
|
|
|
metadata.extra,
|
|
|
|
filechanges,
|
|
|
|
examinepaths,
|
|
|
|
)
|
|
|
|
|
2019-03-13 20:01:31 +03:00
|
|
|
def resolveparentctx(repo, originalparent):
|
|
|
|
if not originalparent:
|
2019-02-26 22:12:38 +03:00
|
|
|
raise error.Abort(_("parent commit must be specified"))
|
|
|
|
|
2019-03-13 20:01:31 +03:00
|
|
|
return repo[originalparent]
|
2019-02-26 22:12:38 +03:00
|
|
|
|
2019-03-13 20:01:31 +03:00
|
|
|
p1 = resolveparentctx(repo, changelist.parent)
|
2019-02-26 22:12:38 +03:00
|
|
|
return cls(p1.node(), [commit], cls._calculatefileconditions(p1, examinepaths))
|
|
|
|
|
2019-02-26 22:12:38 +03:00
|
|
|
@staticmethod
|
|
|
|
def _calculatefileconditions(parentctx, examinepaths):
|
|
|
|
"""calculate 'fileconditions' - filenodes in the signal parent commit
|
|
|
|
"""
|
2018-09-26 02:04:23 +03:00
|
|
|
parentmanifest = parentctx.manifestctx()
|
|
|
|
fileconditions = {}
|
|
|
|
for path in examinepaths:
|
|
|
|
try:
|
|
|
|
filenodemode = parentmanifest.find(path)
|
|
|
|
except KeyError:
|
|
|
|
filenodemode = None
|
|
|
|
fileconditions[path] = filenodemode
|
|
|
|
|
2019-02-26 22:12:38 +03:00
|
|
|
return fileconditions
|
2018-09-26 02:04:23 +03:00
|
|
|
|
2019-02-26 22:12:38 +03:00
|
|
|
def pushonto(self, ctx, getcommitdatefn=None):
|
2018-09-26 02:04:23 +03:00
|
|
|
"""Push the stack onto ctx
|
|
|
|
|
2019-02-26 22:12:38 +03:00
|
|
|
getcommitdatefn is a functor:
|
|
|
|
|
|
|
|
(ui, originalcommithash, originalcommitdate) -> replacementcommitdate
|
|
|
|
|
|
|
|
to allow rewriting replacement commit time as a function of the original
|
|
|
|
commit hash and time. Therefore, it is not required for creating new
|
|
|
|
commits.
|
|
|
|
|
2018-09-26 02:04:23 +03:00
|
|
|
Return (added, replacements)
|
|
|
|
"""
|
|
|
|
self.check(ctx)
|
2019-02-26 22:12:38 +03:00
|
|
|
return self._pushunchecked(ctx, getcommitdatefn=getcommitdatefn)
|
2018-09-26 02:04:23 +03:00
|
|
|
|
|
|
|
def check(self, ctx):
|
|
|
|
"""Check if push onto ctx can be done
|
|
|
|
|
|
|
|
Raise ConflictsError if there are conflicts.
|
|
|
|
"""
|
|
|
|
mctx = ctx.manifestctx()
|
|
|
|
conflicts = []
|
|
|
|
for path, expected in self.fileconditions.iteritems():
|
|
|
|
try:
|
|
|
|
actual = mctx.find(path)
|
|
|
|
except KeyError:
|
|
|
|
actual = None
|
|
|
|
if actual != expected:
|
|
|
|
conflicts.append(path)
|
|
|
|
if conflicts:
|
|
|
|
raise ConflictsError(conflicts)
|
|
|
|
|
2019-02-26 22:12:38 +03:00
|
|
|
def _pushunchecked(self, ctx, getcommitdatefn=None):
|
2018-09-26 02:04:23 +03:00
|
|
|
added = []
|
|
|
|
replacements = {}
|
|
|
|
repo = ctx.repo()
|
|
|
|
for commit in self.pushcommits:
|
2019-02-26 22:12:38 +03:00
|
|
|
newnode = self._pushsingleunchecked(
|
|
|
|
ctx, commit, getcommitdatefn=getcommitdatefn
|
|
|
|
)
|
2018-09-26 02:04:23 +03:00
|
|
|
added.append(newnode)
|
2019-02-21 20:55:42 +03:00
|
|
|
orignode = commit.orignode
|
|
|
|
if orignode:
|
|
|
|
replacements[orignode] = newnode
|
2018-09-26 02:04:23 +03:00
|
|
|
ctx = repo[newnode]
|
|
|
|
return added, replacements
|
|
|
|
|
|
|
|
@staticmethod
|
2019-02-26 22:12:38 +03:00
|
|
|
def _pushsingleunchecked(ctx, commit, getcommitdatefn=None):
|
2018-09-26 02:04:23 +03:00
|
|
|
"""Return newly pushed node"""
|
|
|
|
repo = ctx.repo()
|
|
|
|
|
|
|
|
def getfilectx(repo, memctx, path):
|
|
|
|
assert path in commit.filechanges
|
|
|
|
entry = commit.filechanges[path]
|
|
|
|
if entry is None:
|
|
|
|
# deleted
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
# changed or created
|
|
|
|
mode, content, copysource = entry
|
|
|
|
return context.memfilectx(
|
|
|
|
repo,
|
|
|
|
memctx,
|
|
|
|
path,
|
|
|
|
content,
|
|
|
|
islink=("l" in mode),
|
|
|
|
isexec=("x" in mode),
|
|
|
|
copied=copysource,
|
|
|
|
)
|
|
|
|
|
2018-12-13 21:41:53 +03:00
|
|
|
extra = commit.extra.copy()
|
2019-02-21 20:55:42 +03:00
|
|
|
date = commit.date
|
2019-04-11 01:12:54 +03:00
|
|
|
loginfo = {}
|
2019-02-21 20:55:42 +03:00
|
|
|
|
|
|
|
orignode = commit.orignode
|
|
|
|
if orignode:
|
|
|
|
mutation.record(repo, extra, [orignode], "pushrebase")
|
2019-04-11 01:12:54 +03:00
|
|
|
loginfo = {"predecessors": hex(orignode), "mutation": "pushrebase"}
|
2019-02-26 22:12:38 +03:00
|
|
|
date = getcommitdatefn(repo.ui, hex(orignode), commit.date)
|
2018-12-13 21:41:53 +03:00
|
|
|
|
2018-09-26 02:04:23 +03:00
|
|
|
return context.memctx(
|
|
|
|
repo,
|
|
|
|
[ctx.node(), nullid],
|
|
|
|
commit.desc,
|
|
|
|
sorted(commit.filechanges),
|
|
|
|
getfilectx,
|
|
|
|
commit.user,
|
|
|
|
date,
|
2018-12-13 21:41:53 +03:00
|
|
|
extra,
|
2019-04-11 01:12:54 +03:00
|
|
|
loginfo=loginfo,
|
2018-09-26 02:04:23 +03:00
|
|
|
).commit()
|