Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions git/objects/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
Type,
Union,
cast,
overload,
)

from git.types import PathLike
Expand Down Expand Up @@ -175,6 +176,8 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable):

* Access a specific blob using the ``tree["filename"]`` notation.
* You may likewise access by index, like ``blob = tree[0]``.
* Slicing, like ``tree[:2]``, returns a list of objects.
* ``reversed(tree)`` iterates over objects in reverse order.
"""

type: Literal["tree"] = "tree"
Expand Down Expand Up @@ -377,7 +380,16 @@ def __iter__(self) -> Iterator[IndexObjUnion]:
def __len__(self) -> int:
return len(self._cache)

def __getitem__(self, item: Union[str, int, slice]) -> IndexObjUnion:
@overload
def __getitem__(self, item: Union[str, int]) -> IndexObjUnion: ...

@overload
def __getitem__(self, item: slice) -> List[IndexObjUnion]: ...

def __getitem__(self, item: Union[str, int, slice]) -> Union[IndexObjUnion, List[IndexObjUnion]]:
if isinstance(item, slice):
return list(self._iter_convert_to_object(self._cache[item]))

if isinstance(item, int):
info = self._cache[item]
return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2]))
Expand Down Expand Up @@ -409,7 +421,7 @@ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool:
return False

def __reversed__(self) -> Iterator[IndexObjUnion]:
return reversed(self._iter_convert_to_object(self._cache)) # type: ignore[call-overload]
return self._iter_convert_to_object(reversed(self._cache))

def _serialize(self, stream: "BytesIO") -> "Tree":
"""Ask Git to build this cache as a tree, then copy its raw object contents."""
Expand Down
36 changes: 36 additions & 0 deletions test/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,42 @@ def test_tree_modifier_orders_non_utf8_names_as_git_does():
assert [safe_encode(item.name) for item in tree] == expected


@pytest.mark.parametrize(
"selection",
[slice(None), slice(1, 4), slice(None, None, 2), slice(-3, None), slice(None, None, -1), slice(99, None)],
)
def test_tree_slicing_follows_list_order_without_changing_the_tree(selection):
with fixture_repo("tree-order.sh") as repo:
tree = repo.head.commit.tree
original = list(tree)
selected = tree[selection]
assert isinstance(selected, list)

def describe(items):
return [(type(item), item.path, item.mode, item.hexsha) for item in items]

assert describe(selected) == describe(original[selection])
selected.clear()
assert describe(list(tree)) == describe(original)


def test_tree_slicing_rejects_a_zero_step():
with fixture_repo("tree-order.sh") as repo:
with pytest.raises(ValueError, match="slice step cannot be zero"):
repo.head.commit.tree[::0]


def test_reversed_tree_preserves_order_and_nested_paths():
with fixture_repo("tree-order.sh") as repo:
tree = repo.head.commit.tree
forward = list(tree)
backward = list(reversed(tree))
assert [(type(item), item.path, item.mode, item.hexsha) for item in backward] == [
(type(item), item.path, item.mode, item.hexsha) for item in reversed(forward)
]
assert [item.path for item in reversed(tree["file"])] == ["file/a"]


@ddt.ddt
class TestTree(TestBase):
@with_rw_repo("HEAD")
Expand Down
6 changes: 6 additions & 0 deletions test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,19 @@ def test_execute_output_types() -> None:
from git import Remote, Repo
from git.repo.base import BlameEntry
from git.objects import Commit, Submodule
from git.objects.base import IndexObjUnion

repo = Repo()
remote = Remote(repo, "origin")
removed_names: List[str] = [Remote.remove(repo, "origin"), Remote.rm(repo, "origin"), repo.delete_remote("origin")]
removed_remotes: List[Remote] = [Remote.remove(repo, remote), Remote.rm(repo, remote), repo.delete_remote(remote)]
blame = BlameEntry(repo.head.commit, range(1), "file", range(1))
commit: Commit = blame.commit
tree = commit.tree
indexed_tree_item: IndexObjUnion = tree[0]
named_tree_item: IndexObjUnion = tree["file"]
sliced_tree_items: List[IndexObjUnion] = tree[:2]
reversed_tree_items: List[IndexObjUnion] = list(reversed(tree))
submodule_entry: IndexEntry = IndexEntry.from_blob(Submodule(repo, b"\0" * 20))
Git().get_object_header(b"HEAD")
Git().get_object_data(b"HEAD")
Expand Down
Loading