Skip to content

Commit 90d8789

Browse files
author
Ruben DI BATTISTA
committed
feat: If core.autocrlf is enabled, replace CRLF with LF when adding to index
1 parent d50bc88 commit 90d8789

File tree

2 files changed

+156
-65
lines changed

2 files changed

+156
-65
lines changed

git/index/base.py

+133-65
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import os.path as osp
1010
import subprocess
1111
import tempfile
12+
from contextlib import contextmanager
13+
from enum import auto
1214
from io import BytesIO
15+
from pathlib import Path
1316
from stat import S_ISLNK
1417
from typing import (
1518
IO,
@@ -22,6 +25,7 @@
2225
Iterator,
2326
List,
2427
NoReturn,
28+
Optional,
2529
Sequence,
2630
Tuple,
2731
Type,
@@ -72,7 +76,43 @@
7276
StageType = int
7377
Treeish = Union[Tree, Commit, str, bytes]
7478

75-
# ------------------------------------------------------------------------------------
79+
# Store temporary file modifications -------------------------------------------
80+
81+
class _FileStore:
82+
"""An utility class that stores original files somewhere and restores them
83+
to the original content at the exit"""
84+
85+
_dir: PathLike
86+
def __init__(self, tmp_dir: Optional[PathLike] = None):
87+
self._file_map: dict[PathLike, PathLike] = {}
88+
self._tmp_dir = tempfile.TemporaryDirectory(prefix=str(tmp_dir))
89+
90+
def __enter__(self):
91+
return self
92+
93+
def __exit__(self, exc, value, tb):
94+
for file, store_file in self._file_map.items():
95+
with open(store_file, "rb") as rf, open(file, "wb") as wf:
96+
for line in rf:
97+
wf.write(line)
98+
Path(store_file).unlink()
99+
self._dir.rmdir()
100+
101+
@property
102+
def _dir(self) -> Path:
103+
return Path(self._tmp_dir.name)
104+
105+
def save(self, file: PathLike) -> None:
106+
store_file = self._dir / tempfile.mktemp()
107+
self._file_map[file] = store_file
108+
with open(store_file, "wb") as wf, open(file, "rb") as rf:
109+
for line in rf:
110+
wf.write(line)
111+
112+
113+
114+
115+
# ------------------------------------------------------------------------------
76116

77117

78118
__all__ = ('IndexFile', 'CheckoutError')
@@ -578,8 +618,10 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
578618
raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
579619
return os.path.relpath(path, self.repo.working_tree_dir)
580620

581-
def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule']]
582-
) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
621+
def _preprocess_add_items(self,
622+
items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule']],
623+
file_store: _FileStore
624+
) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
583625
""" Split the items into two lists of path strings and BaseEntries. """
584626
paths = []
585627
entries = []
@@ -589,6 +631,7 @@ def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexE
589631

