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: 11 additions & 5 deletions git/index/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
)
from git.util import IndexFileSHA1Writer, finalize_process

from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
from .typ import CE_EXTENDED, BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
from .util import pack, unpack

# typing -----------------------------------------------------------------------------
Expand Down Expand Up @@ -158,7 +158,7 @@ def write_cache(
write = stream_sha.write

# Header
version = 2
version = 3 if any(entry.extended_flags for entry in entries) else 2
write(b"DIRC")
write(pack(">LL", version, len(entries)))

Expand All @@ -172,6 +172,8 @@ def write_cache(
plen = len(path) & CE_NAMEMASK # Path length
assert plen == len(path), "Path %s too long to fit into index" % entry.path
flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values.
if entry.extended_flags:
flags |= CE_EXTENDED
write(
pack(
">LLLLLL20sH",
Expand All @@ -185,6 +187,8 @@ def write_cache(
flags,
)
)
if entry.extended_flags:
write(pack(">H", entry.extended_flags))
write(path)
real_size = (tell() - beginoffset + 8) & ~7
write(b"\0" * ((beginoffset + real_size) - tell()))
Expand All @@ -206,8 +210,7 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2)))
version, num_entries = unpacked

# TODO: Handle version 3: extended data, see read-cache.c.
assert version in (1, 2), "Unsupported git index version %i, only 1 and 2 are supported" % version
assert version in (1, 2, 3), "Unsupported git index version %i, only 1, 2, and 3 are supported" % version
return version, num_entries


Expand Down Expand Up @@ -260,12 +263,15 @@ def read_cache(
ctime = unpack(">8s", read(8))[0]
mtime = unpack(">8s", read(8))[0]
(dev, ino, mode, uid, gid, size, sha, flags) = unpack(">LLLLLL20sH", read(20 + 4 * 6 + 2))
extended_flags = 0
if flags & CE_EXTENDED:
extended_flags = unpack(">H", read(2))[0]
path_size = flags & CE_NAMEMASK
path = read(path_size).decode(defenc)

real_size = (tell() - beginoffset + 8) & ~7
read((beginoffset + real_size) - tell())
entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size))
entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size, extended_flags))
# entry_key would be the method to use, but we save the effort.
entries[(path, entry.stage)] = entry
count += 1
Expand Down
15 changes: 14 additions & 1 deletion git/index/typ.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
CE_VALID = 0x8000
CE_STAGESHIFT = 12

CE_EXT_SKIP_WORKTREE = 0x4000
CE_EXT_INTENT_TO_ADD = 0x2000

# } END invariants


Expand Down Expand Up @@ -87,6 +90,8 @@ class BaseIndexEntryHelper(NamedTuple):
uid: int = 0
gid: int = 0
size: int = 0
# version 3 extended flags, only when (flags & CE_EXTENDED) is set
extended_flags: int = 0


class BaseIndexEntry(BaseIndexEntryHelper):
Expand All @@ -102,7 +107,7 @@ def __new__(
cls,
inp_tuple: Union[
Tuple[int, bytes, int, PathLike],
Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int],
Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int, int],
],
) -> "BaseIndexEntry":
"""Override ``__new__`` to allow construction from a tuple for backwards
Expand Down Expand Up @@ -134,6 +139,14 @@ def stage(self) -> int:
"""
return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT

@property
def skip_worktree(self) -> bool:
return (self.extended_flags & CE_EXT_SKIP_WORKTREE) > 0

@property
def intent_to_add(self) -> bool:
return (self.extended_flags & CE_EXT_INTENT_TO_ADD) > 0

@classmethod
def from_blob(cls, blob: Blob, stage: int = 0) -> "BaseIndexEntry":
""":return: Fully equipped BaseIndexEntry at the given stage"""
Expand Down
25 changes: 25 additions & 0 deletions test/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,31 @@ def test_index_add_non_normalized_path(self, rw_repo):

rw_repo.index.add(non_normalized_path)

@with_rw_directory
def test_index_version_v3(self, tmp_dir):
tmp_dir = Path(tmp_dir)
with cwd(tmp_dir):
subprocess.run(["git", "init", "-q"], check=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use the Git type instead? That way one would benefit from all the 'extras', especially around finding the right Git binary.

file = tmp_dir / "file.txt"
file.write_text("hello")
subprocess.run(["git", "add", "-N", "file.txt"], check=True)

repo = Repo(tmp_dir)

assert len(repo.index.entries) == 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test this, I think the index.version must be exposed and validated here.
I don't see at all why it would use index version 3 here.

entry = list(repo.index.entries.values())[0]
assert entry.path == "file.txt"
assert entry.intent_to_add

file2 = tmp_dir / "file2.txt"
file2.write_text("world")
repo.index.add(["file2.txt"])
repo.index.write()

status_str = subprocess.check_output(["git", "status", "--porcelain"], text=True)
assert " A file.txt\n" in status_str
assert "A file2.txt\n" in status_str


class TestIndexUtils:
@pytest.mark.parametrize("file_path_type", [str, Path])
Expand Down
Loading