manifest.py
399 lines
| 13.3 KiB
| text/x-python
|
PythonLexer
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Matt Harbison
|
r53389 | import typing | ||
from typing import ( | ||||
Any, | ||||
Matt Harbison
|
r53391 | Iterable, | ||
Matt Harbison
|
r53389 | Iterator, | ||
Set, | ||||
) | ||||
Augie Fackler
|
r44961 | from mercurial import ( | ||
match as matchmod, | ||||
pathutil, | ||||
pycompat, | ||||
util, | ||||
) | ||||
from mercurial.interfaces import ( | ||||
repository, | ||||
) | ||||
from . import gitutil | ||||
Matt Harbison
|
r53389 | if typing.TYPE_CHECKING: | ||
from typing import ( | ||||
ByteString, # TODO: change to Buffer for 3.14 | ||||
) | ||||
Augie Fackler
|
r44961 | |||
Martin von Zweigbergk
|
r44968 | pygit2 = gitutil.get_pygit2() | ||
Matt Harbison
|
r53392 | class gittreemanifest(repository.imanifestdict): | ||
Augie Fackler
|
r44961 | """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 | ||||
Matt Harbison
|
r53388 | def _resolve_entry(self, path) -> tuple[bytes, bytes]: | ||
Augie Fackler
|
r44961 | """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('/') | ||||
Josef 'Jeff' Sipek
|
r45449 | te = self._tree | ||
Augie Fackler
|
r44961 | for comp in comps[:-1]: | ||
Josef 'Jeff' Sipek
|
r45449 | te = te[comp] | ||
Augie Fackler
|
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 | ||||
Matt Harbison
|
r53389 | def __getitem__(self, path: bytes) -> bytes: | ||
Augie Fackler
|
r44961 | return self._resolve_entry(path)[0] | ||
Matt Harbison
|
r53389 | def find(self, path: bytes) -> tuple[bytes, bytes]: | ||
Augie Fackler
|
r44961 | return self._resolve_entry(path) | ||
Matt Harbison
|
r53389 | def __len__(self) -> int: | ||
Augie Fackler
|
r44961 | return len(list(self.walk(matchmod.always()))) | ||
Matt Harbison
|
r53389 | def __nonzero__(self) -> bool: | ||
Augie Fackler
|
r44961 | try: | ||
next(iter(self)) | ||||
return True | ||||
except StopIteration: | ||||
return False | ||||
__bool__ = __nonzero__ | ||||
Matt Harbison
|
r53389 | def __contains__(self, path: bytes) -> bool: | ||
Augie Fackler
|
r44961 | try: | ||
self._resolve_entry(path) | ||||
return True | ||||
except KeyError: | ||||
return False | ||||
Matt Harbison
|
r53389 | def iterkeys(self) -> Iterator[bytes]: | ||
Augie Fackler
|
r44961 | return self.walk(matchmod.always()) | ||
Matt Harbison
|
r53389 | def keys(self) -> list[bytes]: | ||
Augie Fackler
|
r44961 | return list(self.iterkeys()) | ||
Matt Harbison
|
r53389 | def __iter__(self) -> Iterator[bytes]: | ||
Augie Fackler
|
r44961 | return self.iterkeys() | ||
Matt Harbison
|
r53391 | def set(self, path: bytes, node: bytes, flags: bytes) -> None: | ||
raise NotImplementedError # TODO: implement this | ||||
Matt Harbison
|
r53389 | def __setitem__(self, path: bytes, node: bytes) -> None: | ||
Augie Fackler
|
r44961 | self._pending_changes[path] = node, self.flags(path) | ||
Matt Harbison
|
r53389 | def __delitem__(self, path: bytes) -> None: | ||
Augie Fackler
|
r44961 | # TODO: should probably KeyError for already-deleted files? | ||
self._pending_changes[path] = None | ||||
Matt Harbison
|
r53389 | def filesnotin(self, other, match=None) -> Set[bytes]: | ||
Augie Fackler
|
r44961 | 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) | ||||
Matt Harbison
|
r53391 | def dirs(self) -> pathutil.dirs: | ||
return self._dirs # TODO: why is there a prpoertycache? | ||||
Matt Harbison
|
r53389 | def hasdir(self, dir: bytes) -> bool: | ||
Augie Fackler
|
r44961 | return dir in self._dirs | ||
Matt Harbison
|
r53389 | def diff( | ||
self, | ||||
other: Any, # TODO: 'manifestdict' or (better) equivalent interface | ||||
match: Any = lambda x: True, # TODO: Optional[matchmod.basematcher] = None, | ||||
clean: bool = False, | ||||
) -> dict[ | ||||
bytes, | ||||
tuple[tuple[bytes | None, bytes], tuple[bytes | None, bytes]] | None, | ||||
]: | ||||
Augie Fackler
|
r46554 | """Finds changes between the current manifest and m2. | ||
Josef 'Jeff' Sipek
|
r45450 | |||
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. | ||||
Augie Fackler
|
r46554 | """ | ||
Josef 'Jeff' Sipek
|
r45450 | 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 | ||||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53389 | def setflag(self, path: bytes, flag: bytes) -> None: | ||
Augie Fackler
|
r44961 | node, unused_flag = self._resolve_entry(path) | ||
self._pending_changes[path] = node, flag | ||||
Matt Harbison
|
r53389 | def get(self, path: bytes, default=None) -> bytes | None: | ||
Augie Fackler
|
r44961 | try: | ||
return self._resolve_entry(path)[0] | ||||
except KeyError: | ||||
return default | ||||
Matt Harbison
|
r53389 | def flags(self, path: bytes) -> bytes: | ||
Augie Fackler
|
r44961 | try: | ||
return self._resolve_entry(path)[1] | ||||
except KeyError: | ||||
return b'' | ||||
Matt Harbison
|
r53389 | def copy(self) -> 'gittreemanifest': | ||
Augie Fackler
|
r45986 | return gittreemanifest( | ||
self._git_repo, self._tree, dict(self._pending_changes) | ||||
) | ||||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53389 | def items(self) -> Iterator[tuple[bytes, bytes]]: | ||
Augie Fackler
|
r44961 | for f in self: | ||
# TODO: build a proper iterator version of this | ||||
Matt Harbison
|
r53388 | yield f, self[f] | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53389 | def iteritems(self) -> Iterator[tuple[bytes, bytes]]: | ||
Augie Fackler
|
r44961 | return self.items() | ||
Matt Harbison
|
r53389 | def iterentries(self) -> Iterator[tuple[bytes, bytes, bytes]]: | ||
Augie Fackler
|
r44961 | for f in self: | ||
# TODO: build a proper iterator version of this | ||||
Matt Harbison
|
r53388 | yield f, *self._resolve_entry(f) | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53389 | def text(self) -> ByteString: | ||
# TODO can this method move out of the manifest iface? | ||||
raise NotImplementedError | ||||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53391 | def fastdelta( | ||
self, base: ByteString, changes: Iterable[tuple[bytes, bool]] | ||||
) -> tuple[ByteString, ByteString]: | ||||
raise NotImplementedError # TODO: implement this | ||||
Augie Fackler
|
r44961 | 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) | ||||
Josef 'Jeff' Sipek
|
r45447 | if te.type == pygit2.GIT_OBJ_TREE: | ||
Augie Fackler
|
r44961 | for inner in self._walkonetree( | ||
self._git_repo[te.id], match, realname + b'/' | ||||
): | ||||
yield inner | ||||
Josef 'Jeff' Sipek
|
r45448 | elif match(realname): | ||
yield pycompat.fsencode(realname) | ||||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53389 | def walk(self, match: matchmod.basematcher) -> Iterator[bytes]: | ||
Augie Fackler
|
r44961 | # 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)) | ||||
Matt Harbison
|
r53374 | class gittreemanifestctx(repository.imanifestrevisionstored): | ||
Augie Fackler
|
r44961 | def __init__(self, repo, gittree): | ||
self._repo = repo | ||||
self._tree = gittree | ||||
def read(self): | ||||
return gittreemanifest(self._repo, self._tree, None) | ||||
Matt Harbison
|
r53371 | def readfast(self, shallow: bool = False): | ||
Augie Fackler
|
r44963 | return self.read() | ||
Augie Fackler
|
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) | ||||
Matt Harbison
|
r53371 | def find(self, path: bytes) -> tuple[bytes, bytes]: | ||
Matt Harbison
|
r53388 | return self.read().find(path) | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r53378 | class memgittreemanifestctx(repository.imanifestrevisionwritable): | ||
Augie Fackler
|
r44961 | 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) | ||||
Josef 'Jeff' Sipek
|
r45115 | touched_dirs = pathutil.dirs(list(self._pending_changes)) | ||
Augie Fackler
|
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: | ||||
Connor Sheehan
|
r46089 | parent_tree_id = parent[pycompat.fsdecode(part)].id | ||
new = self._repo[parent_tree_id] | ||||
Augie Fackler
|
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 | ||||