590632
for item in items:
591633
if isinstance(item, (str, os.PathLike)):
634+
self._autocrlf(item, file_store)
592635
paths.append(self._to_relative_path(item))
593636
elif isinstance(item, (Blob, Submodule)):
594637
entries.append(BaseIndexEntry.from_blob(item))
@@ -599,6 +642,29 @@ def _preprocess_add_items(self, items: Sequence[Union[PathLike, Blob, BaseIndexE
599642
# END for each item
600643
return paths, entries
601644

645+
def _autocrlf(self, file: PathLike, file_store: _FileStore) -> PathLike:
646+
"""If the config option `autocrlf` is True, replace CRLF with LF"""
647+
648+
reader = self.repo.config_reader()
649+
650+
autocrlf = reader.get_value("core", "autocrlf", False)
651+
652+
if not autocrlf:
653+
return file
654+
655+
file_store.save(file)
656+
657+
with open(file, "rb") as f:
658+
content = f.read()
659+
660+
content = content.replace(b"\r\n", b"\n")
661+
662+
with open(file, "wb") as f:
663+
f.write(content)
664+
665+
return file
666+
667+
602668
def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
603669
"""Store file at filepath in the database and return the base index entry
604670
Needs the git_working_dir decorator active ! This must be assured in the calling code"""
@@ -753,70 +819,72 @@ def add(self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, 'Submodule']
753819
# sort the entries into strings and Entries, Blobs are converted to entries
754820
# automatically
755821
# paths can be git-added, for everything else we use git-update-index
756-
paths, entries = self._preprocess_add_items(items)
757-
entries_added: List[BaseIndexEntry] = []
758-
# This code needs a working tree, therefore we try not to run it unless required.
759-
# That way, we are OK on a bare repository as well.
760-
# If there are no paths, the rewriter has nothing to do either
761-
if paths:
762-
entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
763-
764-
# HANDLE ENTRIES
765-
if entries:
766-
null_mode_entries = [e for e in entries if e.mode == 0]
767-
if null_mode_entries:
768-
raise ValueError(
769-
"At least one Entry has a null-mode - please use index.remove to remove files for clarity")
770-
# END null mode should be remove
771-
772-
# HANDLE ENTRY OBJECT CREATION
773-
# create objects if required, otherwise go with the existing shas
774-
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
775-
if null_entries_indices:
776-
@ git_working_dir
777-
def handle_null_entries(self: 'IndexFile') -> None:
778-
for ei in null_entries_indices:
779-
null_entry = entries[ei]
780-
new_entry = self._store_path(null_entry.path, fprogress)
781-
782-
# update null entry
783-
entries[ei] = BaseIndexEntry(
784-
(null_entry.mode, new_entry.binsha, null_entry.stage, null_entry.path))
785-
# END for each entry index
786-
# end closure
787-
handle_null_entries(self)
788-
# END null_entry handling
789-
790-
# REWRITE PATHS
791-
# If we have to rewrite the entries, do so now, after we have generated
792-
# all object sha's
793-
if path_rewriter:
794-
for i, e in enumerate(entries):
795-
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
822+
with _FileStore() as file_store:
823+
paths, entries = self._preprocess_add_items(items, file_store)
824+
import ipdb; ipdb.set_trace()
825+
entries_added: List[BaseIndexEntry] = []
826+
# This code needs a working tree, therefore we try not to run it unless required.
827+
# That way, we are OK on a bare repository as well.
828+
# If there are no paths, the rewriter has nothing to do either
829+
if paths:
830+
entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
831+
832+
# HANDLE ENTRIES
833+
if entries:
834+
null_mode_entries = [e for e in entries if e.mode == 0]
835+
if null_mode_entries:
836+
raise ValueError(
837+
"At least one Entry has a null-mode - please use index.remove to remove files for clarity")
838+
# END null mode should be remove
839+
840+
# HANDLE ENTRY OBJECT CREATION
841+
# create objects if required, otherwise go with the existing shas
842+
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
843+
if null_entries_indices:
844+
@ git_working_dir
845+
def handle_null_entries(self: 'IndexFile') -> None:
846+
for ei in null_entries_indices:
847+
null_entry = entries[ei]
848+
new_entry = self._store_path(null_entry.path, fprogress)
849+
850+
# update null entry
851+
entries[ei] = BaseIndexEntry(
852+
(null_entry.mode, new_entry.binsha, null_entry.stage, null_entry.path))
853+
# END for each entry index
854+
# end closure
855+
handle_null_entries(self)
856+
# END null_entry handling
857+
858+
# REWRITE PATHS
859+
# If we have to rewrite the entries, do so now, after we have generated
860+
# all object sha's
861+
if path_rewriter:
862+
for i, e in enumerate(entries):
863+
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
864+
# END for each entry
865+
# END handle path rewriting
866+
867+
# just go through the remaining entries and provide progress info
868+
for i, entry in enumerate(entries):
869+
progress_sent = i in null_entries_indices
870+
if not progress_sent:
871+
fprogress(entry.path, False, entry)
872+
fprogress(entry.path, True, entry)
873+
# END handle progress
796874
# END for each entry
797-
# END handle path rewriting
798-
799-
# just go through the remaining entries and provide progress info
800-
for i, entry in enumerate(entries):
801-
progress_sent = i in null_entries_indices
802-
if not progress_sent:
803-
fprogress(entry.path, False, entry)
804-
fprogress(entry.path, True, entry)
805-
# END handle progress
806-
# END for each entry
807-
entries_added.extend(entries)
808-
# END if there are base entries
809-
810-
# FINALIZE
811-
# add the new entries to this instance
812-
for entry in entries_added:
813-
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
814-
815-
if write:
816-
self.write(ignore_extension_data=not write_extension_data)
817-
# END handle write
875+
entries_added.extend(entries)
876+
# END if there are base entries
818877

819-
return entries_added
878+
# FINALIZE
879+
# add the new entries to this instance
880+
for entry in entries_added:
881+
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
882+
883+
if write:
884+
self.write(ignore_extension_data=not write_extension_data)
885+
# END handle write
886+
887+
return entries_added
820888

821889
def _items_to_rela_paths(self, items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]]
822890
) -> List[PathLike]:

test/test_index.py

+23
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from git.cmd import Git
3030
from git.compat import is_win
3131
from git.exc import HookExecutionError, InvalidGitRepositoryError
32+
from git.index.base import _FileStore
3233
from git.index.fun import hook_path
3334
from git.index.typ import BaseIndexEntry, IndexEntry
3435
from git.objects import Blob
@@ -930,3 +931,25 @@ def test_index_add_pathlike(self, rw_repo):
930931
file.touch()
931932

932933
rw_repo.index.add(file)
934+
935+
936+
937+
def test_filestore(tmp_path):
938+
dummy_file = tmp_path / "dummy.txt"
939+
940+
content = "Dummy\n"
941+
942+
with open(dummy_file, "w") as f:
943+
f.write(content)
944+
945+
with _FileStore() as fs:
946+
fs.save(dummy_file)
947+
948+
with open(dummy_file, "w") as f:
949+
f.write(r"Something else\n")
950+
951+
with open(dummy_file, "r") as f:
952+
assert f.read() == content
953+
954+
955+

0 commit comments

Comments
 (0)