Skip to content

Commit 564a043

Browse files
authored
Merge pull request #2081 from blahgeek/support-index-v3
Preliminary support for index format v3
2 parents 5ad35f9 + 107b1b4 commit 564a043

File tree

4 files changed

+67
-6
lines changed

4 files changed

+67
-6
lines changed

git/index/fun.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
)
3737
from git.util import IndexFileSHA1Writer, finalize_process
3838

39-
from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
39+
from .typ import CE_EXTENDED, BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
4040
from .util import pack, unpack
4141

4242
# typing -----------------------------------------------------------------------------
@@ -158,7 +158,7 @@ def write_cache(
158158
write = stream_sha.write
159159

160160
# Header
161-
version = 2
161+
version = 3 if any(entry.extended_flags for entry in entries) else 2
162162
write(b"DIRC")
163163
write(pack(">LL", version, len(entries)))
164164

@@ -172,6 +172,8 @@ def write_cache(
172172
plen = len(path) & CE_NAMEMASK # Path length
173173
assert plen == len(path), "Path %s too long to fit into index" % entry.path
174174
flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values.
175+
if entry.extended_flags:
176+
flags |= CE_EXTENDED
175177
write(
176178
pack(
177179
">LLLLLL20sH",
@@ -185,6 +187,8 @@ def write_cache(
185187
flags,
186188
)
187189
)
190+
if entry.extended_flags:
191+
write(pack(">H", entry.extended_flags))
188192
write(path)
189193
real_size = (tell() - beginoffset + 8) & ~7
190194
write(b"\0" * ((beginoffset + real_size) - tell()))
@@ -206,8 +210,7 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
206210
unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2)))
207211
version, num_entries = unpacked
208212

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

213216

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

266272
real_size = (tell() - beginoffset + 8) & ~7
267273
read((beginoffset + real_size) - tell())
268-
entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size))
274+
entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size, extended_flags))
269275
# entry_key would be the method to use, but we save the effort.
270276
entries[(path, entry.stage)] = entry
271277
count += 1

git/index/typ.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
CE_VALID = 0x8000
3333
CE_STAGESHIFT = 12
3434

35+
CE_EXT_SKIP_WORKTREE = 0x4000
36+
CE_EXT_INTENT_TO_ADD = 0x2000
37+
3538
# } END invariants
3639

3740

@@ -87,6 +90,8 @@ class BaseIndexEntryHelper(NamedTuple):
8790
uid: int = 0
8891
gid: int = 0
8992
size: int = 0
93+
# version 3 extended flags, only when (flags & CE_EXTENDED) is set
94+
extended_flags: int = 0
9095

9196

9297
class BaseIndexEntry(BaseIndexEntryHelper):
@@ -102,7 +107,7 @@ def __new__(
102107
cls,
103108
inp_tuple: Union[
104109
Tuple[int, bytes, int, PathLike],
105-
Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int],
110+
Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int, int],
106111
],
107112
) -> "BaseIndexEntry":
108113
"""Override ``__new__`` to allow construction from a tuple for backwards
@@ -134,6 +139,14 @@ def stage(self) -> int:
134139
"""
135140
return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT
136141

142+
@property
143+
def skip_worktree(self) -> bool:
144+
return (self.extended_flags & CE_EXT_SKIP_WORKTREE) > 0
145+
146+
@property
147+
def intent_to_add(self) -> bool:
148+
return (self.extended_flags & CE_EXT_INTENT_TO_ADD) > 0
149+
137150
@classmethod
138151
def from_blob(cls, blob: Blob, stage: int = 0) -> "BaseIndexEntry":
139152
""":return: Fully equipped BaseIndexEntry at the given stage"""

test/fixtures/index_extended_flags

436 Bytes
Binary file not shown.

test/test_index.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,48 @@ def test_index_add_non_normalized_path(self, rw_repo):
12181218

12191219
rw_repo.index.add(non_normalized_path)
12201220

1221+
def test_index_file_v3(self):
1222+
index = IndexFile(self.rorepo, fixture_path("index_extended_flags"))
1223+
assert index.entries
1224+
assert index.version == 3
1225+
assert len(index.entries) == 4
1226+
assert index.entries[("init.t", 0)].skip_worktree
1227+
1228+
# Write the data - it must match the original.
1229+
with tempfile.NamedTemporaryFile() as tmpfile:
1230+
index.write(tmpfile.name)
1231+
assert Path(tmpfile.name).read_bytes() == Path(fixture_path("index_extended_flags")).read_bytes()
1232+
1233+
@with_rw_directory
1234+
def test_index_file_v3_with_git_command(self, tmp_dir):
1235+
tmp_dir = Path(tmp_dir)
1236+
with cwd(tmp_dir):
1237+
git = Git(tmp_dir)
1238+
git.init()
1239+
1240+
file = tmp_dir / "file.txt"
1241+
file.write_text("hello")
1242+
git.add("--intent-to-add", "file.txt") # intent-to-add sets extended flag
1243+
1244+
repo = Repo(tmp_dir)
1245+
index = repo.index
1246+
1247+
assert len(index.entries) == 1
1248+
assert index.version == 3
1249+
entry = list(index.entries.values())[0]
1250+
assert entry.path == "file.txt"
1251+
assert entry.intent_to_add
1252+
1253+
file2 = tmp_dir / "file2.txt"
1254+
file2.write_text("world")
1255+
index.add(["file2.txt"])
1256+
index.write()
1257+
1258+
status_str = git.status(porcelain=True)
1259+
status_lines = status_str.splitlines()
1260+
assert " A file.txt" in status_lines
1261+
assert "A file2.txt" in status_lines
1262+
12211263

12221264
class TestIndexUtils:
12231265
@pytest.mark.parametrize("file_path_type", [str, Path])

0 commit comments

Comments
 (0)