Skip to content

Commit a4f6f93

Browse files
author
Ruben DI BATTISTA
committed
feat: Add support for autocrlf
i.e. replce CRLF with LF if `core.autocrlf` is True
1 parent a2e297a commit a4f6f93

File tree

2 files changed

+174
-76
lines changed

2 files changed

+174
-76
lines changed

git/index/base.py

+130-74
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838

3939
import git.diff as git_diff
4040
import os.path as osp
41+
from pathlib import Path
42+
from typing import Optional
4143

4244
from .fun import (
4345
entry_key,
@@ -87,6 +89,38 @@
8789
Treeish = Union[Tree, Commit, str, bytes]
8890

8991
# ------------------------------------------------------------------------------------
92+
class _FileStore:
93+
"""An utility class that stores original files somewhere and restores them
94+
to the original content at the exit"""
95+
96+
_dir: PathLike
97+
98+
def __init__(self, tmp_dir: Optional[PathLike] = None):
99+
100+
self._file_map: dict[PathLike, PathLike] = {}
101+
self._tmp_dir = tempfile.TemporaryDirectory(prefix=str(tmp_dir))
102+
103+
def __enter__(self):
104+
return self
105+
106+
def __exit__(self, exc, value, tb):
107+
for file, store_file in self._file_map.items():
108+
with open(store_file, "rb") as rf, open(file, "wb") as wf:
109+
for line in rf:
110+
wf.write(line)
111+
Path(store_file).unlink()
112+
self._dir.rmdir()
113+
114+
@property
115+
def _dir(self) -> Path:
116+
return Path(self._tmp_dir.name)
117+
118+
def save(self, file: PathLike) -> None:
119+
store_file = self._dir / tempfile.mktemp()
120+
self._file_map[file] = store_file
121+
with open(store_file, "wb") as wf, open(file, "rb") as rf:
122+
for line in rf:
123+
wf.write(line)
90124

91125

92126
__all__ = ("IndexFile", "CheckoutError")
@@ -611,7 +645,7 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
611645
return os.path.relpath(path, self.repo.working_tree_dir)
612646

613647
def _preprocess_add_items(
614-
self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]
648+
self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]], file_store: _FileStore
615649
) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
616650
"""Split the items into two lists of path strings and BaseEntries."""
617651
paths = []
@@ -622,6 +656,7 @@ def _preprocess_add_items(
622656

623657
for item in items:
624658
if isinstance(item, (str, os.PathLike)):
659+
self._autocrlf(item, file_store)
625660
paths.append(self._to_relative_path(item))
626661
elif isinstance(item, (Blob, Submodule)):
627662
entries.append(BaseIndexEntry.from_blob(item))
@@ -632,6 +667,30 @@ def _preprocess_add_items(
632667
# END for each item
633668
return paths, entries
634669

670+
def _autocrlf(self, file: PathLike, file_store: _FileStore) -> None:
671+
"""If the config option `autocrlf` is True, replace CRLF with LF"""
672+
673+
reader = self.repo.config_reader()
674+
675+
autocrlf = reader.get_value("core", "autocrlf", False)
676+
677+
if not autocrlf:
678+
return
679+
680+
file_store.save(file)
681+
682+
with tempfile.TemporaryFile("wb+") as tf:
683+
with open(file, "rb") as f:
684+
for line in f:
685+
line = line.replace(b"\r\n", b"\n")
686+
tf.write(line)
687+
688+
tf.seek(0)
689+
690+
with open(file, "wb") as f:
691+
for line in tf:
692+
f.write(line)
693+
635694
def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
636695
"""Store file at filepath in the database and return the base index entry
637696
Needs the git_working_dir decorator active ! This must be assured in the calling code"""
@@ -802,82 +861,79 @@ def add(
802861
Objects that do not have a null sha will be added even if their paths
803862
do not exist.
804863
"""
805-
# sort the entries into strings and Entries, Blobs are converted to entries
806-
# automatically
807-
# paths can be git-added, for everything else we use git-update-index
808-
paths, entries = self._preprocess_add_items(items)
809-
entries_added: List[BaseIndexEntry] = []
810-
# This code needs a working tree, therefore we try not to run it unless required.
811-
# That way, we are OK on a bare repository as well.
812-
# If there are no paths, the rewriter has nothing to do either
813-
if paths:
814-
entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
815-
816-
# HANDLE ENTRIES
817-
if entries:
818-
null_mode_entries = [e for e in entries if e.mode == 0]
819-
if null_mode_entries:
820-
raise ValueError(
821-
"At least one Entry has a null-mode - please use index.remove to remove files for clarity"
822-
)
823-
# END null mode should be remove
824-
825-
# HANDLE ENTRY OBJECT CREATION
826-
# create objects if required, otherwise go with the existing shas
827-
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
828-
if null_entries_indices:
829-
830-
@git_working_dir
831-
def handle_null_entries(self: "IndexFile") -> None:
832-
for ei in null_entries_indices:
833-
null_entry = entries[ei]
834-
new_entry = self._store_path(null_entry.path, fprogress)
835-
836-
# update null entry
837-
entries[ei] = BaseIndexEntry(
838-
(
839-
null_entry.mode,
840-
new_entry.binsha,
841-
null_entry.stage,
842-
null_entry.path,
864+
865+
with _FileStore() as file_store:
866+
# sort the entries into strings and Entries, Blobs are converted to entries
867+
# automatically
868+
# paths can be git-added, for everything else we use git-update-index
869+
paths, entries = self._preprocess_add_items(items, file_store)
870+
entries_added: List[BaseIndexEntry] = []
871+
# This code needs a working tree, therefore we try not to run it unless required.
872+
# That way, we are OK on a bare repository as well.
873+
# If there are no paths, the rewriter has nothing to do either
874+
if paths:
875+
entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
876+
877+
# HANDLE ENTRIES
878+
if entries:
879+
null_mode_entries = [e for e in entries if e.mode == 0]
880+
if null_mode_entries:
881+
raise ValueError(
882+
"At least one Entry has a null-mode - please use index.remove to remove files for clarity"
883+
)
884+
# END null mode should be remove
885+
886+
# HANDLE ENTRY OBJECT CREATION
887+
# create objects if required, otherwise go with the existing shas
888+
null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
889+
if null_entries_indices:
890+
891+
@git_working_dir
892+
def handle_null_entries(self: "IndexFile") -> None:
893+
for ei in null_entries_indices:
894+
null_entry = entries[ei]
895+
new_entry = self._store_path(null_entry.path, fprogress)
896+
897+
# update null entry
898+
entries[ei] = BaseIndexEntry(
899+
(null_entry.mode, new_entry.binsha, null_entry.stage, null_entry.path)
843900
)
844-
)
845-
# END for each entry index
846-
847-
# end closure
848-
handle_null_entries(self)
849-
# END null_entry handling
850-
851-
# REWRITE PATHS
852-
# If we have to rewrite the entries, do so now, after we have generated
853-
# all object sha's
854-
if path_rewriter:
855-
for i, e in enumerate(entries):
856-
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
901+
# END for each entry index
902+
903+
# end closure
904+
handle_null_entries(self)
905+
# END null_entry handling
906+
907+
# REWRITE PATHS
908+
# If we have to rewrite the entries, do so now, after we have generated
909+
# all object sha's
910+
if path_rewriter:
911+
for i, e in enumerate(entries):
912+
entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
913+
# END for each entry
914+
# END handle path rewriting
915+
916+
# just go through the remaining entries and provide progress info
917+
for i, entry in enumerate(entries):
918+
progress_sent = i in null_entries_indices
919+
if not progress_sent:
920+
fprogress(entry.path, False, entry)
921+
fprogress(entry.path, True, entry)
922+
# END handle progress
857923
# END for each entry
858-
# END handle path rewriting
859-
860-
# just go through the remaining entries and provide progress info
861-
for i, entry in enumerate(entries):
862-
progress_sent = i in null_entries_indices
863-
if not progress_sent:
864-
fprogress(entry.path, False, entry)
865-
fprogress(entry.path, True, entry)
866-
# END handle progress
867-
# END for each entry
868-
entries_added.extend(entries)
869-
# END if there are base entries
870-
871-
# FINALIZE
872-
# add the new entries to this instance
873-
for entry in entries_added:
874-
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
875-
876-
if write:
877-
self.write(ignore_extension_data=not write_extension_data)
878-
# END handle write
924+
entries_added.extend(entries)
925+
# END if there are base entries
879926

880-
return entries_added
927+
# FINALIZE
928+
# add the new entries to this instance
929+
for entry in entries_added:
930+
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
931+
932+
if write:
933+
self.write(ignore_extension_data=not write_extension_data)
934+
# END handle write
935+
936+
return entries_added
881937

882938
def _items_to_rela_paths(
883939
self,

test/test_index.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import os
1010
from stat import S_ISLNK, ST_MODE
1111
import tempfile
12-
from unittest import skipIf
12+
from unittest import mock, skipIf
1313
import shutil
1414

1515
from git import (
@@ -24,7 +24,15 @@
2424
CheckoutError,
2525
)
2626
from git.compat import is_win
27+
<<<<<<< HEAD
2728
from git.exc import HookExecutionError, InvalidGitRepositoryError
29+
=======
30+
from git.exc import (
31+
HookExecutionError,
32+
InvalidGitRepositoryError
33+
)
34+
from git.index.base import _FileStore
35+
>>>>>>> d5311c44 (:sparkles: feat: Add support for autocrlf)
2836
from git.index.fun import hook_path
2937
from git.index.typ import BaseIndexEntry, IndexEntry
3038
from git.objects import Blob
@@ -953,4 +961,38 @@ def test_index_add_pathlike(self, rw_repo):
953961
file = git_dir / "file.txt"
954962
file.touch()
955963

956-
rw_repo.index.add(file)
964+
rw_repo.index.add(file)
965+
966+
def test_autocrlf(self):
967+
file_store = mock.MagicMock()
968+
969+
with tempfile.TemporaryDirectory() as d:
970+
dummy_file = Path(d) / "dummy.txt"
971+
972+
with open(dummy_file, "w") as f:
973+
f.write("Hello\r\n")
974+
975+
index = self.rorepo.index
976+
977+
index._autocrlf(dummy_file, file_store)
978+
979+
with open(dummy_file, "r") as f:
980+
assert f.read() == "Hello\n"
981+
982+
983+
def test_filestore(tmp_path):
984+
dummy_file = tmp_path / "dummy.txt"
985+
986+
content = "Dummy\n"
987+
988+
with open(dummy_file, "w") as f:
989+
f.write(content)
990+
991+
with _FileStore() as fs:
992+
fs.save(dummy_file)
993+
994+
with open(dummy_file, "w") as f:
995+
f.write(r"Something else\n")
996+
997+
with open(dummy_file, "r") as f:
998+
assert f.read() == content

0 commit comments

Comments
 (0)