fix(encoding for file): fixed support of non utf-8 files in all backends

This commit is contained in:
RhodeCode Admin 2024-11-27 19:35:02 +01:00
parent f687bfc994
commit d08633d220
42 changed files with 2001 additions and 1973 deletions

View file

@ -21,8 +21,8 @@ GIT commit module
"""
import io
import stat
import configparser
import logging
from itertools import chain
from zope.cachedescriptors.property import Lazy as LazyProperty
@ -32,9 +32,16 @@ from rhodecode.lib.str_utils import safe_bytes, safe_str
from rhodecode.lib.vcs.backends import base
from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
from rhodecode.lib.vcs.nodes import (
FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
ChangedFileNodesGenerator, AddedFileNodesGenerator,
RemovedFileNodesGenerator, LargeFileNode)
FileNode,
DirNode,
NodeKind,
RootNode,
SubModuleNode,
LargeFileNode,
)
from rhodecode.lib.vcs_common import FILEMODE_LINK
log = logging.getLogger(__name__)
class GitCommit(base.BaseCommit):
@ -50,13 +57,11 @@ class GitCommit(base.BaseCommit):
# done through a more complex tree walk on parents
"status",
# mercurial specific property not supported here
"_file_paths",
"obsolete",
# mercurial specific property not supported here
'obsolete',
"phase",
# mercurial specific property not supported here
'phase',
# mercurial specific property not supported here
'hidden'
"hidden",
]
def __init__(self, repository, raw_id, idx, pre_load=None):
@ -69,17 +74,16 @@ class GitCommit(base.BaseCommit):
self._set_bulk_properties(pre_load)
# caches
self._stat_modes = {} # stat info for paths
self._paths = {} # path processed with parse_tree
self.nodes = {}
self._path_mode_cache = {} # path stats cache, e.g filemode etc
self._path_type_cache = {} # path type dir/file/link etc cache
self._submodules = None
def _set_bulk_properties(self, pre_load):
if not pre_load:
return
pre_load = [entry for entry in pre_load
if entry not in self._filter_pre_load]
pre_load = [entry for entry in pre_load if entry not in self._filter_pre_load]
if not pre_load:
return
@ -102,7 +106,7 @@ class GitCommit(base.BaseCommit):
@LazyProperty
def _tree_id(self):
return self._remote[self._commit['tree']]['id']
return self._remote[self._commit["tree"]]["id"]
@LazyProperty
def id(self):
@ -134,13 +138,12 @@ class GitCommit(base.BaseCommit):
"""
Returns modified, added, removed, deleted files for current commit
"""
return self.changed, self.added, self.removed
added, modified, deleted = self._changes_cache
return list(modified), list(modified), list(deleted)
@LazyProperty
def tags(self):
tags = [safe_str(name) for name,
commit_id in self.repository.tags.items()
if commit_id == self.raw_id]
tags = [safe_str(name) for name, commit_id in self.repository.tags.items() if commit_id == self.raw_id]
return tags
@LazyProperty
@ -161,47 +164,33 @@ class GitCommit(base.BaseCommit):
branches = self._remote.branch(self.raw_id)
return self._set_branch(branches)
def _get_tree_id_for_path(self, path):
def _get_path_tree_id_and_type(self, path: bytes):
path = safe_str(path)
if path in self._paths:
return self._paths[path]
if path in self._path_type_cache:
return self._path_type_cache[path]
tree_id = self._tree_id
if path == b"":
self._path_type_cache[b""] = [self._tree_id, NodeKind.DIR]
return self._path_type_cache[path]
path = path.strip('/')
if path == '':
data = [tree_id, "tree"]
self._paths[''] = data
return data
tree_id, tree_type, tree_mode = \
self._remote.tree_and_type_for_path(self.raw_id, path)
tree_id, tree_type, tree_mode = self._remote.tree_and_type_for_path(self.raw_id, path)
if tree_id is None:
raise self.no_node_at_path(path)
self._paths[path] = [tree_id, tree_type]
self._stat_modes[path] = tree_mode
self._path_type_cache[path] = [tree_id, tree_type]
self._path_mode_cache[path] = tree_mode
if path not in self._paths:
raise self.no_node_at_path(path)
return self._paths[path]
return self._path_type_cache[path]
def _get_kind(self, path):
tree_id, type_ = self._get_tree_id_for_path(path)
if type_ == 'blob':
return NodeKind.FILE
elif type_ == 'tree':
return NodeKind.DIR
elif type_ == 'link':
return NodeKind.SUBMODULE
return None
path = self._fix_path(path)
_, path_type = self._get_path_tree_id_and_type(path)
return path_type
def _assert_is_path(self, path):
path = self._fix_path(path)
if self._get_kind(path) != NodeKind.FILE:
raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
raise CommitError(f"File at path={path} does not exist for commit {self.raw_id}")
return path
def _get_file_nodes(self):
@ -237,15 +226,19 @@ class GitCommit(base.BaseCommit):
path = self._assert_is_path(path)
# ensure path is traversed
self._get_tree_id_for_path(path)
self._get_path_tree_id_and_type(path)
return self._stat_modes[path]
return self._path_mode_cache[path]
def is_link(self, path):
return stat.S_ISLNK(self.get_file_mode(path))
def is_link(self, path: bytes):
path = self._assert_is_path(path)
if path not in self._path_mode_cache:
self._path_mode_cache[path] = self._remote.fctx_flags(self.raw_id, path)
return self._path_mode_cache[path] == FILEMODE_LINK
def is_node_binary(self, path):
tree_id, _ = self._get_tree_id_for_path(path)
tree_id, _ = self._get_path_tree_id_and_type(path)
return self._remote.is_binary(tree_id)
def node_md5_hash(self, path):
@ -256,19 +249,19 @@ class GitCommit(base.BaseCommit):
"""
Returns content of the file at given `path`.
"""
tree_id, _ = self._get_tree_id_for_path(path)
tree_id, _ = self._get_path_tree_id_and_type(path)
return self._remote.blob_as_pretty_string(tree_id)
def get_file_content_streamed(self, path):
tree_id, _ = self._get_tree_id_for_path(path)
stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
tree_id, _ = self._get_path_tree_id_and_type(path)
stream_method = getattr(self._remote, "stream:blob_as_pretty_string")
return stream_method(tree_id)
def get_file_size(self, path):
"""
Returns size of the file at given `path`.
"""
tree_id, _ = self._get_tree_id_for_path(path)
tree_id, _ = self._get_path_tree_id_and_type(path)
return self._remote.blob_raw_length(tree_id)
def get_path_history(self, path, limit=None, pre_load=None):
@ -276,12 +269,9 @@ class GitCommit(base.BaseCommit):
Returns history of file as reversed list of `GitCommit` objects for
which file at given `path` has been modified.
"""
path = self._assert_is_path(path)
hist = self._remote.node_history(self.raw_id, path, limit)
return [
self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
for commit_id in hist]
history = self._remote.node_history(self.raw_id, path, limit)
return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load) for commit_id in history]
def get_file_annotate(self, path, pre_load=None):
"""
@ -293,95 +283,105 @@ class GitCommit(base.BaseCommit):
for ln_no, commit_id, content in result:
yield (
ln_no, commit_id,
ln_no,
commit_id,
lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
content)
content,
)
def get_nodes(self, path, pre_load=None):
def get_nodes(self, path: bytes, pre_load=None):
if self._get_kind(path) != NodeKind.DIR:
raise CommitError(
f"Directory does not exist for commit {self.raw_id} at '{path}'")
raise CommitError(f"Directory does not exist for commit {self.raw_id} at '{path}'")
path = self._fix_path(path)
tree_id, _ = self._get_tree_id_for_path(path)
# call and check tree_id for this path
tree_id, _ = self._get_path_tree_id_and_type(path)
dirnodes = []
filenodes = []
path_nodes = []
# extracted tree ID gives us our files...
str_path = safe_str(path) # libgit operates on bytes
for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
if type_ == 'link':
url = self._get_submodule_url('/'.join((str_path, name)))
dirnodes.append(SubModuleNode(
name, url=url, commit=id_, alias=self.repository.alias))
continue
for bytes_name, stat_, tree_item_id, node_kind in self._remote.tree_items(tree_id):
if node_kind is None:
raise CommitError(f"Requested object type={node_kind} cannot be determined")
if str_path != '':
obj_path = '/'.join((str_path, name))
if path != b"":
obj_path = b"/".join((path, bytes_name))
else:
obj_path = name
if obj_path not in self._stat_modes:
self._stat_modes[obj_path] = stat_
obj_path = bytes_name
if type_ == 'tree':
dirnodes.append(DirNode(safe_bytes(obj_path), commit=self))
elif type_ == 'blob':
filenodes.append(FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load))
# cache file mode for git, since we have it already
if obj_path not in self._path_mode_cache:
self._path_mode_cache[obj_path] = stat_
# cache type
if node_kind not in self._path_type_cache:
self._path_type_cache[obj_path] = [tree_item_id, node_kind]
entry = None
if obj_path in self.nodes:
entry = self.nodes[obj_path]
else:
raise CommitError(f"Requested object should be Tree or Blob, is {type_}")
if node_kind == NodeKind.SUBMODULE:
url = self._get_submodule_url(b"/".join((path, bytes_name)))
entry= SubModuleNode(bytes_name, url=url, commit=tree_item_id, alias=self.repository.alias)
elif node_kind == NodeKind.DIR:
entry = DirNode(safe_bytes(obj_path), commit=self)
elif node_kind == NodeKind.FILE:
entry = FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load)
nodes = dirnodes + filenodes
for node in nodes:
if node.path not in self.nodes:
self.nodes[node.path] = node
nodes.sort()
return nodes
if entry:
self.nodes[obj_path] = entry
path_nodes.append(entry)
def get_node(self, path, pre_load=None):
path_nodes.sort()
return path_nodes
def get_node(self, path: bytes, pre_load=None):
path = self._fix_path(path)
if path not in self.nodes:
try:
tree_id, type_ = self._get_tree_id_for_path(path)
except CommitError:
raise NodeDoesNotExistError(
f"Cannot find one of parents' directories for a given "
f"path: {path}")
if type_ in ['link', 'commit']:
# use cached, if we have one
if path in self.nodes:
return self.nodes[path]
try:
tree_id, path_type = self._get_path_tree_id_and_type(path)
except CommitError:
raise NodeDoesNotExistError(f"Cannot find one of parents' directories for a given path: {path}")
if path == b"":
node = RootNode(commit=self)
else:
if path_type == NodeKind.SUBMODULE:
url = self._get_submodule_url(path)
node = SubModuleNode(path, url=url, commit=tree_id,
alias=self.repository.alias)
elif type_ == 'tree':
if path == '':
node = RootNode(commit=self)
else:
node = DirNode(safe_bytes(path), commit=self)
elif type_ == 'blob':
node = SubModuleNode(path, url=url, commit=tree_id, alias=self.repository.alias)
elif path_type == NodeKind.DIR:
node = DirNode(safe_bytes(path), commit=self)
elif path_type == NodeKind.FILE:
node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
self._stat_modes[path] = node.mode
self._path_mode_cache[path] = node.mode
else:
raise self.no_node_at_path(path)
# cache node
self.nodes[path] = node
# cache node
self.nodes[path] = node
return self.nodes[path]
def get_largefile_node(self, path):
tree_id, _ = self._get_tree_id_for_path(path)
def get_largefile_node(self, path: bytes):
tree_id, _ = self._get_path_tree_id_and_type(path)
pointer_spec = self._remote.is_large_file(tree_id)
if pointer_spec:
# content of that file regular FileNode is the hash of largefile
file_id = pointer_spec.get('oid_hash')
if self._remote.in_largefiles_store(file_id):
lf_path = self._remote.store_path(file_id)
return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
file_id = pointer_spec.get("oid_hash")
if not self._remote.in_largefiles_store(file_id):
log.warning(f'Largefile oid={file_id} not found in store')
return None
lf_path = self._remote.store_path(file_id)
return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
@LazyProperty
def affected_files(self):
def affected_files(self) -> list[bytes]:
"""
Gets a fast accessible file changes for given commit
"""
@ -389,7 +389,7 @@ class GitCommit(base.BaseCommit):
return list(added.union(modified).union(deleted))
@LazyProperty
def _changes_cache(self):
def _changes_cache(self) -> tuple[set, set, set]:
added = set()
modified = set()
deleted = set()
@ -416,53 +416,22 @@ class GitCommit(base.BaseCommit):
:param status: one of: *added*, *modified* or *deleted*
"""
added, modified, deleted = self._changes_cache
return sorted({
'added': list(added),
'modified': list(modified),
'deleted': list(deleted)}[status]
)
@LazyProperty
def added(self):
"""
Returns list of added ``FileNode`` objects.
"""
if not self.parents:
return list(self._get_file_nodes())
return AddedFileNodesGenerator(self.added_paths, self)
return sorted({"added": list(added), "modified": list(modified), "deleted": list(deleted)}[status])
@LazyProperty
def added_paths(self):
return [n for n in self._get_paths_for_status('added')]
@LazyProperty
def changed(self):
"""
Returns list of modified ``FileNode`` objects.
"""
if not self.parents:
return []
return ChangedFileNodesGenerator(self.changed_paths, self)
return [n for n in self._get_paths_for_status("added")]
@LazyProperty
def changed_paths(self):
return [n for n in self._get_paths_for_status('modified')]
@LazyProperty
def removed(self):
"""
Returns list of removed ``FileNode`` objects.
"""
if not self.parents:
return []
return RemovedFileNodesGenerator(self.removed_paths, self)
return [n for n in self._get_paths_for_status("modified")]
@LazyProperty
def removed_paths(self):
return [n for n in self._get_paths_for_status('deleted')]
return [n for n in self._get_paths_for_status("deleted")]
def _get_submodule_url(self, submodule_path):
git_modules_path = '.gitmodules'
def _get_submodule_url(self, submodule_path: bytes):
git_modules_path = b".gitmodules"
if self._submodules is None:
self._submodules = {}
@ -476,9 +445,9 @@ class GitCommit(base.BaseCommit):
parser.read_file(io.StringIO(submodules_node.str_content))
for section in parser.sections():
path = parser.get(section, 'path')
url = parser.get(section, 'url')
path = parser.get(section, "path")
url = parser.get(section, "url")
if path and url:
self._submodules[path.strip('/')] = url
self._submodules[safe_bytes(path).strip(b"/")] = url
return self._submodules.get(submodule_path.strip('/'))
return self._submodules.get(submodule_path.strip(b"/"))

View file

@ -425,7 +425,7 @@ class GitRepository(BaseRepository):
return
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
translate_tag=True, maybe_unreachable=False, reference_obj=None):
translate_tag=True, maybe_unreachable=False, reference_obj=None) -> GitCommit:
"""
Returns `GitCommit` object representing commit from git repository
at the given `commit_id` or head (most recent commit) if None given.