manifest.py
370 lines
| 12.0 KiB
| text/x-python
|
PythonLexer
|
r44961 | from __future__ import absolute_import | ||
from mercurial import ( | ||||
match as matchmod, | ||||
pathutil, | ||||
pycompat, | ||||
util, | ||||
) | ||||
from mercurial.interfaces import ( | ||||
repository, | ||||
util as interfaceutil, | ||||
) | ||||
from . import gitutil | ||||
|
r44968 | pygit2 = gitutil.get_pygit2() | ||
|
r44961 | @interfaceutil.implementer(repository.imanifestdict) | ||
class gittreemanifest(object): | ||||
"""Expose git trees (and optionally a builder's overlay) as a manifestdict. | ||||
Very similar to mercurial.manifest.treemanifest. | ||||
""" | ||||
def __init__(self, git_repo, root_tree, pending_changes): | ||||
"""Initializer. | ||||
Args: | ||||
git_repo: The git_repo we're walking (required to look up child | ||||
trees). | ||||
root_tree: The root Git tree object for this manifest. | ||||
pending_changes: A dict in which pending changes will be | ||||
tracked. The enclosing memgittreemanifestctx will use this to | ||||
construct any required Tree objects in Git during it's | ||||
`write()` method. | ||||
""" | ||||
self._git_repo = git_repo | ||||
self._tree = root_tree | ||||
if pending_changes is None: | ||||
pending_changes = {} | ||||
# dict of path: Optional[Tuple(node, flags)] | ||||
self._pending_changes = pending_changes | ||||
def _resolve_entry(self, path): | ||||
"""Given a path, load its node and flags, or raise KeyError if missing. | ||||
This takes into account any pending writes in the builder. | ||||
""" | ||||
upath = pycompat.fsdecode(path) | ||||
ent = None | ||||
if path in self._pending_changes: | ||||
val = self._pending_changes[path] | ||||
if val is None: | ||||
raise KeyError | ||||
return val | ||||
t = self._tree | ||||
comps = upath.split('/') | ||||
|
r45449 | te = self._tree | ||
|
r44961 | for comp in comps[:-1]: | ||
|
r45449 | te = te[comp] | ||
|
r44961 | t = self._git_repo[te.id] | ||
ent = t[comps[-1]] | ||||
if ent.filemode == pygit2.GIT_FILEMODE_BLOB: | ||||
flags = b'' | ||||
elif ent.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE: | ||||
flags = b'x' | ||||
elif ent.filemode == pygit2.GIT_FILEMODE_LINK: | ||||
flags = b'l' | ||||
else: | ||||
raise ValueError('unsupported mode %s' % oct(ent.filemode)) | ||||
return ent.id.raw, flags | ||||
def __getitem__(self, path): | ||||
return self._resolve_entry(path)[0] | ||||
def find(self, path): | ||||
return self._resolve_entry(path) | ||||
def __len__(self): | ||||
return len(list(self.walk(matchmod.always()))) | ||||
def __nonzero__(self): | ||||
try: | ||||
next(iter(self)) | ||||
return True | ||||
except StopIteration: | ||||
return False | ||||
__bool__ = __nonzero__ | ||||
def __contains__(self, path): | ||||
try: | ||||
self._resolve_entry(path) | ||||
return True | ||||
except KeyError: | ||||
return False | ||||
def iterkeys(self): | ||||
return self.walk(matchmod.always()) | ||||
def keys(self): | ||||
return list(self.iterkeys()) | ||||
def __iter__(self): | ||||
return self.iterkeys() | ||||
def __setitem__(self, path, node): | ||||
self._pending_changes[path] = node, self.flags(path) | ||||
def __delitem__(self, path): | ||||
# TODO: should probably KeyError for already-deleted files? | ||||
self._pending_changes[path] = None | ||||
def filesnotin(self, other, match=None): | ||||
if match is not None: | ||||
match = matchmod.badmatch(match, lambda path, msg: None) | ||||
sm2 = set(other.walk(match)) | ||||
return {f for f in self.walk(match) if f not in sm2} | ||||
return {f for f in self if f not in other} | ||||
@util.propertycache | ||||
def _dirs(self): | ||||
return pathutil.dirs(self) | ||||
def hasdir(self, dir): | ||||
return dir in self._dirs | ||||
|
r45450 | def diff(self, other, match=lambda x: True, clean=False): | ||
'''Finds changes between the current manifest and m2. | ||||
The result is returned as a dict with filename as key and | ||||
values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the | ||||
nodeid in the current/other manifest and fl1/fl2 is the flag | ||||
in the current/other manifest. Where the file does not exist, | ||||
the nodeid will be None and the flags will be the empty | ||||
string. | ||||
''' | ||||
result = {} | ||||
def _iterativediff(t1, t2, subdir): | ||||
"""compares two trees and appends new tree nodes to examine to | ||||
the stack""" | ||||
if t1 is None: | ||||
t1 = {} | ||||
if t2 is None: | ||||
t2 = {} | ||||
for e1 in t1: | ||||
realname = subdir + pycompat.fsencode(e1.name) | ||||
if e1.type == pygit2.GIT_OBJ_TREE: | ||||
try: | ||||
e2 = t2[e1.name] | ||||
if e2.type != pygit2.GIT_OBJ_TREE: | ||||
e2 = None | ||||
except KeyError: | ||||
e2 = None | ||||
stack.append((realname + b'/', e1, e2)) | ||||
else: | ||||
n1, fl1 = self.find(realname) | ||||
try: | ||||
e2 = t2[e1.name] | ||||
n2, fl2 = other.find(realname) | ||||
except KeyError: | ||||
e2 = None | ||||
n2, fl2 = (None, b'') | ||||
if e2 is not None and e2.type == pygit2.GIT_OBJ_TREE: | ||||
stack.append((realname + b'/', None, e2)) | ||||
if not match(realname): | ||||
continue | ||||
if n1 != n2 or fl1 != fl2: | ||||
result[realname] = ((n1, fl1), (n2, fl2)) | ||||
elif clean: | ||||
result[realname] = None | ||||
for e2 in t2: | ||||
if e2.name in t1: | ||||
continue | ||||
realname = subdir + pycompat.fsencode(e2.name) | ||||
if e2.type == pygit2.GIT_OBJ_TREE: | ||||
stack.append((realname + b'/', None, e2)) | ||||
elif match(realname): | ||||
n2, fl2 = other.find(realname) | ||||
result[realname] = ((None, b''), (n2, fl2)) | ||||
stack = [] | ||||
_iterativediff(self._tree, other._tree, b'') | ||||
while stack: | ||||
subdir, t1, t2 = stack.pop() | ||||
# stack is populated in the function call | ||||
_iterativediff(t1, t2, subdir) | ||||
return result | ||||
|
r44961 | |||
def setflag(self, path, flag): | ||||
node, unused_flag = self._resolve_entry(path) | ||||
self._pending_changes[path] = node, flag | ||||
def get(self, path, default=None): | ||||
try: | ||||
return self._resolve_entry(path)[0] | ||||
except KeyError: | ||||
return default | ||||
def flags(self, path): | ||||
try: | ||||
return self._resolve_entry(path)[1] | ||||
except KeyError: | ||||
return b'' | ||||
def copy(self): | ||||
|
r45986 | return gittreemanifest( | ||
self._git_repo, self._tree, dict(self._pending_changes) | ||||
) | ||||
|
r44961 | |||
def items(self): | ||||
for f in self: | ||||
# TODO: build a proper iterator version of this | ||||
yield self[f] | ||||
def iteritems(self): | ||||
return self.items() | ||||
def iterentries(self): | ||||
for f in self: | ||||
# TODO: build a proper iterator version of this | ||||
yield self._resolve_entry(f) | ||||
def text(self): | ||||
assert False # TODO can this method move out of the manifest iface? | ||||
def _walkonetree(self, tree, match, subdir): | ||||
for te in tree: | ||||
# TODO: can we prune dir walks with the matcher? | ||||
realname = subdir + pycompat.fsencode(te.name) | ||||
|
r45447 | if te.type == pygit2.GIT_OBJ_TREE: | ||
|
r44961 | for inner in self._walkonetree( | ||
self._git_repo[te.id], match, realname + b'/' | ||||
): | ||||
yield inner | ||||
|
r45448 | elif match(realname): | ||
yield pycompat.fsencode(realname) | ||||
|
r44961 | |||
def walk(self, match): | ||||
# TODO: this is a very lazy way to merge in the pending | ||||
# changes. There is absolutely room for optimization here by | ||||
# being clever about walking over the sets... | ||||
baseline = set(self._walkonetree(self._tree, match, b'')) | ||||
deleted = {p for p, v in self._pending_changes.items() if v is None} | ||||
pend = {p for p in self._pending_changes if match(p)} | ||||
return iter(sorted((baseline | pend) - deleted)) | ||||
@interfaceutil.implementer(repository.imanifestrevisionstored) | ||||
class gittreemanifestctx(object): | ||||
def __init__(self, repo, gittree): | ||||
self._repo = repo | ||||
self._tree = gittree | ||||
def read(self): | ||||
return gittreemanifest(self._repo, self._tree, None) | ||||
|
r44963 | def readfast(self, shallow=False): | ||
return self.read() | ||||
|
r44961 | def copy(self): | ||
# NB: it's important that we return a memgittreemanifestctx | ||||
# because the caller expects a mutable manifest. | ||||
return memgittreemanifestctx(self._repo, self._tree) | ||||
def find(self, path): | ||||
|
r45361 | return self.read()[path] | ||
|
r44961 | |||
@interfaceutil.implementer(repository.imanifestrevisionwritable) | ||||
class memgittreemanifestctx(object): | ||||
def __init__(self, repo, tree): | ||||
self._repo = repo | ||||
self._tree = tree | ||||
# dict of path: Optional[Tuple(node, flags)] | ||||
self._pending_changes = {} | ||||
def read(self): | ||||
return gittreemanifest(self._repo, self._tree, self._pending_changes) | ||||
def copy(self): | ||||
# TODO: if we have a builder in play, what should happen here? | ||||
# Maybe we can shuffle copy() into the immutable interface. | ||||
return memgittreemanifestctx(self._repo, self._tree) | ||||
def write(self, transaction, link, p1, p2, added, removed, match=None): | ||||
# We're not (for now, anyway) going to audit filenames, so we | ||||
# can ignore added and removed. | ||||
# TODO what does this match argument get used for? hopefully | ||||
# just narrow? | ||||
assert not match or isinstance(match, matchmod.alwaysmatcher) | ||||
|
r45115 | touched_dirs = pathutil.dirs(list(self._pending_changes)) | ||
|
r44961 | trees = { | ||
b'': self._tree, | ||||
} | ||||
# path: treebuilder | ||||
builders = { | ||||
b'': self._repo.TreeBuilder(self._tree), | ||||
} | ||||
# get a TreeBuilder for every tree in the touched_dirs set | ||||
for d in sorted(touched_dirs, key=lambda x: (len(x), x)): | ||||
if d == b'': | ||||
# loaded root tree above | ||||
continue | ||||
comps = d.split(b'/') | ||||
full = b'' | ||||
for part in comps: | ||||
parent = trees[full] | ||||
try: | ||||
|
r46089 | parent_tree_id = parent[pycompat.fsdecode(part)].id | ||
new = self._repo[parent_tree_id] | ||||
|
r44961 | except KeyError: | ||
# new directory | ||||
new = None | ||||
full += b'/' + part | ||||
if new is not None: | ||||
# existing directory | ||||
trees[full] = new | ||||
builders[full] = self._repo.TreeBuilder(new) | ||||
else: | ||||
# new directory, use an empty dict to easily | ||||
# generate KeyError as any nested new dirs get | ||||
# created. | ||||
trees[full] = {} | ||||
builders[full] = self._repo.TreeBuilder() | ||||
for f, info in self._pending_changes.items(): | ||||
if b'/' not in f: | ||||
dirname = b'' | ||||
basename = f | ||||
else: | ||||
dirname, basename = f.rsplit(b'/', 1) | ||||
dirname = b'/' + dirname | ||||
if info is None: | ||||
builders[dirname].remove(pycompat.fsdecode(basename)) | ||||
else: | ||||
n, fl = info | ||||
mode = { | ||||
b'': pygit2.GIT_FILEMODE_BLOB, | ||||
b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE, | ||||
b'l': pygit2.GIT_FILEMODE_LINK, | ||||
}[fl] | ||||
builders[dirname].insert( | ||||
pycompat.fsdecode(basename), gitutil.togitnode(n), mode | ||||
) | ||||
# This visits the buffered TreeBuilders in deepest-first | ||||
# order, bubbling up the edits. | ||||
for b in sorted(builders, key=len, reverse=True): | ||||
if b == b'': | ||||
break | ||||
cb = builders[b] | ||||
dn, bn = b.rsplit(b'/', 1) | ||||
builders[dn].insert( | ||||
pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE | ||||
) | ||||
return builders[b''].write().raw | ||||