|
6 | 6 | """ |
7 | 7 |
|
8 | 8 | from collections import Counter |
| 9 | +from glob import glob |
9 | 10 | from importlib import import_module |
10 | 11 | import io |
11 | 12 | from itertools import islice |
@@ -361,6 +362,70 @@ def _index_from_tree(repo: Any, tree: Any) -> Any: |
361 | 362 | return repo.index_from_tree(tree) |
362 | 363 |
|
363 | 364 |
|
| 365 | +def materialize_index(command: Any, source: Any, destination: str, desired: Dict[Any, Any], dirty: Any) -> Any: |
| 366 | + """Write a private index, retaining native metadata for unchanged entries.""" |
| 367 | + if gix is None: |
| 368 | + return NotImplemented |
| 369 | + method = "IndexFile.write" |
| 370 | + command._require_version() |
| 371 | + try: |
| 372 | + if any(stage for _path, stage in desired): |
| 373 | + raise _Unsupported("unmerged index editing") |
| 374 | + if any(not entry.binsha.strip(b"\0") for entry in desired.values()): |
| 375 | + raise _Unsupported("null index object IDs") |
| 376 | + names = {os.fsencode(path) for path, _stage in desired} |
| 377 | + for path in names: |
| 378 | + while b"/" in path: |
| 379 | + path = path.rsplit(b"/", 1)[0] |
| 380 | + if path in names: |
| 381 | + raise _Unsupported("overlapping index paths") |
| 382 | + repo = _repository(command, {}) |
| 383 | + if os.path.exists(source): |
| 384 | + if repo.config_snapshot().boolean("core.splitIndex") or glob( |
| 385 | + os.path.join(os.path.dirname(os.fspath(source)), "sharedindex.*") |
| 386 | + ): |
| 387 | + raise _Unsupported("split index preservation (GIX-13)") |
| 388 | + index = _index(repo, {"env": {"GIT_INDEX_FILE": os.fspath(source)}}) |
| 389 | + if index.version() != 2: |
| 390 | + raise _Unsupported("index version preservation (GIX-13)") |
| 391 | + else: |
| 392 | + index = _index_from_tree(repo, repo.empty_tree()) |
| 393 | + # Keep path-independent flags that GitPython exposes; changed paths get fresh stat data. |
| 394 | + mask = (3 << 12) | (1 << 15) | (1 << 30) |
| 395 | + changed = {os.fsencode(path) for path in dirty} |
| 396 | + with index.entries() as entries: |
| 397 | + current = list(entries) |
| 398 | + for entry in current: |
| 399 | + wanted = desired.get((safe_decode(entry.path()), entry.stage())) |
| 400 | + if wanted is None or (entry.mode, str(entry.id), entry.flags & mask) != ( |
| 401 | + wanted.mode, |
| 402 | + wanted.hexsha, |
| 403 | + wanted.flags & mask, |
| 404 | + ): |
| 405 | + changed.add(entry.path()) |
| 406 | + changed.update(names - {entry.path() for entry in current}) |
| 407 | + # ponytail: per-entry removals shift a vector; use a bulk binding for very large edits. |
| 408 | + for position, entry in reversed(list(enumerate(current))): |
| 409 | + if entry.path() in changed: |
| 410 | + index.remove_entry_at_index(position) |
| 411 | + for (path, _stage), entry in desired.items(): |
| 412 | + if os.fsencode(path) in changed: |
| 413 | + index.dangerously_push_entry( |
| 414 | + gix.IndexStat(), gix.ObjectId(entry.hexsha), entry.flags & mask, entry.mode, os.fsencode(path) |
| 415 | + ) |
| 416 | + index.sort_entries() |
| 417 | + index.verify_entries() |
| 418 | + index.set_path(destination) |
| 419 | + _write("index", index.write) |
| 420 | + except _Unsupported as exc: |
| 421 | + return _fallback(method, str(exc)) |
| 422 | + except gix.Error as exc: |
| 423 | + _logger.debug("materialize_index native preparation: %s", exc) |
| 424 | + return _fallback(method, "native preparation diagnostics") |
| 425 | + record(method, "native") |
| 426 | + return None |
| 427 | + |
| 428 | + |
364 | 429 | def _write(method: str, function: Callable[[], Any]) -> Any: |
365 | 430 | """Once a write starts, an error must not cause a second attempt through Git.""" |
366 | 431 | try: |
|
0 commit comments